The previous post replaced step 13’s harmful fake-URL write with a WiFi configuration step that read three credential constants from the top of DebugChecklistView.swift. Partway through, I stopped and reconsidered: I didn’t want to hardcode my wifi password into a project I was planning to open source. What I actually needed was something like a .env file, or better: give the debug app the ability to connect the mirror to wifi itself, the same way the production app does.
Good instinct, in hindsight. This post is about why, and about the small SwiftUI sheet that took its place.
What was wrong with the constants
Three private let declarations at the top of the file:
private let TEST_WIFI_SSID: String = "" // e.g. "<my-home-wifi>"private let TEST_WIFI_SECURITY: String = "4" // 4 = WPA/WPA2 by defaultprivate let TEST_WIFI_PASSWORD: String = "" // your wifi passwordEmpty by default, // EDIT ME comments. As a quick way to get past the WiFi precondition, this works. As a pattern in a codebase that’s about to go public, it has at least four problems and probably more:
- The constants are committable. Even with the most careful workflow, eventually someone’s local copy gets pushed with credentials still in it. There’s no enforcement mechanism, just hope and
git diff. - Sample SSIDs leak network identifiers. The example value was my actual home network name. It was already showing up in step 14’s logs and in my blog drafts. Putting it into source as an example would have made it permanent.
- The “edit before running” workflow doesn’t survive a
git pull. Anyone using the project would have to re-edit after every update unless they kept their local changes carefully stashed. - It’s the wrong shape for the actual use case. I have a WiFi list I can pick from (step 14 emits it). The mirror knows the security type. The only thing source can’t reasonably know is the password. A picker plus a password field is shorter and clearer than three constants.
A .env-style approach (read credentials from a gitignored file) would address (1) but iOS apps don’t naturally have a place to drop a .env, and the workflow would require sideloading a file alongside the build. More overhead than necessary when the official app has already shown the right pattern: an in-app picker.
What the official app does
ScanInitiationActivity.java and the surrounding fragment classes show the OOBE flow at a high level: scan for available networks, present them to the user, take a password input, write to the mirror, watch for the connection state. The same protocol from NakedV1ConnectToNetworkOperation:
this.mBleManager.doWriteOperation( CHAR_CONFIG_WIFI_SET_SSID_UUID, // 62FD7453-… String.format("%s/%s/%s", ssid, securityType, password).getBytes(), callback);UTF-8 string, slash-separated, written to one characteristic. After the write, the mirror walks wifiConnectionStatus through its enum cases (INVALID(0), CONNECTED(1), CONNECTED_NO_INTERNET(2), CONFIGURED_BAD_AUTH(3), NOT_CONNECTED(4), NOT_CONFIGURED(5), CONNECTING(6)), and I watch.
sequenceDiagram
participant Phone
participant Mirror
Phone->>Mirror: write "ssid/security/password" to wifiSetSSID (62FD7453-…)
Mirror-->>Phone: notify wifiConnectionStatus = CONNECTING (6)
Note over Mirror: mirror attempts to join the network
Mirror-->>Phone: notify wifiConnectionStatus = CONNECTED (1) or CONFIGURED_BAD_AUTH (3)Mirroring this in the diagnostic was about a hundred lines of SwiftUI plus two new engine methods.
What’s in the sheet
A new toolbar button (a wifi SF Symbol) in DebugChecklistView opens a WifiSetupSheet:
- On first appear (and on a refresh button), call
engine.discoverWifiNetworks(). This is the same write-then-read-loop that step 14 uses, factored out so the sheet can refresh independently. Returns a[DiscoveredWifiNetwork]parsed from the mirror’s"index/SSID/security/signal"format. - Render a sortable list of networks (signal strength descending). Each row shows the SSID, the mirror’s security type integer, the mirror’s index for that network, and the signal as a percentage with a wifi-strength SF Symbol next to it.
- Tapping a row reveals a
Formwith aSecureFieldfor the password..autocorrectionDisabled(),.textInputAutocapitalization(.never), the standard pair of annotations that tell iOS “this is a credential, don’t help.” - A Connect button calls
engine.connectToWifi(ssid:security:password:). The result is one of a typed enum:
enum WifiConnectionResult { case connected case badAuth // explicit .configuredBadAuth case timedOut(lastSeen: WifiConnectionStatus?) case writeError(String) case missingChars case discoveryFailed(String)}The sheet renders the result inline: green status text on success, red error text naming the specific cause on failure. No state stored beyond the sheet’s own @State properties; close it and the password is gone.
The toolbar button is disabled when there’s no connected peripheral. The engine grew a new @Published var isPeripheralConnected flipped in didConnect / didDisconnect / reset() to drive that gating, since peripheral.state isn’t observable on its own.
What step 13 became
Step 13’s purpose has been revised three times across this project, and each revision has been a step toward the right shape:
- Original: “Write setScanToken (dry run)”, a write of a fake URL to
cloudTokento prove the BLE write path works. Side effect: triggersscanState=.error / scanError=.processorBusyevery run, poisoning the mirror’s state for step 15. Removed once it became clear the dry-run write was doing more harm than good. - Brief intermediate: “Configure WiFi (skipped unless TEST_WIFI_* set)”, the hardcoded-credentials version. The pivot away from this is what this post is about.
- Current: “WiFi connected”, a pure read of
wifiConnectionStatus. Pass if byte 1 (.connected), fail with a pointer to the toolbar button otherwise. No writes, no state changes, no credentials in source.
The general pattern: any diagnostic step that mutates state will eventually get in the way of subsequent steps. Read steps are safe to repeat, idempotent, and don’t require any external configuration. When the choice is “make this step do interesting work” versus “let you trigger interesting work elsewhere,” in a debug tool the second option is almost always cleaner.
What this build’s next log will show
With no source-side credentials, the diagnostic will not configure WiFi on its own. Step 13 will fail (.notConnected) and step 15 will fail at its own pre-flight WiFi check. That’s correct behavior: failing fast and pointing at the right action.
My flow now:
- Run All → step 13 fails with
.notConnected, the diagnostic stops at step 15 - Tap the WiFi button in the toolbar → the sheet opens
- Pick an SSID from the list, type the password, tap Connect
- Sheet shows polling progress and the final result inline
- Close the sheet, tap Run All again → step 13 should now pass (
.connected), step 15 should get past its WiFi precondition for the first time
Whether step 15 reaches .complete then depends on whatever the next unmet precondition is, probably the scale being on the floor in the cameras’ field of view rather than docked on USB-C charging. I’ll find out in the next log.
Takeaways
- Credentials in source is the wrong default for any project that might go public.
// EDIT MEplaceholders look harmless until they’re not. If you find yourself writing one, the better question is whether the value belongs at runtime instead of compile time. - Mirror the official client’s UX where you can. I’ve now applied this rule three times (to the BLE protocol map, to the scan-trigger sequence, and to WiFi setup). Each time it was a smaller change than building from intuition would have been, and the resulting behavior is closer to what the device expects to see.
- A debug tool’s UI doesn’t have to be elegant, but it does have to expose the operations the device requires. The WifiSetupSheet is intentionally plain: a list, a password field, a button, an inline result. No theming, no animations beyond what SwiftUI does for free. Total: about 140 lines of view code. Most of the value is in having the operation rather than how it looks.
- Read steps are safe to repeat. Write steps are radioactive after one use. Step 13’s three revisions all moved toward the same answer: a precondition check should read state, not change it. Anything that changes state should be triggered intentionally by the user, not as part of an automated walk through the checklist.
The next log will be the first one where I can configure WiFi without recompiling. If WiFi is the only remaining missing precondition, that log shows the mirror moving past alignScale for the first time and I get a scanState walk. If there’s still another precondition, the failure will be specific and I’ll get the next iteration’s hint from it directly.
Follow along
Procedure 7-AIf you want runtime WiFi setup on your own mirror client instead of hardcoding credentials:
- 1Don’t put your WiFi SSID or password in source, even as an empty “EDIT ME” placeholder. It will eventually get committed.
- 2Reuse the discovery flow your diagnostic already has (write
0x31to the action multiplexer, readwifiNumNetworks, loop-readwifiNetworkSSIDEnum) and expose it from a standalone method the UI can call directly. - 3Build a simple sheet: a list of discovered networks sorted by signal strength, a secure password field on tap, and a Connect button.
- 4Write
ssid/security/passwordas one slash-separated UTF-8 string to the WiFi-configuration characteristic (62FD7453-…), matching the official app’s format exactly. - 5Poll or subscribe to
wifiConnectionStatusand surface the specific enum value (connected, bad auth, not connected, and so on) rather than a generic success or failure. - 6Turn any diagnostic step that used to write a test value into a pure read of current state. Read steps are safe to repeat; write steps should be triggered explicitly, not as a side effect of running the checklist.
Agent prompts Procedure 7-A as prompts for a coding agent · 6 prompts
Paste into Claude Code or a similar agent, one at a time, from the root of your iOS project. Each prompt maps to the step with the same number above.