From 12338f491eff864408e51248f8e8f75e5719eb14 Mon Sep 17 00:00:00 2001 From: sl-ios Date: Mon, 6 Apr 2026 16:25:35 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20parse=20HAL=20two-anchor=20ranging=20for?= =?UTF-8?q?mat=20(8-byte=20int32=C3=972)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- SulTee/SulTee/AnchorInfo.swift | 20 ++++++++++++---- SulTee/SulTee/BLEPackets.swift | 40 +++++++++++++++++++++++-------- SulTee/SulTee/BLEStatusView.swift | 8 ++++--- 3 files changed, 50 insertions(+), 18 deletions(-) diff --git a/SulTee/SulTee/AnchorInfo.swift b/SulTee/SulTee/AnchorInfo.swift index fef8c36..fa08bcd 100644 --- a/SulTee/SulTee/AnchorInfo.swift +++ b/SulTee/SulTee/AnchorInfo.swift @@ -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". + 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" + } } diff --git a/SulTee/SulTee/BLEPackets.swift b/SulTee/SulTee/BLEPackets.swift index f13adeb..e92de49 100644 --- a/SulTee/SulTee/BLEPackets.swift +++ b/SulTee/SulTee/BLEPackets.swift @@ -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..