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>
118 lines
3.5 KiB
Swift
118 lines
3.5 KiB
Swift
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, 0–360
|
||
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
|
||
]}
|
||
]
|
||
}
|
||
}
|