Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 33 additions & 12 deletions apps/ios/Origin89/SetupView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -499,8 +499,12 @@ private struct OpenWindowView: View {
)
}
Origin89Notice(
"Press the pairing button on the controller's panel. The window stays open for 120 seconds, and pairing must finish inside it."
"Press the pairing button on the controller's panel. A controller no phone has paired with also opens it for 120 seconds after it powers on. The window stays open for 120 seconds, and pairing must finish inside it."
)
Text(
"This phone connects over Bluetooth once the window is open. The controller's Wi-Fi is off until pairing ends."
)
.foregroundStyle(.secondary)
Button("The window is open, pair now", action: opened)
.buttonStyle(.borderedProminent)
Spacer()
Expand Down Expand Up @@ -561,13 +565,23 @@ private struct WrittenView: View {
var body: some View {
Form {
Section {
Text("Network settings saved")
Origin89Status("Saved", tone: .nominal)
Text(
flow.reportsWiFi
? "The controller accepted version \(version) and passes it to its radio. It may drop the Bluetooth connection while it joins the network. Once it joins, this phone continues over Wi-Fi if it is on the same network."
: "The controller accepted version \(version) and passes it to its radio. Watch the module join the network."
)
if flow.joinsHeldNetwork {
Text("Network settings kept")
Origin89Status("Kept", tone: .nominal)
Text(
"The controller already holds \(flow.network?.ssid ?? "a network") and joins it now that pairing ended. It may drop the Bluetooth connection while it joins. Once it joins, this phone continues over Wi-Fi if it is on the same network."
)
} else {
Text("Network settings saved")
Origin89Status("Saved", tone: .nominal)
Text(
flow.reportsWiFi
? "The controller accepted version \(version) and passes it to its radio. It may drop the Bluetooth connection while it joins the network. Once it joins, this phone continues over Wi-Fi if it is on the same network."
: "The controller accepted version \(version) and passes it to its radio. Watch the module join the network."
)
}
Button("Choose another network") { Task { await flow.changeNetwork() } }
.disabled(flow.isSwitchingToWiFi)
}
if flow.reportsWiFi { JoinSection(join: flow.join, flow: flow) }
LinkSection(flow: flow)
Expand Down Expand Up @@ -649,10 +663,17 @@ private struct JoinSection: View {
Button("Check again") { Task { await flow.watchJoinAgain() } }
case .connectionLost:
Origin89Status("Unknown", tone: .warning)
Text(
"The Bluetooth connection ended before the controller said whether it joined. The network settings are saved."
)
Button("Check again") { Task { await flow.watchJoinAgain() } }
if let unavailable = flow.wifiUnavailable {
Text(
"The Bluetooth connection ended before the controller said whether it joined, and this phone did not find it on Wi-Fi within 30 seconds. The network settings are saved."
)
Origin89Notice(unavailable.reason)
} else {
Text(
"The Bluetooth connection ended before the controller said whether it joined. The network settings are saved."
)
}
Button("Check again over Wi-Fi or Bluetooth") { Task { await flow.watchJoinAgain() } }
}
} header: {
Text("Wi-Fi")
Expand Down
68 changes: 60 additions & 8 deletions apps/ios/SetupBench/Sources/SetupBench/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,19 @@ import SetupKit
let usage = """
usage: setup-bench <command> [options]

pair [--window-open] Pair with the controller whose code is in $ORIGIN89_SETUP_CODE
pair [--window-open] [--ssid NAME] [--watch SECONDS]
Pair with the controller whose code is in $ORIGIN89_SETUP_CODE
and read its network. Without --window-open it waits for Enter
once the pairing window is open on the panel.
once the pairing window is open on the panel. A controller that
holds a network joins it: watch that (default 60 s). With
--ssid, a controller without one gets that network written on
the pairing connection, as write does.
read Continue from the kept enrolment and read the network section.
scan Read, then run a Wi-Fi scan to its end.
write --ssid NAME [--country CC] [--hostname NAME] [--watch SECONDS]
Read, then write the network with the passphrase in
$ORIGIN89_WIFI_PASSPHRASE, and watch the join (default 60 s).
clear Read, then write no network, keeping the country and hostname.
hold --seconds N Read, stay connected and idle for N seconds, then send a request.

options:
Expand Down Expand Up @@ -260,6 +265,7 @@ func label(_ state: SetupFlow.State) -> String {
await flow.reset()
case "scan": try await scan()
case "write": try await write()
case "clear": try await clear()
case "hold": try await hold()
case "help": print(usage)
default: throw BenchError(usage)
Expand All @@ -282,12 +288,27 @@ func label(_ state: SetupFlow.State) -> String {
}
await flow.confirmWindowOpened()
}
guard case .editingNetwork(let settings) = flow.state, let deviceID = flow.controller?.deviceID
else { throw BenchError("pairing stopped: \(label(flow.state))") }
guard let deviceID = flow.controller?.deviceID, let settings = flow.network else {
throw BenchError("pairing stopped: \(label(flow.state))")
}
switch flow.state {
case .editingNetwork, .written: break
default: throw BenchError("pairing stopped: \(label(flow.state))")
}
try BenchFiles.ensureDirectory()
try Data(deviceID.utf8).write(to: BenchFiles.lastDevice, options: [.atomic])
timeline.say("paired with \(deviceID)")
report(settings)
// On the pairing connection: a new one may not be accepted once the window closes.
if case .editingNetwork = flow.state, let ssid = options["ssid"] {
try await write(ssid, flow, settings)
return
}
// A controller holding a network starts its station once Pair closes the window.
if flow.joinsHeldNetwork {
timeline.say("the controller holds a network: watching it join")
await watchJoin(flow, for: try options.seconds("watch", default: 60))
}
await flow.reset()
}

Expand Down Expand Up @@ -338,11 +359,16 @@ func label(_ state: SetupFlow.State) -> String {

func write() async throws {
guard let ssid = options["ssid"] else { throw BenchError("write needs --ssid") }
let (flow, settings) = try await resumed()
report(settings)
try await write(ssid, flow, settings)
}

/// Write `ssid` on the open session, watch the join, then end the session.
func write(_ ssid: String, _ flow: SetupFlow, _ settings: NetworkSettings) async throws {
let name = options["passphrase-env"] ?? "ORIGIN89_WIFI_PASSPHRASE"
let passphrase = BenchFiles.secret(name, file: "wifi-passphrase")
let watchFor = try options.seconds("watch", default: 60)
let (flow, settings) = try await resumed()
report(settings)
var draft = NetworkDraft(
ssid: ssid, settings: settings, region: Locale.current.region?.identifier)
draft.passphrase = passphrase ?? ""
Expand All @@ -359,18 +385,44 @@ func label(_ state: SetupFlow.State) -> String {
await flow.reset()
throw BenchError("write stopped")
}
await watch(flow, for: watchFor) { flow in
await watchJoin(flow, for: watchFor)
await flow.reset()
}

/// Follow the join and the move to Wi-Fi until either has an outcome.
func watchJoin(_ flow: SetupFlow, for limit: Duration) async {
await watch(flow, for: limit) { flow in
if case .failed = flow.state { return true }
switch flow.join {
// A join is followed by the switch to Wi-Fi.
case .joined:
return options.has("bluetooth-only")
|| (!flow.isSwitchingToWiFi && (flow.link != .bluetooth || flow.wifiUnavailable != nil))
case .failed, .noAnswer, .connectionLost: return true
case .failed, .noAnswer: return true
// A Wi-Fi search may follow a lost Bluetooth link.
case .connectionLost: return !flow.isSwitchingToWiFi
case .idle, .waiting: return false
}
}
timeline.say("final state \(label(flow.state)), join \(flow.join), link \(link(flow))")
}

/// Leave the controller with no network, as a unit that was never set up.
func clear() async throws {
let (flow, settings) = try await resumed()
report(settings)
guard let country = settings.country, let hostname = settings.hostname else {
await flow.reset()
throw BenchError("not cleared: the controller holds no country or hostname to keep")
}
timeline.say("clearing the network")
await flow.writeNetwork(
NetworkChange(ssid: nil, passphrase: nil, country: country, hostname: hostname))
timeline.say("state \(label(flow.state))")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
guard case .written = flow.state else {
await flow.reset()
throw BenchError("not cleared")
}
await flow.reset()
}

Expand Down
Loading