c472668d7a
feat: Merge SaltyTag BLE — GPS/IMU streaming to UWB tag, anchor display, UWB position authority
...
BLE Protocol (from SaltyTag Flutter app, exact binary format):
Service: 12345678-1234-5678-1234-56789abcdef0
GPS char: …def3 IMU char: …def4 Ranging char: …def5 (notify)
BLEManager.swift — CoreBluetooth central:
- Scans for peripherals advertising the service UUID; name prefix "UWB_TAG"
- 15 s scan timeout, auto-reconnect with 2 s backoff on disconnect
- Exposes sendGPS(Data) + sendIMU(Data); gpsStreamEnabled / imuStreamEnabled toggles
- Subscribes to ranging notifications → parses → publishes anchors[]
BLEPackets.swift — exact binary encoders matching SaltyTag firmware expectations:
- gpsPacket(CLLocation) → 20 bytes LE: lat×1e7, lon×1e7, alt×10(Int16),
speed×100(UInt16), heading×100(UInt16), accuracy×10(UInt8), fix_type, ts_ms_low32
- imuPacket(CMDeviceMotion) → 22 bytes LE: accel XYZ milli-g (already in g from CoreMotion),
gyro XYZ centi-deg/s (rad/s × 5729.578), mag XYZ μT, ts_ms_low32
- parseRanging(Data) → [AnchorInfo]: count byte + 9 bytes/anchor
(index, Int32-mm range, Int16×10 RSSI, UInt16 age_ms)
AnchorInfo.swift — anchor model with 3 s staleness check
BLEStatusView.swift — "BLE Tag" tab (3rd tab in ContentView):
- Connection card: state dot, peripheral name, Scan/Stop/Disconnect button
- GPS→Tag and IMU→Tag streaming toggles (5 Hz / 10 Hz rates shown)
- Anchor list matching SaltyTag UI: freshness dot, A{id} label, range, RSSI, STALE badge
Green label if <5 m, orange if ≥5 m, gray if stale
SensorManager:
- Owns BLEManager; observes connectionState via Combine → starts/stops BLE timers
- BLE GPS timer: 200 ms (5 Hz), sends lastKnownLocation via BLEPackets.gpsPacket
- BLE IMU timer: 100 ms (10 Hz), sends lastKnownMotion via BLEPackets.imuPacket
- lastKnownMotion updated from 100 Hz CMDeviceMotion callback
- ensureSensorsRunning() called on BLE connect (sensors start even without Follow-Me)
- Subscribes to saltybot/uwb/tag/position — Orin-fused phone absolute position
(robot RTK GPS + UWB tag offset = cm-accurate phone position)
Phone position source hierarchy (updateBestPhonePosition):
1. saltybot/uwb/tag/position fresh < 3 s → UWB authority (more accurate than phone GPS)
2. CoreLocation GPS fallback
- phonePositionSource: PhonePositionSource (.uwb(accuracyM) | .gps(accuracyM))
- userLocation always set to best source; MQTT publish to saltybot/ios/gps unchanged
MapContentView:
- positionSourceBadge (top-left, below UWB badge): "Position: UWB 2cm" or "Position: GPS 5m"
with waveform icon (UWB) or location icon (GPS)
Info.plist:
- NSBluetoothAlwaysUsageDescription added
- NSMotionUsageDescription updated (SAUL-T-MOTE branding)
- UIBackgroundModes: added bluetooth-central
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 12:22:17 -04:00
1d5f196e68
feat: Add UWB integration — follow mode, range presets, UWB status
...
UWBModels.swift (new):
- FollowMode enum: .gps / .uwb — publishes {"mode":"gps|uwb"} to saltybot/follow/mode
- FollowPreset enum: .close(1.5m) / .medium(3m) / .far(5m) — publishes
{"range_m":N,"preset":"..."} to saltybot/follow/range
- UWBPosition struct: x/y/z + timestamp
- UWBRange struct: anchorID + rangeMetres + timestamp
SensorManager:
- Subscribes to saltybot/uwb/range + saltybot/uwb/position on startStreaming
- handleUWBRange: updates uwbRanges[anchorID] (keyed dict)
- handleUWBPosition: updates uwbPosition + sets uwbActive=true
- UWB staleness watchdog (1Hz timer): clears uwbActive and prunes stale ranges >3s
- setFollowMode(_:) / setFollowPreset(_:): update state + publish to MQTT immediately
- Publishes current follow mode+range on connect
MapContentView:
- UWB status badge (top-left): green/gray dot, "UWB Active|Out of Range",
per-anchor range readouts (e.g. A1 2.34m)
- Follow mode segmented control: GPS | UWB
- Follow range segmented control: Close | Medium | Far (shows metres)
- MapCircle follow-range ring around robot: green inside, orange outside range
- Stats bar: distance turns green + checkmark when user is inside follow range;
UWB (x,y) coord shown when UWB mode active and position known
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 12:17:22 -04:00
0ad2b2f5c0
feat: Rename to SAUL-T-MOTE, add map with user + robot positions and follow path
...
Rename:
- CFBundleDisplayName = "SAUL-T-MOTE" in Info.plist
- navigationTitle updated to "SAUL-T-MOTE" in StatusView
- MQTT clientID prefix changed to "saul-t-mote-"
Map view (MapContentView.swift, MapKit):
- Blue marker + fading breadcrumb trail for user (iPhone GPS)
- Orange car marker + fading breadcrumb trail for robot (Pixel 5)
- Dashed yellow line from robot → user (follow path)
- Bottom overlay: distance between user and robot, robot speed
- Auto-follow camera tracks user; manual drag disables it; re-centre button restores
- MapPolyline for trails, per-point Annotation for fading breadcrumb dots
Robot GPS subscription (saltybot/phone/gps):
- MQTTClient extended with SUBSCRIBE (QoS 0) + incoming PUBLISH parser
(handles variable-length remaining-length, multi-packet frames)
- Subscriptions persisted and re-sent on reconnect (CONNACK handler)
- SensorManager.handleRobotGPS() updates robotLocation, robotSpeed,
robotBreadcrumbs, distanceToRobot
iOS GPS publish unchanged (saltybot/ios/gps, 1 Hz) — PR #2 intact.
ContentView restructured as TabView:
- Tab 1: Status (sensor rates, WS URL, follow-me button)
- Tab 2: Map
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 12:17:22 -04:00
f39b9d432d
feat: publish iOS GPS to MQTT topic saltybot/ios/gps at 1 Hz (Issue #681 )
...
Adds minimal MQTT 3.1.1 client (MQTTClient.swift) using Network.framework —
no external dependency. Implements CONNECT + PUBLISH (QoS 0) + PINGREQ keepalive.
- Broker: 192.168.87.29:1883 (user: mqtt_seb)
- Topic: saltybot/ios/gps
- Rate: 1 Hz Timer, decoupled from GPS update rate
- Payload matches sensor_dashboard.py format:
{ts, lat, lon, alt_m, accuracy_m, speed_ms, bearing_deg, provider: "gps"}
- lastKnownLocation cached from CLLocationManagerDelegate, published on timer
- MQTT connect/disconnect tied to startStreaming()/stopStreaming()
- ATS NSExceptionDomains extended to include 192.168.87.29 (MQTT broker LAN IP)
- MQTTClient.swift registered in project.pbxproj Sources build phase
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 12:17:22 -04:00
a7a4ed262a
fix: add ATS exception for Tailscale WebSocket (error -1022) (Issue #709 )
...
NSAllowsLocalNetworking covers CGNAT 100.64.0.0/10 range used by Tailscale,
fixing ATS blocking plain ws:// connections. Also adds NSExceptionDomains
entry for 100.64.0.2 as explicit fallback.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 12:17:22 -04:00
52c9e823ac
fix: switch WebSocket to Tailscale IP, add configurable Orin URL (Issue #709 )
...
- Default URL updated from ws://192.168.86.158:9090 (LAN) to ws://100.64.0.2:9090 (Tailscale)
- URL persisted in UserDefaults under key "orinURL" — survives app restarts
- WebSocketClient.url is now mutable so it can be updated without recreation
- SensorManager.updateURL(_:) applies a new URL when not streaming
- ContentView: editable text field for Orin address with Apply button, disabled while streaming
- Connection banner shows the active URL instead of hardcoded string
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 12:17:22 -04:00
baab4eaeb2
feat: iOS companion app - sensor streaming over WebSocket (Issue #709 )
...
- SulTee SwiftUI app targeting iOS 17+, iPhone 15 Pro
- CoreLocation: dual-frequency GPS (L1+L5) continuous updates, background mode enabled
- CoreMotion: 100 Hz IMU (accel + gyro + attitude + gravity), magnetometer via device motion
- CMAltimeter: barometer relative altitude + pressure streaming
- CLLocationManager heading updates for magnetometer heading
- URLSessionWebSocketTask client connecting to ws://192.168.86.158:9090
- JSON protocol: {type, timestamp, data} for gps/imu/heading/baro messages
- Auto-reconnect on disconnect (2s backoff)
- Haptic feedback on incoming "haptic" messages from bot
- Background streaming: UIBackgroundModes location + external-accessory in Info.plist
- SwiftUI status UI: connection banner, sensor rate counters (Hz), start/stop follow-me button
- Dev team Z37N597UWY (vayrette@gmail.com ), bundle ID com.saltylab.sultee
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 12:17:22 -04:00
d02d8b51a3
Initial commit
2026-04-03 16:46:32 -04:00