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>
This commit is contained in:
sl-ios 2026-04-03 17:24:41 -04:00
parent bd36370e5a
commit 1e7a512c89
3 changed files with 58 additions and 3 deletions

View File

@ -2,11 +2,15 @@ import SwiftUI
struct ContentView: View {
@EnvironmentObject var sensor: SensorManager
@AppStorage("orinURL") private var orinURL: String = SensorManager.defaultOrinURL
@State private var editingURL: String = ""
@FocusState private var urlFieldFocused: Bool
var body: some View {
NavigationStack {
VStack(spacing: 24) {
connectionBanner
orinURLField
Divider()
sensorRatesGrid
Divider()
@ -20,11 +24,41 @@ struct ContentView: View {
}
.padding()
.navigationTitle("Sul-Tee")
.onAppear { editingURL = orinURL }
}
}
// MARK: - Subviews
private var orinURLField: some View {
VStack(alignment: .leading, spacing: 4) {
Text("Orin WebSocket URL")
.font(.caption)
.foregroundStyle(.secondary)
HStack {
TextField("ws://host:port", text: $editingURL)
.textFieldStyle(.roundedBorder)
.keyboardType(.URL)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.focused($urlFieldFocused)
.disabled(sensor.isStreaming)
.onSubmit { applyURL() }
if !sensor.isStreaming {
Button("Apply") { applyURL() }
.buttonStyle(.borderedProminent)
.controlSize(.small)
}
}
}
}
private func applyURL() {
urlFieldFocused = false
orinURL = editingURL
sensor.updateURL(editingURL)
}
private var connectionBanner: some View {
HStack(spacing: 12) {
Circle()
@ -102,7 +136,7 @@ struct ContentView: View {
private var wsLabel: String {
switch sensor.wsState {
case .connected: return "Connected — ws://192.168.86.158:9090"
case .connected: return "Connected — \(orinURL)"
case .connecting: return "Connecting…"
case .disconnected: return "Disconnected"
}

View File

@ -18,7 +18,16 @@ final class SensorManager: NSObject, ObservableObject {
// MARK: - Dependencies
private let ws = WebSocketClient(url: URL(string: "ws://192.168.86.158:9090")!)
static let defaultOrinURL = "ws://100.64.0.2:9090"
private static let orinURLKey = "orinURL"
private(set) var ws: WebSocketClient
/// Current Orin WebSocket URL string (persisted in UserDefaults).
var orinURLString: String {
get { UserDefaults.standard.string(forKey: Self.orinURLKey) ?? Self.defaultOrinURL }
set { UserDefaults.standard.set(newValue, forKey: Self.orinURLKey) }
}
private let locationManager = CLLocationManager()
private let motionManager = CMMotionManager()
private let altimeter = CMAltimeter()
@ -35,6 +44,8 @@ final class SensorManager: NSObject, ObservableObject {
// MARK: - Init
override init() {
let urlString = UserDefaults.standard.string(forKey: Self.orinURLKey) ?? Self.defaultOrinURL
self.ws = WebSocketClient(url: URL(string: urlString) ?? URL(string: Self.defaultOrinURL)!)
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
@ -69,6 +80,16 @@ final class SensorManager: NSObject, ObservableObject {
rateTimer = nil
}
/// Call when the user edits the Orin URL. Persists the value and updates
/// the client URL; takes effect on the next connect().
func updateURL(_ urlString: String) {
guard !isStreaming else { return }
orinURLString = urlString
if let url = URL(string: urlString), url.scheme?.hasPrefix("ws") == true {
ws.url = url
}
}
// MARK: - Sensor start / stop
private func requestPermissionsAndStartSensors() {

View File

@ -11,7 +11,7 @@ final class WebSocketClient: NSObject, ObservableObject {
@Published var state: ConnectionState = .disconnected
private let url: URL
var url: URL
private var session: URLSession!
private var task: URLSessionWebSocketTask?
private var shouldRun = false