Repository navigation
feat/refactor-ab-connector - #25
Merged
Merged
Conversation
…Config.toString Adds SensorReading and pure parsers for every live-measurement format: the V1 ASCII line (AB2/AB3/Mini V1, ported from legacy ResponseParser) and the Mini V2 binary live + sync frames (names/unit match legacy V2). SessionConfig data classes printed the Wi-Fi password and auth token through toString(), and the session controller logs configs.
The controller was the legacy shape in Flow clothing: fire-and-forget dispatch(action) like EventBus.post, seven mutable fields written from several threads, and a hand-rolled reconnect loop with rules nobody had written down. AirBeamLink is the connection only: - suspend connect/configure/disconnect that return their outcome; configure hands back the device's ConfigResult instead of flattening it to HandshakeFailed - one job per link, launched in a process scope, so the connection outlives the screen that started it; connect/disconnect serialized - reconnect per legacy AirBeamReconnector: mobile sessions only, every 2 s, no cap, resume (not reconfigure) on success. Fixed sessions no longer resend Wi-Fi credentials on a drop - LinkState holds no connection handle and no session data - readings: Flow<SensorReading> that carries on across reconnects Connector seam: - connect returns ConnectResult instead of a fake already-failed connection; ConnectionStatus is the life of a live link only - AirBeamConnection gains device, readings, resumeMobileSession - BLE: per-link work runs in the scope Kable's connect() returns, Peripheral is close()d, MTU requested on Android (517 V1 / 247 V2), V2 replies are awaited before the write so none is missed, observe failures are logged instead of escaping - Classic: lines parsed into readings; the 30 s timeout now actually aborts the blocking socket connect - Composite: an unsupported transport is a Failed result, not a crash AirBeamForegroundService follows LinkState and is START_NOT_STICKY: a restarted process has nothing to restore the link from.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
AirBeamSessionControlleris gone. In its place isAirBeamLink, a connection-only owner with suspend calls, and a connector seam that finally delivers measurements, so the RecordingEngine has a single stream to save. All of it is onfeat/currin three commits, and both Android and iOS build.b76101aSensorReading, V1 ASCII + Mini V2 binary parsers, credentials kept out ofSessionConfig.toString()2d60399AirBeamLinkreplaces the controller; connector seam reworked (BLE, classic, composite); foreground service followsLinkStateb0d8d95/docs/: legacy reconnect rules and live-reading formats recordedThe developer did not hand-copy this code: they asked me to do the work myself. The CLAUDE.md teaching-mode rule was set aside for this task, and the code should get a read-through review.
What was wrong
The nudge was right. The old controller rebuilt the legacy app's EventBus and listener shape on top of Flow, and the connector had real bugs underneath it.
dispatch(SessionAction): fire-and-forget, wrong-state actions silently ignoredEventBus.post; callers couldn't learn the outcomeDispatchers.DefaultjobsConnectedcould overwriteIdleafter disconnectConnectedtoo; re-ranconfigure()startTimeMsreset on each reconnectConfigResultbecameHandshakeFailedtoString()of configs loggedSessionState.Recordingwithout a session or measurements; liveAirBeamConnectioninside UI stateSessionStatus; resource handle leaking to UICoroutineScopes never tied to the link; KablePeripheral.close()never called; failed attempts not releasedReady/Nackcould be missedwithTimeoutaround blockingsocket.connect()error()for an unsupported transportSTART_STICKYwith no state restore; never startedNew design
AirBeamLinkowns the one AirBeam connection for the life of the process, and it does connection work only. Sessions, their status and their data stay with the recording side.[embedded content: AirBeam layers after the refactor]
Arrows show who calls whom. Readings travel up the same path, and
AirBeamLink.readingskeeps flowing across reconnects.connect()returnsConnectedorFailed(reason).configure()returns the device's ownConfigResult, addingNotConnectedwhen there is no ready link.disconnect()ends the link and any reconnect loop.connectanddisconnectare serialized by aMutex, and each cancels and joins the previous job. Because the job lives in the link's scope, a screen that leaves doesn't cancel its connect.LinkStatecarries the target and the failure reason, nothing else, so it is safe to expose to UI.connectreturnsConnectResultinstead of a fake connection that has already failed.ConnectionStatuscovers one live link's life:Ready,DisconnectedorDisconnectedUnexpectedly.AirBeamConnection.readings: Flow<SensorReading>is buffered from connect on, so nothing is lost before the engine starts collecting. For Mini V2 it carries the sync backlog too, which gives the engine one save path.connect()returns. The drop watcher runs inperipheral.scope, and teardown callsclose().Business rules taken from legacy
Every rule below comes from the legacy app, not from invention. They are now recorded in
docs/DOMAIN_KNOWLEDGE.md§2 step 6 and §3.AirBeamLink.reconnectAirBeamService.onMessageEvent,AirBeamReconnectorresumeMobileSessionreconnectMobileSession()ContinueSessionifHasSavedSession, nothing ifRunningBleConnection.resumeMobileSessionAirBeamMiniV2Configurator.reconnectMobileSessionAirBeamProtocol.Standard.parseReadingResponseParser,SyncableAirBeamReaderffe1 ffe3 ffe4 ffe5 ffe6, Mini V1ffe4 ffe5readingSourcesAirBeam3Configurator,AirBeamMiniConfiguratorAirBeamMini-PM1/AirBeamMini-PM2.5,µg/m³, device timeAirBeamProtocol.MiniV2.parseMeasurementpostLiveMeasurementEventsrequestMtuIfSupportedSyncableAirBeamConfigurator.MAX_MTU, V2 configuratorOne deliberate split: "stop" and "standalone mode" are both
link.disconnect(). Whether the session becomesFINISHEDor staysDISCONNECTEDis the RecordingEngine's call.Verification
The whole shared suite passes, the Android app assembles, and the iOS simulator target compiles. None of this has been run on a real AirBeam yet.
ReadingParserTest(new)SessionConfigTest(new, credential redaction)AirBeamLinkTest(new, replaces the controller test)ScanViewModelTest,CompositeAirBeamConnectorTest(ported):shared:testAndroidHostTest:androidApp:assembleDebug,:shared:compileKotlinIosSimulatorArm64The link tests run on virtual time in
backgroundScope, so the 2 s interval and the no-cap rule (51 attempts) are checked deterministically.The BLE and classic connectors have no unit tests. They wrap Kable and Android sockets, so they need hardware to test.