pgntui 0.2.2__tar.gz → 0.3.0__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.0}/.github/workflows/release.yml +18 -0
- pgntui-0.3.0/CHANGELOG.md +95 -0
- pgntui-0.3.0/PKG-INFO +229 -0
- pgntui-0.3.0/README.md +173 -0
- pgntui-0.3.0/docs/audits/2026-06-04-audit-A-concurrency.md +64 -0
- pgntui-0.3.0/docs/audits/2026-06-04-audit-B-decoding.md +72 -0
- pgntui-0.3.0/docs/audits/2026-06-04-audit-C-platform.md +74 -0
- pgntui-0.3.0/docs/audits/2026-06-04-audit-D-packaging.md +77 -0
- pgntui-0.3.0/docs/audits/2026-06-04-release-notes-v0.3.0.md +74 -0
- pgntui-0.3.0/docs/audits/2026-06-04-summary.md +70 -0
- pgntui-0.3.0/packaging/README.md +110 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/packaging/homebrew/pgntui.rb +1 -1
- {pgntui-0.2.2 → pgntui-0.3.0}/packaging/winget/phobic.pgntui.yaml +2 -2
- {pgntui-0.2.2 → pgntui-0.3.0}/pyproject.toml +9 -2
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/__init__.py +1 -1
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/__main__.py +20 -6
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/app.py +54 -22
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/config.py +26 -3
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/containers/loader.py +10 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/decode/canboat.py +59 -5
- pgntui-0.3.0/src/pgntui/decode/fastpacket.py +174 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/decode/router.py +4 -1
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/drivers/actisense.py +27 -2
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/drivers/base.py +4 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/drivers/replay.py +30 -6
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/recording/reader.py +10 -1
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/recording/writer.py +5 -3
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/signals/base.py +21 -8
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/signals/widgets.py +8 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/themes/loader.py +20 -3
- pgntui-0.3.0/tests/test_app_worker_cancel_on_exit.py +195 -0
- pgntui-0.3.0/tests/test_canboat.py +130 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_containers_loader.py +56 -0
- pgntui-0.3.0/tests/test_fastpacket.py +187 -0
- pgntui-0.3.0/tests/test_recording_roundtrip.py +107 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_replay_mode.py +10 -8
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_router.py +19 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_signals_base.py +87 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_smoke.py +1 -1
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_themes_loader.py +53 -0
- pgntui-0.3.0/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.0}/.github/workflows/ci.yml +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/.gitignore +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/LICENSE +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/docs/superpowers/plans/2026-06-04-pgntui-implementation.md +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/docs/superpowers/specs/2026-06-04-pgntui-design.md +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/containers/__init__.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/containers/screen.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/debug/__init__.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/debug/tab.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/decode/__init__.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/decode/pgns.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/drivers/__init__.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/examples/__init__.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/examples/config.toml +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/examples/containers/main.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/examples/signals/anchor_light.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/examples/signals/bilge_alarm.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/examples/signals/depth.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/examples/signals/engine_rpm.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/examples/signals/speed.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/examples/signals/target_heading.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/examples/signals/water_temp.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/logging/__init__.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/logging/csv.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/recording/__init__.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/replay_mode.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/signals/__init__.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/themes/__init__.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/themes/builtin/__init__.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/themes/builtin/amber-crt.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/themes/builtin/dark.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/themes/builtin/green-phosphor.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/themes/builtin/light.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/themes/builtin/mono-ascii.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/themes/builtin/rainbow-disco.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/__init__.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/fixtures/__init__.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/fixtures/e2e_containers/engine.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/fixtures/e2e_containers/nav.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/fixtures/e2e_session.pgnlog +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/fixtures/e2e_signals/engine_rpm.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/fixtures/e2e_signals/wind_speed.json +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/fixtures/frames.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/fixtures/sample.pgnlog +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_actisense_driver.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_app_empty_welcome.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_app_frame_loop.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_app_on_write.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_app_quit_binding.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_app_record_toggle.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_app_shell.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_builtin_themes.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_cli.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_config.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_container_screen.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_csv_logger.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_debug_tab.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_drivers_base.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_e2e_replay.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_example_workspace.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_main_replay.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_packaging.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_recording_writer.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_replay_driver.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_signals_loader.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_widgets_analog_in.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_widgets_analog_out.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_widgets_digital_in.py +0 -0
- {pgntui-0.2.2 → pgntui-0.3.0}/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,95 @@
|
|
|
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.0] — 2026-06-04
|
|
9
|
+
|
|
10
|
+
Bedtime audit fix wave: concurrency, decoding, packaging, recording, platform paths,
|
|
11
|
+
validation, and CI gating. Captures audits A/B/C/D from `docs/audits/2026-06-04-*`.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- Fast-packet reassembly for multi-frame PGNs (GNSS, AIS, wind, etc.).
|
|
15
|
+
- Per-OS default workspace via `platformdirs` (Windows %APPDATA%, macOS Application Support, Linux XDG).
|
|
16
|
+
- `--check` headless smoke flag and `--example` workspace scaffolder wiring.
|
|
17
|
+
- `[project.urls]` (Homepage, Source, Bug Tracker, Changelog) so PyPI page is non-empty.
|
|
18
|
+
- Expanded README and new `packaging/README.md` documenting manual homebrew/winget release steps.
|
|
19
|
+
- Threshold field float coercion in signal loader.
|
|
20
|
+
- Test: worker-cancel-on-quit lifecycle.
|
|
21
|
+
- Test: `_writer` race / close-during-write.
|
|
22
|
+
- Test: recording byte-perfect roundtrip incl. priority + destination.
|
|
23
|
+
- Test: container loader rejects overlapping placements.
|
|
24
|
+
- Test: theme loader validates gradient stops (empty / single / malformed hex).
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- canboat `Offset` now applied (23 AC power PGN fields were 2 GW off).
|
|
28
|
+
- Router no longer silently cross-contaminates when binding expects Instance but frame has none.
|
|
29
|
+
- EMA smoothing direction documented (formula clarified vs. standard convention).
|
|
30
|
+
- `_writer` race between worker write and main close.
|
|
31
|
+
- NGT-1 and replay drivers now honor a stop event instead of looping forever.
|
|
32
|
+
- SIGINT (Ctrl+C) closes the recording writer so the tail isn't lost.
|
|
33
|
+
- Worker thread is cancelled on app quit instead of being orphaned.
|
|
34
|
+
- Recording on Windows no longer emits CRLF (LF-only line endings).
|
|
35
|
+
- TOML config syntax errors now surface a clean message instead of a raw traceback.
|
|
36
|
+
- PyInstaller spec bundles `examples/` so `--example` works in the binary.
|
|
37
|
+
- PyInstaller spec includes `copy_metadata` so `entry_points` (drivers) survive in the binary.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
- `textual>=8.0` (was `>=0.80`, too loose for actual API usage).
|
|
41
|
+
- `Frame` now carries `priority` and `destination` for byte-perfect recording roundtrips.
|
|
42
|
+
- Default workspace path moved per OS — see release notes for migration.
|
|
43
|
+
|
|
44
|
+
### CI / Tooling
|
|
45
|
+
- Release workflow now has `timeout-minutes` on every job (macOS-13 hangs no longer block release).
|
|
46
|
+
- Release workflow gates PyPI publish behind CI green.
|
|
47
|
+
- Replay timing assertion loosened to remove flake risk on loaded CI.
|
|
48
|
+
|
|
49
|
+
## [0.2.2] — 2026-06-03
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
- Example scaffolder includes one of each widget kind (analog_out, digital_in, digital_out).
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
- `q` now quits immediately.
|
|
56
|
+
- Welcome panel shown for empty workspace.
|
|
57
|
+
- Visible bottom strips restored.
|
|
58
|
+
|
|
59
|
+
## [0.2.0] — 2026-06-02
|
|
60
|
+
|
|
61
|
+
### Added
|
|
62
|
+
- Driver → decoder → router → widgets wiring end-to-end.
|
|
63
|
+
- ContainerScreen tabs.
|
|
64
|
+
- Record toggle.
|
|
65
|
+
- `--example` scaffolder.
|
|
66
|
+
|
|
67
|
+
### Fixed
|
|
68
|
+
- Replay `iter_frames` honors paused flag with sliding resume.
|
|
69
|
+
- Containers mount widgets before setting `column_span`; expose `widgets` dict.
|
|
70
|
+
|
|
71
|
+
## [0.1.3] — 2026-06-01
|
|
72
|
+
|
|
73
|
+
### Added
|
|
74
|
+
- `--check` headless flag.
|
|
75
|
+
- PgntuiApp wired into CLI.
|
|
76
|
+
|
|
77
|
+
### Fixed
|
|
78
|
+
- EMA smoothing previously stored output as raw, causing exponential lag.
|
|
79
|
+
|
|
80
|
+
## [0.1.2] — 2026-05-31
|
|
81
|
+
|
|
82
|
+
### Fixed
|
|
83
|
+
- Per-file force-include for theme JSONs in wheel build.
|
|
84
|
+
- `[dist]` extras installed for binary builds.
|
|
85
|
+
|
|
86
|
+
## [0.1.1] — 2026-05-30
|
|
87
|
+
|
|
88
|
+
Initial published release.
|
|
89
|
+
|
|
90
|
+
[0.3.0]: https://github.com/phobicdotno/pgntui/releases/tag/v0.3.0
|
|
91
|
+
[0.2.2]: https://github.com/phobicdotno/pgntui/releases/tag/v0.2.2
|
|
92
|
+
[0.2.0]: https://github.com/phobicdotno/pgntui/releases/tag/v0.2.0
|
|
93
|
+
[0.1.3]: https://github.com/phobicdotno/pgntui/releases/tag/v0.1.3
|
|
94
|
+
[0.1.2]: https://github.com/phobicdotno/pgntui/releases/tag/v0.1.2
|
|
95
|
+
[0.1.1]: https://github.com/phobicdotno/pgntui/releases/tag/v0.1.1
|
pgntui-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pgntui
|
|
3
|
+
Version: 0.3.0
|
|
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.0/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.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Audit B: Decoding + Routing + Signal Math + Themes + Recording (2026-06-04)
|
|
2
|
+
Branch: main @ 1f3795d (v0.2.1)
|
|
3
|
+
Auditor: feature-dev:code-reviewer
|
|
4
|
+
|
|
5
|
+
## P1 — wrong output
|
|
6
|
+
|
|
7
|
+
### B-1: canboat `Offset` never applied — 23 fields silently wrong
|
|
8
|
+
- **File:** `src/pgntui/decode/canboat.py:65-93`
|
|
9
|
+
- **What's wrong:** `_decode_fields` reads `Resolution` but never `Offset`. Canboat schema is `raw * resolution + offset`. 23 fields in pgns.json carry `"Offset": -2000000000` (AC power PGNs 65007/65008/65009 Real Power, Apparent Power, etc.). Without offset, those decode 2 GW too high.
|
|
10
|
+
- **Fix:** `offset_val = float(f.get("Offset") or 0); value = raw * resolution + offset_val`
|
|
11
|
+
|
|
12
|
+
### B-2: No fast-packet reassembly — multi-frame PGNs silently truncate
|
|
13
|
+
- **File:** `src/pgntui/decode/canboat.py:51-63`; no reassembly layer
|
|
14
|
+
- **What's wrong:** N2K fast-packet PGNs (129029 GNSS, 129025 Position Rapid, 129026 COG/SOG, 129038/129039 AIS, 130306 Wind Data) arrive as a sequence of 8-byte CAN frames with a sequence counter in byte 0. The decoder parses each 8-byte frame independently. For 43-byte PGN 129029, lat/lon/alt all decode to zero or garbage. No `FastPacketReassembler` exists.
|
|
15
|
+
- **Fix:** Add `FastPacketReassembler` keyed by `(source_addr, pgn, sequence_counter)`, strip header bytes, concatenate payload, call `decode()` only when complete.
|
|
16
|
+
|
|
17
|
+
## P2 — design / semantic flips
|
|
18
|
+
|
|
19
|
+
### B-3: EMA formula direction inverted relative to standard
|
|
20
|
+
- **File:** `src/pgntui/signals/widgets.py:21-23`
|
|
21
|
+
- **What's wrong:** Code: `displayed = a * self._raw + (1 - a) * value` where `a = smoothing`. Standard EMA: `ema = alpha * new + (1 - alpha) * old`. With `smoothing=0.9` the display weights 90% old / 10% new — heavily smoothed, but opposite direction from standard `alpha`. The Wave 1 fix test `test_smoothing_ema_uses_raw_not_blended` documents this as intentional (works at alpha=0.5 symmetry point).
|
|
22
|
+
- **Fix:** Either rename parameter (`history_weight`?) and document, OR flip the formula to standard EMA. Latter is breaking change. Recommend: document clearly and add a docstring noting the semantic.
|
|
23
|
+
|
|
24
|
+
### B-4: Router missing-instance allows cross-contamination
|
|
25
|
+
- **File:** `src/pgntui/decode/router.py:41`
|
|
26
|
+
- **What's wrong:** `if key.instance is not None and instance is not None and key.instance != instance: continue` — short-circuits when frame has no Instance field. A signal keyed to instance 2 receives updates from single-engine PGNs with no instance. Port engine bound to instance 0 gets cross-fire.
|
|
27
|
+
- **Fix:** `if key.instance is not None and key.instance != instance: continue` — let None != 2 mismatch.
|
|
28
|
+
|
|
29
|
+
### B-5: `_FIELD_ALIASES` documentation gap
|
|
30
|
+
- **File:** `src/pgntui/decode/canboat.py:24-28`
|
|
31
|
+
- **What's wrong:** Alias entries are correct for PGN 127488. But users binding `field: "Speed"` thinking it means PGN 128259 "Speed Water Referenced" will see no match (decoder emits the raw canboat name). Documentation/discoverability problem, not a decoder bug.
|
|
32
|
+
- **Fix:** Document the alias table's purpose + how to list canonical field names.
|
|
33
|
+
|
|
34
|
+
### B-6: Recording priority+destination lossy
|
|
35
|
+
- **File:** `src/pgntui/recording/writer.py:31-43`; `src/pgntui/drivers/base.py`
|
|
36
|
+
- **What's wrong:** `Frame` has no `priority` or `destination`. Writer hardcodes priority "3", destination "255". No roundtrip test verifies byte-perfect fidelity. Future addressed-PGN routing silently breaks.
|
|
37
|
+
- **Fix:** Add optional `priority` + `destination` to Frame dataclass. Update writer/reader. Add roundtrip test.
|
|
38
|
+
|
|
39
|
+
## P3 — quality
|
|
40
|
+
|
|
41
|
+
### B-7: Duplicate placement silently passes
|
|
42
|
+
- **File:** `src/pgntui/containers/loader.py:43-55`
|
|
43
|
+
- **What's wrong:** Two `SignalPlacement` entries with overlapping `(row, col, w)` not detected. UI renders one on top of the other silently.
|
|
44
|
+
- **Fix:** Track occupied cells in the validator, raise `ContainerLoadError` on overlap.
|
|
45
|
+
|
|
46
|
+
### B-8: Theme gradient stops not validated
|
|
47
|
+
- **File:** `src/pgntui/themes/loader.py:61-63`
|
|
48
|
+
- **What's wrong:** No validation of stop count (0/1) or hex format. Degenerate theme produces render failure at runtime instead of load-time error.
|
|
49
|
+
- **Fix:** Validate len(stops) >= 2 and each stop matches `#[0-9a-fA-F]{6}`.
|
|
50
|
+
|
|
51
|
+
## Clean
|
|
52
|
+
- `_read_bits` LSB-first; truncated payload graceful break
|
|
53
|
+
- Unknown PGN returns None cleanly
|
|
54
|
+
- Signed two's complement correct for any field size
|
|
55
|
+
- Resolution=0 guard with 1.0 fallback
|
|
56
|
+
- Router source=None wildcard correct; source=0 treated as explicit (correct)
|
|
57
|
+
- Router multi-signal routes: `setdefault().append()` accumulates correctly
|
|
58
|
+
- AnalogInWidget smoothing=0 path: bypasses EMA correctly
|
|
59
|
+
- AnalogInWidget first sample bypasses EMA
|
|
60
|
+
- `compute_state` boundary uses `>=` consistently
|
|
61
|
+
- DigitalIn/Out truthy cast via `bool(value)` handles 2/-1/"yes"
|
|
62
|
+
- `read_log` truncated file: parse returns None, generator skips
|
|
63
|
+
- Timestamp precision: microseconds preserved
|
|
64
|
+
- Container cols<=0 / neg coords / grid overflow rejected (tested)
|
|
65
|
+
- 14 required theme colors validated; all 6 builtin themes complete
|
|
66
|
+
- Widget glyphs hardcoded (●─├┤) — missing theme glyph table doesn't break runtime
|
|
67
|
+
|
|
68
|
+
## Notes
|
|
69
|
+
- B-2 (fast-packet) is the biggest production impact — anyone running GNSS/AIS/wind sees broken decodes silently
|
|
70
|
+
- B-1 (Offset) only hits AC power; most marine TUI users won't notice
|
|
71
|
+
- AnalogInWidget NaN/inf input crashes at `int(NaN * 17)` in `_bar()` — extremely unlikely in real PGN data but worth noting
|
|
72
|
+
- `test_recording_writer.py` has no write→read→compare roundtrip — adding one would surface B-6
|