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.
Files changed (113) hide show
  1. {pgntui-0.2.2 → pgntui-0.3.0}/.github/workflows/release.yml +18 -0
  2. pgntui-0.3.0/CHANGELOG.md +95 -0
  3. pgntui-0.3.0/PKG-INFO +229 -0
  4. pgntui-0.3.0/README.md +173 -0
  5. pgntui-0.3.0/docs/audits/2026-06-04-audit-A-concurrency.md +64 -0
  6. pgntui-0.3.0/docs/audits/2026-06-04-audit-B-decoding.md +72 -0
  7. pgntui-0.3.0/docs/audits/2026-06-04-audit-C-platform.md +74 -0
  8. pgntui-0.3.0/docs/audits/2026-06-04-audit-D-packaging.md +77 -0
  9. pgntui-0.3.0/docs/audits/2026-06-04-release-notes-v0.3.0.md +74 -0
  10. pgntui-0.3.0/docs/audits/2026-06-04-summary.md +70 -0
  11. pgntui-0.3.0/packaging/README.md +110 -0
  12. {pgntui-0.2.2 → pgntui-0.3.0}/packaging/homebrew/pgntui.rb +1 -1
  13. {pgntui-0.2.2 → pgntui-0.3.0}/packaging/winget/phobic.pgntui.yaml +2 -2
  14. {pgntui-0.2.2 → pgntui-0.3.0}/pyproject.toml +9 -2
  15. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/__init__.py +1 -1
  16. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/__main__.py +20 -6
  17. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/app.py +54 -22
  18. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/config.py +26 -3
  19. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/containers/loader.py +10 -0
  20. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/decode/canboat.py +59 -5
  21. pgntui-0.3.0/src/pgntui/decode/fastpacket.py +174 -0
  22. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/decode/router.py +4 -1
  23. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/drivers/actisense.py +27 -2
  24. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/drivers/base.py +4 -0
  25. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/drivers/replay.py +30 -6
  26. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/recording/reader.py +10 -1
  27. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/recording/writer.py +5 -3
  28. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/signals/base.py +21 -8
  29. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/signals/widgets.py +8 -0
  30. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/themes/loader.py +20 -3
  31. pgntui-0.3.0/tests/test_app_worker_cancel_on_exit.py +195 -0
  32. pgntui-0.3.0/tests/test_canboat.py +130 -0
  33. {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_containers_loader.py +56 -0
  34. pgntui-0.3.0/tests/test_fastpacket.py +187 -0
  35. pgntui-0.3.0/tests/test_recording_roundtrip.py +107 -0
  36. {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_replay_mode.py +10 -8
  37. {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_router.py +19 -0
  38. {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_signals_base.py +87 -0
  39. {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_smoke.py +1 -1
  40. {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_themes_loader.py +53 -0
  41. pgntui-0.3.0/tests/test_writer_race.py +115 -0
  42. pgntui-0.2.2/PKG-INFO +0 -66
  43. pgntui-0.2.2/README.md +0 -15
  44. pgntui-0.2.2/tests/test_canboat.py +0 -26
  45. {pgntui-0.2.2 → pgntui-0.3.0}/.github/workflows/ci.yml +0 -0
  46. {pgntui-0.2.2 → pgntui-0.3.0}/.gitignore +0 -0
  47. {pgntui-0.2.2 → pgntui-0.3.0}/LICENSE +0 -0
  48. {pgntui-0.2.2 → pgntui-0.3.0}/docs/superpowers/plans/2026-06-04-pgntui-implementation.md +0 -0
  49. {pgntui-0.2.2 → pgntui-0.3.0}/docs/superpowers/specs/2026-06-04-pgntui-design.md +0 -0
  50. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/containers/__init__.py +0 -0
  51. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/containers/screen.py +0 -0
  52. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/debug/__init__.py +0 -0
  53. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/debug/tab.py +0 -0
  54. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/decode/__init__.py +0 -0
  55. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/decode/pgns.json +0 -0
  56. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/drivers/__init__.py +0 -0
  57. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/examples/__init__.py +0 -0
  58. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/examples/config.toml +0 -0
  59. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/examples/containers/main.json +0 -0
  60. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/examples/signals/anchor_light.json +0 -0
  61. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/examples/signals/bilge_alarm.json +0 -0
  62. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/examples/signals/depth.json +0 -0
  63. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/examples/signals/engine_rpm.json +0 -0
  64. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/examples/signals/speed.json +0 -0
  65. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/examples/signals/target_heading.json +0 -0
  66. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/examples/signals/water_temp.json +0 -0
  67. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/logging/__init__.py +0 -0
  68. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/logging/csv.py +0 -0
  69. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/recording/__init__.py +0 -0
  70. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/replay_mode.py +0 -0
  71. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/signals/__init__.py +0 -0
  72. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/themes/__init__.py +0 -0
  73. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/themes/builtin/__init__.py +0 -0
  74. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/themes/builtin/amber-crt.json +0 -0
  75. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/themes/builtin/dark.json +0 -0
  76. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/themes/builtin/green-phosphor.json +0 -0
  77. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/themes/builtin/light.json +0 -0
  78. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/themes/builtin/mono-ascii.json +0 -0
  79. {pgntui-0.2.2 → pgntui-0.3.0}/src/pgntui/themes/builtin/rainbow-disco.json +0 -0
  80. {pgntui-0.2.2 → pgntui-0.3.0}/tests/__init__.py +0 -0
  81. {pgntui-0.2.2 → pgntui-0.3.0}/tests/fixtures/__init__.py +0 -0
  82. {pgntui-0.2.2 → pgntui-0.3.0}/tests/fixtures/e2e_containers/engine.json +0 -0
  83. {pgntui-0.2.2 → pgntui-0.3.0}/tests/fixtures/e2e_containers/nav.json +0 -0
  84. {pgntui-0.2.2 → pgntui-0.3.0}/tests/fixtures/e2e_session.pgnlog +0 -0
  85. {pgntui-0.2.2 → pgntui-0.3.0}/tests/fixtures/e2e_signals/engine_rpm.json +0 -0
  86. {pgntui-0.2.2 → pgntui-0.3.0}/tests/fixtures/e2e_signals/wind_speed.json +0 -0
  87. {pgntui-0.2.2 → pgntui-0.3.0}/tests/fixtures/frames.py +0 -0
  88. {pgntui-0.2.2 → pgntui-0.3.0}/tests/fixtures/sample.pgnlog +0 -0
  89. {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_actisense_driver.py +0 -0
  90. {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_app_empty_welcome.py +0 -0
  91. {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_app_frame_loop.py +0 -0
  92. {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_app_on_write.py +0 -0
  93. {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_app_quit_binding.py +0 -0
  94. {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_app_record_toggle.py +0 -0
  95. {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_app_shell.py +0 -0
  96. {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_builtin_themes.py +0 -0
  97. {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_cli.py +0 -0
  98. {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_config.py +0 -0
  99. {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_container_screen.py +0 -0
  100. {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_csv_logger.py +0 -0
  101. {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_debug_tab.py +0 -0
  102. {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_drivers_base.py +0 -0
  103. {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_e2e_replay.py +0 -0
  104. {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_example_workspace.py +0 -0
  105. {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_main_replay.py +0 -0
  106. {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_packaging.py +0 -0
  107. {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_recording_writer.py +0 -0
  108. {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_replay_driver.py +0 -0
  109. {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_signals_loader.py +0 -0
  110. {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_widgets_analog_in.py +0 -0
  111. {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_widgets_analog_out.py +0 -0
  112. {pgntui-0.2.2 → pgntui-0.3.0}/tests/test_widgets_digital_in.py +0 -0
  113. {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