feat: Merge SaltyTag BLE — GPS/IMU streaming to UWB tag, anchor display, UWB position authority #5

Open
sl-ios wants to merge 19 commits from sl-ios/saltytag-merge into main
3 changed files with 50 additions and 18 deletions
Showing only changes of commit 12338f491e - Show all commits

View File

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

View File

@ -87,17 +87,36 @@ enum BLEPackets {
// MARK: - Ranging notification parser // MARK: - Ranging notification parser
// //
// 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 // [0] Uint8 anchor count N
// Per anchor (9 bytes, offset = 1 + i×9): // Per anchor (9 bytes):
// [+0] Uint8 anchor index // [+0] Uint8 anchor index
// [+1-4] Int32 LE range mm // [+1-4] Int32 LE range mm
// [+5-6] Int16 LE RSSI × 10 (dBm × 10) // [+5-6] Int16 LE RSSI × 10 (dBm × 10)
// [+7-8] Uint16LE age ms // [+7-8] Uint16LE age ms
static func parseRanging(_ data: Data) -> [AnchorInfo] { 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 [] } guard data.count >= 1 else { return [] }
let count = Int(data[0]) let count = Int(data[0])
let now = Date()
var result: [AnchorInfo] = [] var result: [AnchorInfo] = []
for i in 0..<count { for i in 0..<count {
@ -116,6 +135,7 @@ enum BLEPackets {
ageMs: ageMs, ageMs: ageMs,
receivedAt: now receivedAt: now
)) ))
} }
return result return result
} }

View File

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