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 50096707f3
commit 433d85754f
3 changed files with 58 additions and 3 deletions

View File

@ -2,11 +2,15 @@ import SwiftUI
struct ContentView: View { struct ContentView: View {
@EnvironmentObject var sensor: SensorManager @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 { var body: some View {
NavigationStack { NavigationStack {
VStack(spacing: 24) { VStack(spacing: 24) {
connectionBanner connectionBanner
orinURLField
Divider() Divider()
sensorRatesGrid sensorRatesGrid
Divider() Divider()
@ -20,11 +24,41 @@ struct ContentView: View {
} }
.padding() .padding()
.navigationTitle("Sul-Tee") .navigationTitle("Sul-Tee")
.onAppear { editingURL = orinURL }
} }
} }
// MARK: - Subviews // 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 { private var connectionBanner: some View {
HStack(spacing: 12) { HStack(spacing: 12) {
Circle() Circle()
@ -102,7 +136,7 @@ struct ContentView: View {
private var wsLabel: String { private var wsLabel: String {
switch sensor.wsState { switch sensor.wsState {
case .connected: return "Connected — ws://192.168.86.158:9090" case .connected: return "Connected — \(orinURL)"
case .connecting: return "Connecting…" case .connecting: return "Connecting…"
case .disconnected: return "Disconnected" case .disconnected: return "Disconnected"
} }

View File

@ -18,7 +18,16 @@ final class SensorManager: NSObject, ObservableObject {
// MARK: - Dependencies // 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 locationManager = CLLocationManager()
private let motionManager = CMMotionManager() private let motionManager = CMMotionManager()
private let altimeter = CMAltimeter() private let altimeter = CMAltimeter()
@ -35,6 +44,8 @@ final class SensorManager: NSObject, ObservableObject {
// MARK: - Init // MARK: - Init
override 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() super.init()
locationManager.delegate = self locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
@ -69,6 +80,16 @@ final class SensorManager: NSObject, ObservableObject {
rateTimer = nil 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 // MARK: - Sensor start / stop
private func requestPermissionsAndStartSensors() { private func requestPermissionsAndStartSensors() {

View File

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