Keep the machine connection alive across system standby - #2225
Open
edwin-truthsearch-io wants to merge 1 commit into
Open
edwin-truthsearch-io wants to merge 1 commit into
edwin-truthsearch-io wants to merge 1 commit into
Conversation
edwin-truthsearch-io
force-pushed
the
macos-connection-robustness
branch
from
August 27, 2026 17:19
84f0a45 to
32cf476
Compare
1 task
edwin-truthsearch-io
force-pushed
the
macos-connection-robustness
branch
from
August 27, 2026 17:37
32cf476 to
ad35ac3
Compare
Open
4 tasks
A system standby tears down the USB/Bluetooth link to the machine. Machines watching the communication (like Kaleido) react on that connection loss, on a laptop typically towards the end of a roast when the user did not touch the keyboard for a while. - adds artisanlib/power.py with a SleepInhibitor preventing the idle system sleep while Artisan is ON (macOS: NSProcessInfo activity, which also disables App Nap and the timer coalescing that delays sampling while the display is off, with an IOKit power assertion as fallback; Windows: SetThreadExecutionState; Linux: systemd-inhibit). The display is still allowed to sleep. - adds the 'Prevent Sleep' flag (default ON) to the Sampling dialog - adds a WakeDetector recognizing system suspensions that cannot be inhibited (eg. a laptop closing its lid) and reconnects the machine on wake via the new AsyncComm.reconnect()/KaleidoPort.reconnect(), both sharing the new force_reconnect(), as a connection established before a suspension can be dead without the transport ever reporting an error - reports a lost connection only once instead of on every failing reconnect - logs the connection lifecycle (connect, connected, connection lost, timeout, serial exception) on the default log level to ease the analysis of connection issues Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
edwin-truthsearch-io
force-pushed
the
macos-connection-robustness
branch
from
August 27, 2026 18:09
ad35ac3 to
5ddd533
Compare
Open
1 task
This was referenced Sep 6, 2026
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.
Addresses #2226. Note that this PR fixes the standby part of that report, not the second aspect described there — a Bluetooth serial device node that no longer exists after the link was torn down cannot be re-opened by Artisan — so the issue should stay open after a merge.
Related to #1705 (macOS Bluetooth SPP connection loss with Kaleido machines). That report is about not being able to reconnect between roasts, which this PR does not claim to fix; it addresses the neighbouring symptom of the link being torn down during a roast, and the forced reconnect and the connection logging added here should help with the analysis of that report too.
Problem
On a laptop the machine connection is lost towards the end of a roast. macOS enters idle standby after the user did not touch the keyboard for a while (the roast is running, but Artisan generates no input events), which tears down the USB/Bluetooth link. Machines that watch the communication react on that connection loss — on a Kaleido connected over Bluetooth serial the machine drops out of the roast into cooling, which ruins the batch.
Nothing in Artisan inhibited the system sleep so far. The only power related call is
appnope.nope()at startup, which usesNSActivityUserInitiatedAllowingIdleSystemSleepand thus explicitly permits the idle system sleep.What changed
artisanlib/power.py(new)SleepInhibitorprevents the idle system sleep while Artisan is ON:NSProcessInfoactivity withNSActivityUserInitiated | NSActivityLatencyCritical, which also disables App Nap and the timer coalescing that otherwise delays the sampling of an app whose display is asleep. Falls back to anIOPMAssertionCreateWithName(PreventUserIdleSystemSleep)assertion via ctypes if pyobjc is unavailable (running from source).SetThreadExecutionState(ES_CONTINUOUS|ES_SYSTEM_REQUIRED|ES_AWAYMODE_REQUIRED), retried without away mode if unsupported.systemd-inhibit --what=idle:sleep.WakeDetectorrecognizes a system suspension that could not be inhibited (a laptop closing its lid, or an explicit sleep) by the divergence of the wall clock and the monotonic clock.Wiring (
canvas.py): acquired inOnMonitor, released inOffMonitorCloseDown. A 5sQTimerruns the wake detection while sampling; on wake Artisan logs the suspension and reconnects the machine.Reconnect on wake:
AsyncComm.reconnect()andKaleidoPort.reconnect()close the current writer through the asyncio loop so the existing connect loop re-establishes the connection right away (~0.5–1s, re-sending Kaleido'sSC ARguard). This matters because a connection established before a suspension can be dead without the transport ever reporting an error:AsyncComm.handle_reads()loops onwhile not reader.at_eof()with no timeout, so such a connection could hang forever. The classic pyserial devices re-open their port on the next failing read and the BLE devices reconnect via their disconnect callbacks, so both are deliberately left alone.Setting: a
Prevent Sleepflag in the Sampling dialog, default ON, persisted asKeepAwake, applied immediately when toggled while ON.Connection logging: the connect/connected/connection-lost/timeout messages of the Kaleido and AsyncComm transports were all
_log.debugwhile theartisanlibfile handler logs at INFO. Raw exceptions do reachartisan.log(via the generic_log.error(e)of the connect loops), but there is no record of when a connection was established or lost, which makes it hard to tell a failed reconnect apart from a connection that never came up. Those lifecycle messages are now INFO/WARNING, and theSerialExceptionthatAsyncComm.connect()swallowed silently is logged too. Repeated retries do not flood the log —DuplicateFiltercollapses them.Fix: a lost connection is now reported once instead of on every failing reconnect attempt (the Kaleido serial loop emitted
Kaleido disconnectedevery 0.5s while the machine was unreachable;AsyncCommnever reset itswas_connectedflag although its comment says it reports ONE message).Verification
test_power.pyplus additions totest_canvas.py,test_kaleido.py,test_async_comm.py) covering the inhibitor state machine, the platform dispatch and failure paths, both real macOS backends, the wake detection incl. its false positive cases (long/irregular check intervals, sub-threshold gaps), the reconnect methods and the ON/OFF wiring.pmset -g assertionsasPreventUserIdleSystemSleep named: "Artisan is connected to a roasting machine"while ON and disappears on OFF. The change was also built into an app bundle withartisan-mac.specto confirm that PyInstaller picks up the lazily importedartisanlib.power.Please note what is not verified on real hardware: I have no Windows or Linux machine. The Windows backend is covered by tests against a faked
ctypes.windll(flags, the away-mode retry, the platform dispatch) and the Linux one only by the platform dispatch, so both had a code review but no run. A smoke test on Windows would be welcome, in particular that the machine stays awake with Artisan ON and sleeps normally again after OFF.The parts that affect every platform are the transport changes:
AsyncCommandKaleidoPortnow track the current writer and report a lost connection once instead of per retry attempt, which touches all machines using those transports (Santoker, Hottop, Mugma, Orbiter, ColorTrack, Acaia, Kaleido), and thePrevent Sleepdefault of ON applies everywhere.Notes for reviewers
Prevent Sleep.ArtisanTimeusestime.perf_counter(), which does not advance while the system is suspended, so a suspension during a recording silently shortens the roast timeline. The wake warning in the log points this out; changing the clock base would affect every recording and did not belong in this PR.