From 9ce630b97c171ce9bff8ec3fb62181b89781ead2 Mon Sep 17 00:00:00 2001 From: sl-ios Date: Fri, 3 Apr 2026 17:24:41 -0400 Subject: [PATCH] fix: switch WebSocket to Tailscale IP, add configurable Orin URL (Issue #709) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- SulTee/SulTee/ContentView.swift | 36 ++++++++++++++++++++++++++++- SulTee/SulTee/SensorManager.swift | 23 +++++++++++++++++- SulTee/SulTee/WebSocketClient.swift | 2 +- 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/SulTee/SulTee/ContentView.swift b/SulTee/SulTee/ContentView.swift index 14741c8..c9b1ad0 100644 --- a/SulTee/SulTee/ContentView.swift +++ b/SulTee/SulTee/ContentView.swift @@ -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" } diff --git a/SulTee/SulTee/SensorManager.swift b/SulTee/SulTee/SensorManager.swift index 654cb77..70531e2 100644 --- a/SulTee/SulTee/SensorManager.swift +++ b/SulTee/SulTee/SensorManager.swift @@ -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() { diff --git a/SulTee/SulTee/WebSocketClient.swift b/SulTee/SulTee/WebSocketClient.swift index d898d5c..5c8e2e9 100644 --- a/SulTee/SulTee/WebSocketClient.swift +++ b/SulTee/SulTee/WebSocketClient.swift @@ -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