reacher2p 3.0.0b4__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 (64) hide show
  1. reacher2p-3.0.0b4/LICENSE +24 -0
  2. reacher2p-3.0.0b4/PKG-INFO +436 -0
  3. reacher2p-3.0.0b4/README.md +399 -0
  4. reacher2p-3.0.0b4/pyproject.toml +72 -0
  5. reacher2p-3.0.0b4/setup.cfg +4 -0
  6. reacher2p-3.0.0b4/src/reacher/__init__.py +22 -0
  7. reacher2p-3.0.0b4/src/reacher/__main__.py +6 -0
  8. reacher2p-3.0.0b4/src/reacher/api/__init__.py +0 -0
  9. reacher2p-3.0.0b4/src/reacher/api/app.py +339 -0
  10. reacher2p-3.0.0b4/src/reacher/api/middleware/__init__.py +0 -0
  11. reacher2p-3.0.0b4/src/reacher/api/middleware/auth.py +74 -0
  12. reacher2p-3.0.0b4/src/reacher/api/routers/__init__.py +0 -0
  13. reacher2p-3.0.0b4/src/reacher/api/routers/data.py +78 -0
  14. reacher2p-3.0.0b4/src/reacher/api/routers/discovery.py +348 -0
  15. reacher2p-3.0.0b4/src/reacher/api/routers/file.py +344 -0
  16. reacher2p-3.0.0b4/src/reacher/api/routers/firmware.py +193 -0
  17. reacher2p-3.0.0b4/src/reacher/api/routers/hardware.py +241 -0
  18. reacher2p-3.0.0b4/src/reacher/api/routers/lifecycle.py +42 -0
  19. reacher2p-3.0.0b4/src/reacher/api/routers/pairing.py +97 -0
  20. reacher2p-3.0.0b4/src/reacher/api/routers/program.py +187 -0
  21. reacher2p-3.0.0b4/src/reacher/api/routers/proxy.py +212 -0
  22. reacher2p-3.0.0b4/src/reacher/api/routers/serial.py +164 -0
  23. reacher2p-3.0.0b4/src/reacher/api/routers/session.py +93 -0
  24. reacher2p-3.0.0b4/src/reacher/api/routers/update.py +200 -0
  25. reacher2p-3.0.0b4/src/reacher/api/routers/validate.py +25 -0
  26. reacher2p-3.0.0b4/src/reacher/api/routers/validators.py +382 -0
  27. reacher2p-3.0.0b4/src/reacher/api/routers/websocket.py +406 -0
  28. reacher2p-3.0.0b4/src/reacher/device_id.py +33 -0
  29. reacher2p-3.0.0b4/src/reacher/discovery.py +291 -0
  30. reacher2p-3.0.0b4/src/reacher/hex/mega/fr.hex +2154 -0
  31. reacher2p-3.0.0b4/src/reacher/hex/mega/omission.hex +2086 -0
  32. reacher2p-3.0.0b4/src/reacher/hex/mega/pavlovian.hex +2220 -0
  33. reacher2p-3.0.0b4/src/reacher/hex/mega/pr.hex +2166 -0
  34. reacher2p-3.0.0b4/src/reacher/hex/mega/vi.hex +2138 -0
  35. reacher2p-3.0.0b4/src/reacher/hex/uno/fr.hex +1866 -0
  36. reacher2p-3.0.0b4/src/reacher/hex/uno/omission.hex +1803 -0
  37. reacher2p-3.0.0b4/src/reacher/hex/uno/pavlovian.hex +2011 -0
  38. reacher2p-3.0.0b4/src/reacher/hex/uno/pr.hex +1877 -0
  39. reacher2p-3.0.0b4/src/reacher/hex/uno/vi.hex +1848 -0
  40. reacher2p-3.0.0b4/src/reacher/kernel/__init__.py +19 -0
  41. reacher2p-3.0.0b4/src/reacher/kernel/commands.py +741 -0
  42. reacher2p-3.0.0b4/src/reacher/kernel/reacher.py +1548 -0
  43. reacher2p-3.0.0b4/src/reacher/kernel/simulator.py +650 -0
  44. reacher2p-3.0.0b4/src/reacher/machines.py +68 -0
  45. reacher2p-3.0.0b4/src/reacher/monitor.py +324 -0
  46. reacher2p-3.0.0b4/src/reacher/pairing.py +180 -0
  47. reacher2p-3.0.0b4/src/reacher/pin_overrides.py +233 -0
  48. reacher2p-3.0.0b4/src/reacher/session_manager.py +216 -0
  49. reacher2p-3.0.0b4/src/reacher/uploader/__init__.py +12 -0
  50. reacher2p-3.0.0b4/src/reacher/uploader/boards.py +83 -0
  51. reacher2p-3.0.0b4/src/reacher/uploader/uploader.py +411 -0
  52. reacher2p-3.0.0b4/src/reacher2p.egg-info/PKG-INFO +436 -0
  53. reacher2p-3.0.0b4/src/reacher2p.egg-info/SOURCES.txt +62 -0
  54. reacher2p-3.0.0b4/src/reacher2p.egg-info/dependency_links.txt +1 -0
  55. reacher2p-3.0.0b4/src/reacher2p.egg-info/entry_points.txt +3 -0
  56. reacher2p-3.0.0b4/src/reacher2p.egg-info/requires.txt +17 -0
  57. reacher2p-3.0.0b4/src/reacher2p.egg-info/top_level.txt +1 -0
  58. reacher2p-3.0.0b4/tests/test_api.py +727 -0
  59. reacher2p-3.0.0b4/tests/test_command_parity.py +84 -0
  60. reacher2p-3.0.0b4/tests/test_commands.py +171 -0
  61. reacher2p-3.0.0b4/tests/test_pin_overrides.py +181 -0
  62. reacher2p-3.0.0b4/tests/test_session_manager.py +117 -0
  63. reacher2p-3.0.0b4/tests/test_validate.py +561 -0
  64. reacher2p-3.0.0b4/tests/test_websocket.py +270 -0
