feat(windows): touch activates screen — switch to touched computer#147
feat(windows): touch activates screen — switch to touched computer#147stefanverleysen wants to merge 3 commits intosymless:masterfrom
Conversation
src/lib/server/Server.cpp
Outdated
| #if WINAPI_MSWINDOWS | ||
| #define WIN32_LEAN_AND_MEAN | ||
| #include <Windows.h> | ||
| #endif |
There was a problem hiding this comment.
The project structure is designed so that platform/arch specific code goes in the platform and arch dirs.
Generally, we avoid #if WINAPI_MSWINDOWS when we can, and in this case it should certainly be possible to write the platform-specific code in the right place.
Why? #if WINAPI_MSWINDOWS blocks in platform-agnostic files like Server.cpp makes life harder for every developer who touches the code after you. Right now, if someone needs to fix a Windows bug, they know to look in src/lib/platform/MSWindows*.cpp, but if platform-specific code is scattered across Server.cpp and other core files behind #ifdef blocks, now they have to hunt through the whole codebase to find all the places where Windows behavior lives. It only gets worse over time because once one person does it, the next person (or LLM) does too, and eventually you end up with a tangled mess of conditional compilation blocks that increases tech debt and adds to the maintenance burden.
There was a problem hiding this comment.
Fixed. Removed all #if WINAPI_MSWINDOWS and #include <Windows.h> from Server.cpp.
Added activateWindowAt(SInt32 x, SInt32 y) through the platform abstraction chain:
IPlatformScreen— virtual with empty defaultScreen/PrimaryClient— forwardingMSWindowsScreen— Windows-specificSetForegroundWindowimplementation
Also removed the dead mouseDown/mouseUp calls — PrimaryClient ignores them.
| } | ||
| } | ||
|
|
||
| void Server::handleTouchActivatedPrimaryEvent(const Event &event, void *) |
There was a problem hiding this comment.
Please review all comments in this function. It reads like a student "hello world" project. As I mentioned in my last PR review, code comments "rot" -- they eventually drift away from reality and become lies, confusing other devs in future.
Check out the Deskflow guidance on comments: https://github.com/deskflow/deskflow/wiki/Hacking-Guide#6-do-not-add-redundant-comments
The most important part:
a comment can be added but it must explain why we are doing something and never what the code is doing.
There was a problem hiding this comment.
Fixed. Removed all play-by-play narration comments from both handleTouchActivatedPrimaryEvent and handleGrabScreenEvent.
Remaining comments only explain why:
- Jump zone clamping — avoids triggering immediate edge switch
activateWindowAt— hook eats the original touch event- Cooldown reset — prevents edge switches from undoing the touch switch
There was a problem hiding this comment.
Still an issue, loads of student-project style comments.
@stefanverleysen The PR description (and future PR descriptions) could benefit from some improvements: Move API-level details (hook types, raw input flags, specific Win32 calls) into code comments but make sure they explain why the code does what it does (never what the code does). The PR description should describe behavior and scope, not implementation mechanics. Separate the cursor-hiding fix into its own PR if possible. Bundling a related bugfix into a feature PR makes both harder to review and harder to revert independently (often something that needs to be done after the PR lands if the bug fix turns out to have too many negative side effects). In this case you can add a "blocked by" note at the top, linking to the other PR. It's important that bug fixes are tested separately. Trim the test environment table to OS version and touch device type. Hardware specs like GPU and CPU model are noise for this kind of change. The test results section: Replace with a list of "To test" numbered steps (so the PR reviewer knows how to test the PR). |
There was a problem hiding this comment.
Pull request overview
Adds a Windows-only “touch activates screen” feature that switches keyboard/mouse focus to the touched computer, plus supporting protocol/config plumbing and improved cursor hiding on touch hardware.
Changes:
- Adds
touchActivateScreenoption (withtouchInputLocalbackward-compat) and GUI toggle. - Introduces protocol v1.9 message for client→server “grab screen” requests.
- Implements Windows touch detection (LL hook + Raw Input + WM_POINTER) and a more reliable cursor hider on touch devices.
Reviewed changes
Copilot reviewed 31 out of 31 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/lib/server/Server.h | Adds handlers and cooldown state for touch-triggered switching. |
| src/lib/server/Server.cpp | Implements touch activation + client grab handling and applies cooldown to edge switching. |
| src/lib/server/PrimaryClient.h | Adds window-activation API for primary screen. |
| src/lib/server/PrimaryClient.cpp | Forwards activateWindowAt to platform screen. |
| src/lib/server/Config.cpp | Parses new option name with backward-compatible alias. |
| src/lib/server/ClientProxyUnknown.cpp | Routes protocol minor 1.9 to new proxy. |
| src/lib/server/ClientProxy1_9.h | New proxy type for protocol v1.9. |
| src/lib/server/ClientProxy1_9.cpp | Parses CGRB (grab screen) message and emits an event. |
| src/lib/platform/dfwhook.h | Adds new hook message ID for touch events. |
| src/lib/platform/MSWindowsScreen.h | Adds touch option state/debounce and platform activateWindowAt. |
| src/lib/platform/MSWindowsScreen.cpp | Implements touch detection paths (WM_POINTER + hook message handling) and window activation. |
| src/lib/platform/MSWindowsHook.h | Exposes toggles for touch activate + primary/secondary role. |
| src/lib/platform/MSWindowsHook.cpp | Detects touch-originated mouse events and posts touch messages. |
| src/lib/platform/MSWindowsDesks.cpp | Improves cursor hiding on touchscreen clients; adds Raw Input forwarding for touch. |
| src/lib/deskflow/protocol_types.h | Bumps protocol minor to 1.9; declares CGRB message. |
| src/lib/deskflow/protocol_types.cpp | Defines kMsgCGrabScreen. |
| src/lib/deskflow/option_types.h | Adds kOptionTouchActivateScreen. |
| src/lib/deskflow/Screen.h | Adds activateWindowAt API. |
| src/lib/deskflow/Screen.cpp | Forwards activateWindowAt to platform screen. |
| src/lib/deskflow/IPlatformScreen.h | Adds default no-op activateWindowAt to platform interface. |
| src/lib/client/ServerProxy.h | Adds grabScreen() API documentation. |
| src/lib/client/ServerProxy.cpp | Sends CGRB message to server. |
| src/lib/client/Client.h | Adds handler for local grabScreen events. |
| src/lib/client/Client.cpp | Forwards local grabScreen events to server; updates compatibility table. |
| src/lib/base/EventTypes.h | Adds new event types for touch-activated primary + grabScreen. |
| src/lib/base/EventTypes.cpp | Registers new event types. |
| src/gui/src/ServerConfigDialogBase.ui | Adds GUI checkbox for touch activation. |
| src/gui/src/ServerConfigDialog.cpp | Wires checkbox to ServerConfig. |
| src/gui/src/ServerConfig.h | Adds touchActivateScreen storage + accessors. |
| src/gui/src/ServerConfig.cpp | Persists/loads/txt-dumps touchActivateScreen (with alias fallback). |
| src/gui/CMakeLists.txt | Adds include path for multi-config Qt autogen headers. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (!GetCursorPos(&pt)) | ||
| return false; |
There was a problem hiding this comment.
WM_POINTER* handling uses GetCursorPos() to determine the activation point. For touch, the mouse cursor may be elsewhere (or not updated), causing a switch at the wrong coordinates. Use the pointer message coordinates (e.g., screen coords from lParam such as GET_X_LPARAM(lParam) / GET_Y_LPARAM(lParam), or query pointer info via the pointer APIs) instead of the cursor position.
| if (!GetCursorPos(&pt)) | |
| return false; | |
| pt.x = GET_X_LPARAM(lParam); | |
| pt.y = GET_Y_LPARAM(lParam); |
| DWORD foreThread = GetWindowThreadProcessId(GetForegroundWindow(), NULL); | ||
| DWORD curThread = GetCurrentThreadId(); | ||
| if (foreThread != curThread) { | ||
| AttachThreadInput(foreThread, curThread, TRUE); | ||
| } | ||
| SetForegroundWindow(root); | ||
| if (foreThread != curThread) { |
There was a problem hiding this comment.
GetForegroundWindow() can return NULL (e.g., during certain desktop transitions). In that case GetWindowThreadProcessId(NULL, ...) yields 0, and AttachThreadInput(0, ...) is invalid and may fail unpredictably. Capture the foreground HWND into a variable, check for NULL, and only call AttachThreadInput when you have a valid non-zero foreground thread id; ensure detach happens only if attach succeeded.
| DWORD foreThread = GetWindowThreadProcessId(GetForegroundWindow(), NULL); | |
| DWORD curThread = GetCurrentThreadId(); | |
| if (foreThread != curThread) { | |
| AttachThreadInput(foreThread, curThread, TRUE); | |
| } | |
| SetForegroundWindow(root); | |
| if (foreThread != curThread) { | |
| HWND foreground = GetForegroundWindow(); | |
| DWORD foreThread = 0; | |
| if (foreground != NULL) { | |
| foreThread = GetWindowThreadProcessId(foreground, NULL); | |
| } | |
| DWORD curThread = GetCurrentThreadId(); | |
| BOOL attached = FALSE; | |
| if (foreThread != 0 && foreThread != curThread) { | |
| attached = AttachThreadInput(foreThread, curThread, TRUE); | |
| } | |
| SetForegroundWindow(root); | |
| if (attached) { |
| if (foreThread != curThread) { | ||
| AttachThreadInput(foreThread, curThread, FALSE); | ||
| } | ||
| LOG((CLOG_DEBUG1 "touch: forced foreground window 0x%08x", root)); |
There was a problem hiding this comment.
Logging an HWND with 0x%08x truncates the value on 64-bit builds. Prefer %p (casting to void*) so logs remain correct across architectures.
| LOG((CLOG_DEBUG1 "touch: forced foreground window 0x%08x", root)); | |
| LOG((CLOG_DEBUG1 "touch: forced foreground window %p", static_cast<void*>(root))); |
| case WM_INPUT: { | ||
| // Raw touch input from digitizer. Forward to main thread as | ||
| // DESKFLOW_MSG_TOUCH for the same handling as the LL mouse hook. | ||
| UINT size = 0; | ||
| GetRawInputData( | ||
| reinterpret_cast<HRAWINPUT>(msg.lParam), RID_INPUT, | ||
| NULL, &size, sizeof(RAWINPUTHEADER)); | ||
| if (size > 0 && size <= 1024) { | ||
| BYTE buffer[1024]; | ||
| if (GetRawInputData( | ||
| reinterpret_cast<HRAWINPUT>(msg.lParam), RID_INPUT, | ||
| buffer, &size, sizeof(RAWINPUTHEADER)) != static_cast<UINT>(-1)) { | ||
| RAWINPUT *raw = reinterpret_cast<RAWINPUT *>(buffer); | ||
| if (raw->header.dwType == RIM_TYPEHID && | ||
| raw->data.hid.dwCount > 0 && raw->data.hid.dwSizeHid > 0) { | ||
| POINT pt; | ||
| GetCursorPos(&pt); | ||
| LOG((CLOG_DEBUG1 "desk raw touch at %d,%d", pt.x, pt.y)); | ||
| PostThreadMessage(m_threadID, DESKFLOW_MSG_TOUCH, | ||
| static_cast<WPARAM>(pt.x), static_cast<LPARAM>(pt.y)); | ||
| } | ||
| } | ||
| } | ||
| continue; | ||
| } |
There was a problem hiding this comment.
This forwards/logs every WM_INPUT digitizer event unconditionally, even when touchActivateScreen is disabled (the main thread later discards it). On touch-heavy devices this can create avoidable CPU/log overhead. Consider gating the forwarding/logging on an enabled flag (shared option state) and/or only registering raw input when the feature is enabled.
| if (m_touchSwitchCooldown.getTime() < kTouchSwitchCooldownTime) { | ||
| LOG((CLOG_DEBUG1 "edge switch blocked by touch cooldown (%.2fs remaining)", | ||
| kTouchSwitchCooldownTime - m_touchSwitchCooldown.getTime())); |
There was a problem hiding this comment.
If Stopwatch starts at 0 on construction (common), this blocks all edge switching for the first 0.5s after server startup (and potentially after any lifecycle that reconstructs Server). If the intent is 'only block immediately after a touch/grab switch', consider initializing the cooldown as 'expired' (e.g., via an explicit boolean m_touchSwitchCooldownActive that is set on touch/grab and cleared once elapsed), or use a hasElapsed()-style check if available instead of comparing against a stopwatch that begins at 0.
| if (m_touchSwitchCooldown.getTime() < kTouchSwitchCooldownTime) { | |
| LOG((CLOG_DEBUG1 "edge switch blocked by touch cooldown (%.2fs remaining)", | |
| kTouchSwitchCooldownTime - m_touchSwitchCooldown.getTime())); | |
| double elapsedTouchCooldown = m_touchSwitchCooldown.getTime(); | |
| if (elapsedTouchCooldown > 0.0 && elapsedTouchCooldown < kTouchSwitchCooldownTime) { | |
| LOG((CLOG_DEBUG1 "edge switch blocked by touch cooldown (%.2fs remaining)", | |
| kTouchSwitchCooldownTime - elapsedTouchCooldown)); |
nbolton
left a comment
There was a problem hiding this comment.
💡 Idea: Can this PR be further improved to allow client mouse input to take focus rather than just touch input? If so that would be massively useful to a much bigger number of Synergy users.
Review summary
Please find everywhere that says "screen grab" or grabScreen etc and change to "grab input" or "grab focus" -- we avoid using "screen" in new code because it's ambiguous (computers can have multiple screens).
There are also still so any AI slop "hello world" comments that make this diff read like student's homework. Purely as an example/starting point -- Here's what Claude found (this is by no means an exhaustive list and may be innacruate):
- Client.cpp (line in diff): // Forward grab screen request to server via protocol — the function name handleGrabScreen and the two lines of code make this obvious.
- MSWindowsDesks.cpp deskEnter: // Restore WS_EX_TRANSPARENT that was removed in deskLeave — just restates what | WS_EX_TRANSPARENT does. Better: explain why the pairing matters (e.g. "so hit-testing passes through again").
- MSWindowsDesks.cpp deskLeave: // Brief delay for cursor hiding to take effect, then center the cursor. — just describes the sleep + move. The original comment at least explained why 30ms was chosen and the tradeoffs. This lost the "why".
- MSWindowsDesks.cpp WM_INPUT case: // Raw touch input from digitizer. Forward to main thread as DESKFLOW_MSG_TOUCH... — the first sentence ("Raw touch input from digitizer") is pure "what"; the case label and code make that clear. The "for the same handling as the LL mouse hook" part is useful though.
- MSWindowsScreen.cpp: // WM_POINTER stuff (Windows 8+) — section label describing what, not why.
- MSWindowsScreen.cpp setOptions: // check for touch input local option — self-evident from kOptionTouchActivateScreen on the next line.
- MSWindowsScreen.cpp isPointerTypeTouch: // Dynamically load GetPointerType for Windows 7 compatibility — duplicates the comment already at the declaration (// Function pointer type for GetPointerType (loaded dynamically for Win7 compat)).
- MSWindowsScreen.h: // When true, touching this screen activates it (switches focus here) — m_touchActivateScreen already says exactly this.
- Server.h: // state for touch-triggered screen switching cooldown — the member name m_touchSwitchCooldown already says this. (The next line — "prevents edge-triggered switches from immediately undoing touch switches" — is the useful "why" part.)
| @@ -0,0 +1,36 @@ | |||
| /* | |||
| * Deskflow -- mouse and keyboard sharing utility | |||
| * Copyright (C) 2012-2016 Symless Ltd. | |||
There was a problem hiding this comment.
Only 10 years off 😉
| * Copyright (C) 2012-2016 Symless Ltd. | |
| * Copyright (C) 2012-2026 Symless Ltd. |
| @@ -0,0 +1,59 @@ | |||
| /* | |||
| * Deskflow -- mouse and keyboard sharing utility | |||
| * Copyright (C) 2012-2016 Symless Ltd. | |||
There was a problem hiding this comment.
| * Copyright (C) 2012-2016 Symless Ltd. | |
| * Copyright (C) 2012-2026 Symless Ltd. |
| // grab screen request: secondary -> primary | ||
| // Client requests to become the active screen (e.g., due to touch input). | ||
| // $1 = x position, $2 = y position where activation occurred | ||
| extern const char *const kMsgCGrabScreen; |
There was a problem hiding this comment.
I need to spend some time figuring out if it's really necessary to change the protocol. We're able to switch screens without a protocol change, but I suppose the issue here is that the client has no way currently of saying to a server "Hey, I need to be the active input"
| // grab screen request: secondary -> primary | ||
| // Client requests to become the active screen (e.g., due to touch input). | ||
| // $1 = x position, $2 = y position where activation occurred | ||
| extern const char *const kMsgCGrabScreen; |
There was a problem hiding this comment.
Assuming the protocol change is necessary, we try to avoid the word "screen" since computers can have multiple screens... I wonder if "input" would make sense?
| // grab screen request: secondary -> primary | |
| // Client requests to become the active screen (e.g., due to touch input). | |
| // $1 = x position, $2 = y position where activation occurred | |
| extern const char *const kMsgCGrabScreen; | |
| // grab input request: secondary -> primary | |
| // Client requests to become the active computer | |
| // $1 = x position, $2 = y position where activation occurred | |
| extern const char *const kMsgCGrabInput; |
... or maybe "focus"?
| // grab screen request: secondary -> primary | |
| // Client requests to become the active screen (e.g., due to touch input). | |
| // $1 = x position, $2 = y position where activation occurred | |
| extern const char *const kMsgCGrabScreen; | |
| // grab focus request: secondary -> primary | |
| // Client requests to become the active computer | |
| // $1 = x position, $2 = y position where activation occurred | |
| extern const char *const kMsgCGrabFocus; |
Afterthought: Borderline semantics here but I wonder if "take focus" or "take input" would make more sense?
| // grab screen request: secondary -> primary | |
| // Client requests to become the active screen (e.g., due to touch input). | |
| // $1 = x position, $2 = y position where activation occurred | |
| extern const char *const kMsgCGrabScreen; | |
| // take focus request: secondary -> primary | |
| // Client requests to become the active computer | |
| // $1 = x position, $2 = y position where input occurred | |
| extern const char *const kMsgCTakeFocus; |
| const std::map<int, std::set<int>> compatibleTable{ | ||
| {6, {7, 8}}, // 1.6 is compatible with 1.7 and 1.8 | ||
| {7, {8}} // 1.7 is compatible with 1.8 | ||
| {7, {8}}, // 1.7 is compatible with 1.8 | ||
| {8, {9}} // 1.8 is compatible with 1.9 (touch-to-switch unavailable) | ||
| }; |
There was a problem hiding this comment.
This table has been removed upstream, as it doesn't make sense. I wonder if it's pointless modifying it.
| { | ||
| } |
There was a problem hiding this comment.
| { | |
| } | |
| { | |
| // do nothing | |
| } |
| # single-config generators (Make) use include/, multi-config (VS) use include_$<CONFIG>/ | ||
| include_directories(${PROJECT_BINARY_DIR}/src/lib/gui/gui_autogen/include) | ||
| include_directories(${PROJECT_BINARY_DIR}/src/lib/gui/gui_autogen/include_$<CONFIG>) |
There was a problem hiding this comment.
I wonder why this change was needed. Maybe it should be a separate PR?
|
|
||
| void Client::handleGrabScreen(const Event &event, void *) | ||
| { | ||
| // Forward grab screen request to server via protocol |
There was a problem hiding this comment.
Comment doesn't explain why -- is it redundant?
| // WM_POINTER stuff (Windows 8+) | ||
| #if !defined(WM_POINTERDOWN) | ||
| #define WM_POINTERDOWN 0x0246 | ||
| #define WM_POINTERUP 0x0247 | ||
| #define WM_POINTERUPDATE 0x0245 | ||
| #define WM_POINTERENTER 0x0249 | ||
| #define WM_POINTERLEAVE 0x024A | ||
| #define GET_POINTERID_WPARAM(wParam) (LOWORD(wParam)) | ||
| #endif | ||
|
|
||
| #if !defined(PT_POINTER) | ||
| #define PT_POINTER 1 | ||
| #define PT_TOUCH 2 | ||
| #define PT_PEN 3 | ||
| #define PT_MOUSE 4 | ||
| #endif | ||
|
|
||
| // Function pointer type for GetPointerType (loaded dynamically for Win7 compat) | ||
| typedef BOOL(WINAPI *GetPointerTypeFunc)(UINT32 pointerId, DWORD *pointerType); | ||
| static GetPointerTypeFunc s_getPointerType = NULL; | ||
| static bool s_pointerApiChecked = false; |
There was a problem hiding this comment.
I asked you to remove this kind of backward compat stuff in your old PR.
It's redundant. WM_POINTER* and PT_* have been in the Windows SDK since Windows 8 (SDK 8.0+). Any SDK that can target Win11 will have them. The #if !defined guards are unnecessary
Same story as the VS2005 hack in dfwhook.h:L21-27
The dynamic loading of GetPointerType via GetProcAddress a bit further down is the same pattern. It's a Win8+ API that will always be present on Win11. Could just #include <winuser.h> and call it directly.
| } | ||
| } | ||
|
|
||
| void Server::handleTouchActivatedPrimaryEvent(const Event &event, void *) |
There was a problem hiding this comment.
Still an issue, loads of student-project style comments.
Add a touchActivateScreen option that switches keyboard/mouse focus to whichever computer the user touches. Works bidirectionally between server and clients, and detects touch in all applications including Chrome, Electron, UWP, and legacy Win32 apps. Touch detection uses three independent paths for broad hardware coverage: low-level mouse hook (dwExtraInfo MI_WP_SIGNATURE), raw input (RIDEV_INPUTSINK on desk thread for WM_INPUT), and WM_POINTER messages (Win8+ API). All three converge through debounced event dispatch to the server's screen-switch logic. Raw input is registered on the desk thread window rather than the main window because the main event loop uses QS_ALLPOSTMESSAGE, which never wakes for WM_INPUT messages. Cursor hiding on touchscreen clients uses a full-screen hider window with a blank cursor class instead of ShowCursor(FALSE), which is unreliable on touch hardware. WS_EX_TRANSPARENT is toggled on leave/enter for correct hit-testing. The server synthesizes a click and forces the foreground window after touch-triggered switches so the window under the touch point receives focus. A 500ms cooldown prevents edge-triggered switches from immediately undoing touch switches. Tested on: - Server: ASUS 3090DEV, i9-12900K, RTX 3090, Windows 11 Enterprise Build 26100 (64-bit), USB HID touch screen (VID 0457 / PID 0819) - Client: Microsoft Surface Book (1st gen), integrated touchscreen, Windows 11 - Applications tested: Chrome, Cursor (Electron), Windows Settings (UWP), Notepad, Synergy GUI, Start menu, File Explorer
…on, comment cleanup - Create ClientProxy1_9 for kMsgCGrabScreen (was incorrectly in ClientProxy1_0, breaking backward compatibility with older clients) - Bump protocol version 1.8 → 1.9 for touch-activated screen switching - Move Windows-specific SetForegroundWindow logic from Server.cpp to MSWindowsScreen::activateWindowAt() via platform abstraction chain - Remove dead mouseDown/mouseUp calls (PrimaryClient ignores them) - Remove #if WINAPI_MSWINDOWS and Windows.h include from Server.cpp - Clean up comments: keep only "why" comments, remove "what" comments Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…vers A 1.9 client connecting to a 1.8 server would be rejected as incompatible. Add compat table entry so the client downgrades to 1.8 (touch-to-switch unavailable, but connection works). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
debe224 to
fa14779
Compare
Summary
Adds a touchActivateScreen option that lets users switch keyboard/mouse focus by touching a screen. When the cursor is on computer A and you touch computer B's screen, focus switches to B at the touch point. Works bidirectionally (server/client).
This feature is Windows-only. Touch detection relies on Windows-specific APIs (WH_MOUSE_LL, RegisterRawInputDevices, WM_POINTER), and cursor hiding uses Win32 window management. The option plumbing and protocol messages are cross-platform, but detection and activation are only implemented in the MSWindows* platform layer. macOS and Linux are unaffected.
Three independent detection paths ensure coverage across all Windows apps:
Also fixes cursor hiding on touchscreen clients (ShowCursor(FALSE) is unreliable on touch hardware — replaced with full-screen hider window + blank cursor class).
Test environment
Applications tested: Chrome, Cursor (Electron), Windows Settings (UWP), Notepad, Synergy GUI, Start menu, File Explorer
Test results
Development notes
This feature was developed with assistance from Claude Code (Opus 4.6) for code exploration, debugging, and review. All changes were manually tested on physical hardware.