feat: Merge SaltyTag BLE — GPS/IMU streaming to UWB tag, anchor display, UWB position authority #5
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -87,27 +87,46 @@ enum BLEPackets {
|
|||||||
|
|
||||||
// MARK: - Ranging notification parser
|
// MARK: - Ranging notification parser
|
||||||
//
|
//
|
||||||
// [0] Uint8 anchor count N
|
// HAL firmware format (8 bytes, 2 fixed anchors):
|
||||||
// Per anchor (9 bytes, offset = 1 + i×9):
|
// [0-3] Int32 LE front anchor range mm (id=0)
|
||||||
// [+0] Uint8 anchor index
|
// [4-7] Int32 LE back anchor range mm (id=1)
|
||||||
// [+1-4] Int32 LE range mm
|
//
|
||||||
// [+5-6] Int16 LE RSSI × 10 (dBm × 10)
|
// Legacy multi-anchor format (future):
|
||||||
// [+7-8] Uint16LE age ms
|
// [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] {
|
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 {
|
||||||
let base = 1 + i * 9
|
let base = 1 + i * 9
|
||||||
guard base + 9 <= data.count else { break }
|
guard base + 9 <= data.count else { break }
|
||||||
|
|
||||||
let anchorID = data[base]
|
let anchorID = data[base]
|
||||||
let rangeMM = data.readInt32LE(at: base + 1)
|
let rangeMM = data.readInt32LE(at: base + 1)
|
||||||
let rssiTimes10 = data.readInt16LE(at: base + 5)
|
let rssiTimes10 = data.readInt16LE(at: base + 5)
|
||||||
let ageMs = data.readUInt16LE(at: base + 7)
|
let ageMs = data.readUInt16LE(at: base + 7)
|
||||||
|
|
||||||
result.append(AnchorInfo(
|
result.append(AnchorInfo(
|
||||||
id: anchorID,
|
id: anchorID,
|
||||||
@ -116,6 +135,7 @@ enum BLEPackets {
|
|||||||
ageMs: ageMs,
|
ageMs: ageMs,
|
||||||
receivedAt: now
|
receivedAt: now
|
||||||
))
|
))
|
||||||
|
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
.font(.system(size: 11))
|
Text(rssi)
|
||||||
.foregroundStyle(.secondary)
|
.font(.system(size: 11))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
if anchor.isStale {
|
if anchor.isStale {
|
||||||
Text("STALE")
|
Text("STALE")
|
||||||
.font(.system(size: 10, weight: .bold))
|
.font(.system(size: 10, weight: .bold))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user