feat: UWB integration — follow mode selector, range presets, UWB status badge #4

Open
sl-ios wants to merge 6 commits from sl-ios/uwb-integration into main
Collaborator

Summary

Adds full UWB integration to the SAUL-T-MOTE map view: live UWB anchor ranges, follow mode switching (GPS/UWB), follow distance presets, and a range circle on the map.

MQTT topics

Direction Topic Payload
Subscribe saltybot/uwb/range {"anchor_id":"A1","range_m":2.34,"ts":...}
Subscribe saltybot/uwb/position {"x":1.2,"y":3.4,"z":0.0,"ts":...}
Publish saltybot/follow/mode {"mode":"gps"} or {"mode":"uwb"}
Publish saltybot/follow/range {"range_m":3.0,"preset":"medium"}

UWB status badge (map, top-left)

  • Green dot "UWB Active" when saltybot/uwb/position received within 3s
  • Gray dot "UWB Out of Range" when stale
  • Per-anchor range readouts: A1 2.34m A2 4.10m

Follow mode segmented control

  • GPS — robot follows user GPS (default)
  • UWB — robot follows UWB tag (cm-precision indoors)
  • Selection published to saltybot/follow/mode on change and on connect

Follow range presets

  • Close 1.5 m · Medium 3.0 m (default) · Far 5.0 m
  • Published to saltybot/follow/range on change and on connect

Map additions

  • MapCircle follow-range ring around robot: green (user inside), orange (user outside)
  • Stats bar: distance turns green + checkmark when inside follow range
  • UWB (x, y) local coordinate shown in stats bar when UWB mode is active

Test plan

  • Deploy, tap Start Follow-Me
  • Verify saltybot/follow/mode + saltybot/follow/range published on connect
  • mosquitto_pub … -t saltybot/uwb/position -m '{"x":1.2,"y":3.4,"z":0}' → green UWB badge
  • Wait 3s without publishing → badge turns gray (staleness watchdog)
  • mosquitto_pub … -t saltybot/uwb/range -m '{"anchor_id":"A1","range_m":2.1}' → A1 range shown
  • Switch Follow Mode to UWB → mode published; UWB (x,y) appears in stats bar
  • Change range to Close → 1.5m circle on map, published to saltybot/follow/range
  • Move inside 1.5m of robot → circle turns green, stats bar distance turns green

🤖 Generated with Claude Code

## Summary Adds full UWB integration to the SAUL-T-MOTE map view: live UWB anchor ranges, follow mode switching (GPS/UWB), follow distance presets, and a range circle on the map. ## MQTT topics | Direction | Topic | Payload | |---|---|---| | Subscribe | `saltybot/uwb/range` | `{"anchor_id":"A1","range_m":2.34,"ts":...}` | | Subscribe | `saltybot/uwb/position` | `{"x":1.2,"y":3.4,"z":0.0,"ts":...}` | | Publish | `saltybot/follow/mode` | `{"mode":"gps"}` or `{"mode":"uwb"}` | | Publish | `saltybot/follow/range` | `{"range_m":3.0,"preset":"medium"}` | ## UWB status badge (map, top-left) - **Green dot** "UWB Active" when `saltybot/uwb/position` received within 3s - **Gray dot** "UWB Out of Range" when stale - Per-anchor range readouts: `A1 2.34m A2 4.10m` ## Follow mode segmented control - **GPS** — robot follows user GPS (default) - **UWB** — robot follows UWB tag (cm-precision indoors) - Selection published to `saltybot/follow/mode` on change and on connect ## Follow range presets - **Close** 1.5 m · **Medium** 3.0 m (default) · **Far** 5.0 m - Published to `saltybot/follow/range` on change and on connect ## Map additions - `MapCircle` follow-range ring around robot: **green** (user inside), **orange** (user outside) - Stats bar: distance turns **green + checkmark** when inside follow range - UWB `(x, y)` local coordinate shown in stats bar when UWB mode is active ## Test plan - [ ] Deploy, tap **Start Follow-Me** - [ ] Verify `saltybot/follow/mode` + `saltybot/follow/range` published on connect - [ ] `mosquitto_pub … -t saltybot/uwb/position -m '{"x":1.2,"y":3.4,"z":0}'` → green UWB badge - [ ] Wait 3s without publishing → badge turns gray (staleness watchdog) - [ ] `mosquitto_pub … -t saltybot/uwb/range -m '{"anchor_id":"A1","range_m":2.1}'` → A1 range shown - [ ] Switch Follow Mode to UWB → mode published; UWB (x,y) appears in stats bar - [ ] Change range to Close → 1.5m circle on map, published to `saltybot/follow/range` - [ ] Move inside 1.5m of robot → circle turns green, stats bar distance turns green 🤖 Generated with [Claude Code](https://claude.com/claude-code)
sl-ios added 6 commits 2026-04-04 12:12:41 -04:00
- 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>
- 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>
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>
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>
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>
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>
This pull request can be merged automatically.
You are not authorized to merge this pull request.

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin sl-ios/uwb-integration:sl-ios/uwb-integration
git checkout sl-ios/uwb-integration
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: seb/saltylab-ios#4
No description provided.