saltylab-ios/SulTee/SulTee/RouteModels.swift
sl-ios cd90d6dbee feat: Phase 1 — Route Recording with waypoints (GPS track at 1Hz)
New 'Routes' tab added to SAUL-T-MOTE:

RECORDING
- Record button starts 1Hz GPS capture (lat/lon/alt/speed/bearing/ts)
- Live stats bar: elapsed time, point count, distance, waypoint count
- Live map shows recorded polyline + waypoint annotations in real-time
- 'Add Waypoint' sheet: label + robot action (none/stop/slow/photo)
- 'Stop' ends recording → Save sheet to name the route

STORAGE
- JSON files in app Documents/routes/<uuid>.json
- RouteStore: save/rename/delete; auto-sorts newest first
- Route list with duration, distance, waypoint count

MQTT FORMAT DEFINED (Phase 3 playback — robot side TBD)
- Topic: saltybot/route/command
- Payload: {action, route_id, route_name, points:[{lat,lon,alt,speed,bearing,ts}],
            waypoints:[{lat,lon,alt,ts,label,action}]}

New files: RouteModels.swift, RouteStore.swift, RouteRecorder.swift, RoutesView.swift
SensorManager: lastKnownLocation promoted to private(set) for recorder access

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 18:58:33 -04:00

118 lines
3.5 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Foundation
import CoreLocation
// MARK: - Route data types
/// A single GPS sample recorded at 1 Hz.
struct RoutePoint: Codable {
let latitude: Double
let longitude: Double
let altitude: Double
let speed: Double // m/s
let bearing: Double // degrees, 0360
let timestamp: Double // Unix epoch seconds
}
/// A named marker added by the user during recording.
struct Waypoint: Codable, Identifiable {
let id: UUID
let latitude: Double
let longitude: Double
let altitude: Double
let timestamp: Double
var label: String
var action: WaypointAction
init(location: CLLocation, label: String, action: WaypointAction = .none) {
self.id = UUID()
self.latitude = location.coordinate.latitude
self.longitude = location.coordinate.longitude
self.altitude = location.altitude
self.timestamp = location.timestamp.timeIntervalSince1970
self.label = label
self.action = action
}
}
enum WaypointAction: String, Codable, CaseIterable, Identifiable {
case none = "none"
case stop = "stop"
case slow = "slow"
case photo = "photo"
var id: String { rawValue }
var displayName: String {
switch self {
case .none: return "Marker"
case .stop: return "Stop"
case .slow: return "Slow down"
case .photo: return "Take photo"
}
}
var systemImage: String {
switch self {
case .none: return "mappin"
case .stop: return "stop.circle"
case .slow: return "tortoise"
case .photo: return "camera"
}
}
}
/// A complete recorded route persisted to disk.
struct SavedRoute: Codable, Identifiable {
let id: UUID
var name: String
let date: Date
var points: [RoutePoint]
var waypoints: [Waypoint]
/// Total elapsed recording time in seconds.
var durationSeconds: Double {
guard let first = points.first, let last = points.last else { return 0 }
return last.timestamp - first.timestamp
}
/// Approximate distance in metres (sum of point-to-point segments).
var distanceMetres: Double {
var total = 0.0
for i in 1..<points.count {
let a = CLLocation(latitude: points[i-1].latitude, longitude: points[i-1].longitude)
let b = CLLocation(latitude: points[i].latitude, longitude: points[i].longitude)
total += a.distance(from: b)
}
return total
}
// MARK: - MQTT payload for robot route-following (Phase 3)
//
// Topic: saltybot/route/command
// {
// "action": "start",
// "route_id": "<uuid>",
// "route_name":"<name>",
// "points": [{"lat","lon","alt","speed","bearing","ts"}, ...],
// "waypoints": [{"lat","lon","alt","ts","label","action"}, ...]
// }
func mqttPayload(action: String = "start") -> [String: Any] {
[
"action": action,
"route_id": id.uuidString,
"route_name": name,
"points": points.map {[
"lat": $0.latitude, "lon": $0.longitude,
"alt": $0.altitude, "speed": $0.speed,
"bearing": $0.bearing, "ts": $0.timestamp
]},
"waypoints": waypoints.map {[
"lat": $0.latitude, "lon": $0.longitude,
"alt": $0.altitude, "ts": $0.timestamp,
"label": $0.label, "action": $0.action.rawValue
]}
]
}
}