sticklink 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. sticklink-0.1.1/CHANGELOG.md +29 -0
  2. sticklink-0.1.1/MANIFEST.in +7 -0
  3. sticklink-0.1.1/PKG-INFO +135 -0
  4. sticklink-0.1.1/README.md +114 -0
  5. sticklink-0.1.1/THIRD_PARTY_NOTICES.md +35 -0
  6. sticklink-0.1.1/docs/api.md +77 -0
  7. sticklink-0.1.1/docs/architecture.md +54 -0
  8. sticklink-0.1.1/docs/cli.md +51 -0
  9. sticklink-0.1.1/docs/development.md +74 -0
  10. sticklink-0.1.1/docs/getting-started.md +69 -0
  11. sticklink-0.1.1/docs/img/classic-trail-before-after.png +0 -0
  12. sticklink-0.1.1/docs/img/fx-arcade.png +0 -0
  13. sticklink-0.1.1/docs/img/fx-hacker.png +0 -0
  14. sticklink-0.1.1/docs/img/fx-neon.png +0 -0
  15. sticklink-0.1.1/docs/img/hud-battery-arcade.png +0 -0
  16. sticklink-0.1.1/docs/img/hud-corners-hacker.png +0 -0
  17. sticklink-0.1.1/docs/img/hud-corners-neon.png +0 -0
  18. sticklink-0.1.1/docs/img/hud-gps-inferno.png +0 -0
  19. sticklink-0.1.1/docs/img/hud-link-clean.png +0 -0
  20. sticklink-0.1.1/docs/img/hud-row-synthwave.png +0 -0
  21. sticklink-0.1.1/docs/img/modes.png +0 -0
  22. sticklink-0.1.1/docs/img/setup.png +0 -0
  23. sticklink-0.1.1/docs/img/swagger.png +0 -0
  24. sticklink-0.1.1/docs/obs-setup.md +61 -0
  25. sticklink-0.1.1/docs/overlay-guide.md +157 -0
  26. sticklink-0.1.1/docs/protocol.md +62 -0
  27. sticklink-0.1.1/docs/radio-setup.md +83 -0
  28. sticklink-0.1.1/docs/scenes.md +71 -0
  29. sticklink-0.1.1/docs/troubleshooting.md +85 -0
  30. sticklink-0.1.1/docs/validation.md +79 -0
  31. sticklink-0.1.1/fx/VENDORED_FROM +3 -0
  32. sticklink-0.1.1/fx/package-lock.json +530 -0
  33. sticklink-0.1.1/fx/package.json +16 -0
  34. sticklink-0.1.1/fx/src/config.ts +80 -0
  35. sticklink-0.1.1/fx/src/curve.ts +21 -0
  36. sticklink-0.1.1/fx/src/drone3d.ts +64 -0
  37. sticklink-0.1.1/fx/src/element.ts +223 -0
  38. sticklink-0.1.1/fx/src/hud/blocks.ts +214 -0
  39. sticklink-0.1.1/fx/src/hud/catalog.ts +146 -0
  40. sticklink-0.1.1/fx/src/hud/geo.ts +103 -0
  41. sticklink-0.1.1/fx/src/hud/history.ts +21 -0
  42. sticklink-0.1.1/fx/src/hud/hud-element.ts +246 -0
  43. sticklink-0.1.1/fx/src/hud/index.ts +3 -0
  44. sticklink-0.1.1/fx/src/hud/layout.ts +50 -0
  45. sticklink-0.1.1/fx/src/hud/look.ts +179 -0
  46. sticklink-0.1.1/fx/src/hud/map.ts +100 -0
  47. sticklink-0.1.1/fx/src/index.ts +3 -0
  48. sticklink-0.1.1/fx/src/live.ts +243 -0
  49. sticklink-0.1.1/fx/src/mapping.ts +117 -0
  50. sticklink-0.1.1/fx/src/modes/model.ts +73 -0
  51. sticklink-0.1.1/fx/src/modes/modes-page.ts +273 -0
  52. sticklink-0.1.1/fx/src/modes/scale.ts +34 -0
  53. sticklink-0.1.1/fx/src/motion.ts +34 -0
  54. sticklink-0.1.1/fx/src/permalink-ui.ts +44 -0
  55. sticklink-0.1.1/fx/src/permalink.ts +66 -0
  56. sticklink-0.1.1/fx/src/render/draw.ts +803 -0
  57. sticklink-0.1.1/fx/src/render/layout.ts +59 -0
  58. sticklink-0.1.1/fx/src/render/style.ts +112 -0
  59. sticklink-0.1.1/fx/src/setup.ts +183 -0
  60. sticklink-0.1.1/fx/src/track.ts +12 -0
  61. sticklink-0.1.1/fx/test/catalog.test.ts +125 -0
  62. sticklink-0.1.1/fx/test/geo.test.ts +83 -0
  63. sticklink-0.1.1/fx/test/layout.test.ts +67 -0
  64. sticklink-0.1.1/fx/test/live.test.ts +191 -0
  65. sticklink-0.1.1/fx/test/look.test.ts +30 -0
  66. sticklink-0.1.1/fx/test/map.test.ts +54 -0
  67. sticklink-0.1.1/fx/test/mapping.test.ts +69 -0
  68. sticklink-0.1.1/fx/test/modes.test.ts +107 -0
  69. sticklink-0.1.1/fx/test/permalink.test.ts +77 -0
  70. sticklink-0.1.1/fx/tsconfig.json +7 -0
  71. sticklink-0.1.1/packaging/entry.py +5 -0
  72. sticklink-0.1.1/packaging/smoke_test.py +101 -0
  73. sticklink-0.1.1/packaging/sticklink.spec +15 -0
  74. sticklink-0.1.1/pyproject.toml +38 -0
  75. sticklink-0.1.1/setup.cfg +4 -0
  76. sticklink-0.1.1/src/sticklink/__init__.py +1 -0
  77. sticklink-0.1.1/src/sticklink/__main__.py +3 -0
  78. sticklink-0.1.1/src/sticklink/analysis.py +152 -0
  79. sticklink-0.1.1/src/sticklink/api.py +284 -0
  80. sticklink-0.1.1/src/sticklink/cli.py +115 -0
  81. sticklink-0.1.1/src/sticklink/config.py +11 -0
  82. sticklink-0.1.1/src/sticklink/fxconfig.py +242 -0
  83. sticklink-0.1.1/src/sticklink/geo.py +19 -0
  84. sticklink-0.1.1/src/sticklink/gui.py +132 -0
  85. sticklink-0.1.1/src/sticklink/obs.py +193 -0
  86. sticklink-0.1.1/src/sticklink/openapi.py +342 -0
  87. sticklink-0.1.1/src/sticklink/pipeline.py +34 -0
  88. sticklink-0.1.1/src/sticklink/protocol/__init__.py +3 -0
  89. sticklink-0.1.1/src/sticklink/protocol/ddlog.py +112 -0
  90. sticklink-0.1.1/src/sticklink/radio/DDSTK.lua +197 -0
  91. sticklink-0.1.1/src/sticklink/recorder.py +108 -0
  92. sticklink-0.1.1/src/sticklink/scenes.py +275 -0
  93. sticklink-0.1.1/src/sticklink/server.py +104 -0
  94. sticklink-0.1.1/src/sticklink/service.py +83 -0
  95. sticklink-0.1.1/src/sticklink/sinks/__init__.py +0 -0
  96. sticklink-0.1.1/src/sticklink/sinks/jsonl_log.py +46 -0
  97. sticklink-0.1.1/src/sticklink/sources/__init__.py +0 -0
  98. sticklink-0.1.1/src/sticklink/sources/base.py +11 -0
  99. sticklink-0.1.1/src/sticklink/sources/demo.py +60 -0
  100. sticklink-0.1.1/src/sticklink/sources/replay.py +46 -0
  101. sticklink-0.1.1/src/sticklink/sources/serial_port.py +37 -0
  102. sticklink-0.1.1/src/sticklink/state.py +172 -0
  103. sticklink-0.1.1/src/sticklink/web/client.js +45 -0
  104. sticklink-0.1.1/src/sticklink/web/components.js +180 -0
  105. sticklink-0.1.1/src/sticklink/web/docs.html +16 -0
  106. sticklink-0.1.1/src/sticklink/web/fonts/LICENSES/bungee-OFL.txt +93 -0
  107. sticklink-0.1.1/src/sticklink/web/fonts/LICENSES/fredoka-OFL.txt +93 -0
  108. sticklink-0.1.1/src/sticklink/web/fonts/LICENSES/orbitron-OFL.txt +93 -0
  109. sticklink-0.1.1/src/sticklink/web/fonts/LICENSES/vt323-OFL.txt +93 -0
  110. sticklink-0.1.1/src/sticklink/web/fonts/bungee-latin-400-normal.woff2 +0 -0
  111. sticklink-0.1.1/src/sticklink/web/fonts/fredoka-latin-700-normal.woff2 +0 -0
  112. sticklink-0.1.1/src/sticklink/web/fonts/orbitron-latin-900-normal.woff2 +0 -0
  113. sticklink-0.1.1/src/sticklink/web/fonts/vt323-latin-400-normal.woff2 +0 -0
  114. sticklink-0.1.1/src/sticklink/web/fx.html +17 -0
  115. sticklink-0.1.1/src/sticklink/web/hud.html +17 -0
  116. sticklink-0.1.1/src/sticklink/web/hud.js +32 -0
  117. sticklink-0.1.1/src/sticklink/web/modes.html +53 -0
  118. sticklink-0.1.1/src/sticklink/web/modes.js +21 -0
  119. sticklink-0.1.1/src/sticklink/web/overlay.html +16 -0
  120. sticklink-0.1.1/src/sticklink/web/setup.html +33 -0
  121. sticklink-0.1.1/src/sticklink/web/setup.js +21 -0
  122. sticklink-0.1.1/src/sticklink/web/stickfx.js +29 -0
  123. sticklink-0.1.1/src/sticklink/web/swagger/LICENSE +202 -0
  124. sticklink-0.1.1/src/sticklink/web/swagger/NOTICE +2 -0
  125. sticklink-0.1.1/src/sticklink/web/swagger/README.txt +1 -0
  126. sticklink-0.1.1/src/sticklink/web/swagger/favicon-32x32.png +0 -0
  127. sticklink-0.1.1/src/sticklink/web/swagger/init.js +6 -0
  128. sticklink-0.1.1/src/sticklink/web/swagger/swagger-ui-bundle.js +2 -0
  129. sticklink-0.1.1/src/sticklink/web/swagger/swagger-ui.css +3 -0
  130. sticklink-0.1.1/src/sticklink.egg-info/PKG-INFO +135 -0
  131. sticklink-0.1.1/src/sticklink.egg-info/SOURCES.txt +146 -0
  132. sticklink-0.1.1/src/sticklink.egg-info/dependency_links.txt +1 -0
  133. sticklink-0.1.1/src/sticklink.egg-info/entry_points.txt +2 -0
  134. sticklink-0.1.1/src/sticklink.egg-info/requires.txt +9 -0
  135. sticklink-0.1.1/src/sticklink.egg-info/top_level.txt +1 -0
  136. sticklink-0.1.1/tests/fake_obs.py +86 -0
  137. sticklink-0.1.1/tests/test_api.py +303 -0
  138. sticklink-0.1.1/tests/test_components.cjs +59 -0
  139. sticklink-0.1.1/tests/test_fxconfig.py +82 -0
  140. sticklink-0.1.1/tests/test_gps.py +142 -0
  141. sticklink-0.1.1/tests/test_hud_browser.py +282 -0
  142. sticklink-0.1.1/tests/test_log_replay.py +126 -0
  143. sticklink-0.1.1/tests/test_obs.py +159 -0
  144. sticklink-0.1.1/tests/test_protocol_state.py +100 -0
  145. sticklink-0.1.1/tests/test_radio_script.py +111 -0
  146. sticklink-0.1.1/tests/test_scene_api.py +175 -0
  147. sticklink-0.1.1/tests/test_scenes.py +252 -0
  148. sticklink-0.1.1/tests/test_service.py +98 -0
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ ## 0.1.1 (2026-10-06)
4
+
5
+ - A small window (`sticklink gui`, also what you get when you start the program without arguments): pick the radio or Demo, Start / Stop, status light, buttons for the web pages.
6
+ - Released on PyPI: `uvx sticklink run --demo`, `pip install sticklink`.
7
+ - README links and images are absolute, so the PyPI page renders.
8
+
9
+ ## 0.1.0 (2026-10-06)
10
+
11
+ First packaged version.
12
+
13
+ - Live stick, switch and telemetry capture from an EdgeTX radio over USB serial (`DDSTK.lua`, DDLOG protocol v1).
14
+ - Overlay pages for OBS browser sources: classic panel (`/overlay`) and the effects overlay (`/fx`, stickcam renderer, 8 styles).
15
+ - Double-click settings panel; receiver-style setup page (`/setup`) with a 3D quad, channel monitor and learn-by-moving mapping.
16
+ - Smooth, lag-free trails from 20 Hz radio data.
17
+ - Session recordings (JSONL), `replay`, `check-log` quality reports.
18
+ - REST API with OpenAPI spec and offline Swagger UI (`/docs`).
19
+ - The effects overlay fills any Browser Source size and keeps the gimbals centered in it.
20
+ - Permanent links: the settings panel shows a copyable `?cfg=` link for the current configuration (the address bar never changes). A link page uses exactly
21
+ that configuration and does not write to the server; plain URLs keep using the saved settings.
22
+ - Scene switching: `/modes` page (Betaflight Modes style) to switch OBS scenes from radio switches, with ranges per scene, priority sorting, learn-by-flipping,
23
+ safe defaults (off until enabled, adopts the current positions, never acts on stale data); OBS WebSocket client; new API under `/api/v1/obs` and `/api/v1/scene-modes`.
24
+ - Every POST now requires `Content-Type: application/json`, even with an empty body (closes cross-site form triggers).
25
+ - Telemetry HUD (`/hud`, `/hud/link`, `/hud/battery`, `/hud/gps`): Link, Battery, GPS and Status blocks in the stickcam styles, three layouts, and a GPS map
26
+ with live track, home marker and distance (OpenStreetMap tiles by default, switchable, with a track-only fallback).
27
+ - The radio script forwards a broad sensor list (round-robin, per-sensor failure isolation) and the GPS position; new `G` protocol record; sensor names may contain `%`.
28
+ - New API: `GET /api/v1/gps`, `GET`/`DELETE /api/v1/gps/track`; `hud` settings.
29
+ - Self-contained binaries for Linux, Windows and macOS, plus a Python wheel and source package.
@@ -0,0 +1,7 @@
1
+ include README.md CHANGELOG.md THIRD_PARTY_NOTICES.md
2
+ recursive-include docs *.md *.png
3
+ recursive-include tests *.py *.cjs
4
+ recursive-include packaging *
5
+ graft fx
6
+ prune fx/node_modules
7
+ global-exclude __pycache__ *.pyc
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.4
2
+ Name: sticklink
3
+ Version: 0.1.1
4
+ Summary: Live stick and telemetry overlays for OBS, from an EdgeTX radio (RadioMaster Pocket)
5
+ Project-URL: Repository, https://github.com/rotordeck/sticklink
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Environment :: Console
8
+ Classifier: Topic :: Multimedia :: Video
9
+ Classifier: Operating System :: POSIX :: Linux
10
+ Classifier: Operating System :: Microsoft :: Windows
11
+ Classifier: Operating System :: MacOS
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: aiohttp<4,>=3.10
15
+ Requires-Dist: pyserial<4,>=3.5
16
+ Provides-Extra: dev
17
+ Requires-Dist: openapi-spec-validator>=0.7; extra == "dev"
18
+ Requires-Dist: build>=1.2; extra == "dev"
19
+ Provides-Extra: binary
20
+ Requires-Dist: pyinstaller>=6.10; extra == "binary"
21
+
22
+ # Sticklink
23
+
24
+ **Live stick and telemetry overlays for OBS, straight from your radio.**
25
+ Sticklink reads your sticks, switches and link telemetry from an EdgeTX radio (tested on a RadioMaster Pocket) over USB,
26
+ serves them as an OBS Browser Source with the same look as [stickcam](https://github.com/rotordeck/stickcam)'s after-the-fact
27
+ overlays, and records every session so you can check and reuse the data later.
28
+
29
+ <p align="center">
30
+ <img src="https://raw.githubusercontent.com/rotordeck/sticklink/main/docs/img/fx-neon.png" width="32%" alt="Neon style">
31
+ <img src="https://raw.githubusercontent.com/rotordeck/sticklink/main/docs/img/fx-arcade.png" width="32%" alt="Arcade style">
32
+ <img src="https://raw.githubusercontent.com/rotordeck/sticklink/main/docs/img/fx-hacker.png" width="32%" alt="Hacker style">
33
+ </p>
34
+ <p align="center">
35
+ <img src="https://raw.githubusercontent.com/rotordeck/sticklink/main/docs/img/hud-corners-neon.png" width="49%" alt="Telemetry HUD, neon">
36
+ <img src="https://raw.githubusercontent.com/rotordeck/sticklink/main/docs/img/hud-corners-hacker.png" width="49%" alt="Telemetry HUD, hacker">
37
+ </p>
38
+
39
+ ## What you get
40
+
41
+ - **Effects overlay** (`/fx`): eight styles (Clean, Minimal, Neon, Arcade, Synthwave, Inferno, Unicorn, Hacker) with glowing
42
+ trails, sparks, shockwaves and live throttle timers. Trails are smooth curves with no added delay.
43
+ - **Telemetry HUD** (`/hud`): **Link**, **Battery**, **GPS** and **Status** blocks as badges, in the same eight styles. GPS shows a live
44
+ map with your track, home marker and distance. Each block is also its own page (`/hud/link`, `/hud/battery`, `/hud/gps`), so you can
45
+ put them in OBS as separate sources. It shows what your radio really receives (see [limits](https://github.com/rotordeck/sticklink#status-and-honest-limits)).
46
+ - **Receiver setup page** (`/setup`): a live 3D quad, stick bars and a 16-channel monitor, like Betaflight Configurator's
47
+ receiver tab. Press **Learn** and move a stick or switch to assign roll, pitch, yaw, throttle, ARM and Crash Flip, so a
48
+ swapped or reversed stick is a ten-second fix.
49
+ - **Double-click to configure**: style, effect strength, size and video delay, saved on the
50
+ server so an OBS source updates by itself. The panel also shows a **permanent link** for the current look: use it as a source's URL to pin
51
+ that source to its own configuration.
52
+ - **Scene switching** (`/modes`): a Betaflight-style Modes page for your **OBS scenes**: give each scene ranges on a switch channel, sort them so the
53
+ upper scene wins, and OBS follows your switches (via OBS's built-in WebSocket). Off until you turn it on, and it never acts on stale radio data.
54
+ - **Recordings**: one JSONL file per session, a `check-log` quality report (rate, gaps, lost records, channel ranges) and
55
+ `replay` to play a session back through the overlay.
56
+ - **REST API + Swagger UI** (`/docs`, works offline) and a WebSocket stream for your own tools.
57
+ - **No dependencies to install**: downloads are single self-contained programs for Linux, Windows and macOS (they carry
58
+ their own Python). A pip-installable Python package is available too.
59
+
60
+ ## Quick start
61
+
62
+ 1. **Download** the build for your system from the [Releases](https://github.com/rotordeck/sticklink/releases) page, and unpack it.
63
+ 1b. **Prefer a window?** Start the program with no arguments (double-click it): pick your radio (or *Demo*), press **Start**, and use the buttons to open the pages. Same as `sticklink gui`.
64
+ 2. **Try it without a radio**:
65
+ ```bash
66
+ ./sticklink run --demo # Windows: sticklink.exe run --demo
67
+ ```
68
+ Open <http://127.0.0.1:8765/fx>. You should see two gimbals moving by themselves.
69
+ 3. **Connect your radio**: install the script and set USB-VCP to LUA ([radio setup](https://github.com/rotordeck/sticklink/blob/main/docs/radio-setup.md)), then
70
+ ```bash
71
+ ./sticklink list-ports
72
+ ./sticklink run --port /dev/ttyACM0 --input-label sticks # Windows: --port COM5
73
+ ```
74
+ 4. **Assign your sticks and switches** at <http://127.0.0.1:8765/setup> (press *Learn*, move the control).
75
+ 5. **Add it to OBS** as a Browser Source with the URL `http://127.0.0.1:8765/fx`, size 576x450, 60 fps
76
+ ([OBS setup](https://github.com/rotordeck/sticklink/blob/main/docs/obs-setup.md)). Double-click the page (in OBS: right-click, *Interact*) to change the look.
77
+
78
+ Prefer Python? Once published on PyPI: `uvx sticklink run --demo` (no install, needs [uv](https://docs.astral.sh/uv/)) or
79
+ `pip install sticklink`. Or `pip install sticklink-<version>-py3-none-any.whl` (from the release page), or from a checkout
80
+ `pip install .`; then run `sticklink ...`.
81
+
82
+ ## Pages and commands
83
+
84
+ | Address / command | What it does |
85
+ |---|---|
86
+ | `/fx` | Effects overlay for OBS (double-click for settings) |
87
+ | `/hud`, `/hud/link`, `/hud/battery`, `/hud/gps` | Telemetry HUD (combined, or one block per page) |
88
+ | `/setup` | Receiver screen: 3D quad, channel monitor, learn-by-moving assignment |
89
+ | `/modes` | Pick which OBS scene a switch selects (ranges per scene, sorted by priority) |
90
+ | `/overlay` | The plain panel with two sticks, ARM/flip lamps and telemetry |
91
+ | `/docs` | Swagger UI for the REST API (spec at `/api/v1/openapi.json`) |
92
+ | `sticklink run --port P \| --demo` | Serve the overlays from a radio or from synthetic data |
93
+ | `sticklink replay FILE` | Serve a recorded session as if it were live |
94
+ | `sticklink check-log FILE` | Quality report for a recording |
95
+ | `sticklink radio-script [DIR]` | Copy the EdgeTX Lua script `DDSTK.lua` (e.g. onto the radio's SD card) |
96
+ | `sticklink list-ports` | List serial ports |
97
+
98
+ More in [docs/cli.md](https://github.com/rotordeck/sticklink/blob/main/docs/cli.md).
99
+
100
+ ## Documentation
101
+
102
+ | | |
103
+ |---|---|
104
+ | [Getting started](https://github.com/rotordeck/sticklink/blob/main/docs/getting-started.md) | Install, run, first checks |
105
+ | [Radio setup](https://github.com/rotordeck/sticklink/blob/main/docs/radio-setup.md) | EdgeTX script, USB-VCP, per-OS port names |
106
+ | [OBS setup](https://github.com/rotordeck/sticklink/blob/main/docs/obs-setup.md) | Browser source, Linux/Wayland notes |
107
+ | [Overlay guide](https://github.com/rotordeck/sticklink/blob/main/docs/overlay-guide.md) | `/fx`, `/setup`, the classic overlay, settings and URL options |
108
+ | [Scene switching](https://github.com/rotordeck/sticklink/blob/main/docs/scenes.md) | `/modes`: switch OBS scenes from radio switches |
109
+ | [CLI reference](https://github.com/rotordeck/sticklink/blob/main/docs/cli.md) | All commands and options |
110
+ | [REST API](https://github.com/rotordeck/sticklink/blob/main/docs/api.md) | Endpoints, examples, security model |
111
+ | [Protocol and log format](https://github.com/rotordeck/sticklink/blob/main/docs/protocol.md) | DDLOG v1, the JSONL recording format |
112
+ | [Architecture](https://github.com/rotordeck/sticklink/blob/main/docs/architecture.md) | Modules and data flow |
113
+ | [Troubleshooting](https://github.com/rotordeck/sticklink/blob/main/docs/troubleshooting.md) | Silent port, crashes, wrong sticks, OBS |
114
+ | [Development](https://github.com/rotordeck/sticklink/blob/main/docs/development.md) | Tests, rebuilding the overlay, releases |
115
+ | [Validation log](https://github.com/rotordeck/sticklink/blob/main/docs/validation.md) | What was tested on real hardware, and what was not |
116
+
117
+ ## Status and honest limits
118
+
119
+ - Verified on real hardware: a **RadioMaster Pocket (EdgeTX 2.10)** with ExpressLRS, on **Linux**, with OBS 32. The radio
120
+ reports sticks at about **20 Hz** (EdgeTX runs function scripts every 50 ms); the overlay smooths between readings.
121
+ - The Windows and macOS builds are produced and smoke-tested by CI (unit tests, integration tests and a run of the frozen
122
+ program), but they have **not been tried with a radio** on those systems yet.
123
+ - The **HUD shows only telemetry your radio actually receives**. Betaflight does not send motor outputs, RPM, PID or gyro over
124
+ the radio link (those live in the blackbox on the quad), so there is no live motor screen. So far only **link quality (`RQly`) has been seen live on a real
125
+ radio**; the other link sensors, battery, GPS and attitude were tested with simulated data until the radio script is updated and the quad sends them.
126
+ - The GPS map downloads tiles from the internet (OpenStreetMap by default); see [overlay guide](https://github.com/rotordeck/sticklink/blob/main/docs/overlay-guide.md#telemetry-hud).
127
+ - The effects overlay shows what sticks and switches can drive: trails, sparks, snaps, punch-outs, full-throttle and hang-time
128
+ timers, arm/disarm. Flips, rolls and crash detection in stickcam need gyro and accelerometer data that the radio does not have.
129
+ - Sticklink only **reads** from the radio. It never writes to it and never changes ExpressLRS or model settings.
130
+ - Unsigned downloads: macOS Gatekeeper and Windows SmartScreen will warn once ([getting started](https://github.com/rotordeck/sticklink/blob/main/docs/getting-started.md)).
131
+
132
+ ## Licence
133
+
134
+ No licence has been chosen yet, so all rights are reserved. Third-party components are listed in
135
+ [THIRD_PARTY_NOTICES.md](https://github.com/rotordeck/sticklink/blob/main/THIRD_PARTY_NOTICES.md).
@@ -0,0 +1,114 @@
1
+ # Sticklink
2
+
3
+ **Live stick and telemetry overlays for OBS, straight from your radio.**
4
+ Sticklink reads your sticks, switches and link telemetry from an EdgeTX radio (tested on a RadioMaster Pocket) over USB,
5
+ serves them as an OBS Browser Source with the same look as [stickcam](https://github.com/rotordeck/stickcam)'s after-the-fact
6
+ overlays, and records every session so you can check and reuse the data later.
7
+
8
+ <p align="center">
9
+ <img src="https://raw.githubusercontent.com/rotordeck/sticklink/main/docs/img/fx-neon.png" width="32%" alt="Neon style">
10
+ <img src="https://raw.githubusercontent.com/rotordeck/sticklink/main/docs/img/fx-arcade.png" width="32%" alt="Arcade style">
11
+ <img src="https://raw.githubusercontent.com/rotordeck/sticklink/main/docs/img/fx-hacker.png" width="32%" alt="Hacker style">
12
+ </p>
13
+ <p align="center">
14
+ <img src="https://raw.githubusercontent.com/rotordeck/sticklink/main/docs/img/hud-corners-neon.png" width="49%" alt="Telemetry HUD, neon">
15
+ <img src="https://raw.githubusercontent.com/rotordeck/sticklink/main/docs/img/hud-corners-hacker.png" width="49%" alt="Telemetry HUD, hacker">
16
+ </p>
17
+
18
+ ## What you get
19
+
20
+ - **Effects overlay** (`/fx`): eight styles (Clean, Minimal, Neon, Arcade, Synthwave, Inferno, Unicorn, Hacker) with glowing
21
+ trails, sparks, shockwaves and live throttle timers. Trails are smooth curves with no added delay.
22
+ - **Telemetry HUD** (`/hud`): **Link**, **Battery**, **GPS** and **Status** blocks as badges, in the same eight styles. GPS shows a live
23
+ map with your track, home marker and distance. Each block is also its own page (`/hud/link`, `/hud/battery`, `/hud/gps`), so you can
24
+ put them in OBS as separate sources. It shows what your radio really receives (see [limits](https://github.com/rotordeck/sticklink#status-and-honest-limits)).
25
+ - **Receiver setup page** (`/setup`): a live 3D quad, stick bars and a 16-channel monitor, like Betaflight Configurator's
26
+ receiver tab. Press **Learn** and move a stick or switch to assign roll, pitch, yaw, throttle, ARM and Crash Flip, so a
27
+ swapped or reversed stick is a ten-second fix.
28
+ - **Double-click to configure**: style, effect strength, size and video delay, saved on the
29
+ server so an OBS source updates by itself. The panel also shows a **permanent link** for the current look: use it as a source's URL to pin
30
+ that source to its own configuration.
31
+ - **Scene switching** (`/modes`): a Betaflight-style Modes page for your **OBS scenes**: give each scene ranges on a switch channel, sort them so the
32
+ upper scene wins, and OBS follows your switches (via OBS's built-in WebSocket). Off until you turn it on, and it never acts on stale radio data.
33
+ - **Recordings**: one JSONL file per session, a `check-log` quality report (rate, gaps, lost records, channel ranges) and
34
+ `replay` to play a session back through the overlay.
35
+ - **REST API + Swagger UI** (`/docs`, works offline) and a WebSocket stream for your own tools.
36
+ - **No dependencies to install**: downloads are single self-contained programs for Linux, Windows and macOS (they carry
37
+ their own Python). A pip-installable Python package is available too.
38
+
39
+ ## Quick start
40
+
41
+ 1. **Download** the build for your system from the [Releases](https://github.com/rotordeck/sticklink/releases) page, and unpack it.
42
+ 1b. **Prefer a window?** Start the program with no arguments (double-click it): pick your radio (or *Demo*), press **Start**, and use the buttons to open the pages. Same as `sticklink gui`.
43
+ 2. **Try it without a radio**:
44
+ ```bash
45
+ ./sticklink run --demo # Windows: sticklink.exe run --demo
46
+ ```
47
+ Open <http://127.0.0.1:8765/fx>. You should see two gimbals moving by themselves.
48
+ 3. **Connect your radio**: install the script and set USB-VCP to LUA ([radio setup](https://github.com/rotordeck/sticklink/blob/main/docs/radio-setup.md)), then
49
+ ```bash
50
+ ./sticklink list-ports
51
+ ./sticklink run --port /dev/ttyACM0 --input-label sticks # Windows: --port COM5
52
+ ```
53
+ 4. **Assign your sticks and switches** at <http://127.0.0.1:8765/setup> (press *Learn*, move the control).
54
+ 5. **Add it to OBS** as a Browser Source with the URL `http://127.0.0.1:8765/fx`, size 576x450, 60 fps
55
+ ([OBS setup](https://github.com/rotordeck/sticklink/blob/main/docs/obs-setup.md)). Double-click the page (in OBS: right-click, *Interact*) to change the look.
56
+
57
+ Prefer Python? Once published on PyPI: `uvx sticklink run --demo` (no install, needs [uv](https://docs.astral.sh/uv/)) or
58
+ `pip install sticklink`. Or `pip install sticklink-<version>-py3-none-any.whl` (from the release page), or from a checkout
59
+ `pip install .`; then run `sticklink ...`.
60
+
61
+ ## Pages and commands
62
+
63
+ | Address / command | What it does |
64
+ |---|---|
65
+ | `/fx` | Effects overlay for OBS (double-click for settings) |
66
+ | `/hud`, `/hud/link`, `/hud/battery`, `/hud/gps` | Telemetry HUD (combined, or one block per page) |
67
+ | `/setup` | Receiver screen: 3D quad, channel monitor, learn-by-moving assignment |
68
+ | `/modes` | Pick which OBS scene a switch selects (ranges per scene, sorted by priority) |
69
+ | `/overlay` | The plain panel with two sticks, ARM/flip lamps and telemetry |
70
+ | `/docs` | Swagger UI for the REST API (spec at `/api/v1/openapi.json`) |
71
+ | `sticklink run --port P \| --demo` | Serve the overlays from a radio or from synthetic data |
72
+ | `sticklink replay FILE` | Serve a recorded session as if it were live |
73
+ | `sticklink check-log FILE` | Quality report for a recording |
74
+ | `sticklink radio-script [DIR]` | Copy the EdgeTX Lua script `DDSTK.lua` (e.g. onto the radio's SD card) |
75
+ | `sticklink list-ports` | List serial ports |
76
+
77
+ More in [docs/cli.md](https://github.com/rotordeck/sticklink/blob/main/docs/cli.md).
78
+
79
+ ## Documentation
80
+
81
+ | | |
82
+ |---|---|
83
+ | [Getting started](https://github.com/rotordeck/sticklink/blob/main/docs/getting-started.md) | Install, run, first checks |
84
+ | [Radio setup](https://github.com/rotordeck/sticklink/blob/main/docs/radio-setup.md) | EdgeTX script, USB-VCP, per-OS port names |
85
+ | [OBS setup](https://github.com/rotordeck/sticklink/blob/main/docs/obs-setup.md) | Browser source, Linux/Wayland notes |
86
+ | [Overlay guide](https://github.com/rotordeck/sticklink/blob/main/docs/overlay-guide.md) | `/fx`, `/setup`, the classic overlay, settings and URL options |
87
+ | [Scene switching](https://github.com/rotordeck/sticklink/blob/main/docs/scenes.md) | `/modes`: switch OBS scenes from radio switches |
88
+ | [CLI reference](https://github.com/rotordeck/sticklink/blob/main/docs/cli.md) | All commands and options |
89
+ | [REST API](https://github.com/rotordeck/sticklink/blob/main/docs/api.md) | Endpoints, examples, security model |
90
+ | [Protocol and log format](https://github.com/rotordeck/sticklink/blob/main/docs/protocol.md) | DDLOG v1, the JSONL recording format |
91
+ | [Architecture](https://github.com/rotordeck/sticklink/blob/main/docs/architecture.md) | Modules and data flow |
92
+ | [Troubleshooting](https://github.com/rotordeck/sticklink/blob/main/docs/troubleshooting.md) | Silent port, crashes, wrong sticks, OBS |
93
+ | [Development](https://github.com/rotordeck/sticklink/blob/main/docs/development.md) | Tests, rebuilding the overlay, releases |
94
+ | [Validation log](https://github.com/rotordeck/sticklink/blob/main/docs/validation.md) | What was tested on real hardware, and what was not |
95
+
96
+ ## Status and honest limits
97
+
98
+ - Verified on real hardware: a **RadioMaster Pocket (EdgeTX 2.10)** with ExpressLRS, on **Linux**, with OBS 32. The radio
99
+ reports sticks at about **20 Hz** (EdgeTX runs function scripts every 50 ms); the overlay smooths between readings.
100
+ - The Windows and macOS builds are produced and smoke-tested by CI (unit tests, integration tests and a run of the frozen
101
+ program), but they have **not been tried with a radio** on those systems yet.
102
+ - The **HUD shows only telemetry your radio actually receives**. Betaflight does not send motor outputs, RPM, PID or gyro over
103
+ the radio link (those live in the blackbox on the quad), so there is no live motor screen. So far only **link quality (`RQly`) has been seen live on a real
104
+ radio**; the other link sensors, battery, GPS and attitude were tested with simulated data until the radio script is updated and the quad sends them.
105
+ - The GPS map downloads tiles from the internet (OpenStreetMap by default); see [overlay guide](https://github.com/rotordeck/sticklink/blob/main/docs/overlay-guide.md#telemetry-hud).
106
+ - The effects overlay shows what sticks and switches can drive: trails, sparks, snaps, punch-outs, full-throttle and hang-time
107
+ timers, arm/disarm. Flips, rolls and crash detection in stickcam need gyro and accelerometer data that the radio does not have.
108
+ - Sticklink only **reads** from the radio. It never writes to it and never changes ExpressLRS or model settings.
109
+ - Unsigned downloads: macOS Gatekeeper and Windows SmartScreen will warn once ([getting started](https://github.com/rotordeck/sticklink/blob/main/docs/getting-started.md)).
110
+
111
+ ## Licence
112
+
113
+ No licence has been chosen yet, so all rights are reserved. Third-party components are listed in
114
+ [THIRD_PARTY_NOTICES.md](https://github.com/rotordeck/sticklink/blob/main/THIRD_PARTY_NOTICES.md).
@@ -0,0 +1,35 @@
1
+ # Third-party notices
2
+
3
+ Sticklink bundles or builds on the following. Licence texts are shipped next to the files they cover.
4
+
5
+ ## Bundled in the app
6
+
7
+ | Component | Licence | Where |
8
+ |---|---|---|
9
+ | [Swagger UI](https://github.com/swagger-api/swagger-ui) 5.33.1 (`swagger-ui-dist`, unmodified) | Apache-2.0 | `src/sticklink/web/swagger/` (`LICENSE`, `NOTICE`) |
10
+ | Fonts: Bungee, Orbitron (900), Fredoka (700), VT323, via [Fontsource](https://fontsource.org) | SIL Open Font License 1.1 | `src/sticklink/web/fonts/`, texts in `fonts/LICENSES/` |
11
+ | Stick overlay renderer (`draw.ts`, `style.ts`, `layout.ts`) | Own code, copied unchanged from [rotordeck/stickcam](https://github.com/rotordeck/stickcam) @ `ca82bf34` | `fx/src/render/`, see `fx/VENDORED_FROM` |
12
+
13
+ ## Map tiles (loaded at runtime, not bundled)
14
+
15
+ The GPS block can show map tiles. Map data is (c) OpenStreetMap contributors, available under the [Open Database Licence](https://www.openstreetmap.org/copyright).
16
+ Tiles are requested from the provider the user selects: the OpenStreetMap standard tile servers (subject to the
17
+ [OSMF Tile Usage Policy](https://operations.osmfoundation.org/policies/tiles/)), CARTO basemaps (free for non-commercial use only; see CARTO's terms), or a
18
+ custom server. Each provider's credit is always drawn on the map.
19
+
20
+ ## Python dependencies (installed or frozen into the binaries)
21
+
22
+ | Package | Licence |
23
+ |---|---|
24
+ | [aiohttp](https://github.com/aio-libs/aiohttp) and its dependencies | Apache-2.0 (and permissive licences of its dependencies) |
25
+ | [pyserial](https://github.com/pyserial/pyserial) | BSD-3-Clause |
26
+ | [PyInstaller](https://pyinstaller.org) (only used to build the binaries) | GPL-2.0 with a special exception that permits distributing the frozen programs it creates under any licence |
27
+ | Python itself (embedded in the binaries) | PSF License |
28
+
29
+ ## Build-time only (not shipped)
30
+
31
+ TypeScript, esbuild, Node.js test tooling, openapi-spec-validator, build.
32
+
33
+ ## This project
34
+
35
+ No licence has been chosen yet; until one is added, all rights are reserved by the authors.
@@ -0,0 +1,77 @@
1
+ # REST API
2
+
3
+ Interactive documentation (Swagger UI, offline): <http://127.0.0.1:8765/docs>. The machine-readable OpenAPI 3.0 description
4
+ is at `/api/v1/openapi.json`. A test checks that every route is in that description and that real responses match its schemas.
5
+
6
+ ![Swagger UI](img/swagger.png)
7
+
8
+ ## Endpoints
9
+
10
+ | Area | Method and path | What it does |
11
+ |---|---|---|
12
+ | Live | `GET /api/v1/status` | Service version, uptime, radio status, diagnostics, WebSocket clients, recording |
13
+ | | `GET /api/v1/state` | The full snapshot (also broadcast on the WebSocket) |
14
+ | | `GET /api/v1/controls` | Sticks normalised to -1..1, raw inputs, switch states |
15
+ | | `GET /api/v1/telemetry`, `/telemetry/{sensor}` | All sensors, or one by name (`RQly`, `RxBt`, ...) |
16
+ | | `GET /api/v1/channels`, `/channels/{n}` | Mixer outputs CH1-CH16, or one (1-16) |
17
+ | | `GET /api/v1/gps` | Position, home, distance and bearing from home, fix state |
18
+ | | `GET`, `DELETE /api/v1/gps/track` | The flown track (thinned to one point per 2 m, at most 5000); forget track and home |
19
+ | Settings | `GET /api/v1/styles` | The overlay styles |
20
+ | | `GET`, `PUT`, `PATCH`, `DELETE /api/v1/settings` | Read; replace everything; change some; reset to defaults |
21
+ | | `GET`, `PUT /api/v1/settings/mapping` | Which inputs drive roll, pitch, yaw, throttle, ARM, Crash Flip |
22
+ | Recording | `GET`, `POST`, `DELETE /api/v1/recording` | Current recording; start (optional `label`); stop |
23
+ | | `GET /api/v1/recordings` | Recordings in the folder, newest first |
24
+ | | `GET`, `DELETE /api/v1/recordings/{name}` | Download the raw JSONL; delete |
25
+ | | `GET /api/v1/recordings/{name}/report` | Quality report, like `check-log` |
26
+ | OBS scenes | `GET /api/v1/obs`, `PATCH /api/v1/obs/connection`, `POST /api/v1/obs/scene` | Connection to OBS, its scenes; change the connection; switch a scene now |
27
+ | | `GET`, `PUT /api/v1/scene-modes`, `GET /api/v1/scene-modes/state`, `POST /api/v1/scene-modes/apply` | The ranges per scene; live state; sync OBS to the switches now (see [scene switching](scenes.md)) |
28
+ | Stream | WebSocket `/ws` | The state object as JSON text, about 30 times a second (receive-only) |
29
+
30
+ `/api/state` and `/api/fx-config` from earlier versions still work and are marked deprecated.
31
+
32
+ ## Examples
33
+
34
+ ```bash
35
+ curl localhost:8765/api/v1/controls
36
+ curl localhost:8765/api/v1/channels/8
37
+ curl localhost:8765/api/v1/gps
38
+ curl -X PATCH localhost:8765/api/v1/settings -H 'content-type: application/json' -d '{"hud":{"layout":"row","cells":6,"map":{"provider":"none"}}}'
39
+ curl -X PATCH localhost:8765/api/v1/settings -H 'content-type: application/json' -d '{"style":"hacker","mode":2}'
40
+ curl -X POST localhost:8765/api/v1/recording -H 'content-type: application/json' -d '{"label":"first-flight"}'
41
+ curl -X DELETE localhost:8765/api/v1/recording
42
+ curl localhost:8765/api/v1/recordings/20261006-143000-first-flight.jsonl/report
43
+ ```
44
+
45
+ ```python
46
+ import json, websockets, asyncio # pip install websockets
47
+ async def main():
48
+ async with websockets.connect("ws://127.0.0.1:8765/ws") as ws:
49
+ async for message in ws:
50
+ state = json.loads(message)
51
+ print(state["status"], state["controls"])
52
+ asyncio.run(main())
53
+ ```
54
+
55
+ Settings include a `hud` object (style, layout, blocks, battery cells, units and the map provider); a partial `hud` or `mapping` in a PATCH only changes what it names.
56
+
57
+ Values: `controls` are null unless the data is live (a reading within the last 500 ms); `channels` is null until the radio
58
+ script reports them; `status` is `live`, `demo`, `paused` or `disconnected`.
59
+
60
+ ## Errors
61
+
62
+ Always JSON: `{"error": {"code": "invalid_settings", "message": "..."}}`, with the usual statuses: 400 invalid input, 403 foreign
63
+ Host header, 404 not found, 405 wrong method, 409 conflict (e.g. already recording), 413 body over 64 KB, 415 not JSON.
64
+
65
+ ## Security model
66
+
67
+ There is no authentication, because the server only listens on `127.0.0.1`. Because a web page you visit could still
68
+ try to reach a local server, it also:
69
+
70
+ - rejects any request whose `Host` header is not `localhost`, `127.0.0.1` or `[::1]` (blocks DNS rebinding);
71
+ - requires `Content-Type: application/json` for everything that changes something, **including POSTs with an empty body** (a cross-site form
72
+ cannot send that without a pre-flight request that the server does not answer, so a web page cannot start a recording or switch your OBS scene);
73
+ - creates recordings only in its recordings folder, with names it generates (`YYYYMMDD-HHMMSS[-label].jsonl`); clients can
74
+ not choose paths, and file names in URLs are matched against a strict pattern;
75
+ - limits request bodies to 64 KB.
76
+
77
+ Do not expose the port to a network (for example by forwarding it) without putting authentication in front of it.
@@ -0,0 +1,54 @@
1
+ # Architecture
2
+
3
+ ```text
4
+ radio (DDSTK.lua) --USB serial--> Source --> Pipeline --> OverlayServer --WebSocket--> overlay pages (OBS)
5
+ | RadioState \--> REST API (/api/v1)
6
+ \--> JsonlLog (recording)
7
+ ```
8
+
9
+ | Module (`src/sticklink/`) | Role |
10
+ |---|---|
11
+ | `protocol/ddlog.py` | Line framing and the parser for the DDLOG records |
12
+ | `state.py` | `RadioState`: sessions, sequence gaps, aging, GPS fix / home / track, the snapshot sent to overlays |
13
+ | `pipeline.py` | `Pipeline`: sources call `connection()` / `accept()`; state and recording hang off it |
14
+ | `sources/` | `Source.run(pipeline)`: `serial_port.py` (reconnects), `demo.py`, `replay.py`. New inputs go here |
15
+ | `sinks/jsonl_log.py`, `recorder.py` | The recording file format; start/stop and the recordings folder |
16
+ | `server.py`, `api.py`, `openapi.py` | aiohttp app: pages, WebSocket, REST API, Host guard, the OpenAPI document |
17
+ | `geo.py` | Distance and bearing for the GPS block |
18
+ | `obs.py` | A small obs-websocket v5 client: scenes, current scene, switching; reconnects by itself |
19
+ | `scenes.py` | The scene-mode store (private file, validation) and `SceneEngine`: channel ranges to scene switches, with debounce, priority and safety rules |
20
+ | `fxconfig.py` | Validated, atomically written overlay settings and defaults (including the HUD and map settings) |
21
+ | `analysis.py` | `check-log` / report |
22
+ | `cli.py` | The command line |
23
+ | `web/` | Pages and scripts served to the browser: `fx.html`, `setup.html`, `overlay.html`, `docs.html`, bundles, fonts, Swagger UI |
24
+ | `radio/DDSTK.lua` | The EdgeTX script |
25
+
26
+ ## The overlay (TypeScript, `fx/`)
27
+
28
+ | File | Role |
29
+ |---|---|
30
+ | `src/render/{draw,style,layout}.ts` | stickcam's canvas renderer, copied unchanged (`fx/VENDORED_FROM`) |
31
+ | `src/live.ts` | `LiveFeed`: builds the frame history the renderer reads, one 60 fps frame at a time. Redraws earlier frames as smooth curves when a reading arrives; detects snaps, punch-outs, full-throttle and hang-time spans, arm/disarm |
32
+ | `src/mapping.ts` | Which input drives which function, and the learn-by-moving detector |
33
+ | `src/element.ts`, `src/index.ts` | The `<stick-fx>` page element (canvas, settings panel, config sync) |
34
+ | `src/setup.ts`, `src/drone3d.ts` | The `/setup` page and its 3D quad |
35
+ | `src/modes/` | The `/modes` page: microsecond scale and handle maths (`scale.ts`), the card list and edits (`model.ts`), the DOM page (`modes-page.ts`) |
36
+ | `src/curve.ts` | Monotone cubic interpolation |
37
+ | `src/hud/catalog.ts` | Sensor names to labelled badges, units, bars and warning levels; builds the Link, Battery and GPS models (pure) |
38
+ | `src/hud/geo.ts`, `layout.ts`, `history.ts` | Map projection and tile maths, block placement for each layout, sparkline buffers (pure) |
39
+ | `src/hud/look.ts` | Turns a stickcam `Style` into badge plates, dials, bars and text for the HUD |
40
+ | `src/hud/blocks.ts`, `map.ts` | The four blocks, and the policy-conscious map tile loader |
41
+ | `src/hud/hud-element.ts` | The `<stick-hud>` page element: data, settings panel, GPS track upkeep, render loop |
42
+
43
+ `npm run build` (in `fx/`) bundles it with esbuild into `src/sticklink/web/stickfx.js` and `setup.js`. **The bundles are
44
+ committed**, so installing or running Sticklink never needs Node.
45
+
46
+ ## Design decisions worth knowing
47
+
48
+ - **Receive-only and local.** The bridge never writes to the radio and only listens on `127.0.0.1`.
49
+ - **Snapshots, not events.** The server publishes the whole state about 30 times a second; pages do the rest. A page can
50
+ reconnect at any time and is correct after one message.
51
+ - **One owner per serial port.** Two programs reading the same port split the data between them. Sticklink cannot detect this.
52
+ - **History is data.** The renderer is a pure function of the frame history, so smoothing is done by rewriting history
53
+ (no lag) rather than by delaying the display.
54
+ - **Settings live on the server**, not in the browser, so the OBS source and a normal browser tab show the same thing.
@@ -0,0 +1,51 @@
1
+ # Command-line reference
2
+
3
+ `sticklink <command> [options]` (the downloaded binary, or `python -m sticklink`).
4
+
5
+ ## `sticklink gui` (or no command)
6
+
7
+ A small window: radio picker (or Demo), **Start / Stop**, a status light (stopped, waiting for the radio, connected) and buttons that open the web pages. It serves on port 8765 and stops everything when closed. Needs tkinter (included in the downloads; on Linux with pip install `python3-tk`).
8
+
9
+ ## `run` - serve the overlays
10
+
11
+ Exactly one of `--port` or `--demo`.
12
+
13
+ | Option | Default | Meaning |
14
+ |---|---|---|
15
+ | `--port NAME` | | Serial port of the radio (`COM5`, `/dev/ttyACM0`, `/dev/cu.usbmodem...`) |
16
+ | `--demo` | | Synthetic sticks, switches and telemetry; no radio needed |
17
+ | `--baud N` | 115200 | Serial baud rate (USB serial ignores it in practice) |
18
+ | `--http-port N` | 8765 | Port of the web server (it always binds to 127.0.0.1) |
19
+ | `--input-label sticks\|outputs\|unknown` | unknown | What the radio's control values represent; `sticks` for the bundled script |
20
+ | `--stale-ms N` | 500 | No control sample for this long means "paused" |
21
+ | `--scale N` | 1024 | Channel value that counts as full deflection |
22
+ | `--arm-threshold N`, `--crash-threshold N` | 0 | Value above which the default channels 5 / 8 count as on |
23
+ | `--log FILE` | | Start a recording to this file straight away (a path you choose) |
24
+ | `--recordings-dir DIR` | `~/.local/share/sticklink/recordings` | Folder for recordings started through the API |
25
+ | `--fx-config FILE` | `~/.config/sticklink/fx.json` | Overlay settings file |
26
+ | `--scenes-config FILE` | `~/.config/sticklink/scenes.json` | OBS connection (including its password) and the scene modes; readable only by you |
27
+
28
+ If the radio is unplugged, Sticklink keeps running and reconnects to the same port every two seconds.
29
+
30
+ ## `replay FILE`
31
+
32
+ Plays a recording back through the overlays at its original speed. Takes the same options as `run` (except the source
33
+ ones), plus `--speed N` (default 1) and `--loop`.
34
+
35
+ ## `check-log FILE`
36
+
37
+ Prints a quality report for a recording: duration, control rate, gaps on the radio's clock and the computer's clock, lost
38
+ records, resets, channel ranges, which output channels moved, telemetry rates, notes sent by the radio, and warnings
39
+ (low rate, stalls, lost records, sticks that never moved, no telemetry).
40
+
41
+ ## `radio-script [DIR]`
42
+
43
+ Copies the EdgeTX Lua script `DDSTK.lua` into `DIR` (default: the current folder). See [radio setup](radio-setup.md).
44
+
45
+ ## `list-ports`
46
+
47
+ Prints the serial ports your system knows, with their descriptions.
48
+
49
+ ## Stopping
50
+
51
+ Press Ctrl+C. An active recording is closed properly. Background launches from some shells ignore Ctrl+C; use `kill` (SIGTERM).
@@ -0,0 +1,74 @@
1
+ # Development
2
+
3
+ ## Set up
4
+
5
+ ```bash
6
+ git clone git@github.com:rotordeck/sticklink.git && cd sticklink
7
+ python -m venv .venv && . .venv/bin/activate # Windows: .venv\Scripts\activate
8
+ pip install -e ".[dev]"
9
+ (cd fx && npm ci) # only needed to change the overlay code
10
+ ```
11
+
12
+ ## Tests
13
+
14
+ | What | Command |
15
+ |---|---|
16
+ | Python (protocol, state, log/replay, settings, REST API contract, security) | `python -m unittest discover -s tests` |
17
+ | Overlay logic (live feed, mapping, learn detector, config) | `cd fx && npm test` |
18
+ | Overlay type check | `cd fx && npm run typecheck` |
19
+ | HUD in a real browser: every page, style and layout, the permanent links, and the map's tile requests (needs Chrome and Node 22+) | `STICKLINK_BROWSER_TESTS=1 python -m unittest tests.test_hud_browser` |
20
+ | Radio script under a desktop Lua with a mocked EdgeTX (needs `lua`) | `python -m unittest tests.test_radio_script` |
21
+ | DOM test of the classic overlay (optional, needs jsdom) | `JSDOM_PATH=/path/to/node_modules/jsdom node tests/test_components.cjs` |
22
+
23
+ The API contract test needs the `dev` extra (`openapi-spec-validator`); it is skipped without it. It checks that the
24
+ OpenAPI document is valid, that every route is documented (and vice versa), and that real responses match the documented schemas.
25
+
26
+ Without a radio: `sticklink run --demo`, or `sticklink replay some-recording.jsonl --loop`.
27
+
28
+ ## Changing the overlay (`fx/`)
29
+
30
+ Edit `fx/src`, then `cd fx && npm run build`. This writes `src/sticklink/web/stickfx.js` and `setup.js`; **commit them**
31
+ (CI fails if the committed bundles differ from a fresh build). Rules to remember:
32
+
33
+ - `fx/src/render/{draw,style,layout}.ts` are stickcam's, copied unchanged. Fix bugs upstream in `rotordeck/stickcam`, then re-copy and update `fx/VENDORED_FROM`.
34
+ - Node runs the TypeScript tests with type stripping: no enums, no constructor parameter properties, `.ts` extensions in imports.
35
+
36
+ ## Changing the radio script
37
+
38
+ `src/sticklink/radio/DDSTK.lua` runs on EdgeTX's Lua runtime, which is stricter and smaller than desktop Lua. Check the syntax
39
+ with `luac -p`, and keep lines short (the longest line it sends is about 58 bytes). Anything optional is wrapped in `pcall`
40
+ so it cannot stop the stick stream. Test on a radio: see [validation log](validation.md).
41
+
42
+ ## Building the binaries locally
43
+
44
+ ```bash
45
+ pip install ".[binary]" # non-editable install: the spec collects the package's data files from it
46
+ cd packaging && pyinstaller --noconfirm --clean --distpath ../dist --workpath ../build sticklink.spec
47
+ ../dist/sticklink run --demo
48
+ ```
49
+
50
+ The result is one self-contained program (Python included). PyInstaller builds for the system it runs on, so each
51
+ platform is built on its own CI runner. After building, reinstall the editable copy: `pip install -e .`.
52
+
53
+ ## Continuous integration
54
+
55
+ | Workflow | Runs on | Does |
56
+ |---|---|---|
57
+ | `.github/workflows/ci.yml` | every push and pull request | Python tests on Linux, Windows and macOS (Python 3.10 and 3.13); overlay tests, type check and a check that the committed bundles are up to date |
58
+ | `.github/workflows/release.yml` | tags `v*`, or run by hand | builds the wheel and source package, and the PyInstaller binaries for Linux (x86-64), Windows (x86-64), macOS (Apple Silicon and Intel); smoke-tests each binary; on a tag, attaches everything with `SHA256SUMS.txt` to a GitHub Release, then uploads the wheel and source package to PyPI (so `uvx sticklink` works) |
59
+
60
+ ## Releasing
61
+
62
+ 1. Update `__version__` in `src/sticklink/__init__.py` and `CHANGELOG.md`; commit to `main`.
63
+ 2. `git tag v0.1.0 && git push origin v0.1.0` (the tag must equal `v` + `__version__`; the workflow checks it).
64
+ 3. The release workflow builds, tests the binaries and publishes the release. Tags with a `-` (`v0.2.0-rc1`) are marked pre-release.
65
+
66
+ **One-time PyPI setup** (the first upload fails until this is done): on pypi.org, *Your projects, Publishing, Add a pending publisher*:
67
+ project `sticklink`, owner `rotordeck`, repository `sticklink`, workflow `release.yml`, environment `pypi`. In GitHub, *Settings, Environments*,
68
+ create `pypi` (optionally with required reviewers). No API token is stored anywhere. Pre-release tags are not uploaded.
69
+
70
+ To try the pipeline without releasing: *Actions, Release, Run workflow*. Builds are attached to the run as artifacts.
71
+
72
+ ## Code map
73
+
74
+ [Architecture](architecture.md), [protocol](protocol.md), [REST API](api.md).