fix: parse HAL two-anchor ranging format (8-byte int32×2)

The app's old parser expected a multi-anchor protocol:
  [count][anchorID+rangeMM+RSSI+age]×N (min 10 bytes)
HAL's firmware sends a fixed 8-byte packet:
  [int32 front_mm LE][int32 back_mm LE]

With the old parser data[0] was interpreted as anchor count
(e.g. 0xF8 = 248 for a 4.6m reading), the loop guard failed
immediately, and every notify returned [] — hence "waiting for
ranging data" despite the tag showing live ranges on OLED.

Changes:
- BLEPackets: detect 8-byte HAL format by length; decode as
  anchor id=0 (Front) and id=1 (Back); legacy multi-anchor path
  retained for forward compatibility
- AnchorInfo: rssiDBm is now Optional (nil when not reported);
  label maps id 0→"F", 1→"B" for the two-anchor HAL format
- BLEStatusView: guard on optional rssiString before rendering

Auto-reconnect confirmed correct (2s delay, bluetooth-central
background mode declared in Info.plist).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sl-ios 2026-04-06 16:25:35 -04:00
parent 7f9f159016
commit 12338f491e
3 changed files with 50 additions and 18 deletions

View File

@ -4,15 +4,22 @@ import Foundation
struct AnchorInfo: Identifiable {
let id: UInt8
let rangeMetres: Double
let rssiDBm: Double
let ageMs: UInt16 // age reported by tag firmware
let rssiDBm: Double? // nil when not reported (HAL two-anchor format)
let ageMs: UInt16 // age reported by tag firmware; 0 when not reported
let receivedAt: Date
/// True when the measurement is more than 3 seconds old (local wall-clock).
var isStale: Bool { Date().timeIntervalSince(receivedAt) > 3.0 }
/// Display string for the anchor identifier.
var label: String { "A\(id)" }
/// IDs 0/1 map to Front/Back (HAL two-anchor format); others show "A<n>".
var label: String {
switch id {
case 0: return "F"
case 1: return "B"
default: return "A\(id)"
}
}
/// Formatted range string matching the Flutter app style.
var rangeString: String {
@ -21,6 +28,9 @@ struct AnchorInfo: Identifiable {
: String(format: "%.1f m", rangeMetres)
}
/// Formatted RSSI string.
var rssiString: String { "\(Int(rssiDBm.rounded())) dBm" }
/// Formatted RSSI string, or nil when RSSI was not reported.
var rssiString: String? {
guard let r = rssiDBm else { return nil }
return "\(Int(r.rounded())) dBm"
}
}

View File

@ -87,27 +87,46 @@ enum BLEPackets {
// MARK: - Ranging notification parser
//
// [0] Uint8 anchor count N
// Per anchor (9 bytes, offset = 1 + i×9):
// [+0] Uint8 anchor index
// [+1-4] Int32 LE range mm
// [+5-6] Int16 LE RSSI × 10 (dBm × 10)
// [+7-8] Uint16LE age ms
// HAL firmware format (8 bytes, 2 fixed anchors):
// [0-3] Int32 LE front anchor range mm (id=0)
// [4-7] Int32 LE back anchor range mm (id=1)
//
// Legacy multi-anchor format (future):
// [0] Uint8 anchor count N
// Per anchor (9 bytes):
// [+0] Uint8 anchor index
// [+1-4] Int32 LE range mm
// [+5-6] Int16 LE RSSI × 10 (dBm × 10)
// [+7-8] Uint16LE age ms
static func parseRanging(_ data: Data) -> [AnchorInfo] {
let now = Date()
// HAL two-anchor format: exactly 8 bytes, two Int32 LE range values
if data.count == 8 {
let frontMM = data.readInt32LE(at: 0)
let backMM = data.readInt32LE(at: 4)
return [
AnchorInfo(id: 0, rangeMetres: Double(frontMM) / 1000.0,
rssiDBm: nil, ageMs: 0, receivedAt: now),
AnchorInfo(id: 1, rangeMetres: Double(backMM) / 1000.0,
rssiDBm: nil, ageMs: 0, receivedAt: now)
]
}
// Legacy multi-anchor format: [count][anchorID+rangeMM+RSSI+age] × N
guard data.count >= 1 else { return [] }
let count = Int(data[0])
let now = Date()
var result: [AnchorInfo] = []
for i in 0..<count {
let base = 1 + i * 9
guard base + 9 <= data.count else { break }
let anchorID = data[base]
let rangeMM = data.readInt32LE(at: base + 1)
let anchorID = data[base]
let rangeMM = data.readInt32LE(at: base + 1)
let rssiTimes10 = data.readInt16LE(at: base + 5)
let ageMs = data.readUInt16LE(at: base + 7)
let ageMs = data.readUInt16LE(at: base + 7)
result.append(AnchorInfo(
id: anchorID,
@ -116,6 +135,7 @@ enum BLEPackets {
ageMs: ageMs,
receivedAt: now
))
}
return result
}

View File

@ -144,9 +144,11 @@ struct BLEStatusView: View {
Spacer()
VStack(alignment: .trailing, spacing: 2) {
Text(anchor.rssiString)
.font(.system(size: 11))
.foregroundStyle(.secondary)
if let rssi = anchor.rssiString {
Text(rssi)
.font(.system(size: 11))
.foregroundStyle(.secondary)
}
if anchor.isStale {
Text("STALE")
.font(.system(size: 10, weight: .bold))