pgntui 0.2.2__tar.gz → 0.3.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.
- {pgntui-0.2.2 → pgntui-0.3.1}/.github/workflows/release.yml +18 -0
- pgntui-0.3.1/CHANGELOG.md +104 -0
- pgntui-0.3.1/PKG-INFO +229 -0
- pgntui-0.3.1/README.md +173 -0
- pgntui-0.3.1/docs/audits/2026-06-04-audit-A-concurrency.md +64 -0
- pgntui-0.3.1/docs/audits/2026-06-04-audit-B-decoding.md +72 -0
- pgntui-0.3.1/docs/audits/2026-06-04-audit-C-platform.md +74 -0
- pgntui-0.3.1/docs/audits/2026-06-04-audit-D-packaging.md +77 -0
- pgntui-0.3.1/docs/audits/2026-06-04-release-notes-v0.3.0.md +74 -0
- pgntui-0.3.1/docs/audits/2026-06-04-round2-audit.md +71 -0
- pgntui-0.3.1/docs/audits/2026-06-04-summary.md +70 -0
- pgntui-0.3.1/packaging/README.md +110 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/packaging/homebrew/pgntui.rb +1 -1
- {pgntui-0.2.2 → pgntui-0.3.1}/packaging/winget/phobic.pgntui.yaml +2 -2
- {pgntui-0.2.2 → pgntui-0.3.1}/pyproject.toml +9 -2
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/__init__.py +1 -1
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/__main__.py +20 -6
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/app.py +54 -22
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/config.py +26 -3
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/containers/loader.py +10 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/decode/canboat.py +59 -5
- pgntui-0.3.1/src/pgntui/decode/fastpacket.py +174 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/decode/router.py +4 -1
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/drivers/actisense.py +27 -2
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/drivers/base.py +4 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/drivers/replay.py +30 -6
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/recording/reader.py +15 -3
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/recording/writer.py +5 -3
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/signals/base.py +21 -8
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/signals/widgets.py +8 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/themes/loader.py +20 -3
- pgntui-0.3.1/tests/test_app_worker_cancel_on_exit.py +195 -0
- pgntui-0.3.1/tests/test_canboat.py +130 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/test_containers_loader.py +56 -0
- pgntui-0.3.1/tests/test_fastpacket.py +187 -0
- pgntui-0.3.1/tests/test_recording_roundtrip.py +103 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/test_replay_mode.py +14 -9
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/test_router.py +19 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/test_signals_base.py +87 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/test_smoke.py +1 -1
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/test_themes_loader.py +53 -0
- pgntui-0.3.1/tests/test_writer_race.py +115 -0
- pgntui-0.2.2/PKG-INFO +0 -66
- pgntui-0.2.2/README.md +0 -15
- pgntui-0.2.2/tests/test_canboat.py +0 -26
- {pgntui-0.2.2 → pgntui-0.3.1}/.github/workflows/ci.yml +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/.gitignore +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/LICENSE +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/docs/superpowers/plans/2026-06-04-pgntui-implementation.md +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/docs/superpowers/specs/2026-06-04-pgntui-design.md +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/containers/__init__.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/containers/screen.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/debug/__init__.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/debug/tab.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/decode/__init__.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/decode/pgns.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/drivers/__init__.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/examples/__init__.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/examples/config.toml +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/examples/containers/main.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/examples/signals/anchor_light.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/examples/signals/bilge_alarm.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/examples/signals/depth.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/examples/signals/engine_rpm.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/examples/signals/speed.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/examples/signals/target_heading.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/examples/signals/water_temp.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/logging/__init__.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/logging/csv.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/recording/__init__.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/replay_mode.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/signals/__init__.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/themes/__init__.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/themes/builtin/__init__.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/themes/builtin/amber-crt.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/themes/builtin/dark.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/themes/builtin/green-phosphor.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/themes/builtin/light.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/themes/builtin/mono-ascii.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/src/pgntui/themes/builtin/rainbow-disco.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/__init__.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/fixtures/__init__.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/fixtures/e2e_containers/engine.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/fixtures/e2e_containers/nav.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/fixtures/e2e_session.pgnlog +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/fixtures/e2e_signals/engine_rpm.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/fixtures/e2e_signals/wind_speed.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/fixtures/frames.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/fixtures/sample.pgnlog +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/test_actisense_driver.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/test_app_empty_welcome.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/test_app_frame_loop.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/test_app_on_write.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/test_app_quit_binding.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/test_app_record_toggle.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/test_app_shell.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/test_builtin_themes.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/test_cli.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/test_config.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/test_container_screen.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/test_csv_logger.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/test_debug_tab.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/test_drivers_base.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/test_e2e_replay.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/test_example_workspace.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/test_main_replay.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/test_packaging.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/test_recording_writer.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/test_replay_driver.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/test_signals_loader.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/test_widgets_analog_in.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/test_widgets_analog_out.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/test_widgets_digital_in.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.1}/tests/test_widgets_digital_out.py +0 -0
|
@@ -6,8 +6,23 @@ on:
|
|
|
6
6
|
tags: ["v*"]
|
|
7
7
|
|
|
8
8
|
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
timeout-minutes: 10
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: actions/setup-python@v5
|
|
15
|
+
with: { python-version: "3.12" }
|
|
16
|
+
- run: pip install -e ".[dev,dist]"
|
|
17
|
+
- run: python -m ruff check src/ tests/
|
|
18
|
+
- run: python -m ruff format --check src/ tests/
|
|
19
|
+
- run: python -m mypy src/pgntui
|
|
20
|
+
- run: python -m pytest tests/ -v
|
|
21
|
+
|
|
9
22
|
pypi:
|
|
23
|
+
needs: test
|
|
10
24
|
runs-on: ubuntu-latest
|
|
25
|
+
timeout-minutes: 10
|
|
11
26
|
permissions:
|
|
12
27
|
id-token: write
|
|
13
28
|
contents: read
|
|
@@ -21,6 +36,7 @@ jobs:
|
|
|
21
36
|
uses: pypa/gh-action-pypi-publish@release/v1
|
|
22
37
|
|
|
23
38
|
binaries:
|
|
39
|
+
needs: test
|
|
24
40
|
strategy:
|
|
25
41
|
fail-fast: false
|
|
26
42
|
matrix:
|
|
@@ -38,6 +54,7 @@ jobs:
|
|
|
38
54
|
arch: x86_64
|
|
39
55
|
asset: pgntui-windows-x86_64.exe
|
|
40
56
|
runs-on: ${{ matrix.os }}
|
|
57
|
+
timeout-minutes: 30
|
|
41
58
|
permissions:
|
|
42
59
|
contents: write
|
|
43
60
|
steps:
|
|
@@ -54,6 +71,7 @@ jobs:
|
|
|
54
71
|
homebrew_winget:
|
|
55
72
|
needs: [pypi, binaries]
|
|
56
73
|
runs-on: ubuntu-latest
|
|
74
|
+
timeout-minutes: 5
|
|
57
75
|
steps:
|
|
58
76
|
- uses: actions/checkout@v4
|
|
59
77
|
- name: Update homebrew tap stub (manual follow-up to phobicdotno/homebrew-tap)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.3.1] — 2026-06-04
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Recording reader now parses timestamps as UTC. Replay frame deltas were unaffected, but CSV export and debug log absolute timestamps were off by the local UTC offset.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- `test_iter_frames_honors_pause_with_sliding_resume` now polls instead of hard-sleeping; faster suite, less flake attractor.
|
|
15
|
+
|
|
16
|
+
## [0.3.0] — 2026-06-04
|
|
17
|
+
|
|
18
|
+
Bedtime audit fix wave: concurrency, decoding, packaging, recording, platform paths,
|
|
19
|
+
validation, and CI gating. Captures audits A/B/C/D from `docs/audits/2026-06-04-*`.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- Fast-packet reassembly for multi-frame PGNs (GNSS, AIS, wind, etc.).
|
|
23
|
+
- Per-OS default workspace via `platformdirs` (Windows %APPDATA%, macOS Application Support, Linux XDG).
|
|
24
|
+
- `--check` headless smoke flag and `--example` workspace scaffolder wiring.
|
|
25
|
+
- `[project.urls]` (Homepage, Source, Bug Tracker, Changelog) so PyPI page is non-empty.
|
|
26
|
+
- Expanded README and new `packaging/README.md` documenting manual homebrew/winget release steps.
|
|
27
|
+
- Threshold field float coercion in signal loader.
|
|
28
|
+
- Test: worker-cancel-on-quit lifecycle.
|
|
29
|
+
- Test: `_writer` race / close-during-write.
|
|
30
|
+
- Test: recording byte-perfect roundtrip incl. priority + destination.
|
|
31
|
+
- Test: container loader rejects overlapping placements.
|
|
32
|
+
- Test: theme loader validates gradient stops (empty / single / malformed hex).
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
- canboat `Offset` now applied (23 AC power PGN fields were 2 GW off).
|
|
36
|
+
- Router no longer silently cross-contaminates when binding expects Instance but frame has none.
|
|
37
|
+
- EMA smoothing direction documented (formula clarified vs. standard convention).
|
|
38
|
+
- `_writer` race between worker write and main close.
|
|
39
|
+
- NGT-1 and replay drivers now honor a stop event instead of looping forever.
|
|
40
|
+
- SIGINT (Ctrl+C) closes the recording writer so the tail isn't lost.
|
|
41
|
+
- Worker thread is cancelled on app quit instead of being orphaned.
|
|
42
|
+
- Recording on Windows no longer emits CRLF (LF-only line endings).
|
|
43
|
+
- TOML config syntax errors now surface a clean message instead of a raw traceback.
|
|
44
|
+
- PyInstaller spec bundles `examples/` so `--example` works in the binary.
|
|
45
|
+
- PyInstaller spec includes `copy_metadata` so `entry_points` (drivers) survive in the binary.
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
- `textual>=8.0` (was `>=0.80`, too loose for actual API usage).
|
|
49
|
+
- `Frame` now carries `priority` and `destination` for byte-perfect recording roundtrips.
|
|
50
|
+
- Default workspace path moved per OS — see release notes for migration.
|
|
51
|
+
|
|
52
|
+
### CI / Tooling
|
|
53
|
+
- Release workflow now has `timeout-minutes` on every job (macOS-13 hangs no longer block release).
|
|
54
|
+
- Release workflow gates PyPI publish behind CI green.
|
|
55
|
+
- Replay timing assertion loosened to remove flake risk on loaded CI.
|
|
56
|
+
|
|
57
|
+
## [0.2.2] — 2026-06-03
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
- Example scaffolder includes one of each widget kind (analog_out, digital_in, digital_out).
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
- `q` now quits immediately.
|
|
64
|
+
- Welcome panel shown for empty workspace.
|
|
65
|
+
- Visible bottom strips restored.
|
|
66
|
+
|
|
67
|
+
## [0.2.0] — 2026-06-02
|
|
68
|
+
|
|
69
|
+
### Added
|
|
70
|
+
- Driver → decoder → router → widgets wiring end-to-end.
|
|
71
|
+
- ContainerScreen tabs.
|
|
72
|
+
- Record toggle.
|
|
73
|
+
- `--example` scaffolder.
|
|
74
|
+
|
|
75
|
+
### Fixed
|
|
76
|
+
- Replay `iter_frames` honors paused flag with sliding resume.
|
|
77
|
+
- Containers mount widgets before setting `column_span`; expose `widgets` dict.
|
|
78
|
+
|
|
79
|
+
## [0.1.3] — 2026-06-01
|
|
80
|
+
|
|
81
|
+
### Added
|
|
82
|
+
- `--check` headless flag.
|
|
83
|
+
- PgntuiApp wired into CLI.
|
|
84
|
+
|
|
85
|
+
### Fixed
|
|
86
|
+
- EMA smoothing previously stored output as raw, causing exponential lag.
|
|
87
|
+
|
|
88
|
+
## [0.1.2] — 2026-05-31
|
|
89
|
+
|
|
90
|
+
### Fixed
|
|
91
|
+
- Per-file force-include for theme JSONs in wheel build.
|
|
92
|
+
- `[dist]` extras installed for binary builds.
|
|
93
|
+
|
|
94
|
+
## [0.1.1] — 2026-05-30
|
|
95
|
+
|
|
96
|
+
Initial published release.
|
|
97
|
+
|
|
98
|
+
[0.3.1]: https://github.com/phobicdotno/pgntui/releases/tag/v0.3.1
|
|
99
|
+
[0.3.0]: https://github.com/phobicdotno/pgntui/releases/tag/v0.3.0
|
|
100
|
+
[0.2.2]: https://github.com/phobicdotno/pgntui/releases/tag/v0.2.2
|
|
101
|
+
[0.2.0]: https://github.com/phobicdotno/pgntui/releases/tag/v0.2.0
|
|
102
|
+
[0.1.3]: https://github.com/phobicdotno/pgntui/releases/tag/v0.1.3
|
|
103
|
+
[0.1.2]: https://github.com/phobicdotno/pgntui/releases/tag/v0.1.2
|
|
104
|
+
[0.1.1]: https://github.com/phobicdotno/pgntui/releases/tag/v0.1.1
|
pgntui-0.3.1/PKG-INFO
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pgntui
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: Cross-platform TUI for NMEA 2000 with canboat decoding and pluggable drivers
|
|
5
|
+
Project-URL: Homepage, https://github.com/phobicdotno/pgntui
|
|
6
|
+
Project-URL: Source, https://github.com/phobicdotno/pgntui
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/phobicdotno/pgntui/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/phobicdotno/pgntui/releases
|
|
9
|
+
Author: phobicdotno
|
|
10
|
+
License: MIT License
|
|
11
|
+
|
|
12
|
+
Copyright (c) 2026 phobicdotno
|
|
13
|
+
|
|
14
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
15
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
16
|
+
in the Software without restriction, including without limitation the rights
|
|
17
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
18
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
19
|
+
furnished to do so, subject to the following conditions:
|
|
20
|
+
|
|
21
|
+
The above copyright notice and this permission notice shall be included in all
|
|
22
|
+
copies or substantial portions of the Software.
|
|
23
|
+
|
|
24
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
25
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
26
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
27
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
28
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
29
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
30
|
+
SOFTWARE.
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Keywords: canboat,marine,n2k,nmea2000,tui
|
|
33
|
+
Classifier: Development Status :: 3 - Alpha
|
|
34
|
+
Classifier: Environment :: Console :: Curses
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Operating System :: MacOS
|
|
37
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
38
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
42
|
+
Classifier: Topic :: Terminals
|
|
43
|
+
Requires-Python: >=3.11
|
|
44
|
+
Requires-Dist: platformdirs>=4.0
|
|
45
|
+
Requires-Dist: pyserial>=3.5
|
|
46
|
+
Requires-Dist: textual>=8.0
|
|
47
|
+
Provides-Extra: dev
|
|
48
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
49
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
50
|
+
Requires-Dist: pytest-textual-snapshot>=1.0; extra == 'dev'
|
|
51
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
52
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
53
|
+
Provides-Extra: dist
|
|
54
|
+
Requires-Dist: pyinstaller>=6.6; extra == 'dist'
|
|
55
|
+
Description-Content-Type: text/markdown
|
|
56
|
+
|
|
57
|
+
# pgntui
|
|
58
|
+
|
|
59
|
+
Cross-platform TUI for NMEA 2000 with canboat decoding.
|
|
60
|
+
|
|
61
|
+
Read live N2K frames through pluggable drivers, decode them with the
|
|
62
|
+
canboat PGN database, and route values into JSON-defined dashboards. Record
|
|
63
|
+
sessions to `.pgnlog` and replay them later — no boat required.
|
|
64
|
+
|
|
65
|
+
## Install
|
|
66
|
+
|
|
67
|
+
The easiest path is `pipx`:
|
|
68
|
+
|
|
69
|
+
pipx install pgntui
|
|
70
|
+
|
|
71
|
+
If your system Python is older than 3.11, point pipx at a newer interpreter:
|
|
72
|
+
|
|
73
|
+
pipx install --python python3.12 pgntui
|
|
74
|
+
|
|
75
|
+
Or run in a project venv:
|
|
76
|
+
|
|
77
|
+
python3 -m venv .venv && . .venv/bin/activate
|
|
78
|
+
pip install pgntui
|
|
79
|
+
|
|
80
|
+
Standalone single-file binaries for macOS (arm64, x86_64), Linux (x86_64) and
|
|
81
|
+
Windows (x86_64) are attached to each GitHub release.
|
|
82
|
+
|
|
83
|
+
## Quickstart
|
|
84
|
+
|
|
85
|
+
Scaffold the example workspace and launch:
|
|
86
|
+
|
|
87
|
+
pgntui --example # writes the example workspace at the OS default location
|
|
88
|
+
pgntui # opens the TUI; no driver yet, debug tab will be empty
|
|
89
|
+
|
|
90
|
+
Replay a recording:
|
|
91
|
+
|
|
92
|
+
pgntui replay path/to/session.pgnlog
|
|
93
|
+
|
|
94
|
+
Run with a real driver — pgntui picks the driver named in `config.toml`:
|
|
95
|
+
|
|
96
|
+
pgntui # uses driver.name from <workspace>/config.toml
|
|
97
|
+
|
|
98
|
+
## Workspace layout
|
|
99
|
+
|
|
100
|
+
`pgntui` reads everything from a workspace directory. By default the location
|
|
101
|
+
follows `platformdirs.user_config_dir("pgntui")`:
|
|
102
|
+
|
|
103
|
+
| OS | Default workspace |
|
|
104
|
+
|---------|--------------------------------------------|
|
|
105
|
+
| Linux | `~/.config/pgntui` |
|
|
106
|
+
| macOS | `~/Library/Application Support/pgntui` |
|
|
107
|
+
| Windows | `%APPDATA%\pgntui` |
|
|
108
|
+
|
|
109
|
+
Override with `--workspace <path>` on the command line.
|
|
110
|
+
|
|
111
|
+
Layout:
|
|
112
|
+
|
|
113
|
+
<workspace>/
|
|
114
|
+
config.toml # driver + theme + paths
|
|
115
|
+
signals/*.json # signal definitions (PGN -> field -> widget)
|
|
116
|
+
containers/*.json # dashboard layouts (tab -> grid of signals)
|
|
117
|
+
recordings/ # .pgnlog files written by the R hotkey
|
|
118
|
+
logs/ # CSV exports
|
|
119
|
+
|
|
120
|
+
Run `pgntui --example` to drop a working sample inside this directory.
|
|
121
|
+
|
|
122
|
+
## Drivers
|
|
123
|
+
|
|
124
|
+
Built-in driver entry points (`pgntui.drivers`):
|
|
125
|
+
|
|
126
|
+
- `actisense-ngt1` — Actisense NGT-1 USB serial gateway (`pyserial`)
|
|
127
|
+
- `file-replay` — replay an Actisense `.pgnlog` capture
|
|
128
|
+
|
|
129
|
+
Pick one in `config.toml`:
|
|
130
|
+
|
|
131
|
+
[driver]
|
|
132
|
+
name = "actisense-ngt1"
|
|
133
|
+
port = "/dev/tty.usbserial-XXXX" # macOS / Linux
|
|
134
|
+
# port = "COM4" # Windows
|
|
135
|
+
baud = 115200
|
|
136
|
+
|
|
137
|
+
Third-party drivers can register additional entry points under the
|
|
138
|
+
`pgntui.drivers` group.
|
|
139
|
+
|
|
140
|
+
## Signal types
|
|
141
|
+
|
|
142
|
+
Signal JSON files declare how each PGN field renders:
|
|
143
|
+
|
|
144
|
+
- `analog_in` — gauge / numeric readout (RPM, speed, depth, temperature)
|
|
145
|
+
- `digital_in` — boolean lamp (alarm, anchor light, bilge pump state)
|
|
146
|
+
- `analog_out` — write-back analog control (sends an outbound frame)
|
|
147
|
+
- `digital_out` — write-back toggle (sends an outbound frame)
|
|
148
|
+
|
|
149
|
+
`analog_out` and `digital_out` need `--enable-write` on the CLI **and**
|
|
150
|
+
`app.write_enabled = true` in `config.toml`; otherwise they render as
|
|
151
|
+
read-only.
|
|
152
|
+
|
|
153
|
+
## Themes
|
|
154
|
+
|
|
155
|
+
Six builtins ship in the wheel:
|
|
156
|
+
|
|
157
|
+
- `dark` (default)
|
|
158
|
+
- `light`
|
|
159
|
+
- `amber-crt`
|
|
160
|
+
- `green-phosphor`
|
|
161
|
+
- `mono-ascii`
|
|
162
|
+
- `rainbow-disco`
|
|
163
|
+
|
|
164
|
+
Custom themes are JSON files referenced from `config.toml`:
|
|
165
|
+
|
|
166
|
+
[app]
|
|
167
|
+
theme = "dark"
|
|
168
|
+
|
|
169
|
+
## Replay
|
|
170
|
+
|
|
171
|
+
Replay an Actisense-format `.pgnlog`:
|
|
172
|
+
|
|
173
|
+
pgntui replay capture.pgnlog
|
|
174
|
+
|
|
175
|
+
The replay driver respects the original frame spacing. Press the `R` hotkey
|
|
176
|
+
inside the TUI to start/stop recording the live stream to a new
|
|
177
|
+
`.pgnlog` file under `<workspace>/recordings/`.
|
|
178
|
+
|
|
179
|
+
## Hotkeys
|
|
180
|
+
|
|
181
|
+
Tab / Shift+Tab next / previous container tab
|
|
182
|
+
D jump to Debug tab
|
|
183
|
+
R start / stop recording
|
|
184
|
+
Q / Ctrl+Q quit immediately
|
|
185
|
+
? show help line in status bar
|
|
186
|
+
|
|
187
|
+
## Layout sketch
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
+---------------------------------------------------+
|
|
191
|
+
| pgntui |
|
|
192
|
+
+---------------------------------------------------+
|
|
193
|
+
| [Main] [Engine] [Nav] [Debug] |
|
|
194
|
+
+---------------------------------------------------+
|
|
195
|
+
| RPM 1450 Speed 6.8 kn Depth 12.3 m |
|
|
196
|
+
| +----+ +----+ +----+ |
|
|
197
|
+
| |####| |## | |# | |
|
|
198
|
+
| +----+ +----+ +----+ |
|
|
199
|
+
| |
|
|
200
|
+
| Bilge OFF Anchor Light ON |
|
|
201
|
+
+---------------------------------------------------+
|
|
202
|
+
| [Tab] Next [D] Debug [R] Rec [Q] Quit |
|
|
203
|
+
| status: idle |
|
|
204
|
+
+---------------------------------------------------+
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Status
|
|
208
|
+
|
|
209
|
+
Alpha. Reasonable to dogfood on a known-good boat or a bench rig.
|
|
210
|
+
|
|
211
|
+
- Works: TUI shell, canboat decoder, signal routing, file replay,
|
|
212
|
+
Actisense NGT-1 driver (read), recording.
|
|
213
|
+
- Partial: NGT-1 write-back is wired but field-tested only against a tiny
|
|
214
|
+
PGN subset.
|
|
215
|
+
- Not yet: TwoCAN / Yacht Devices native drivers, more layout primitives,
|
|
216
|
+
per-signal alarm thresholds in the UI.
|
|
217
|
+
|
|
218
|
+
Bug reports and patches welcome — file an issue at
|
|
219
|
+
<https://github.com/phobicdotno/pgntui/issues>.
|
|
220
|
+
|
|
221
|
+
## License
|
|
222
|
+
|
|
223
|
+
MIT. See [LICENSE](LICENSE).
|
|
224
|
+
|
|
225
|
+
## Links
|
|
226
|
+
|
|
227
|
+
- Source: <https://github.com/phobicdotno/pgntui>
|
|
228
|
+
- Issues: <https://github.com/phobicdotno/pgntui/issues>
|
|
229
|
+
- Releases: <https://github.com/phobicdotno/pgntui/releases>
|
pgntui-0.3.1/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# pgntui
|
|
2
|
+
|
|
3
|
+
Cross-platform TUI for NMEA 2000 with canboat decoding.
|
|
4
|
+
|
|
5
|
+
Read live N2K frames through pluggable drivers, decode them with the
|
|
6
|
+
canboat PGN database, and route values into JSON-defined dashboards. Record
|
|
7
|
+
sessions to `.pgnlog` and replay them later — no boat required.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
The easiest path is `pipx`:
|
|
12
|
+
|
|
13
|
+
pipx install pgntui
|
|
14
|
+
|
|
15
|
+
If your system Python is older than 3.11, point pipx at a newer interpreter:
|
|
16
|
+
|
|
17
|
+
pipx install --python python3.12 pgntui
|
|
18
|
+
|
|
19
|
+
Or run in a project venv:
|
|
20
|
+
|
|
21
|
+
python3 -m venv .venv && . .venv/bin/activate
|
|
22
|
+
pip install pgntui
|
|
23
|
+
|
|
24
|
+
Standalone single-file binaries for macOS (arm64, x86_64), Linux (x86_64) and
|
|
25
|
+
Windows (x86_64) are attached to each GitHub release.
|
|
26
|
+
|
|
27
|
+
## Quickstart
|
|
28
|
+
|
|
29
|
+
Scaffold the example workspace and launch:
|
|
30
|
+
|
|
31
|
+
pgntui --example # writes the example workspace at the OS default location
|
|
32
|
+
pgntui # opens the TUI; no driver yet, debug tab will be empty
|
|
33
|
+
|
|
34
|
+
Replay a recording:
|
|
35
|
+
|
|
36
|
+
pgntui replay path/to/session.pgnlog
|
|
37
|
+
|
|
38
|
+
Run with a real driver — pgntui picks the driver named in `config.toml`:
|
|
39
|
+
|
|
40
|
+
pgntui # uses driver.name from <workspace>/config.toml
|
|
41
|
+
|
|
42
|
+
## Workspace layout
|
|
43
|
+
|
|
44
|
+
`pgntui` reads everything from a workspace directory. By default the location
|
|
45
|
+
follows `platformdirs.user_config_dir("pgntui")`:
|
|
46
|
+
|
|
47
|
+
| OS | Default workspace |
|
|
48
|
+
|---------|--------------------------------------------|
|
|
49
|
+
| Linux | `~/.config/pgntui` |
|
|
50
|
+
| macOS | `~/Library/Application Support/pgntui` |
|
|
51
|
+
| Windows | `%APPDATA%\pgntui` |
|
|
52
|
+
|
|
53
|
+
Override with `--workspace <path>` on the command line.
|
|
54
|
+
|
|
55
|
+
Layout:
|
|
56
|
+
|
|
57
|
+
<workspace>/
|
|
58
|
+
config.toml # driver + theme + paths
|
|
59
|
+
signals/*.json # signal definitions (PGN -> field -> widget)
|
|
60
|
+
containers/*.json # dashboard layouts (tab -> grid of signals)
|
|
61
|
+
recordings/ # .pgnlog files written by the R hotkey
|
|
62
|
+
logs/ # CSV exports
|
|
63
|
+
|
|
64
|
+
Run `pgntui --example` to drop a working sample inside this directory.
|
|
65
|
+
|
|
66
|
+
## Drivers
|
|
67
|
+
|
|
68
|
+
Built-in driver entry points (`pgntui.drivers`):
|
|
69
|
+
|
|
70
|
+
- `actisense-ngt1` — Actisense NGT-1 USB serial gateway (`pyserial`)
|
|
71
|
+
- `file-replay` — replay an Actisense `.pgnlog` capture
|
|
72
|
+
|
|
73
|
+
Pick one in `config.toml`:
|
|
74
|
+
|
|
75
|
+
[driver]
|
|
76
|
+
name = "actisense-ngt1"
|
|
77
|
+
port = "/dev/tty.usbserial-XXXX" # macOS / Linux
|
|
78
|
+
# port = "COM4" # Windows
|
|
79
|
+
baud = 115200
|
|
80
|
+
|
|
81
|
+
Third-party drivers can register additional entry points under the
|
|
82
|
+
`pgntui.drivers` group.
|
|
83
|
+
|
|
84
|
+
## Signal types
|
|
85
|
+
|
|
86
|
+
Signal JSON files declare how each PGN field renders:
|
|
87
|
+
|
|
88
|
+
- `analog_in` — gauge / numeric readout (RPM, speed, depth, temperature)
|
|
89
|
+
- `digital_in` — boolean lamp (alarm, anchor light, bilge pump state)
|
|
90
|
+
- `analog_out` — write-back analog control (sends an outbound frame)
|
|
91
|
+
- `digital_out` — write-back toggle (sends an outbound frame)
|
|
92
|
+
|
|
93
|
+
`analog_out` and `digital_out` need `--enable-write` on the CLI **and**
|
|
94
|
+
`app.write_enabled = true` in `config.toml`; otherwise they render as
|
|
95
|
+
read-only.
|
|
96
|
+
|
|
97
|
+
## Themes
|
|
98
|
+
|
|
99
|
+
Six builtins ship in the wheel:
|
|
100
|
+
|
|
101
|
+
- `dark` (default)
|
|
102
|
+
- `light`
|
|
103
|
+
- `amber-crt`
|
|
104
|
+
- `green-phosphor`
|
|
105
|
+
- `mono-ascii`
|
|
106
|
+
- `rainbow-disco`
|
|
107
|
+
|
|
108
|
+
Custom themes are JSON files referenced from `config.toml`:
|
|
109
|
+
|
|
110
|
+
[app]
|
|
111
|
+
theme = "dark"
|
|
112
|
+
|
|
113
|
+
## Replay
|
|
114
|
+
|
|
115
|
+
Replay an Actisense-format `.pgnlog`:
|
|
116
|
+
|
|
117
|
+
pgntui replay capture.pgnlog
|
|
118
|
+
|
|
119
|
+
The replay driver respects the original frame spacing. Press the `R` hotkey
|
|
120
|
+
inside the TUI to start/stop recording the live stream to a new
|
|
121
|
+
`.pgnlog` file under `<workspace>/recordings/`.
|
|
122
|
+
|
|
123
|
+
## Hotkeys
|
|
124
|
+
|
|
125
|
+
Tab / Shift+Tab next / previous container tab
|
|
126
|
+
D jump to Debug tab
|
|
127
|
+
R start / stop recording
|
|
128
|
+
Q / Ctrl+Q quit immediately
|
|
129
|
+
? show help line in status bar
|
|
130
|
+
|
|
131
|
+
## Layout sketch
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
+---------------------------------------------------+
|
|
135
|
+
| pgntui |
|
|
136
|
+
+---------------------------------------------------+
|
|
137
|
+
| [Main] [Engine] [Nav] [Debug] |
|
|
138
|
+
+---------------------------------------------------+
|
|
139
|
+
| RPM 1450 Speed 6.8 kn Depth 12.3 m |
|
|
140
|
+
| +----+ +----+ +----+ |
|
|
141
|
+
| |####| |## | |# | |
|
|
142
|
+
| +----+ +----+ +----+ |
|
|
143
|
+
| |
|
|
144
|
+
| Bilge OFF Anchor Light ON |
|
|
145
|
+
+---------------------------------------------------+
|
|
146
|
+
| [Tab] Next [D] Debug [R] Rec [Q] Quit |
|
|
147
|
+
| status: idle |
|
|
148
|
+
+---------------------------------------------------+
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Status
|
|
152
|
+
|
|
153
|
+
Alpha. Reasonable to dogfood on a known-good boat or a bench rig.
|
|
154
|
+
|
|
155
|
+
- Works: TUI shell, canboat decoder, signal routing, file replay,
|
|
156
|
+
Actisense NGT-1 driver (read), recording.
|
|
157
|
+
- Partial: NGT-1 write-back is wired but field-tested only against a tiny
|
|
158
|
+
PGN subset.
|
|
159
|
+
- Not yet: TwoCAN / Yacht Devices native drivers, more layout primitives,
|
|
160
|
+
per-signal alarm thresholds in the UI.
|
|
161
|
+
|
|
162
|
+
Bug reports and patches welcome — file an issue at
|
|
163
|
+
<https://github.com/phobicdotno/pgntui/issues>.
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
MIT. See [LICENSE](LICENSE).
|
|
168
|
+
|
|
169
|
+
## Links
|
|
170
|
+
|
|
171
|
+
- Source: <https://github.com/phobicdotno/pgntui>
|
|
172
|
+
- Issues: <https://github.com/phobicdotno/pgntui/issues>
|
|
173
|
+
- Releases: <https://github.com/phobicdotno/pgntui/releases>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Audit A: Concurrency + Lifecycle (2026-06-04)
|
|
2
|
+
Branch: main @ 1f3795d (v0.2.1)
|
|
3
|
+
Auditor: feature-dev:code-reviewer
|
|
4
|
+
|
|
5
|
+
## Findings
|
|
6
|
+
|
|
7
|
+
### Finding A-1: Worker thread not cancelled before `self.exit()` — orphaned blocking thread on quit
|
|
8
|
+
- **File:** `src/pgntui/app.py:315-323`
|
|
9
|
+
- **Severity:** P1
|
|
10
|
+
- **What's wrong:** `action_force_quit` calls `self.exit()` without cancelling the `frame_loop` worker first. After the Textual event loop shuts down, the worker thread is still alive blocked inside `NGT1Driver.read_frames()` (loops `while True: byte = self._serial.read(1)` with 0.1s timeout) or inside `_interruptible_sleep` for replay. `call_from_thread` posted by the still-running worker can fire into a dead event loop and raise `RuntimeError`.
|
|
11
|
+
- **Repro:** Press Q during active NGT-1 streaming.
|
|
12
|
+
- **Fix:** `self.get_worker("frame_loop").cancel()` before `self.exit()`; close the driver via existing `finally` block.
|
|
13
|
+
|
|
14
|
+
### Finding A-2: `_writer` written by main thread, read by worker thread — unsynchronised access
|
|
15
|
+
- **File:** `src/pgntui/app.py:183` (worker reads), `app.py:297-309` (main writes)
|
|
16
|
+
- **Severity:** P1
|
|
17
|
+
- **What's wrong:** Worker `_handle_frame` reads `self._writer`; `action_toggle_record` writes it from event loop. `_stop_recording` calls `close()` and then sets `_writer=None`. Worker can read non-None, enter `writer.write(frame)`, while main closes the file handle mid-write — produces `ValueError: I/O operation on closed file` swallowed by `except Exception: pass`. Data integrity loss.
|
|
18
|
+
- **Repro:** Press R to stop recording while frames arrive rapidly.
|
|
19
|
+
- **Fix:** Add `threading.Lock` protecting `_writer`, OR swap order in `_stop_recording`: null `self._writer` first, then close the local reference.
|
|
20
|
+
|
|
21
|
+
### Finding A-3: `NGT1Driver.read_frames()` infinite loop — no exit mechanism
|
|
22
|
+
- **File:** `src/pgntui/drivers/actisense.py:102-117`
|
|
23
|
+
- **Severity:** P1
|
|
24
|
+
- **What's wrong:** `while True` with `self._serial.read(1)` timeout-and-continue. Textual `Worker.cancel()` on a `thread=True` worker only sets state and joins with timeout — does NOT inject an exception. Loop only exits when `serial.close()` raises `SerialException`. Timing window between `app.run()` returning and `driver.close()` being called can leak serial port on crash.
|
|
25
|
+
- **Fix:** Add `_stop: threading.Event` to `NGT1Driver`, check inside loop, set in `close()`.
|
|
26
|
+
|
|
27
|
+
### Finding A-4: `FileReplayDriver.close()` only nulls `_path`; open file handle untracked
|
|
28
|
+
- **File:** `src/pgntui/drivers/replay.py:41-42`
|
|
29
|
+
- **Severity:** P2
|
|
30
|
+
- **What's wrong:** `read_log` opens via `with` (correct), but if `close()` is called mid-iteration, only `_path = None` runs. Generator file handle stays open until GC. No deterministic flush/close.
|
|
31
|
+
- **Fix:** Store generator/handle, explicitly close in `close()`, or add `_stop` event in `_interruptible_sleep`.
|
|
32
|
+
|
|
33
|
+
### Finding A-5: `ActisenseLogWriter` not closed on Ctrl+C (SIGINT)
|
|
34
|
+
- **File:** `src/pgntui/app.py:315-323`, `src/pgntui/__main__.py:239-246`
|
|
35
|
+
- **Severity:** P2
|
|
36
|
+
- **What's wrong:** `action_force_quit` flushes the writer. Ctrl+C raises `KeyboardInterrupt` past `app.run()`; `main()` `finally` only closes the driver. Recording tail in Python text buffer is lost.
|
|
37
|
+
- **Fix:** In `main()` finally OR via `PgntuiApp.shutdown()`, close `app._writer` if non-None.
|
|
38
|
+
|
|
39
|
+
### Finding A-6: `AnalogInWidget.update_value` — no documented thread-safety contract
|
|
40
|
+
- **File:** `src/pgntui/signals/widgets.py:20-28`
|
|
41
|
+
- **Severity:** P2 (future-proofing)
|
|
42
|
+
- **What's wrong:** Today only called via `call_from_thread`. No assertion or docstring enforces this. A future direct call from a non-UI thread would race the renderer (two STORE_ATTR for `displayed_value` and `state_class`).
|
|
43
|
+
- **Fix:** Add docstring requiring UI thread, optional `assert` guard.
|
|
44
|
+
|
|
45
|
+
### Finding A-7: `_interruptible_sleep` — pause leftover_delay analysis
|
|
46
|
+
- **File:** `src/pgntui/drivers/replay.py:52-70`
|
|
47
|
+
- **Severity:** P3 — **no fix required**
|
|
48
|
+
- **Notes:** `remaining` is preserved correctly across pause cycles; `_paused` is a single bool (GIL-atomic). Worst case: 50ms latency on resume.
|
|
49
|
+
|
|
50
|
+
## Clean
|
|
51
|
+
- `call_from_thread` used correctly for every widget mutation from worker (debug log, widget updates, status)
|
|
52
|
+
- `SignalRouter.route` returns frozen `SignalUpdate`; no shared mutable state
|
|
53
|
+
- `Signal` subclasses frozen+slots — read-only after construction
|
|
54
|
+
- `CSVSignalLogger.log` flushes on every write
|
|
55
|
+
- `ActisenseLogWriter.write` only called from worker (single producer)
|
|
56
|
+
- `NGT1Driver.close()` / `FileReplayDriver.close()` guard `is not None`
|
|
57
|
+
- `_stop_recording` uses try/finally
|
|
58
|
+
- `frame_loop` decorated `exclusive=True` — prevents double-open races
|
|
59
|
+
- Day-boundary rotation closes stale handles synchronously
|
|
60
|
+
|
|
61
|
+
## Notes
|
|
62
|
+
- `driver.close()` in `main()` finally is the primary backstop; works for normal exit but not `os._exit()`, SIGKILL, hard crash. Acceptable.
|
|
63
|
+
- `_set_status` called from both threads — works because event loop calls direct, worker uses `call_from_thread`. Easy to misread during future edits.
|
|
64
|
+
- `_make_analog_write` / `_make_digital_write` callbacks invoke `_set_status` directly — fine today since Textual widget callbacks fire on event loop, but unsafe if ever invoked from non-UI context.
|