diff --git a/.gitignore b/.gitignore index 8e02128..442c24f 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ pen_capture.bin # OS Thumbs.db .DS_Store + +docs/ diff --git a/README.md b/README.md index 56f96b4..84f45dc 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,31 @@ # remarkable-input-tablet -Use a reMarkable 2 as a pressure-sensitive drawing tablet on Windows and Linux — no drivers, no root modifications, no third-party services. +Use a reMarkable 2 (stable) or reMarkable Paper Pro (experimental — see [Paper Pro status](#paper-pro-status)) as a pressure-sensitive drawing tablet on Windows and Linux. No drivers, no root modifications, no third-party services. The tablet connects over SSH (USB or Wi-Fi). The pen's raw evdev events are streamed to the PC and injected as native pointer input, giving you full pressure sensitivity, tilt, hover detection, and eraser support in any compatible application. Optional multi-touch gestures (pinch / pan / rotate) inject as real touch contacts to apps that consume them. ## Requirements -### Windows +### Tablet — reMarkable 2 -- **Tablet**: reMarkable 2 (firmware tested: Wacom I2C Digitizer, version 1231) -- **PC**: Windows 10 1809 or later (Windows Ink pointer injection API) -- **Connection**: USB cable (SSH at `10.11.99.1`) or Wi-Fi (same SSH, different IP) -- **Root password**: Settings → Help → Copyrights and licenses → scroll to bottom +- Firmware tested: Wacom I2C Digitizer, version 1231 +- Root password: Settings → Help → Copyrights and licenses → scroll to bottom -### Linux +### Tablet — reMarkable Paper Pro + +- **Developer Mode must be enabled first**: Settings → General → Paper Tablet → Software → Advanced → Developer Mode. The root password then appears at Settings → Help → About → Copyrights and Licenses. See reMarkable's [Developer mode article](https://support.remarkable.com/s/article/Developer-mode). +- Experimental — most device constants are seeded from community data (`Evidlo/remarkable_mouse` `rmpro` branch) and have not been independently verified against hardware. See [Paper Pro status](#paper-pro-status) for the current verification gaps. + +### Host — Windows -- **Tablet**: reMarkable 2 (same as above) -- **PC**: Any x86-64 Linux with kernel 4.5+ (uinput module) -- **Connection**: USB cable or Wi-Fi (same as Windows) -- **Permissions**: membership in the `input` group (one-time setup, see below) +- Windows 10 1809 or later (Windows Ink pointer injection API) +- Connection: USB cable (SSH at `10.11.99.1`) or Wi-Fi (same SSH, different IP) + +### Host — Linux + +- Any x86-64 Linux with kernel 4.5+ (uinput module) +- Connection: USB cable or Wi-Fi (same as Windows) +- Permissions: membership in the `input` group (one-time setup, see below) ## Quick start @@ -72,6 +79,7 @@ Press **Ctrl-C** to stop. | `--debug` | off | Print pipeline stage info on startup | | `--gestures ` | `off` | `touch` (inject multi-touch contacts for pinch / pan / rotate) or `off`. The rM2 firmware suppresses touch while the pen is in proximity, so two-finger gestures only register when the pen is set aside. | | `--pressure ` | `linear` | Pressure response curve. `linear` (1:1), `soft` (boosts light strokes — pen feels lighter), or `hard` (suppresses light strokes — pen feels stiffer). | +| `--device ` | `auto` | `auto` (probe via `uname -m`), `rm2`, or `rmpp`. Auto-detect runs a short SSH command before the streaming pipeline starts; force a specific profile only if detection fails. | ## Orientation @@ -277,6 +285,27 @@ reMarkable 2 Host PC | `RemarkableTablet.App` | Windows | `RemarkableTablet.App.exe` — system tray GUI, WPF + WinForms | | `tools/EventDiagnostics` | Windows | Live evdev event stream logger — streams events to console for debugging | +## Paper Pro status + +The reMarkable Paper Pro is on a 64-bit aarch64 platform (NXP i.MX 8MM) versus the rM2's 32-bit armv7l, which changes the kernel `struct input_event` size from 16 to 24 bytes. The shared event parser is parameterised by `EvdevLayout` (see `src/RemarkableTablet.Core/Devices/`) and a regression test feeds a 24-byte stream to guard against the desync signature documented in [`Evidlo/remarkable_mouse` Issue #92](https://github.com/Evidlo/remarkable_mouse/issues/92). + +Auto-detection via `uname -m` routes the right profile automatically. You can also pass `--device rmpp` explicitly. + +**What is verified:** + +- Build + unit tests (Windows + Linux, Core + Windows test suites). +- 64-bit event-struct decoding (synthetic frames). +- Device-name to profile mapping. + +**What is not yet verified on real hardware (search the source for `TODO(rmpp-phase0)`):** + +- Pen tilt and hover-distance axis ranges (placeholders match rM2 conventions). +- Touchscreen axis ranges (placeholders match the 1620 × 2160 display). +- Whether the pen suppresses touch in proximity (assumed true; rM2 behavior). +- That `uname -m` returns `aarch64` on the production firmware build (community sources strongly imply yes). + +If you have a Paper Pro and want to help, run `tools/EventDiagnostics` against `/dev/input/event2` and `/dev/input/event3` and open an issue with the captured axis ranges. Until those replace the placeholders, treat Paper Pro support as *experimental*. + ## Hardware details ### Pen digitizer (`/dev/input/event1`) @@ -317,6 +346,34 @@ Capacitive multi-touch panel, driver `pt_mt`. Confirmed via `evtest` 2026-05-07. > highlights point the wrong direction, the four cases in > `CoordinateMapper.RotateTilt` are the place to flip signs. +## Hardware details — Paper Pro + +> **Experimental.** Values below are from the [`Evidlo/remarkable_mouse`](https://github.com/Evidlo/remarkable_mouse) `rmpro` branch (Issue #92), not independently verified. See [Paper Pro status](#paper-pro-status) for what still needs hardware confirmation. The placement of pen / touch device nodes matches the community reports: `event0` = power button, `event1` = pen attach/detach, `event2` = pen, `event3` = touch. + +### Pen digitizer (`/dev/input/event2`) + +| Axis | Range | Notes | +|------|-------|-------| +| ABS_X | 0 – 11180 | Resolution 2832 ticks/mm | +| ABS_Y | 0 – 15340 | Resolution 2064 ticks/mm | +| Pressure | 0 – 4096 | 12-bit, mapped to 0–1024 via shaping curve | +| Tilt X/Y | ±9000 | Placeholder; not in the community data | +| Distance | 0 – 255 | Placeholder; not in the community data | + +The active "Marker Plus" stylus is battery-powered and inductively charged, not Wacom EMR. Old rM2/LAMY EMR pens do not work on the Paper Pro. + +### Touchscreen (`/dev/input/event3`) + +| Axis | Range | Notes | +|------|-------|-------| +| ABS_MT_POSITION_X | 0 – 1619 | Placeholder; assumes display-aligned 1620 × 2160 | +| ABS_MT_POSITION_Y | 0 – 2159 | Placeholder; assumes display-aligned | +| ABS_MT_PRESSURE | 0 – 255 | Per-contact pressure (assumed; not verified) | + +### Event struct + +`struct input_event` on aarch64 Linux is 24 bytes: 8-byte `tv_sec`, 8-byte `tv_usec`, then `__u16 type`, `__u16 code`, `__s32 value`. Field offsets are 16 / 18 / 20 versus rM2's 8 / 10 / 12. The shared `EvdevParser` reads these from a per-profile `EvdevLayout`. + ## License MIT diff --git a/docs/FEASIBILITY_PINCH_PAN.md b/docs/FEASIBILITY_PINCH_PAN.md deleted file mode 100644 index d117fb3..0000000 --- a/docs/FEASIBILITY_PINCH_PAN.md +++ /dev/null @@ -1,121 +0,0 @@ -# Feasibility — Pinch-Zoom and Pan via the rM2 Touchscreen - -**Status:** research, no code changes. -**Scope:** Can `remarkable-input-tablet` use the rM2's capacitive multi-touch panel (in addition to the pen digitizer it already uses) to drive pinch-to-zoom and two-finger pan on the host PC? -**TL;DR:** Mechanically feasible on both Windows and Linux, but the dominant UX question is a hardware/firmware behavior on the rM2 (does touch fire while the pen is in proximity?) that must be verified on-device before committing to any path. There are three host-side design forks with very different reach, fidelity, and effort profiles. None of them is small. - ---- - -## 1. What the project does today (baseline) - -A single SSH stream, `cat /dev/input/event1`, pumps 16-byte evdev structs from the **pen** digitizer into a parser → state machine → coordinate mapper → output (Windows Ink injection, Win32 mouse, or Linux uinput). See `src/RemarkableTablet.Core/Pipeline/TabletPipeline.cs` and `src/RemarkableTablet.Core/Transport/SshTransport.cs`. - -The pen device is verified hardware (firmware Wacom I2C Digitizer, version 1231; see `ReMarkable2Constants.cs`). The pipeline is single-source and single-output. - -## 2. What the rM2 provides at the kernel level - -The rM2 exposes three input devices on `/dev/input/event*`: - -| Device | Conventional path | Confirmed in this codebase | Notes | -|---|---|---|---| -| Hardware buttons | `event0` | No | Power / pen-pairing button. Not relevant here. | -| Pen (Wacom digitizer) | `event1` | **Yes** | Already consumed by this project. | -| Capacitive touchscreen | `event2` | **No — unverified** | The candidate input for pinch/pan. | - -> **Verification step before any work begins:** SSH into the device and run `evtest /dev/input/event2`. We need to capture (1) the real device path on this firmware, (2) the multi-touch protocol variant, (3) coordinate ranges, (4) reported max contacts, and (5) whether touch events arrive while the pen is hovering or in contact. The rest of this report assumes the modern Linux MT-B "slot" protocol with `ABS_MT_SLOT`, `ABS_MT_TRACKING_ID`, `ABS_MT_POSITION_X/Y`. If the firmware reports MT-A (no slots) the parser is different, but feasibility doesn't change. - -### Likely-but-unverified facts - -These are reasonable defaults from rM-community knowledge that should be confirmed, not built on: - -- Touch reports a different (lower) coordinate range than the pen — typically aligned to the 1404×1872 display pixels rather than the 20966×15725 pen-digitizer space. This is a separate `CoordinateMapper` calibration. -- The kernel evdev struct on `event2` is 16 bytes (same 32-bit ARM `timeval` layout the pen uses), so `EvdevParser.cs` should bind without changes. -- The rM firmware **palm-rejects touch while the pen is in proximity.** This is the convention on Wacom-equipped Android/Windows tablets and matches reMarkable's drawing-app behavior. **If true, simultaneous "draw with pen + pinch with off-hand" is impossible at the source — no host-side workaround can recover it.** Workflow then becomes "lift pen → pan/zoom → resume drawing," which is acceptable but worth setting expectations on. - -## 3. Architectural impact on the existing pipeline - -### 3.1 Transport — the real lift - -`SshTransport` currently runs **one** `SshCommand`: `cat /dev/input/event1`. To consume `event2` you have three options, each with a real cost: - -| Option | Sketch | Cost | Risks | -|---|---|---|---| -| **A. Second `SshCommand` on the same `SshClient`** | Open a second `cat event2` command on the existing connection, second `PumpBlocking` thread, second `Pipe`. | Smallest delta. Probably one new class `SshTouchTransport` parallel to `SshTransport`, or a refactor that shares an `SshClient`. | The codebase has not exercised concurrent `SshCommand`s. SSH.NET supports multiple channels per session, but the existing `CleanupConnectionAsync` ordering (dispose command → disconnect client) needs careful re-work to avoid the same `OutputStream.Read()` deadlock the pen path already had to fix. | -| **B. Second independent `SshClient`** | A whole second SSH session for touch. | Simple isolation; the existing `SshTransport` is reused verbatim with a different device path. | Doubles SSH overhead (two TCP connections, two auth handshakes, two reconnect loops). Two independent reconnection state machines complicate the App's connection-state UI. | -| **C. Single combined shell with tagging** | One `cat event1 event2 \| awk` … or a small helper binary deployed to the device that interleaves and tags the streams. | Lowest network overhead. | Requires a deployed helper or fragile shell pipework; previous design intentionally avoids touching the device's filesystem. Not recommended unless A and B both prove problematic. | - -A is the natural choice if SSH.NET's multi-channel support is solid; B is the safe fallback. Either way, **transport, not gesture logic, is where this project's existing code structure has to flex the most.** - -### 3.2 Parsing - -A new `MtEvdevParser` (or a generalization of `EvdevParser`) is needed because the touch protocol uses MT-specific codes (`ABS_MT_SLOT`, `ABS_MT_TRACKING_ID`, `ABS_MT_POSITION_X`, `ABS_MT_POSITION_Y`, possibly `ABS_MT_PRESSURE`/`ABS_MT_TOUCH_MAJOR`). The frame boundary is still `SYN_REPORT`. The structural pattern of `TabletStateMachine` (accumulate → snapshot on SYN_REPORT) carries over cleanly; the snapshot type changes from a single `PenFrame` to a multi-contact `TouchFrame { Contact[] }`. - -Ballpark: ~200–400 LOC + tests, comparable to the existing pen state machine. - -### 3.3 Coordinate mapping - -A second `CoordinateMapper` instance with its own scale factors. The rotation/orientation logic from `CoordinateMapper.cs` is reusable. New unit tests required because touch coordinate ranges differ from pen. - -## 4. Output — the central design fork - -This is the decision the project actually has to make. Each of the three options is feasible; they trade reach against fidelity. - -### Option 1 — Inject real multi-touch contacts - -- **Windows:** `InitializeTouchInjection` + `InjectTouchInput` (user32.dll, no driver, available since Windows 8). Multiple `POINTER_TOUCH_INFO` contacts per frame. Apps see genuine touch points and run their own gesture recognition. -- **Linux:** Extend the existing uinput device — or add a sibling — with MT slots and `INPUT_PROP_DIRECT`. Standard kernel pattern; well-trodden in `libevdev`/`uinput` examples. -- **Pros:** Highest fidelity. Works correctly with apps that have real multi-touch gestures (canvas pan, rotate, zoom in Krita / Photoshop / Affinity / Inkscape via xdotool-style multi-touch, browser pinch zoom). No host-side gesture recognizer required — the OS or app does it. -- **Cons:** - - Many Windows desktop apps still don't support touch gestures even when they support touch (or their support is patchy). What works on a Surface for pinch-zoom doesn't always work the same way through synthetic injection. - - Touch injection on Windows must be initialized once per process and contacts must be tracked with stable IDs across frames — non-trivial state to keep right. - - Apps that handle pen via Windows Ink and touch separately may double-process (e.g., the pen injects through one path, touch through another, and the app sees them as different devices — usually fine, occasionally not). - -### Option 2 — Recognize gestures on the host, emit synthesized input - -- Host runs a small recognizer: two-finger horizontal/vertical movement → mouse wheel scroll; two-finger pinch/spread → `Ctrl + mouse wheel` (the de facto zoom gesture in nearly every desktop app); two-finger drag → middle-mouse-button drag (pan in many apps). -- **Windows:** `SendInput` for wheel deltas and modifier keys. Already adjacent to `MouseOutput.cs`. -- **Linux:** uinput relative-mouse device emitting wheel events. -- **Pros:** Works in **every** desktop app, including non-touch-aware ones. Predictable. The recognizer is small and testable. -- **Cons:** Lower fidelity. No native pan inertia. The "pinch" gesture becomes discrete wheel ticks, which feels notchy compared to true touch. Custom gestures (rotate, three-finger swipe) don't map cleanly to keyboard/wheel events without per-app shortcuts. - -### Option 3 — Hybrid - -- Inject real touch where it works, expose a "fallback to synthesized scroll" mode (CLI flag / settings checkbox) for apps that don't behave with injected touch. Likely the eventual production answer; double the implementation surface to get there. - -A reasonable phasing if the project commits: Option 2 first (smaller scope, broadest immediate value), Option 1 as a follow-on once the touch transport and parser are stable. - -## 5. UX questions that don't have a software answer - -These need user-testing on real hardware to settle, not more code reading: - -1. **Does the rM2 firmware suppress touch while the pen hovers?** If yes, the experience is "lift pen, then pinch." If no, off-hand pinch while drawing becomes possible — a notably better workflow. -2. **Latency.** Touch sample rate and SSH transport jitter combined will determine whether pinch feels responsive. Pen at ~100 Hz over USB SSH is already fine; touch is typically 60–80 Hz. Should be fine but is unmeasured. -3. **Palm rejection on the host side.** If the user rests their hand on the screen while drawing and the firmware does *not* suppress, the pipeline must drop touch contacts whose `BTN_TOOL_PEN` companion is active. Cheap to implement; needs the cross-device state to flow into one place. -4. **Wi-Fi vs USB.** Two parallel evdev streams over Wi-Fi may be where the SSH bandwidth budget actually starts to matter. Worth a back-of-envelope check (touch packets are small but bursty during gestures). - -## 6. Effort estimate (rough, for relative sizing only) - -| Slice | Estimate | Comment | -|---|---|---| -| On-device verification (`evtest`, capture sample streams) | 0.5–1 day | Required before any of the below has firm shape. | -| Transport extension (Option A: shared `SshClient`) | 1–2 days | Including reconnect-loop integration and tests. | -| MT parser + touch state machine + tests | 1–2 days | Mirrors the existing pen pipeline. | -| Coordinate mapping + calibration | 0.5–1 day | New scale factors; reuse rotation logic. | -| **Output: synthesized scroll/zoom (Option 2)** | 1–2 days | Includes a small gesture recognizer with unit tests. | -| **Output: real touch injection (Option 1, Windows)** | 3–5 days | `InitializeTouchInjection` + multi-contact tracking + per-app testing matrix. | -| **Output: real touch injection (Option 1, Linux uinput-MT)** | 1–2 days | Standard kernel pattern. | -| App + CLI surface (settings, flags, status UI) | 0.5–1 day | Minor. | - -**Lower bound** (Option 2 only, Windows + Linux): ~5–7 working days. -**Upper bound** (Option 1 + Option 2 hybrid, both platforms): ~10–14 working days. - -These numbers assume one engineer familiar with the codebase, exclude polish and per-app compatibility hunting, and are wide on purpose — the verification step in §2 can move them. - -## 7. Recommended next step (process, not implementation) - -1. Run `evtest /dev/input/event2` on the rM2 with a representative session: tap, two-finger pinch, two-finger drag, palm rest, and pinch-while-pen-hovering. Capture the output. -2. Confirm or refute the four "likely-but-unverified" facts in §2. -3. Decide on the design fork in §4 with the latency/palm-rejection answers in hand. -4. Then plan implementation against the chosen fork. - -No code changes should land before step 1. diff --git a/docs/IMPLEMENTATION_PLAN_TOUCH.md b/docs/IMPLEMENTATION_PLAN_TOUCH.md deleted file mode 100644 index 8a8167f..0000000 --- a/docs/IMPLEMENTATION_PLAN_TOUCH.md +++ /dev/null @@ -1,460 +0,0 @@ -# Implementation Plan — Touchscreen Gestures (Pinch / Zoom / Pan / Rotate) - -**Goal:** Add support for the rM2 capacitive touchscreen so users can pinch-zoom, two-finger pan, and two-finger rotate on the host PC, on both Windows and Linux, alongside existing pen input. - -**Companion doc:** `docs/FEASIBILITY_PINCH_PAN.md` (the feasibility analysis this plan operationalizes). - -**Non-goals (this iteration):** -- Three+ finger gestures (swipe, four-finger task switch). -- Per-app gesture profiles. -- Custom user-bindable gestures. -- Touchpad-style cursor mode (touch ≠ mouse). - ---- - -## 1. Architectural overview - -### 1.1 Pipeline today - -``` -SshTransport (event1) - └─ EvdevParser → Channel - └─ TabletStateMachine → Channel - └─ CoordinateMapper → MappedFrame - └─ IOutputMode (WindowsInk | Mouse | Uinput) -``` - -### 1.2 Pipeline after this work - -``` -SshTransport (multiplexed) - ├─ event1 → EvdevParser → Channel ──► PenStateMachine ──► Channel ──► PenMapper ──► IPenOutput - └─ event2 → EvdevParser → Channel ──► TouchStateMachine ──► Channel - │ - ▼ - GestureEngine (recognises pinch / pan / rotate) - │ - ▼ - ITouchOutput - ├─ WindowsTouchInjectionOutput (Win32 InjectTouchInput) - ├─ LinuxUinputTouchOutput (uinput MT-B device) - └─ SynthesizedScrollOutput (Ctrl+wheel / wheel / middle-drag) - -PenToolGate (cross-stream coordination — palm rejection, optional pen-priority) -``` - -Two evdev streams over a single SSH client. Two parallel state machines feeding two parallel output sinks. A small **PenToolGate** is the only place the two streams share state (so that, e.g., touch is suppressed while the pen is in range, *if* the user opts in). - -### 1.3 Output strategy — committed decisions - -| Decision | Choice | Rationale | -|---|---|---| -| Default Windows output | Real touch injection (`InitializeTouchInjection` + `InjectTouchInput`) | App sees real contacts; OS/app does gesture recognition. Highest fidelity. | -| Default Linux output | uinput MT-B touch device with `INPUT_PROP_DIRECT` | Standard kernel pattern; sibling to existing `UinputOutput`. | -| Fallback both platforms | Host-side gesture recognizer → synthesized `Ctrl+MouseWheel` (zoom), wheel (pan), middle-drag (pan), no rotate | Works in apps that don't accept injected touch. | -| User selection | New flag `--gestures ` (default `touch`) | Lets user pick when an app misbehaves. | -| Rotate | **Only in `touch` mode.** | No universal desktop shortcut for canvas rotate. | -| Pen / touch interaction | **Firmware already suppresses touch when pen is in proximity** (verified 2026-05-07 — see `tools/EventDiagnostics/samples/README.md`). `PenToolGate` is therefore demoted from required to optional defense-in-depth, defaulting to `coexist` (no host-side gating). The two non-default modes (`pen-priority`, `palm-reject`) are kept as escape valves but should rarely be needed. | - -### 1.4 Transport strategy — committed decision - -**Single `SshClient`, two `SshCommand` channels.** Reasons: -- One auth handshake, one TCP connection, one reconnect loop to surface in the GUI. -- SSH.NET supports multiple concurrent commands on a single client (each is a separate channel under the SSH session). -- The lifecycle bug already solved in `SshTransport.CleanupConnectionAsync` (dispose `_command` first to unblock the blocking read) generalizes cleanly to multiple commands. - -We refactor `SshTransport` into a thin owner of the `SshClient` and a per-stream `SshDeviceStream` that wraps one `SshCommand` + one `Pipe` + one pump task. - ---- - -## 2. Phase 0 — On-device verification (gate) - -**No code lands before this completes.** Outcomes feed directly into Phase 1 constants. - -### 2.1 Tasks - -1. SSH into the rM2: `ssh root@10.11.99.1`. -2. `evtest /dev/input/event2` — capture full output for: - - Header (device name, vendor/product, axes, MT slot count, resolution). - - Single-finger tap: full sequence including `ABS_MT_SLOT`, `ABS_MT_TRACKING_ID`, `ABS_MT_POSITION_X/Y`, `BTN_TOUCH`, `SYN_REPORT`. - - Two-finger pinch: confirm two slots fire concurrently, capture sample rate. - - Two-finger rotate: same. - - Pen hovering above touchscreen (no touch contact): does `event2` go silent? **This is the dominant unknown.** - - Pen drawing while finger rests on screen: same question. -3. Run `cat /dev/input/event2 | wc -c` for ~5 s during active touch — validates struct size (should be `16 × event_count`; if not, our 16-byte assumption is wrong for this device). -4. Confirm `/dev/input/event2` is the touch device (could be `event3` or differently numbered on alternate firmware). - -### 2.2 Deliverable - -A captured-evtest log committed to `tools/EventDiagnostics/samples/event2-touch.log` plus an updated `ReMarkable2Constants.cs` section adding: - -```csharp -// Touchscreen — verified 2026-MM-DD via evtest /dev/input/event2 -public const string TouchDevicePath = "/dev/input/event2"; -public const int TouchXMin = 0; -public const int TouchXMax = ???; // from evtest -public const int TouchYMin = 0; -public const int TouchYMax = ???; // from evtest -public const int TouchMaxContacts = ???; // from evtest (typical: 5–10) -public const int TouchPressureMax = ???; // 0 if not reported -``` - -### 2.3 Decision gate - -After Phase 0, lock answers to: -- Touch device path (variable in code, not assumed). -- Coordinate range (different from pen). -- Whether touch fires while pen is in proximity. **If suppressed by firmware, `PenToolGate` defaults are simpler — no host-side suppression needed.** If concurrent, host-side gating is mandatory. - ---- - -## 3. Phase 1 — Core: types, parser, state machine - -All work in `src/RemarkableTablet.Core/`. - -### 3.1 Evdev MT codes - -Extend `EvdevCodes.cs`: - -```csharp -// ABS multi-touch slot protocol (Type B) -public const ushort ABS_MT_SLOT = 47; -public const ushort ABS_MT_POSITION_X = 53; -public const ushort ABS_MT_POSITION_Y = 54; -public const ushort ABS_MT_TRACKING_ID = 57; -public const ushort ABS_MT_PRESSURE = 58; // optional; presence verified in Phase 0 -public const ushort ABS_MT_TOUCH_MAJOR = 48; // optional -``` - -`EvdevParser` and `EvdevTypes` are already protocol-agnostic — no change. - -### 3.2 Touch frame & state machine - -New files in `src/RemarkableTablet.Core/Tablet/`: - -- `TouchContact.cs` — record `(int Slot, int TrackingId, int X, int Y, int Pressure, bool Active)`. -- `TouchFrame.cs` — record `(IReadOnlyList Contacts, long FrameTicks)`. -- `TouchStateMachine.cs` — mirrors `TabletStateMachine.cs`. Maintains an array of slots (size = `TouchMaxContacts`). On each evdev event: - - `ABS_MT_SLOT n` → set `_currentSlot = n`. - - `ABS_MT_TRACKING_ID -1` → release `_slots[_currentSlot]`. - - `ABS_MT_TRACKING_ID >= 0` → start tracking in `_slots[_currentSlot]`. - - `ABS_MT_POSITION_X/Y` → update slot. - - `SYN_REPORT` → emit immutable `TouchFrame` of currently-active slots. - -Tests in `tests/RemarkableTablet.Core.Tests/TouchStateMachineTests.cs`: synthetic byte streams covering single tap, two-finger touch, slot release, mid-frame `SYN_DROPPED` recovery. - -### 3.3 Coordinate mapping - -`TouchCoordinateMapper.cs` — separate class (not generalized over pen) because: -- Coordinate range differs (touch ≈ display-aligned, pen ≈ digitizer-aligned). -- Pressure mapping not needed. -- Output is geometry, not a single frame. - -Reuses the orientation rotation block from `CoordinateMapper.cs` (extract a small `RotationTransform.Apply(double nx, double ny, Orientation)` helper to share). Each contact in a `TouchFrame` is mapped independently to screen pixels. - -### 3.4 Gesture engine - -New file: `src/RemarkableTablet.Core/Gestures/GestureEngine.cs`. - -**Responsibility:** consume `TouchFrame`s, produce semantic `GestureEvent`s for the synthesized-output fallback. Real-touch-injection bypasses this entirely (the OS/app does recognition). - -**Output events:** - -```csharp -public abstract record GestureEvent; -public record GestureBegin(int CenterX, int CenterY) : GestureEvent; -public record GesturePinch(double ScaleDelta) : GestureEvent; // 1.0 = no change -public record GesturePan(int DeltaX, int DeltaY) : GestureEvent; // pixels, screen space -public record GestureRotate(double DegreesDelta) : GestureEvent; // signed -public record GestureEnd : GestureEvent; -``` - -**Recognizer (two-finger only, this iteration):** -- Begin when contact count transitions 0/1 → 2. -- Each frame, compute centroid and the vector between the two contacts: - - `pan = centroid_now - centroid_prev` - - `pinch = distance_now / distance_prev` (multiplicative) - - `rotate = angle_now - angle_prev` (degrees, normalized to ±180) -- Emit a single combined event (or split, depending on dominant component) per frame. -- End when contact count drops below 2. -- Hysteresis / dead-zones to avoid jitter — start values picked from analogous touchpad recognizers, tuned in Phase 5. - -Tests: synthetic `TouchFrame` sequences covering pure pinch, pure pan, pure rotate, mixed; assert the emitted gesture stream. - ---- - -## 4. Phase 2 — Transport: dual-stream SSH - -All work in `src/RemarkableTablet.Core/Transport/`. - -### 4.1 Refactor `SshTransport` - -Split into: - -- `SshTransport` — owns the `SshClient`, exposes `OpenStream(devicePath)` returning an `SshDeviceStream`. Owns reconnect logic. -- `SshDeviceStream` — wraps one `SshCommand`, one `Pipe`, one pump task; exposes a `PipeReader`. New file. - -**Lifecycle (critical — generalization of the bug fix already in `CleanupConnectionAsync`):** -- Cleanup order: dispose **all** `SshCommand`s first (this closes their `OutputStream`s and unblocks the blocking `Read` in each pump), then disconnect the client, then await all pump tasks. -- Reconnect = teardown all streams + reconnect client + re-open all streams. Single source of truth for "connection up/down." - -### 4.2 Update `TabletPipeline` - -`TabletPipeline` becomes parameterized by a list of stream descriptors and per-stream consumers, or — simpler — a fixed pen + touch pair: - -```csharp -public sealed class TabletPipeline( - SshTransport transport, - PenStageWiring pen, - TouchStageWiring? touch, // null = touch disabled - PenToolGate gate) -``` - -`RunOnceAsync` opens both streams, spawns parsers + state machines + output loops in parallel under one `Task.WhenAll`, and the first failure cancels the lot. This already mirrors the existing `Task.WhenAll(parser, state, output)` pattern. - -### 4.3 PenToolGate - -```csharp -public sealed class PenToolGate -{ - public PenToolGateMode Mode { get; init; } // Coexist | PenPriority | PalmReject - public bool ShouldSuppressTouch(PenSnapshot pen) => Mode switch { ... }; -} -``` - -Pen pipeline writes its latest snapshot (in-range, in-contact) into the gate after each frame; touch output checks the gate before emitting. - ---- - -## 5. Phase 3 — Output: per-platform sinks - -### 5.1 Windows — real touch injection - -New file: `src/RemarkableTablet.Windows/Output/WindowsTouchInjectionOutput.cs`. - -**Win32 surface (P/Invoke into `user32.dll`):** -- `InitializeTouchInjection(uint maxCount, uint dwMode)` — call once on `Initialize()` with `maxCount = TouchMaxContacts` and `TOUCH_FEEDBACK_DEFAULT`. -- `InjectTouchInput(uint count, POINTER_TOUCH_INFO[] info)` — called per `TouchFrame`. - -**`POINTER_TOUCH_INFO` layout:** depends on `POINTER_INFO` which already has the same `[FieldOffset(8)]` 64-bit-alignment quirk noted in the existing project memory for `POINTER_TYPE_INFO` — apply the same fix here. - -**Contact lifecycle (the hard part):** -- Each contact has a stable `pointerId` (we use the slot index 0–9 plus a process-wide base). -- First frame per contact: flag `POINTER_FLAG_DOWN | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT`. -- Subsequent frames: `POINTER_FLAG_UPDATE | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT`. -- Released contact: `POINTER_FLAG_UP` (one frame), then drop. -- All currently-active contacts must be in every `InjectTouchInput` call until released — Windows requires the full set, not deltas. - -Add `tests/RemarkableTablet.Windows.Tests/WindowsTouchInjectionOutputTests.cs` exercising contact-state transitions against a fake P/Invoke wrapper. - -### 5.2 Linux — uinput MT-B touch device - -New file: `src/RemarkableTablet.Linux/Output/UinputTouchOutput.cs`. Mirror structure of `UinputOutput.cs` but configure a touch device: - -- `EVIOC_SET_ABSBIT` for `ABS_MT_SLOT`, `ABS_MT_POSITION_X/Y`, `ABS_MT_TRACKING_ID`, `ABS_MT_PRESSURE`. -- `EVIOC_SET_PROPBIT(INPUT_PROP_DIRECT)`. -- `UI_SET_KEYBIT` for `BTN_TOUCH`. -- Per-frame: write slot updates + tracking IDs + positions + `SYN_REPORT`. - -Reuses the `Libc` P/Invoke surface that already exists. - -### 5.3 Synthesized scroll/keys (both OSes) - -New file: `src/RemarkableTablet.Core/Output/SynthesizedScrollOutput.cs` (lives in Core because the gesture-to-input mapping is OS-agnostic; the actual injection delegates to `IInputBackend`). - -Backend per OS: -- **Windows:** extend `MouseOutput` (or new sibling) with `SendInput`-based wheel + key-down/up for `VK_CONTROL`. -- **Linux:** new uinput relative-mouse device with `REL_WHEEL`, `REL_X/Y`, `BTN_MIDDLE`, plus `KEY_LEFTCTRL`. - -Mapping: -- `GesturePinch(scaleDelta)` → press Ctrl, emit wheel ticks proportional to `log(scaleDelta)`, release Ctrl when gesture ends. -- `GesturePan(dx, dy)` → either two-direction wheel (most apps) or middle-mouse-button drag (configurable; some apps prefer one over the other). -- `GestureRotate` → ignored (no universal mapping; see §1.3). - -### 5.4 Dispatching - -New `ITouchOutput` interface (parallel to `IOutputMode`, kept distinct because the data shape differs). The pipeline picks the implementation based on `--gestures`: - -| `--gestures` value | Touch output | -|---|---| -| `touch` (default) | `WindowsTouchInjectionOutput` / `UinputTouchOutput` | -| `synth` | `SynthesizedScrollOutput` (driven by `GestureEngine` output) | -| `off` | none — `event2` not opened | - ---- - -## 6. Phase 4 — Pipeline integration - -### 6.1 `TabletPipeline` wiring - -Update `RunOnceAsync` to run, in parallel under one `Task.WhenAll`: - -``` -PenStream pump -TouchStream pump (skipped if --gestures off) -Pen EvdevParser → PenStateMachine → CoordinateMapper → IPenOutput -Touch EvdevParser → TouchStateMachine → (TouchCoordinateMapper → ITouchOutput) - OR (GestureEngine → ITouchOutput) for synth -PenToolGate update from PenStateMachine -``` - -Cancellation propagates the same way it does today. - -### 6.2 Reconnection - -`TabletPipeline` already emits a synthetic pen-up before each reconnect. Add: on reconnect, emit a synthetic "all contacts released" via `ITouchOutput` to avoid stuck contacts. Same code path triggered by the same lifecycle event. - -### 6.3 Disposal ordering - -`TabletPipeline.DisposeAsync`: -1. Cancel CTS. -2. Dispose `SshTransport` (closes both streams; pump tasks exit). -3. Dispose `IPenOutput`, `ITouchOutput`. -4. Dispose CTS. - ---- - -## 7. Phase 5 — User-facing surface - -### 7.1 CLI - -Add to `src/RemarkableTablet.Cli/Program.cs`: - -``` ---gestures default: touch ---pen-priority default: pen-priority ---touch-area reserved for later; no-op for now -``` - -Refresh `--help` and the README "CLI options" table. - -### 7.2 GUI app (Windows-only) - -In `src/RemarkableTablet.App/SettingsWindow.xaml`: -- Group box "Touch gestures": - - Mode dropdown (Touch injection / Scroll fallback / Off). - - Pen interaction dropdown (Coexist / Pen priority / Palm reject only). -- Persisted in `AppSettings` JSON. -- Tray icon menu shows current gesture mode at a glance. - -### 7.3 Documentation - -- README: new "Touch gestures" section explaining what works in which apps + the `--gestures` flag. -- README: app-compatibility table updated. Common cases: Krita / Photoshop / Affinity Photo / browsers / Inkscape — note pinch-zoom expected to work in `touch` mode; add results from on-device testing. -- README: explicit caveat that touch may be suppressed during pen use depending on Phase 0 findings. - ---- - -## 8. Testing strategy - -### 8.1 Unit tests (deterministic, no hardware) - -| Component | Test cases | -|---|---| -| `TouchStateMachine` | Single-tap, two-finger touch, slot release via `TRACKING_ID = -1`, mid-frame `SYN_DROPPED`, slot reuse with new tracking ID. | -| `TouchCoordinateMapper` | All four orientations, edge contacts (corners), aspect-ratio mismatches with screen. | -| `GestureEngine` | Pure pinch (in & out), pure pan, pure rotate, mixed, dead-zone respected, gesture begins/ends correctly on contact-count transitions, two-finger-down-then-third-finger-down does not break the active gesture. | -| `WindowsTouchInjectionOutput` | Contact-state transitions emit correct `POINTER_FLAG_*` sequences (against a fake injection backend). | -| `UinputTouchOutput` | Correct evdev byte stream emitted (against an in-memory `Libc` fake). | -| `SynthesizedScrollOutput` | Pinch scale delta → wheel-tick count, Ctrl pressed only during pinch, pan dx/dy → wheel direction. | -| `PenToolGate` | All three modes correctly gate touch output. | -| `SshDeviceStream` lifecycle | Two streams open/close cleanly under cancellation; reconnect fires both stream-down then both stream-up; no deadlock when blocked `Read` is interrupted by command dispose. | - -### 8.2 Integration tests (with hardware) - -Manual test matrix run before tagging the release: - -| App | Pinch zoom | Pan | Rotate | Notes | -|---|---|---|---|---| -| Krita | ✓ | ✓ | ✓ | Real touch + Windows Ink simultaneously | -| Photoshop | ✓ | ✓ | ✓ | | -| Affinity Photo | ✓ | ✓ | ✓ | | -| Clip Studio Paint | ✓ | ✓ | ✓ | | -| Microsoft Edge / Chrome | ✓ | ✓ | n/a | Browser pinch-zoom of pages | -| Windows Photos | ✓ | ✓ | n/a | Windows-native | -| GIMP (Linux) | ✓ | ✓ | ✓ | | -| Inkscape (Linux) | ✓ | ✓ | n/a | | -| Blender (Linux) | ✓ | ✓ | n/a | | - -For each app, also test in `synth` mode (expected: pinch + pan work, rotate doesn't). - -### 8.3 Regression tests - -Existing pen tests must continue to pass unchanged. The pen pipeline shape must not regress (no extra latency, no dropped pen frames when touch is also active). - ---- - -## 9. Risk register & mitigations - -| Risk | Likelihood | Impact | Mitigation | -|---|---|---|---| -| `event2` is suppressed while pen is in proximity (hardware behavior). | **Confirmed 2026-05-07** | Forces "lift pen → pinch" UX; can't draw + pinch simultaneously. | Documented in README; not a code problem. Removes the need for a load-bearing `PenToolGate`. | -| SSH.NET multi-channel concurrency has unexpected limits. | Low | Have to fall back to two `SshClient`s. | Spike during Phase 2 day 1; if multi-channel works, proceed; else swap. | -| Touch sample rate over Wi-Fi causes jitter. | Medium | Pinch feels notchy on Wi-Fi. | Measure during Phase 0; document USB recommendation; consider a small jitter buffer in `TouchStateMachine` if needed. | -| `InjectTouchInput` works in Windows but specific app rejects synthetic touch. | Medium | App-by-app inconsistency. | `--gestures synth` fallback gives users an out; document in README compat table. | -| Stuck contacts on disconnect (host thinks fingers are still down after SSH drops). | Medium | Cursor jammed; users hit Ctrl-C. | Synthetic "all contacts released" emitted on reconnect (§6.2). Same shape as the existing pen-up-on-reconnect. | -| 64-bit struct alignment bugs in P/Invoke (already burned us once). | Medium | Crash or silent garbage. | Mirror the existing `[FieldOffset(8)]` fix; cover with unit tests using `Marshal.SizeOf`/`Marshal.OffsetOf`. | -| Slot reuse with new tracking ID confuses the gesture engine. | Low | Gesture stutter mid-pinch. | `TouchStateMachine` keys frame contacts by tracking ID, not slot, when emitting; tested in §8.1. | -| Two-finger gesture starts mid-pinch with three fingers down. | Low | Recognizer misbehavior. | Lock the recognizer to the first two contacts that began the gesture; ignore additional contacts until count drops back to 0. Tested. | - ---- - -## 10. Sequencing & milestones - -Each phase is gated on the previous; no parallel speculative work. - -| Milestone | Status | Definition of done | Estimated effort | -|---|---|---|---| -| **M0 — Verification** | ✅ DONE | Phase 0 evtest logs committed; constants populated; pen-proximity touch suppression confirmed (firmware-level). | 0.5–1 day | -| **M1 — Core types** | ✅ DONE | `TouchContact`, `TouchFrame`, `TouchStateMachine`, `TouchCoordinateMapper`, `GestureEngine` + 22 unit tests green. | 2–3 days | -| **M2 — Transport** | ✅ DONE | `SshTransport` refactored to multi-stream, `SshDeviceStream` extracted, backwards-compat `GetReader()` retained, all 43 tests green. | 1–2 days | -| **M3 — Linux output** | ✅ DONE (code) | `UinputTouchOutput` + `ITouchOutput` interface + pipeline integration + CLI `--gestures` flag. End-to-end Krita / GIMP validation pending real hardware. | 1–2 days | -| **M4 — Windows output** | ✅ DONE (code) | `WindowsTouchInjectionOutput` (PT_TOUCH synthetic pointer device, multi-contact lifecycle tracked, [FieldOffset(8)] alignment matches the existing pen quirk). 6 smoke tests green (instantiate real Win32 API). End-to-end app testing pending real hardware. | 3–5 days | -| **M5 — Synth fallback** | DROPPED | Real touch injection covers all current use cases (Krita confirmed working on real hardware 2026-05-07). Out of scope unless a specific app proves incompatible. | — | -| **M6 — UX surface** | ✅ DONE | CLI `--gestures` flag wired; GUI Settings → "Touch Gestures" group box with persisted `Gestures` field in `AppSettings`; README "Touch gestures" section + app compatibility matrix added. | 1 day | -| **M7 — Hardening** | ✅ DONE (code) | Reconnect emits all-contacts-released (`TabletPipeline.EmitTouchReleaseAll`); 6 Windows smoke tests cover Initialize / Send / ReleaseAll / Dispose lifecycle. Remaining work is hands-on app compatibility validation by user. | 1–2 days | - -**Total:** ~10–18 working days, single engineer, hardware in hand. The wide range tracks unknowns from M0 — particularly app-compatibility surprises in M4 and M7. - -A reasonable shippable mid-point is **M0 + M1 + M2 + M3 + M5 + M6** (Linux only, both modes). Windows can ship in a follow-up release. - ---- - -## 11. Out of scope (explicit non-goals for this iteration) - -- Three+ finger gestures. -- Rotate in `synth` mode (no universal target). -- Per-app gesture profiles or remappings. -- Touch as a cursor (point with finger). Touch is gestures only; pointing remains the pen's job. -- iPadOS-style "Scribble" (handwriting → text). Out of scope; different problem. -- macOS support. The project is Windows + Linux today; touch doesn't change that. - ---- - -## 12. M0 verification — answers (resolved 2026-05-07) - -1. **Device path:** `/dev/input/event2`. Driver name `pt_mt`. ✓ -2. **Protocol:** MT-B (Slot protocol). ✓ -3. **Coordinate range:** X 0–1403, Y 0–1871 (display-aligned). ✓ -4. **Max slots reported:** 32 (we cap our state machine at 5 — `TouchMaxTracked = 5`). -5. **Pressure:** reported, range 0–255. ✓ -6. **Sample rate:** ~85 Hz over USB SSH (faster than typical 60 Hz capacitive touch). -7. **Pen-proximity behavior:** **firmware suppresses touch while the pen is in proximity.** No host-side `PenToolGate` is required for correctness — see §1.3. -8. **`BTN_TOUCH`:** **NOT reported on this device.** The only `EV_KEY` codes are `KEY_F1`–`F8`. Contact lifecycle must be derived purely from `ABS_MT_TRACKING_ID` transitions (positive ⇒ start, `-1` ⇒ release). The state machine has no fallback signal — be strict about tracking IDs. -9. **`ABS_MT_TOOL_TYPE`:** reported (range 0–1). May enable future palm rejection via tool-type discrimination — capture in `TouchContact` for forward compatibility, but don't act on it in v1. -10. **Other axes:** `ABS_MT_TOUCH_MAJOR/MINOR` (0–255 each) and `ABS_MT_ORIENTATION` (-127–127) are reported. Capture but don't use yet. - -**No remaining blockers.** Implementation can proceed against M1. - -### Locked-in constants (to add to `ReMarkable2Constants.cs` in M1) - -```csharp -// Touchscreen — verified 2026-05-07 via evtest /dev/input/event2 (driver: pt_mt) -public const string TouchDevicePath = "/dev/input/event2"; -public const int TouchXMin = 0, TouchXMax = 1403; -public const int TouchYMin = 0, TouchYMax = 1871; -public const int TouchPressureMin = 0, TouchPressureMax = 255; -public const int TouchMaxSlots = 32; // hardware reports -public const int TouchMaxTracked = 5; // we cap our state machine here -// BTN_TOUCH is NOT reported — contact lifecycle uses ABS_MT_TRACKING_ID only. -``` diff --git a/src/RemarkableTablet.App/App.xaml.cs b/src/RemarkableTablet.App/App.xaml.cs index 8f8099b..057a844 100644 --- a/src/RemarkableTablet.App/App.xaml.cs +++ b/src/RemarkableTablet.App/App.xaml.cs @@ -1,5 +1,6 @@ using System.IO; using System.Windows; +using RemarkableTablet.Core.Devices; using RemarkableTablet.Core.Mapping; using RemarkableTablet.Core.Output; using RemarkableTablet.Core.Pipeline; @@ -34,6 +35,14 @@ public App() public event Action? PipelineStateChanged; + /// + /// Raised on the UI thread when StartPipeline fails before reaching + /// the streaming loop — typically a probe timeout or auth failure. + /// Gives SettingsWindow something to display instead of a silent + /// transition to Disconnected. + /// + public event Action? ConnectionErrorOccurred; + protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); @@ -69,31 +78,89 @@ internal static void WriteLog(string message) } public void StartPipeline(ConnectionOptions connOpts, MappingOptions mappingOpts, string outputMode, - bool gestures = false, string? pressureCurve = null) + bool gestures = false, string? pressureCurve = null, string device = "auto") { if (_pipeline is not null) return; + _ = StartPipelineAsync(connOpts, mappingOpts, outputMode, gestures, pressureCurve, device); + } - var mapper = new CoordinateMapper(mappingOpts, PressureCurve.FromName(pressureCurve)); - var output = outputMode == OutputModes.Mouse - ? (IOutputMode)new MouseOutput() - : new WindowsInkOutput(); - - TouchCoordinateMapper? touchMapper = null; - ITouchOutput? touchOutput = null; - if (gestures) + private async Task StartPipelineAsync(ConnectionOptions connOpts, MappingOptions mappingOpts, + string outputMode, bool gestures, string? pressureCurve, string device) + { + SshTransport? transport = null; + var transferred = false; + try { - touchMapper = new TouchCoordinateMapper(mappingOpts); - touchOutput = new WindowsTouchInjectionOutput(); + // Resolve the profile. For "auto", we connect once for the probe + // and then hand the same SshTransport to the pipeline — its first + // ConnectAsync is a no-op (idempotent on already-connected + // clients), which avoids paying for two SSH handshakes back-to- + // back and halves the chance of a transient USB-Ethernet flake + // stalling startup. For explicit rm2/rmpp the pipeline does its + // own connect on the empty transport, same as before. + transport = new SshTransport(connOpts); + + DeviceProfile profile; + var named = DeviceDetector.ByName(device); + if (named is not null) + { + profile = named; + } + else + { + await transport.ConnectAsync(CancellationToken.None); + profile = await DeviceDetector.DetectAsync(transport, CancellationToken.None); + WriteLog($"Detected: {profile.Name}"); + } + + var mapper = new CoordinateMapper(mappingOpts, profile, PressureCurve.FromName(pressureCurve)); + var output = outputMode == OutputModes.Mouse + ? (IOutputMode)new MouseOutput() + : new WindowsInkOutput(); + + TouchCoordinateMapper? touchMapper = null; + ITouchOutput? touchOutput = null; + if (gestures) + { + touchMapper = new TouchCoordinateMapper(mappingOpts, profile); + touchOutput = new WindowsTouchInjectionOutput(profile.Touch.MaxTracked); + } + + var pipeline = new TabletPipeline(transport, profile, mapper, output, touchMapper, touchOutput); + pipeline.ConnectionStateChanged += OnPipelineStateChanged; + pipeline.Error += ex => + { + WriteLog($"Pipeline error: {ex}"); + // Forward to the UI so reconnect-loop failures (e.g. wrong IP, + // device asleep) show a useful message instead of a silent + // "Disconnected." Run on the dispatcher because the pipeline + // raises Error from a thread-pool thread. + var msg = ex.Message; + _ = Dispatcher.BeginInvoke(() => ConnectionErrorOccurred?.Invoke(msg)); + }; + + _pipeline = pipeline; + transferred = true; // pipeline owns the transport from here + await RunPipelineAsync(pipeline); + } + catch (Exception ex) + { + WriteLog($"StartPipeline failed: {ex}"); + if (!transferred && transport is not null) + { + try { await transport.DisposeAsync(); } catch { /* best-effort */ } + } + _pipeline = null; + var msg = ex.Message; + await Dispatcher.BeginInvoke(() => + { + // Order matters: fire Disconnected first so the SettingsWindow + // updates its status text, then fire the error so the more + // useful message overwrites the generic "Disconnected." line. + PipelineStateChanged?.Invoke(ConnectionState.Disconnected); + ConnectionErrorOccurred?.Invoke(msg); + }); } - - var transport = new SshTransport(connOpts); - - var pipeline = new TabletPipeline(transport, mapper, output, touchMapper, touchOutput); - pipeline.ConnectionStateChanged += OnPipelineStateChanged; - pipeline.Error += ex => WriteLog($"Pipeline error: {ex}"); - - _pipeline = pipeline; - _ = RunPipelineAsync(pipeline); } public void StopPipeline() diff --git a/src/RemarkableTablet.App/AppSettings.cs b/src/RemarkableTablet.App/AppSettings.cs index 2275edd..94f2243 100644 --- a/src/RemarkableTablet.App/AppSettings.cs +++ b/src/RemarkableTablet.App/AppSettings.cs @@ -29,6 +29,9 @@ public sealed class AppSettings /// "linear" (default), "soft" (boost light strokes), or "hard" (suppress light strokes). public string PressureCurve { get; set; } = "linear"; + /// "auto" (probe via uname -m), "rm2", or "rmpp". + public string Device { get; set; } = "auto"; + public static AppSettings Load() { try diff --git a/src/RemarkableTablet.App/OutputModes.cs b/src/RemarkableTablet.App/OutputModes.cs index de70d40..5c81a35 100644 --- a/src/RemarkableTablet.App/OutputModes.cs +++ b/src/RemarkableTablet.App/OutputModes.cs @@ -5,4 +5,4 @@ internal static class OutputModes { public const string Ink = "ink"; public const string Mouse = "mouse"; -} +} \ No newline at end of file diff --git a/src/RemarkableTablet.App/SettingsWindow.xaml b/src/RemarkableTablet.App/SettingsWindow.xaml index 7f1c270..38b6ed2 100644 --- a/src/RemarkableTablet.App/SettingsWindow.xaml +++ b/src/RemarkableTablet.App/SettingsWindow.xaml @@ -29,13 +29,21 @@ + -