@@ -0,0 +1,24 @@
1
+ This is free and unencumbered software released into the public domain.
2
+
3
+ Anyone is free to copy, modify, publish, use, compile, sell, or
4
+ distribute this software, either in source code form or as a compiled
5
+ binary, for any purpose, commercial or non-commercial, and by any
6
+ means.
7
+
8
+ In jurisdictions that recognize copyright laws, the author or authors
9
+ of this software dedicate any and all copyright interest in the
10
+ software to the public domain. We make this dedication for the benefit
11
+ of the public at large and to the detriment of our heirs and
12
+ successors. We intend this dedication to be an overt act of
13
+ relinquishment in perpetuity of all present and future rights to this
14
+ software under copyright law.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ For more information, please refer to <https://unlicense.org>
@@ -0,0 +1,436 @@
1
+ Metadata-Version: 2.4
2
+ Name: reacher2p
3
+ Version: 3.0.0b4
4
+ Summary: REACHER — Rodent Experiment Application Controls and Handling Ecosystem for Research
5
+ Author-email: Joshua Boquiren <thejoshbq@proton.me>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Otis-Lab-MUSC/REACHER
8
+ Project-URL: Repository, https://github.com/Otis-Lab-MUSC/reacher
9
+ Project-URL: Issues, https://github.com/Otis-Lab-MUSC/reacher/issues
10
+ Keywords: neuroscience,arduino,operant-conditioning,experiment-control,fastapi
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: pyserial<4.0,>=3.5
22
+ Requires-Dist: fastapi<1.0,>=0.110
23
+ Requires-Dist: uvicorn[standard]<1.0,>=0.29
24
+ Requires-Dist: websockets<15.0,>=12.0
25
+ Requires-Dist: httpx<1.0,>=0.27
26
+ Requires-Dist: zeroconf<1.0,>=0.131
27
+ Requires-Dist: rich<15.0,>=13.0
28
+ Provides-Extra: tray
29
+ Requires-Dist: pystray>=0.19; extra == "tray"
30
+ Requires-Dist: Pillow>=10.0; extra == "tray"
31
+ Provides-Extra: dev
32
+ Requires-Dist: pytest>=8.0; extra == "dev"
33
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
34
+ Requires-Dist: pytest-mock>=3.12; extra == "dev"
35
+ Requires-Dist: ruff>=0.4; extra == "dev"
36
+ Dynamic: license-file
37
+
38
+ # REACHER — Python Backend
39
+
40
+ **FastAPI server, serial communication kernel, and session manager for the REACHER ecosystem**
41
+
42
+ [![Version](https://img.shields.io/badge/version-3.0.0--beta.4-blue)](https://github.com/Otis-Lab-MUSC/reacher/releases)
43
+ [![Python](https://img.shields.io/badge/python-3.10%E2%80%933.13-blue)](https://www.python.org)
44
+ [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
45
+ [![Changelog](https://img.shields.io/badge/changelog-CHANGELOG.md-orange)](CHANGELOG.md)
46
+ [![REACHER Suite](https://img.shields.io/badge/REACHER_Suite-member-orange)](https://github.com/Otis-Lab-MUSC)
47
+
48
+ *Written by*: Joshua Boquiren
49
+
50
+ [![](https://img.shields.io/badge/@thejoshbq-grey?style=flat&logo=github)](https://github.com/thejoshbq)
51
+
52
+ ---
53
+
54
+ ## Overview
55
+
56
+ The Python backend is the core of the REACHER system. It provides:
57
+
58
+ - A **FastAPI REST API** for session management, hardware control, experiment execution, and data export
59
+ - A **WebSocket server** for real-time event streaming to the browser UI
60
+ - A **multi-threaded serial communication kernel** for bidirectional JSON messaging with Arduino hardware
61
+ - A **session manager** coordinating multiple simultaneous experiment sessions with port locking
62
+ - A **firmware uploader** for flashing Arduino `.hex` files via `avrdude`
63
+
64
+ When running as a standalone executable, the backend also serves the React frontend as static files and opens a browser window automatically.
65
+
66
+ ---
67
+
68
+ ## Role in the REACHER Ecosystem
69
+
70
+ The Python backend is the bridge between the Arduino hardware and the browser-based UI. It:
71
+
72
+ 1. Communicates with one or more Arduinos over USB serial (115200 baud, JSON messages)
73
+ 2. Exposes a REST API and WebSocket endpoint on port 6229
74
+ 3. Serves the React frontend as static files at the root URL
75
+ 4. Manages experiment sessions — starting, stopping, pausing, and collecting data
76
+ 5. Handles firmware uploads to Arduino boards via `avrdude`
77
+ 6. Logs all serial events and behavioral data for post-experiment analysis
78
+
79
+ ```
80
+ Arduino ◄──USB Serial──► REACHER Kernel ◄──► FastAPI ◄──► React Frontend
81
+ (threads) (REST+WS) (browser)
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Architecture
87
+
88
+ ### Project Structure
89
+
90
+ ```
91
+ reacher/
92
+ ├── pyproject.toml # Package metadata and dependencies
93
+ ├── src/reacher/
94
+ │ ├── __init__.py # Exports: REACHER, COMMAND_REGISTRY, CommandCode, PARADIGMS
95
+ │ ├── __main__.py # Entry point for `python -m reacher`
96
+ │ ├── session_manager.py # Multi-session coordinator with port locking
97
+ │ ├── api/
98
+ │ │ ├── app.py # FastAPI app, CORS, lifespan, static file mount
99
+ │ │ └── routers/
100
+ │ │ ├── session.py # Session CRUD
101
+ │ │ ├── serial.py # Port listing and serial connections
102
+ │ │ ├── firmware.py # Paradigm listing and firmware upload
103
+ │ │ ├── hardware.py # Command dispatch and config retrieval
104
+ │ │ ├── program.py # Start/stop/pause and limit configuration
105
+ │ │ ├── data.py # Behavior events, frames, CSV export
106
+ │ │ ├── file.py # Filename and destination configuration
107
+ │ │ ├── lifecycle.py # Graceful shutdown
108
+ │ │ └── websocket.py # Real-time event streaming
109
+ │ ├── kernel/
110
+ │ │ ├── reacher.py # Core REACHER class (serial I/O, threading, data)
111
+ │ │ └── commands.py # CommandCode enum, CommandSpec, COMMAND_REGISTRY
112
+ │ ├── uploader/
113
+ │ │ └── uploader.py # FirmwareUploader (avrdude wrapper)
114
+ │ └── hex/<board>/*.hex # Committed firmware artifacts (package data, shipped in wheel)
115
+ ├── firmware/ # Arduino firmware source (folded in from reacher-firmware)
116
+ │ ├── compile.sh # Builds all 5 paradigms -> ../src/reacher/hex/<board>/
117
+ │ ├── fr/ pr/ vi/ omission/ pavlovian/ # Per-paradigm sketches
118
+ │ └── libraries/REACHERDevices/ # Shared C++ device library + Commands.h
119
+ └── tests/
120
+ ├── test_commands.py
121
+ ├── test_command_parity.py # Asserts Commands.h matches the CommandCode enum
122
+ ├── test_session_manager.py
123
+ ├── test_api.py
124
+ └── core/
125
+ ```
126
+
127
+ ### Kernel — Multi-Threaded Serial I/O
128
+
129
+ The `REACHER` class manages all communication with a single Arduino. Each instance runs three daemon threads:
130
+
131
+ | Thread | Target | Purpose |
132
+ |---|---|---|
133
+ | `serial_thread` | `read_serial()` | Continuously reads incoming serial data and queues it |
134
+ | `queue_thread` | `handle_queue()` | Processes queued messages, delegates to event handlers |
135
+ | `time_check_thread` | `monitor_time_limit()` | Enforces time and infusion limits during experiments |
136
+
137
+ Thread coordination uses `threading.Event` flags:
138
+ - `serial_flag` — cleared to read, set to stop
139
+ - `program_flag` — cleared when running, set when paused/stopped
140
+ - `time_check_flag` — monitors limit conditions
141
+
142
+ ### Session Manager
143
+
144
+ The `SessionManager` coordinates multiple independent `REACHER` instances:
145
+
146
+ - **Port locking** — prevents two sessions from binding to the same COM port
147
+ - **Session states** — `idle` → `uploading` → `connected` → `running` → `paused` → `stopped`
148
+ - **Session IDs** — 12-character hexadecimal identifiers (from `uuid4`)
149
+ - **Event broadcasting** — state changes are forwarded to connected WebSocket clients
150
+
151
+ ### Firmware Uploader
152
+
153
+ The `FirmwareUploader` wraps `avrdude` to flash compiled `.hex` files onto the Arduino (Mega 2560, ATmega2560; legacy UNO artifacts still ship):
154
+
155
+ - Async subprocess execution with progress parsing from `avrdude` stderr
156
+ - Hex resolution order: PyInstaller bundle (`_MEIPASS/hex/`) → `REACHER_HEX_DIR` → package data (`src/reacher/hex/`, canonical) → cwd `firmware/hex/` → `~/.reacher/hex` GitHub cache. Set `REACHER_SKIP_HEX_FETCH=1` to disable the network fallback on airgapped hosts.
157
+
158
+ ### Firmware Source
159
+
160
+ Firmware source lives in `firmware/` (folded in from the now-archived `Otis-Lab-MUSC/reacher-firmware`). Five Arduino sketches share the `REACHERDevices` C++ library. `firmware/Commands.h` and `kernel/commands.py` are kept in lockstep — `tests/test_command_parity.py` fails on drift.
161
+
162
+ ```bash
163
+ arduino-cli core install arduino:avr # one-time toolchain install
164
+ bash firmware/compile.sh # recompile -> src/reacher/hex/<board>/ (commit the result)
165
+ ```
166
+
167
+ The compiled `hex/<board>/*.hex` files are **committed** package data (`pyproject.toml` glob `hex/**/*.hex`) and ship inside the wheel — there is no firmware build step in CI. Firmware version strings (`library.properties` + each sketch's `SendIdentification()`) are stamped by `scripts/bump-version.py` from the package version; recompile hex after bumping. The microscope timestamp pin (INT0) is fixed in firmware and not remappable.
168
+
169
+ ---
170
+
171
+ ## API Reference
172
+
173
+ ### REST Endpoints
174
+
175
+ #### Sessions (`/api/sessions`)
176
+
177
+ | Method | Path | Description |
178
+ |---|---|---|
179
+ | `GET` | `/api/sessions` | List all active sessions |
180
+ | `POST` | `/api/sessions` | Create a new session (body: `{port, paradigm?}`) |
181
+ | `GET` | `/api/sessions/{id}` | Get session details |
182
+ | `POST` | `/api/sessions/{id}/reset` | Reset a session instance |
183
+ | `DELETE` | `/api/sessions/{id}` | Destroy a session |
184
+
185
+ #### Serial (`/api/serial`)
186
+
187
+ | Method | Path | Description |
188
+ |---|---|---|
189
+ | `GET` | `/api/serial/ports` | List available COM/serial ports |
190
+ | `POST` | `/api/serial/{id}/connect` | Connect session to its serial port |
191
+ | `POST` | `/api/serial/{id}/disconnect` | Disconnect serial |
192
+
193
+ #### Firmware (`/api/firmware`)
194
+
195
+ | Method | Path | Description |
196
+ |---|---|---|
197
+ | `GET` | `/api/firmware/paradigms` | List available paradigm hex files |
198
+ | `POST` | `/api/firmware/upload/{id}` | Upload firmware to Arduino (body: `{paradigm}`) |
199
+
200
+ #### Hardware (`/api/hardware`)
201
+
202
+ | Method | Path | Description |
203
+ |---|---|---|
204
+ | `POST` | `/api/hardware/{id}/command` | Send command by code (body: `{code, value?}`) |
205
+ | `GET` | `/api/hardware/{id}/commands` | List commands available for the current paradigm |
206
+ | `GET` | `/api/hardware/{id}/config` | Get firmware info and hardware settings |
207
+
208
+ #### Program (`/api/program`)
209
+
210
+ | Method | Path | Description |
211
+ |---|---|---|
212
+ | `POST` | `/api/program/{id}/start` | Start the experiment |
213
+ | `POST` | `/api/program/{id}/stop` | Stop the experiment |
214
+ | `POST` | `/api/program/{id}/pause` | Toggle pause/resume |
215
+ | `POST` | `/api/program/{id}/limit` | Set limits (body: `{type, time_limit?, infusion_limit?, delay?}`) |
216
+
217
+ #### Data (`/api/data`)
218
+
219
+ | Method | Path | Description |
220
+ |---|---|---|
221
+ | `GET` | `/api/data/{id}/behavior` | Get behavioral events (supports `?since=` for pagination) |
222
+ | `GET` | `/api/data/{id}/frames` | Get frame timestamps |
223
+ | `GET` | `/api/data/{id}/export/csv` | Export behavior data as CSV download |
224
+
225
+ #### File (`/api/file`)
226
+
227
+ | Method | Path | Description |
228
+ |---|---|---|
229
+ | `POST` | `/api/file/{id}/config` | Set output filename and destination (body: `{filename?, destination?}`) |
230
+ | `POST` | `/api/file/{id}/create_folder` | Create data output folder |
231
+
232
+ #### Lifecycle (`/api/lifecycle`)
233
+
234
+ | Method | Path | Description |
235
+ |---|---|---|
236
+ | `POST` | `/api/lifecycle/shutdown` | Graceful shutdown (3-second grace period) |
237
+
238
+ ### WebSocket
239
+
240
+ | Endpoint | Description |
241
+ |---|---|
242
+ | `ws://localhost:6229/ws/{session_id}` | Real-time event stream for a session |
243
+
244
+ **Event types sent over WebSocket:**
245
+
246
+ | Type | Description |
247
+ |---|---|
248
+ | `event` | Behavioral event (lever press, pump infusion, lick, etc.) |
249
+ | `frame` | Microscope frame timestamp |
250
+ | `config` | Firmware identification and hardware settings |
251
+ | `upload_progress` | Firmware upload progress (`{percent, stage}`) |
252
+ | `session_state` | Session state change notification |
253
+
254
+ ---
255
+
256
+ ## Serial Protocol
257
+
258
+ Communication with Arduino hardware uses the following protocol:
259
+
260
+ | Parameter | Value |
261
+ |---|---|
262
+ | Baud rate | 115200 |
263
+ | Encoding | UTF-8 |
264
+ | Message format | Newline-delimited JSON |
265
+ | Identification query | `*IDN?` (SCPI-style) |
266
+
267
+ ### Event code meanings (firmware → backend)
268
+
269
+ | Code | Meaning |
270
+ |---|---|
271
+ | `000` | Configuration / firmware identification |
272
+ | `001` | Log messages (arm/disarm state changes) |
273
+ | `006` | Error messages |
274
+ | `007` | Behavioral events (lever presses, pump activations, licks, etc.) |
275
+ | `008` | Microscope frame timestamps |
276
+
277
+ ### Command code ranges (backend → firmware)
278
+
279
+ | Range | Target |
280
+ |---|---|
281
+ | 100–105 | Controller (start, stop, identify, pause) |
282
+ | 201–220 | Session setup (ratio, paradigm parameters, Pavlovian settings) |
283
+ | 300–382 | Cue/speaker control (primary and secondary) |
284
+ | 400–482 | Pump control (primary and secondary) |
285
+ | 500–501 | Lick circuit (arm/disarm) |
286
+ | 600–682 | Laser control |
287
+ | 900–903 | Microscope control |
288
+ | 1000–1081 | Right-hand lever control |
289
+ | 1300–1381 | Left-hand lever control |
290
+
291
+ The backend's `COMMAND_REGISTRY` contains 71 `CommandSpec` entries with paradigm filtering — commands are only exposed for paradigms that use them.
292
+
293
+ ---
294
+
295
+ ## Installation
296
+
297
+ ### From a wheel file
298
+
299
+ ```bash
300
+ pip install reacher2p-3.0.0b4-py3-none-any.whl
301
+ ```
302
+
303
+ ### From source
304
+
305
+ ```bash
306
+ git clone https://github.com/otis-lab-musc/reacher.git
307
+ pip install -e reacher/
308
+ ```
309
+
310
+ ---
311
+
312
+ ## Running
313
+
314
+ ### CLI command
315
+
316
+ ```bash
317
+ reacher
318
+ ```
319
+
320
+ ### Module invocation
321
+
322
+ ```bash
323
+ python -m reacher
324
+ ```
325
+
326
+ Both start the FastAPI server on `http://localhost:6229` and open a browser window. Set the `REACHER_STATIC_DIR` environment variable to point to a built frontend directory, or run from the [labrynth](https://github.com/Otis-Lab-MUSC/labrynth) root where `web/dist/` will be found automatically.
327
+
328
+ ### Port configuration
329
+
330
+ Set the `REACHER_PORT` environment variable to change the default port:
331
+
332
+ ```bash
333
+ REACHER_PORT=8080 reacher
334
+ ```
335
+
336
+ ---
337
+
338
+ ## Development
339
+
340
+ ### Setting up a development environment
341
+
342
+ ```bash
343
+ git clone https://github.com/otis-lab-musc/reacher.git
344
+ cd reacher
345
+ python -m venv .venv
346
+ source .venv/bin/activate # Linux/macOS
347
+ # .venv\Scripts\activate # Windows
348
+ pip install -e ".[dev]"
349
+ ```
350
+
351
+ ### Running tests
352
+
353
+ ```bash
354
+ pytest
355
+ ```
356
+
357
+ ### Linting
358
+
359
+ ```bash
360
+ ruff check .
361
+ ```
362
+
363
+ ---
364
+
365
+ ## Building the Standalone Executable
366
+
367
+ See [labrynth](https://github.com/Otis-Lab-MUSC/labrynth) for standalone packaging via PyInstaller.
368
+
369
+ ---
370
+
371
+ ## Configuration
372
+
373
+ ### Environment Variables
374
+
375
+ | Variable | Default | Description |
376
+ |---|---|---|
377
+ | `REACHER_PORT` | `6229` | HTTP/WebSocket server port |
378
+ | `REACHER_STATIC_DIR` | (CWD/web/dist) | Path to built React frontend directory |
379
+ | `REACHER_HEX_DIR` | (CWD/firmware/hex) | Path to pre-compiled firmware hex files |
380
+ | `REACHER_AVRDUDE_PATH` | (system PATH) | Path to `avrdude` binary (set during build/packaging, not runtime) |
381
+
382
+ ### Data Directory
383
+
384
+ REACHER stores logs and data under `~/REACHER/`:
385
+
386
+ ```
387
+ ~/REACHER/
388
+ ├── LOG/
389
+ │ └── YYYY-MM-DD_HH-MM-SS/
390
+ │ ├── controller_log.json # JSON events from firmware
391
+ │ └── interface_log.log # Python logging output
392
+ └── DATA/ # Default data export destination
393
+ ```
394
+
395
+ The data export destination can be customized per session via the File API.
396
+
397
+ ---
398
+
399
+ ## Dependencies
400
+
401
+ ### Runtime
402
+
403
+ | Package | Version | Purpose |
404
+ |---|---|---|
405
+ | pyserial | ≥3.5 | Serial port communication |
406
+ | fastapi | ≥0.110 | REST API framework |
407
+ | uvicorn[standard] | ≥0.29 | ASGI server |
408
+ | websockets | ≥12.0 | WebSocket protocol support |
409
+
410
+ ### Optional (tray extra: `pip install reacher2p[tray]`)
411
+
412
+ | Package | Version | Purpose |
413
+ |---|---|---|
414
+ | pystray | ≥0.19 | System tray icon (standalone mode) |
415
+ | Pillow | ≥10.0 | Image support for tray icon |
416
+
417
+ ### Development
418
+
419
+ | Package | Version | Purpose |
420
+ |---|---|---|
421
+ | pytest | ≥8.0 | Test runner |
422
+ | pytest-asyncio | ≥0.23 | Async test support |
423
+ | httpx | ≥0.27 | HTTP test client |
424
+ | ruff | ≥0.4 | Linter and formatter |
425
+
426
+ ---
427
+
428
+ ## License
429
+
430
+ This project is licensed under the MIT License. See [LICENSE](LICENSE) for details.
431
+
432
+ ## Contact
433
+
434
+ Joshua Boquiren — [thejoshbq@proton.me](mailto:thejoshbq@proton.me)
435
+
436
+ [GitHub: otis-lab-musc/reacher](https://github.com/otis-lab-musc/reacher)