termapy 0.48.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 (150) hide show
  1. termapy-0.48.0/PKG-INFO +1178 -0
  2. termapy-0.48.0/README.md +1143 -0
  3. termapy-0.48.0/pyproject.toml +74 -0
  4. termapy-0.48.0/src/termapy/__init__.py +3 -0
  5. termapy-0.48.0/src/termapy/__main__.py +5 -0
  6. termapy-0.48.0/src/termapy/app.py +3681 -0
  7. termapy-0.48.0/src/termapy/builtins/__init__.py +0 -0
  8. termapy-0.48.0/src/termapy/builtins/crc/__init__.py +0 -0
  9. termapy-0.48.0/src/termapy/builtins/crc/sum16.py +16 -0
  10. termapy-0.48.0/src/termapy/builtins/crc/sum8.py +16 -0
  11. termapy-0.48.0/src/termapy/builtins/demo/.gitignore +9 -0
  12. termapy-0.48.0/src/termapy/builtins/demo/__init__.py +0 -0
  13. termapy-0.48.0/src/termapy/builtins/demo/demo.cfg +44 -0
  14. termapy-0.48.0/src/termapy/builtins/demo/plugin/__init__.py +0 -0
  15. termapy-0.48.0/src/termapy/builtins/demo/plugin/cmd.py +42 -0
  16. termapy-0.48.0/src/termapy/builtins/demo/plugin/probe.py +143 -0
  17. termapy-0.48.0/src/termapy/builtins/demo/plugin/temp_plot.py +116 -0
  18. termapy-0.48.0/src/termapy/builtins/demo/proto/at_test.pro +32 -0
  19. termapy-0.48.0/src/termapy/builtins/demo/proto/bitfield_inline.pro +55 -0
  20. termapy-0.48.0/src/termapy/builtins/demo/proto/modbus_inline.pro +53 -0
  21. termapy-0.48.0/src/termapy/builtins/demo/run/at_demo.run +57 -0
  22. termapy-0.48.0/src/termapy/builtins/demo/run/doc_screenshots.run +78 -0
  23. termapy-0.48.0/src/termapy/builtins/demo/run/expect_test.run +17 -0
  24. termapy-0.48.0/src/termapy/builtins/demo/run/gps_demo.run +36 -0
  25. termapy-0.48.0/src/termapy/builtins/demo/run/smoke_test.run +63 -0
  26. termapy-0.48.0/src/termapy/builtins/demo/run/status_check.run +6 -0
  27. termapy-0.48.0/src/termapy/builtins/demo/run/var_demo.run +58 -0
  28. termapy-0.48.0/src/termapy/builtins/demo/run/welcome.run +22 -0
  29. termapy-0.48.0/src/termapy/builtins/plugins/cap.py +389 -0
  30. termapy-0.48.0/src/termapy/builtins/plugins/cfg.py +482 -0
  31. termapy-0.48.0/src/termapy/builtins/plugins/cls.py +29 -0
  32. termapy-0.48.0/src/termapy/builtins/plugins/confirm.py +37 -0
  33. termapy-0.48.0/src/termapy/builtins/plugins/echo.py +70 -0
  34. termapy-0.48.0/src/termapy/builtins/plugins/edit.py +199 -0
  35. termapy-0.48.0/src/termapy/builtins/plugins/env_var.py +164 -0
  36. termapy-0.48.0/src/termapy/builtins/plugins/eol.py +48 -0
  37. termapy-0.48.0/src/termapy/builtins/plugins/exit.py +29 -0
  38. termapy-0.48.0/src/termapy/builtins/plugins/grep.py +90 -0
  39. termapy-0.48.0/src/termapy/builtins/plugins/help.py +430 -0
  40. termapy-0.48.0/src/termapy/builtins/plugins/include.py +273 -0
  41. termapy-0.48.0/src/termapy/builtins/plugins/os_cmd.py +63 -0
  42. termapy-0.48.0/src/termapy/builtins/plugins/ping.py +80 -0
  43. termapy-0.48.0/src/termapy/builtins/plugins/port.py +185 -0
  44. termapy-0.48.0/src/termapy/builtins/plugins/print.py +55 -0
  45. termapy-0.48.0/src/termapy/builtins/plugins/proto.py +913 -0
  46. termapy-0.48.0/src/termapy/builtins/plugins/run_edit.py +31 -0
  47. termapy-0.48.0/src/termapy/builtins/plugins/seq.py +74 -0
  48. termapy-0.48.0/src/termapy/builtins/plugins/show.py +82 -0
  49. termapy-0.48.0/src/termapy/builtins/plugins/ss.py +48 -0
  50. termapy-0.48.0/src/termapy/builtins/plugins/stop.py +37 -0
  51. termapy-0.48.0/src/termapy/builtins/plugins/var.py +379 -0
  52. termapy-0.48.0/src/termapy/builtins/plugins/ver.py +29 -0
  53. termapy-0.48.0/src/termapy/builtins/plugins/verbose.py +30 -0
  54. termapy-0.48.0/src/termapy/builtins/plugins/xfer.py +59 -0
  55. termapy-0.48.0/src/termapy/builtins/plugins/xmodem_xfer.py +227 -0
  56. termapy-0.48.0/src/termapy/builtins/plugins/ymodem_xfer.py +156 -0
  57. termapy-0.48.0/src/termapy/builtins/viz/__init__.py +0 -0
  58. termapy-0.48.0/src/termapy/builtins/viz/hex_view.py +61 -0
  59. termapy-0.48.0/src/termapy/builtins/viz/modbus_view.py.old +163 -0
  60. termapy-0.48.0/src/termapy/builtins/viz/text_view.py +61 -0
  61. termapy-0.48.0/src/termapy/capture.py +336 -0
  62. termapy-0.48.0/src/termapy/cli.py +865 -0
  63. termapy-0.48.0/src/termapy/config.py +547 -0
  64. termapy-0.48.0/src/termapy/crc_codegen.py +370 -0
  65. termapy-0.48.0/src/termapy/defaults.py +410 -0
  66. termapy-0.48.0/src/termapy/demo.py +1516 -0
  67. termapy-0.48.0/src/termapy/dialogs.py +1447 -0
  68. termapy-0.48.0/src/termapy/folders.py +82 -0
  69. termapy-0.48.0/src/termapy/help/commands.md +124 -0
  70. termapy-0.48.0/src/termapy/help/config.md +136 -0
  71. termapy-0.48.0/src/termapy/help/custom-buttons.md +50 -0
  72. termapy-0.48.0/src/termapy/help/data-capture.md +108 -0
  73. termapy-0.48.0/src/termapy/help/demo.md +103 -0
  74. termapy-0.48.0/src/termapy/help/device-help.md +95 -0
  75. termapy-0.48.0/src/termapy/help/file-transfer.md +214 -0
  76. termapy-0.48.0/src/termapy/help/getting-started.md +138 -0
  77. termapy-0.48.0/src/termapy/help/img/doc_01_main_tui.svg +202 -0
  78. termapy-0.48.0/src/termapy/help/img/doc_02_help.svg +205 -0
  79. termapy-0.48.0/src/termapy/help/img/doc_03_help_proto.svg +206 -0
  80. termapy-0.48.0/src/termapy/help/img/doc_04_proto_send.svg +203 -0
  81. termapy-0.48.0/src/termapy/help/img/doc_05_proto_crc.svg +203 -0
  82. termapy-0.48.0/src/termapy/help/img/doc_06_proto_delay.svg +204 -0
  83. termapy-0.48.0/src/termapy/help/img/doc_07_crc_python.svg +201 -0
  84. termapy-0.48.0/src/termapy/help/img/doc_08_variables.svg +203 -0
  85. termapy-0.48.0/src/termapy/help/img/doc_09_target_help.svg +205 -0
  86. termapy-0.48.0/src/termapy/help/img/doc_10_config_info.svg +204 -0
  87. termapy-0.48.0/src/termapy/help/img/new_cfg.png +0 -0
  88. termapy-0.48.0/src/termapy/help/index.md +22 -0
  89. termapy-0.48.0/src/termapy/help/installation.md +51 -0
  90. termapy-0.48.0/src/termapy/help/protocol-testing.md +167 -0
  91. termapy-0.48.0/src/termapy/help/scripting.md +68 -0
  92. termapy-0.48.0/src/termapy/help/serial-tools.md +141 -0
  93. termapy-0.48.0/src/termapy/help/toolbar.md +50 -0
  94. termapy-0.48.0/src/termapy/help/using-git.md +110 -0
  95. termapy-0.48.0/src/termapy/help/variables.md +120 -0
  96. termapy-0.48.0/src/termapy/help/writing-plugins.md +192 -0
  97. termapy-0.48.0/src/termapy/help.md +591 -0
  98. termapy-0.48.0/src/termapy/html/404.html +731 -0
  99. termapy-0.48.0/src/termapy/html/assets/images/favicon.png +0 -0
  100. termapy-0.48.0/src/termapy/html/assets/javascripts/LICENSE +29 -0
  101. termapy-0.48.0/src/termapy/html/assets/javascripts/bundle.91a19a9e.min.js +3 -0
  102. termapy-0.48.0/src/termapy/html/assets/javascripts/workers/search.e2d2d235.min.js +1 -0
  103. termapy-0.48.0/src/termapy/html/assets/stylesheets/classic/main.96fc3bb8.min.css +1 -0
  104. termapy-0.48.0/src/termapy/html/assets/stylesheets/classic/palette.7dc9a0ad.min.css +1 -0
  105. termapy-0.48.0/src/termapy/html/assets/stylesheets/modern/main.53a7feaf.min.css +1 -0
  106. termapy-0.48.0/src/termapy/html/assets/stylesheets/modern/palette.dfe2e883.min.css +1 -0
  107. termapy-0.48.0/src/termapy/html/commands.html +1339 -0
  108. termapy-0.48.0/src/termapy/html/config.html +1256 -0
  109. termapy-0.48.0/src/termapy/html/custom-buttons.html +961 -0
  110. termapy-0.48.0/src/termapy/html/data-capture.html +1118 -0
  111. termapy-0.48.0/src/termapy/html/demo.html +1281 -0
  112. termapy-0.48.0/src/termapy/html/device-help.html +1082 -0
  113. termapy-0.48.0/src/termapy/html/file-transfer.html +1514 -0
  114. termapy-0.48.0/src/termapy/html/getting-started.html +1145 -0
  115. termapy-0.48.0/src/termapy/html/img/doc_01_main_tui.svg +202 -0
  116. termapy-0.48.0/src/termapy/html/img/doc_02_help.svg +205 -0
  117. termapy-0.48.0/src/termapy/html/img/doc_03_help_proto.svg +206 -0
  118. termapy-0.48.0/src/termapy/html/img/doc_04_proto_send.svg +203 -0
  119. termapy-0.48.0/src/termapy/html/img/doc_05_proto_crc.svg +203 -0
  120. termapy-0.48.0/src/termapy/html/img/doc_06_proto_delay.svg +204 -0
  121. termapy-0.48.0/src/termapy/html/img/doc_07_crc_python.svg +201 -0
  122. termapy-0.48.0/src/termapy/html/img/doc_08_variables.svg +203 -0
  123. termapy-0.48.0/src/termapy/html/img/doc_09_target_help.svg +205 -0
  124. termapy-0.48.0/src/termapy/html/img/doc_10_config_info.svg +204 -0
  125. termapy-0.48.0/src/termapy/html/img/new_cfg.png +0 -0
  126. termapy-0.48.0/src/termapy/html/index.html +805 -0
  127. termapy-0.48.0/src/termapy/html/installation.html +995 -0
  128. termapy-0.48.0/src/termapy/html/objects.inv +0 -0
  129. termapy-0.48.0/src/termapy/html/protocol-testing.html +1235 -0
  130. termapy-0.48.0/src/termapy/html/scripting.html +1049 -0
  131. termapy-0.48.0/src/termapy/html/search.json +1 -0
  132. termapy-0.48.0/src/termapy/html/serial-tools.html +1136 -0
  133. termapy-0.48.0/src/termapy/html/sitemap.xml +3 -0
  134. termapy-0.48.0/src/termapy/html/toolbar.html +1047 -0
  135. termapy-0.48.0/src/termapy/html/using-git.html +992 -0
  136. termapy-0.48.0/src/termapy/html/variables.html +1214 -0
  137. termapy-0.48.0/src/termapy/html/writing-plugins.html +1440 -0
  138. termapy-0.48.0/src/termapy/migration.py +158 -0
  139. termapy-0.48.0/src/termapy/plugins.py +670 -0
  140. termapy-0.48.0/src/termapy/port_control.py +251 -0
  141. termapy-0.48.0/src/termapy/proto_debug.py +1161 -0
  142. termapy-0.48.0/src/termapy/proto_runner.py +284 -0
  143. termapy-0.48.0/src/termapy/protocol.py +1468 -0
  144. termapy-0.48.0/src/termapy/protocol_crc.py +292 -0
  145. termapy-0.48.0/src/termapy/protocol_viz.py +132 -0
  146. termapy-0.48.0/src/termapy/repl.py +926 -0
  147. termapy-0.48.0/src/termapy/scripting.py +203 -0
  148. termapy-0.48.0/src/termapy/serial_engine.py +238 -0
  149. termapy-0.48.0/src/termapy/serial_port.py +302 -0
  150. termapy-0.48.0/src/termapy/smoke.txt +41 -0
@@ -0,0 +1,1178 @@
1
+ Metadata-Version: 2.4
2
+ Name: termapy
3
+ Version: 0.48.0
4
+ Summary: A TUI serial terminal with ANSI color support, built on Textual and pyserial
5
+ Keywords: serial,terminal,tui,textual,pyserial,embedded,xmodem,ymodem
6
+ Author: Chuck Bass
7
+ Author-email: Chuck Bass <chuck@acrocad.net>
8
+ License-Expression: MIT
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Communications
20
+ Classifier: Topic :: System :: Hardware
21
+ Classifier: Topic :: Terminals :: Serial
22
+ Requires-Dist: textual[syntax]>=0.80
23
+ Requires-Dist: pyserial>=3.5
24
+ Requires-Dist: pyreadline3>=3.4 ; sys_platform == 'win32'
25
+ Requires-Dist: pygments>=2.20.0
26
+ Requires-Dist: xmodem>=0.4.7
27
+ Requires-Dist: ymodem>=1.5.3
28
+ Requires-Python: >=3.11
29
+ Project-URL: Homepage, https://github.com/hucker/termapy
30
+ Project-URL: Documentation, https://hucker.github.io/termapy/
31
+ Project-URL: Repository, https://github.com/hucker/termapy
32
+ Project-URL: Changelog, https://github.com/hucker/termapy/blob/main/CHANGELOG.md
33
+ Project-URL: Issues, https://github.com/hucker/termapy/issues
34
+ Description-Content-Type: text/markdown
35
+
36
+ # termapy
37
+
38
+ **Project Status:** [![CI](https://github.com/hucker/termapy/actions/workflows/tests.yml/badge.svg)](https://github.com/hucker/termapy/actions/workflows/tests.yml) [![codecov](https://codecov.io/gh/hucker/termapy/graph/badge.svg)](https://codecov.io/gh/hucker/termapy) ![license](https://img.shields.io/badge/license-MIT-green) [![docs](https://img.shields.io/badge/docs-GitHub%20Pages-blue)](https://hucker.github.io/termapy/)
39
+
40
+ **Powered by:** [![Textual](https://img.shields.io/badge/Textual-TUI-blue?logo=python)](https://textual.textualize.io/) [![pySerial](https://img.shields.io/badge/pySerial-serial%20I%2FO-orange?logo=python)](https://pyserial.readthedocs.io/) [![zensical](https://img.shields.io/badge/zensical-docs-green)](https://github.com/hucker/zensical)
41
+
42
+ **Built with:** ![python](https://img.shields.io/badge/python-3.11--3.14-blue) [![uv](https://img.shields.io/badge/uv-package%20manager-blueviolet?logo=astral)](https://docs.astral.sh/uv/) [![pytest](https://img.shields.io/badge/pytest-testing-yellow?logo=pytest)](https://pytest.org/) [![coverage](https://img.shields.io/badge/coverage-testing-yellow)](https://coverage.readthedocs.io/)
43
+
44
+ *Pronounced "ter-map-ee"*
45
+
46
+ A serial interface terminal like PuTTY or Tera Term — but it runs in your terminal, installs in seconds, and comes with scripting, protocol testing, and a plugin system built in.
47
+
48
+ ![termapy screenshot](img/main.png)
49
+
50
+ ## Install and Connect
51
+
52
+ ```sh
53
+ pip install termapy
54
+ termapy --demo
55
+ ```
56
+
57
+ Or with [uv](https://docs.astral.sh/uv/):
58
+
59
+ ```sh
60
+ uv tool install termapy
61
+ termapy --demo
62
+ ```
63
+
64
+ That starts a simulated device — no hardware needed. You're typing commands in seconds.
65
+
66
+ For a real device, just point at your config:
67
+
68
+ ```sh
69
+ termapy my_device # finds termapy_cfg/my_device/my_device.cfg
70
+ termapy my_device.cfg # same, explicit extension
71
+ termapy termapy_cfg/my_device # same, explicit path to folder
72
+ ```
73
+
74
+ For a plain-text terminal (no TUI), use CLI mode:
75
+
76
+ ```sh
77
+ termapy --cli my_device # interactive CLI
78
+ termapy --cli smoke_test.run # run a .run script and exit
79
+ termapy --cli my_device --run test.run # explicit config + script
80
+ ```
81
+
82
+ There's a lot more — scripting, binary protocol testing, 62 CRC algorithms, custom buttons, plugins, packet visualizers — expand any section below.
83
+
84
+ ---
85
+
86
+ <details>
87
+ <summary><strong>First 60 Seconds</strong> — connect, type, change settings</summary>
88
+
89
+ 1. **Connect** — click the port button in the title bar, pick your COM port, click the status button to connect (it turns green)
90
+ 2. **Type** — enter commands in the input box at the bottom and press Enter
91
+ 3. **Change settings** — click `Cfg` to edit port, baud rate, and other settings through the UI
92
+
93
+ Everything works through the UI — no config files to edit unless you want to.
94
+
95
+ </details>
96
+
97
+ <details>
98
+
99
+ <summary><strong>Why Not Just Use PuTTY?</strong> — what termapy adds</summary>
100
+
101
+ PuTTY works. So does minicom, screen, and CoolTerm. Use them if they do what you need. Here's where termapy goes further:
102
+
103
+ - **Runs anywhere Python does** — same tool on Windows, macOS, Linux. No GUI installer, no system dependencies.
104
+ - **Session logging and screenshots** — every session is logged. Ctrl+S saves an SVG screenshot you can paste into a report or email.
105
+ - **Scripting** — record a sequence of commands in a text file and replay it with one click. Add delays, prompts, and REPL commands.
106
+ - **Data capture** — capture serial text (timed) or binary data (by byte/record count) to files. Binary captures use the same format spec language as protocol testing to decode mixed-type records into CSV/TSV.
107
+ - **Binary protocol testing** — send raw hex, run scripted send/expect tests with pass/fail, decode Modbus and custom protocols with pluggable visualizers.
108
+ - **Plugin system** — add custom commands with a simple Python API. Drop a file in a folder, define a handler, done. Includes examples to get started.
109
+ - **Everything in one folder** — each device config gets its own subfolder with logs, screenshots, scripts, and plugins. Check it into git so the whole team has the same config.
110
+
111
+ See [COMPARISON.md](COMPARISON.md) for a detailed feature comparison against RealTerm, CoolTerm, Tera Term, Docklight, and HTerm.
112
+
113
+ </details>
114
+
115
+ <details>
116
+ <summary><strong>The Basics</strong> — keyboard shortcuts, title bar, REPL commands</summary>
117
+
118
+ ### Keyboard Shortcuts
119
+
120
+ | Key | Action |
121
+ | ------- | ----------------------------------- |
122
+ | Ctrl+Q | Quit (also closes any open dialog) |
123
+ | Ctrl+S | Save SVG screenshot |
124
+ | Ctrl+T | Save text screenshot |
125
+ | Ctrl+P | Command palette |
126
+ | Up/Down | Cycle through command history |
127
+ | Escape | Clear input / exit history browsing |
128
+ | Right | Accept type-ahead suggestion |
129
+
130
+ ### Title Bar
131
+
132
+ | Button | Action |
133
+ | ------ | ------------------------------------------------------------------- |
134
+ | `?` | Open the help guide |
135
+ | `#` | Toggle line numbers (green when active) |
136
+ | `Cfg` | Open the config picker |
137
+ | `Run` | Open the script picker |
138
+ | Center | Click to edit the current config |
139
+ | Port | Click to select a serial port |
140
+ | Status | Click to connect/disconnect (red = disconnected, green = connected) |
141
+
142
+ ### REPL Commands
143
+
144
+ Type `/` to access built-in commands (the prefix is configurable). Type `/help` to list them all.
145
+
146
+ The most common ones:
147
+
148
+ | Command | Description |
149
+ | -------------------- | ---------------------------------- |
150
+ | `/help [cmd]` | List commands or show help for one |
151
+ | `/port.list` | List available serial ports |
152
+ | `/port.open {name}` | Connect to a port |
153
+ | `/port.info` | Show port status and parameters |
154
+ | `/cfg [key [value]]` | Show or change in-memory config |
155
+ | `/ss.svg [name]` | Save SVG screenshot |
156
+ | `/cls` | Clear the terminal |
157
+ | `/run <filename>` | Run a script file |
158
+ | `/echo [on \| off]` | Toggle command echo |
159
+ | `/grep <pattern>` | Search scrollback |
160
+ | `/exit` | Exit termapy |
161
+
162
+ <details>
163
+ <summary>Full command list</summary>
164
+
165
+ | Command | Description |
166
+ | -------------------------------- | -------------------------------------------------------------------------------- |
167
+ | `/help [cmd]` | List commands or show extended help for one |
168
+ | `/help.dev <cmd>` | Show a command handler's Python docstring |
169
+ | `/port [name]` | Open a port by name, or show subcommands |
170
+ | `/port.list` | List available serial ports |
171
+ | `/port.open {name}` | Connect to the serial port (optional port override) |
172
+ | `/port.close` | Disconnect from the serial port |
173
+ | `/port.info` | Show port status, serial parameters, and hardware lines |
174
+ | `/port.baud_rate {value}` | Show or set baud rate (hardware only) |
175
+ | `/port.byte_size {value}` | Show or set data bits (hardware only) |
176
+ | `/port.parity {value}` | Show or set parity (hardware only) |
177
+ | `/port.stop_bits {value}` | Show or set stop bits (hardware only) |
178
+ | `/port.flow_control {m}` | Show or set flow control: none, rtscts, xonxoff, manual |
179
+ | `/port.dtr {0\|1}` | Show or set DTR line |
180
+ | `/port.rts {0\|1}` | Show or set RTS line |
181
+ | `/port.cts` | Show CTS state (read-only) |
182
+ | `/port.dsr` | Show DSR state (read-only) |
183
+ | `/port.ri` | Show RI state (read-only) |
184
+ | `/port.cd` | Show CD state (read-only) |
185
+ | `/port.break {ms}` | Send break signal (default 250ms) |
186
+ | `/cfg [key [value]]` | Show config, show a key, or change in-memory value (with confirmation) |
187
+ | `/cfg.auto <key> <value>` | Set an in-memory config key immediately (no confirmation) |
188
+ | `/cfg.configs` | List all config files |
189
+ | `/cfg.load <name>` | Switch to a different config by name |
190
+ | `/ss.svg [name]` | Save SVG screenshot |
191
+ | `/ss.txt [name]` | Save text screenshot |
192
+ | `/ss.dir` | Show the screenshot folder |
193
+ | `/cls` | Clear the terminal screen |
194
+ | `/run <filename> {-v}` | Run a script file (-v/--verbose for per-line timing); nests up to 5 levels deep |
195
+ | `/run.list` | List .run files in the run/ directory |
196
+ | `/run.load <filename>` | Run a script file (same as /run) |
197
+ | `/delay <duration>` | Wait for a duration (e.g. `500ms`, `1.5s`) |
198
+ | `/confirm {message}` | Show Yes/Cancel dialog; Cancel stops a running script (see `at_demo.run`) |
199
+ | `/stop` | Abort a running script |
200
+ | `/seq` | Show sequence counters |
201
+ | `/seq.reset` | Reset all sequence counters to zero |
202
+ | `/print <text>` | Print a message to the terminal |
203
+ | `/print.r <text>` | Print Rich markup text (e.g. `[bold red]Warning![/]`) |
204
+ | `/show <name>` | Show a file |
205
+ | `/show.cfg` | Show the current config file |
206
+ | `/echo [on \| off]` | Toggle REPL command echo |
207
+ | `/echo.quiet <on \| off>` | Set echo on/off silently (for scripts and on_connect_cmd) |
208
+ | `/edit <file>` | Edit a project file (`run/`/`proto/` path) |
209
+ | `/edit.cfg` | Edit the current config file |
210
+ | `/edit.log` | Open the session log in the system viewer |
211
+ | `/edit.info` | Open the info report in the system viewer |
212
+ | `/show_line_endings [on \| off]` | Toggle visible `\r` `\n` markers for line-ending troubleshooting |
213
+ | `/os <cmd>` | Run a shell command (10s timeout, requires `os_cmd_enabled`) |
214
+ | `/grep <pattern>` | Search scrollback for regex matches (case-insensitive, skips own output) |
215
+ | `/cfg.info {--display}` | Show project summary; `--display` opens full report in system viewer |
216
+ | `/cfg.files` | Show project directory tree |
217
+ | `/proto.send <hex>` | Send raw hex bytes and/or quoted text, display response as hex (see below) |
218
+ | `/proto.run <file>` | Run a binary protocol test script (.pro) with pass/fail |
219
+ | `/proto.list` | List .pro files in the proto/ directory |
220
+ | `/proto.load <file>` | Run a protocol test script (same as /proto.run) |
221
+ | `/proto.hex [on \| off]` | Toggle hex display mode for serial I/O |
222
+ | `/proto.crc.list {pat}` | List available CRC algorithms (optional glob filter) |
223
+ | `/proto.crc.help <name>` | Show CRC algorithm parameters and description |
224
+ | `/proto.crc.calc <n> {d}` | Compute CRC over hex bytes, text, or file; omit data to verify check string |
225
+ | `/proto.status` | Show current protocol mode state |
226
+ | `/var {name}` | List user variables, or show one by name |
227
+ | `/var.set <NAME> <value>` | Set a user variable |
228
+ | `/var.clear` | Clear all user variables |
229
+ | `/env.list {pattern}` | List environment variables (all, by name, or glob) |
230
+ | `/env.set <name> <value>` | Set a session-scoped environment variable |
231
+ | `/env.reload` | Re-snapshot variables from the OS environment |
232
+ | `/cap.text <f> ...` | Capture serial text to file for a timed duration |
233
+ | `/cap.bin <f> ...` | Capture raw binary bytes to a file |
234
+ | `/cap.struct <f> ...` | Capture binary data, decode with format spec to CSV |
235
+ | `/cap.hex <f> ...` | Capture hex text lines, decode with format spec to CSV |
236
+ | `/cap.stop` | Stop an active capture |
237
+ | `/raw <text>` | Send text to serial with no variable expansion or transforms |
238
+ | `/exit` | Exit termapy |
239
+
240
+ </details>
241
+
242
+ Screenshots and logs are saved in the config's subfolder (`termapy_cfg/<name>/`).
243
+
244
+ </details>
245
+
246
+ <details>
247
+ <summary><strong>Project Files</strong> — config layout, version control, env vars, examples</summary>
248
+
249
+ On first run, termapy prompts for a config name and creates one with defaults. If one config exists it loads automatically; if multiple exist, a picker appears. You can edit the config file through the UI (`Cfg` button), or change in-memory settings for the current session with `/cfg baud_rate 9600`.
250
+
251
+ Everything termapy creates — configs, scripts, test files, plugins, logs — lives in one folder. Run `termapy --demo` and you'll see this structure:
252
+
253
+ ```text
254
+ termapy_cfg/
255
+ ├── plugin/ # global plugins (all configs)
256
+ └── demo/
257
+ ├── demo.cfg # config file
258
+ ├── demo.log # session log
259
+ ├── .cmd_history.txt # command history
260
+ ├── ss/ # screenshots
261
+ ├── run/ # script files for /run
262
+ │ ├── at_demo.run
263
+ │ ├── smoke_test.run
264
+ │ └── status_check.run
265
+ ├── plugin/ # per-config plugins
266
+ │ └── probe.py
267
+ ├── cap/ # data capture output files
268
+ └── proto/ # protocol test scripts
269
+ ├── at_test.pro
270
+ ├── bitfield_inline.pro
271
+ └── modbus_inline.pro
272
+ ```
273
+
274
+ Your own configs follow the same layout — create one with `Cfg` → `New` and termapy builds the folder structure automatically.
275
+
276
+ ### Version Control
277
+
278
+ Because everything is in one folder, you can commit it to git alongside your firmware source. Point `--cfg-dir` at a folder in your repo:
279
+
280
+ ```sh
281
+ termapy --cfg-dir ./termapy_cfg
282
+ ```
283
+
284
+ Clone on another machine, run the same command — all configs, scripts, and test files are ready to go.
285
+
286
+ Since COM port names differ between machines, use `$(env.NAME)` placeholders in your config so the same file works everywhere. Set a `COMPORT` environment variable on each machine, and reference it with a fallback:
287
+
288
+ ```json
289
+ {
290
+ "port": "$(env.COMPORT|COM4)",
291
+ "baud_rate": 115200,
292
+ "auto_connect": true
293
+ }
294
+ ```
295
+
296
+ On a machine with `COMPORT=COM7`, termapy connects to COM7. On a machine without `COMPORT` set, it falls back to COM4. The config file on disk keeps the raw `$(env.COMPORT|COM4)` template — it's expanded in memory at load time, so your checked-in config stays portable.
297
+
298
+ Environment variables work in any string config value, not just `port`:
299
+
300
+ ```json
301
+ {
302
+ "port": "$(env.COMPORT|COM4)",
303
+ "title": "$(env.DEVICE_NAME|Dev Board)",
304
+ "log_file": "$(env.LOG_DIR|logs)/session.log"
305
+ }
306
+ ```
307
+
308
+ You can also manage environment variables at runtime with REPL commands:
309
+
310
+ | Command | Description |
311
+ | ----------------------- | -------------------------------------------------- |
312
+ | `/env.list {pattern}` | List variables (all, by name, or glob like `COM*`) |
313
+ | `/env.set <name> <val>` | Set a session-scoped variable (in-memory only) |
314
+ | `/env.reload` | Re-snapshot variables from the OS environment |
315
+
316
+ Variables set with `/env.set` are available immediately for `$(env.NAME)` expansion in REPL commands but do not modify the OS environment or the config file.
317
+
318
+ #### User Variables (`$(NAME)`)
319
+
320
+ User variables let you define values once and reuse them across commands and scripts. This is especially useful when a test references the same address, register, or port in multiple places — change it once at the top instead of everywhere.
321
+
322
+ Assign a variable by typing `$(name) = value` (no `/` prefix needed):
323
+
324
+ ```text
325
+ $(slave) = 01
326
+ $(reg) = 0064
327
+ $(count) = 05
328
+ ```
329
+
330
+ Use variables in any command — REPL or serial:
331
+
332
+ ```text
333
+ /proto.send $(slave) 03 00 $(reg) 00 $(count)
334
+ /print Reading $(count) registers from $(slave) at $(reg)
335
+ AT+ADDR=$(slave)
336
+ ```
337
+
338
+ A typical workflow is a setup script that configures a test, then a test script that uses the variables:
339
+
340
+ ```text
341
+ # setup_modbus.run — run this first to configure the test
342
+ $(SLAVE) = 01
343
+ $(BASE_REG) = 0064
344
+ $(NUM_REGS) = 05
345
+ /print Configured: slave=$(SLAVE) base=$(BASE_REG) count=$(NUM_REGS)
346
+ ```
347
+
348
+ ```text
349
+ # test_registers.run — uses variables from setup
350
+ /proto.send $(SLAVE) 03 00 $(BASE_REG) 00 $(NUM_REGS)
351
+ /delay 500ms
352
+ /proto.send $(SLAVE) 06 00 $(BASE_REG) 04 D2
353
+ ```
354
+
355
+ Run `/run setup_modbus.run` then `/run test_registers.run` — the variables persist across interactive `/run` calls.
356
+
357
+ | Command | Description |
358
+ | -------------------- | -------------------------------------- |
359
+ | `$(NAME) = value` | Set a variable (no `/` prefix needed) |
360
+ | `/var` | List all defined variables |
361
+ | `/var NAME` | Show one variable's value (or $(NAME)) |
362
+ | `/var.set NAME val` | Set a variable (explicit command form) |
363
+ | `/var.clear` | Clear all variables |
364
+
365
+ **Scope:** Variables persist for the interactive session. They are automatically cleared when a script is launched from the Scripts button or Run menu, but *not* when `/run` is typed interactively or called within a script. This lets you run a setup script to define variables, then run a test script that uses them. Use `/var.clear` to reset manually.
366
+
367
+ **Naming:** Variable names are case-sensitive (`$(PORT)` and `$(port)` are different variables). Names must start with a letter or underscore and contain only letters, digits, and underscores.
368
+
369
+ **Built-in time variables:**
370
+
371
+ | Variable | Set when | Updates? |
372
+ | --- | --- | --- |
373
+ | `$(LAUNCH_DATETIME)` | App starts | Never - frozen |
374
+ | `$(SESSION_DATETIME)` | Script launched (Scripts button / Run menu) | Per script launch |
375
+ | `$(DATETIME)` | Every expansion | Always current clock |
376
+
377
+ Each group also has `_DATE` and `_TIME` variants (e.g. `$(LAUNCH_DATE)`, `$(SESSION_TIME)`).
378
+
379
+ **vs. environment variables:** `$(env.NAME)` pulls from the OS environment and works in config files. `$(NAME)` is for user-defined session variables in commands and scripts. Both use the `$(...)` syntax — `env.` is required to access environment variables explicitly.
380
+
381
+ **Escaping:** Use `\$` to prevent expansion of a single reference, or `/raw` to skip expansion for an entire line.
382
+
383
+ Add a `.gitignore` for session files you don't need to track:
384
+
385
+ ```gitignore
386
+ # termapy_cfg — keep configs and scripts, ignore session files
387
+ termapy_cfg/*/*.log
388
+ termapy_cfg/*/.cmd_history.txt
389
+ termapy_cfg/*/ss/
390
+ ```
391
+
392
+ To specify a config file directly:
393
+
394
+ ```sh
395
+ termapy my_device.cfg
396
+ ```
397
+
398
+ To override the config directory:
399
+
400
+ ```sh
401
+ termapy --cfg-dir /path/to/configs
402
+ ```
403
+
404
+ ### Config Validation
405
+
406
+ Termapy validates config files on load and when saving from the editor. Invalid serial settings (baud rate, parity, data bits, stop bits, flow control, encoding) and unknown keys (typos) produce yellow warnings in the log window. Non-standard baud rates are flagged but allowed — some hardware uses custom rates.
407
+
408
+ To validate a config from the command line without launching the UI:
409
+
410
+ ```sh
411
+ termapy --check my_device.cfg
412
+ ```
413
+
414
+ This prints a JSON result to stdout and exits:
415
+
416
+ ```json
417
+ {"status": "ok"}
418
+ ```
419
+
420
+ ```json
421
+ {"status": "warn", "warnings": ["baud_rate: 115201 is not a standard rate (110, 300, ...)"]}
422
+ ```
423
+
424
+ The `--check` flag is read-only — it never modifies the config file.
425
+
426
+ ### Config Examples
427
+
428
+ When you create a new config, termapy writes a complete `.cfg` file with all defaults (~30 lines). Here are some of the settings you can change:
429
+
430
+ ```json
431
+ {
432
+ "port": "COM4",
433
+ "baud_rate": 115200,
434
+ "auto_connect": true,
435
+ "auto_reconnect": true,
436
+ "title": "Sensor A",
437
+ "border_color": "blue",
438
+ "on_connect_cmd": "rev \n help dev"
439
+ }
440
+ ```
441
+
442
+ ### Custom Buttons
443
+
444
+ The demo project's "Info" button runs the `/cfg.info` command via a custom button:
445
+
446
+ ```json
447
+ {"enabled": true, "name": "Info", "command": "/cfg.info", "tooltip": "Project info"}
448
+ ```
449
+
450
+ ![Custom Info button in the toolbar](img/custom_info_button.png)
451
+
452
+ Add toolbar buttons that send commands, run scripts, or chain multiple actions. Use `\n` to separate multiple commands:
453
+
454
+ ```json
455
+ {
456
+ "custom_buttons": [
457
+ {"enabled": true, "name": "Reset", "command": "ATZ", "tooltip": "Reset device"},
458
+ {"enabled": true, "name": "Init", "command": "ATZ\\nAT+BAUD=115200\\n/sleep 500ms\\nAT+INFO", "tooltip": "Full init sequence"},
459
+ {"enabled": true, "name": "Status", "command": "/run status_check.run", "tooltip": "Run status script"}
460
+ ]
461
+ }
462
+ ```
463
+
464
+ ### Hardware Line Control
465
+
466
+ Set `flow_control` to `"manual"` to get DTR, RTS, and Break buttons in the toolbar — useful for devices that use these lines for reset or bootloader entry:
467
+
468
+ ```json
469
+ {
470
+ "port": "COM4",
471
+ "baud_rate": 115200,
472
+ "flow_control": "manual",
473
+ "title": "Hardware Debug"
474
+ }
475
+ ```
476
+
477
+ <details>
478
+ <summary>Full config reference</summary>
479
+
480
+ ```json
481
+ {
482
+ "config_version": 5,
483
+ "port": "COM4",
484
+ "baud_rate": 115200,
485
+ "byte_size": 8,
486
+ "parity": "N",
487
+ "stop_bits": 1,
488
+ "flow_control": "none",
489
+ "encoding": "utf-8",
490
+ "cmd_delay_ms": 0,
491
+ "line_ending": "\r",
492
+ "send_bare_enter": false,
493
+ "auto_connect": false,
494
+ "auto_reconnect": false,
495
+ "on_connect_cmd": "",
496
+ "echo_input": false,
497
+ "echo_input_fmt": "[purple]> {cmd}[/]",
498
+ "log_file": "",
499
+ "show_timestamps": false,
500
+ "show_line_endings": false,
501
+ "max_grep_lines": 100,
502
+ "title": "",
503
+ "border_color": "",
504
+ "max_lines": 10000,
505
+ "cmd_prefix": "/",
506
+ "config_read_only": false,
507
+ "os_cmd_enabled": false,
508
+ "show_traceback": false,
509
+ "custom_buttons": []
510
+ }
511
+ ```
512
+
513
+ | Field | Default | Description |
514
+ | -------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------- |
515
+ | `config_version` | `5` | Schema version — managed automatically by the migration system, do not edit |
516
+ | `port` | `""` | Serial port name -- auto-detected when only one port available (supports `$(env.NAME\|fallback)`) |
517
+ | `baud_rate` | `115200` | Baud rate |
518
+ | `byte_size` | `8` | Data bits (5, 6, 7, 8) |
519
+ | `parity` | `"N"` | Parity: `"N"`, `"E"`, `"O"`, `"M"`, `"S"` |
520
+ | `stop_bits` | `1` | Stop bits (1, 1.5, 2) |
521
+ | `flow_control` | `"none"` | `"none"`, `"rtscts"` (hardware), `"xonxoff"` (software), or `"manual"` (shows DTR/RTS/Break buttons) |
522
+ | `encoding` | `"utf-8"` | Character encoding for serial data. Common values: `"utf-8"`, `"latin-1"`, `"ascii"`, `"cp437"` |
523
+ | `cmd_delay_ms` | `0` | Delay in milliseconds between commands in autoconnect sequences and multi-command input (`cmd1 \n cmd2`) |
524
+ | `line_ending` | `"\r"` | Appended to each command. `"\r"` CR, `"\r\n"` CRLF, `"\n"` LF |
525
+ | `send_bare_enter` | `false` | Send the line ending when Enter is pressed with no input (for "press enter to continue" prompts) |
526
+ | `auto_connect` | `false` | Connect to the port on startup |
527
+ | `auto_reconnect` | `false` | Retry every 2.5s if the port drops or fails to open (does not control startup) |
528
+ | `on_connect_cmd` | `""` | Commands to send after connecting, separated by `\n`. Waits for idle between each |
529
+ | `echo_input` | `false` | Echo sent commands locally |
530
+ | `echo_input_fmt` | `"[purple]> {cmd}[/]"` | Rich markup format for echoed commands. `{cmd}` is replaced with the command text |
531
+ | `log_file` | `""` | Session log path. If empty, uses `<name>.log` in the config's subfolder |
532
+ | `show_timestamps` | `false` | Prefix each line in the terminal display with `[HH:MM:SS.mmm]` |
533
+ | `show_line_endings` | `false` | Show dim `\r` and `\n` markers in serial output for line-ending debugging (see note below) |
534
+ | `show_line_numbers` | `false` | Show line numbers in serial output |
535
+ | `hex_mode` | `false` | Display serial I/O as hex bytes instead of text |
536
+ | `max_grep_lines` | `100` | Maximum number of matching lines shown by `/grep` |
537
+ | `proto_frame_gap_ms` | `50` | Silence gap (ms) to detect end of a binary protocol frame |
538
+ | `title` | `""` | Title bar center text. Defaults to the config filename |
539
+ | `border_color` | `""` | Title bar and output border color. Any CSS color name or hex value |
540
+ | `max_lines` | `10000` | Maximum lines in the scrollback buffer |
541
+ | `cmd_prefix` | `"/"` | Prefix for local REPL commands (e.g. `/help`, `/cls`) |
542
+ | `config_read_only` | `false` | Disable the Edit button in config/script/proto pickers (`/cfg` still changes in-memory values) |
543
+ | `os_cmd_enabled` | `false` | Enable the `/os` REPL command to run shell commands |
544
+ | `show_traceback` | `false` | Include full stack trace in serial exception output (for debugging) |
545
+ | `custom_buttons` | `[]` | Array of custom button objects (see Custom Buttons above) |
546
+
547
+ **Note on `show_line_endings`:** This is a debug mode for troubleshooting line-ending mismatches (`\r` vs `\n` vs `\r\n`). When enabled, dim `\r` and `\n` markers appear inline in serial output before the characters are consumed by line splitting. Sent commands also show the configured line ending. Since the markers use ANSI escape sequences, they may interfere with device ANSI color output — turn `show_line_endings` off when not actively debugging.
548
+
549
+ </details>
550
+
551
+ </details>
552
+
553
+ <details>
554
+ <summary><strong>Scripting</strong> — automate command sequences with text files</summary>
555
+
556
+ ![Run menu / script picker dialog](img/run.png)
557
+
558
+ Create text files with one command per line and run them from the Run button or with the `/run` or the Scripts button. IN the file ines starting with `/` are REPL commands, lines starting with `#` are comments and everything else is sent to the device.
559
+
560
+ ```text
561
+ # Quick status check
562
+ AT+STATUS
563
+ /delay 300ms
564
+ AT+TEMP
565
+ /delay 300ms
566
+ ```
567
+
568
+ Scripts support delays (`/delay 500ms`), screen clearing (`/cls`), confirmation prompts (`/confirm Reset device?`), screenshots, and sequence counters with auto-increment for batch testing. See the demo scripts (`at_demo.run`, `smoke_test.run`) for examples.
569
+
570
+ </details>
571
+
572
+ <details>
573
+ <summary><strong>Data Capture</strong> — timed text capture, structured binary capture to CSV</summary>
574
+
575
+ Capture serial data to files without interrupting normal terminal display.
576
+
577
+ **Text capture** — timed, writes decoded text lines:
578
+
579
+ ```sh
580
+ /cap.text log.txt timeout=3s cmd=AT+INFO # capture 3 seconds of text
581
+ /cap.text session.txt timeout=10s mode=append # append, just listen (no command)
582
+ ```
583
+
584
+ **Binary capture** — raw bytes to file:
585
+
586
+ ```sh
587
+ /cap.bin raw.bin bytes=256 cmd=read_all
588
+ ```
589
+
590
+ **Structured capture** — binary data decoded via format spec to CSV:
591
+
592
+ ```sh
593
+ # Single-type column — 50 big-endian unsigned 16-bit values
594
+ /cap.struct data.csv fmt=Val:U1-2 records=50 cmd=AT+BINDUMP u16 50
595
+
596
+ # Mixed-type record — string + u8 + u16 + u32 + float (little-endian)
597
+ /cap.struct mixed.csv fmt=Label:S1-10 Counter:U11 Val16:U13-12 Val32:U17-14 Temp:F21-18 records=20 cmd=AT+BINDUMP 20
598
+
599
+ # Tab-separated output with echo to terminal
600
+ /cap.struct log.tsv fmt=A:U1-2 B:F3-6 records=100 sep=tab echo=on cmd=read
601
+ ```
602
+
603
+ The `fmt=` parameter uses the same format spec language as `/proto` — type codes `H` (hex), `U` (unsigned), `I` (signed), `S` (string), `F` (float), `B` (bit) with 1-based byte ranges. Byte range order determines endianness: `U1-2` = big-endian, `U2-1` = little-endian. Named columns (`Temp:U1-2`) produce a CSV header row; unnamed columns (`U1-2`) omit it.
604
+
605
+ | Format spec | C type | Meaning |
606
+ |-------------|------------|--------------------------------|
607
+ | `U1` | `uint8_t` | 1 unsigned byte |
608
+ | `U1-2` | `uint16_t` | 2-byte unsigned, big-endian |
609
+ | `U2-1` | `uint16_t` | 2-byte unsigned, little-endian |
610
+ | `U1-4` | `uint32_t` | 4-byte unsigned, big-endian |
611
+ | `U1-8` | `uint64_t` | 8-byte unsigned, big-endian |
612
+ | `I1` | `int8_t` | 1 signed byte |
613
+ | `I1-2` | `int16_t` | 2-byte signed, big-endian |
614
+ | `I1-4` | `int32_t` | 4-byte signed, big-endian |
615
+ | `I1-8` | `int64_t` | 8-byte signed, big-endian |
616
+ | `F1-4` | `float` | 4-byte IEEE 754 float |
617
+ | `F1-8` | `double` | 8-byte IEEE 754 double |
618
+ | `S1-10` | `char[10]` | 10-byte ASCII string |
619
+ | `H1-4` | | 4 bytes as hex (e.g. `0A1BFF03`) |
620
+
621
+ Auto-numbered filenames: use `$(n000)` for a 3-digit rotating sequence (000–999), tracked across sessions in a counter file.
622
+
623
+ ```sh
624
+ /cap.text log_$(n000).txt timeout=3s cmd=AT+INFO # log_000.txt, log_001.txt, ...
625
+ /cap.struct data_$(n00).csv fmt=V:U1-2 records=100 cmd=read
626
+ ```
627
+
628
+ A progress bar and Stop button overlay the toolbar during capture. The `Cap` button opens the cap/ folder.
629
+
630
+ </details>
631
+
632
+ <details>
633
+ <summary><strong>Binary Protocol Testing</strong> — hex send/receive, .pro test scripts, CRC</summary>
634
+
635
+ Send raw hex bytes and see the response:
636
+
637
+ ```sh
638
+ /proto.send 01 03 00 00 00 01 84 0A
639
+ TX: 01 03 00 00 00 01 84 0A
640
+ RX: 01 03 02 00 07 F9 86
641
+ (7 bytes, 12ms)
642
+ ```
643
+
644
+ Mix hex and quoted text:
645
+
646
+ ```text
647
+ /proto.send "AT+RST\r\n"
648
+ /proto.send FF 00 "hello" 0D 0A
649
+ ```
650
+
651
+ No line ending is appended — you send exactly the bytes you specify. Toggle `/proto.hex` to show all normal serial I/O as hex bytes.
652
+
653
+ ### Proto Test Scripts
654
+
655
+ Write `.pro` files (TOML format) for repeatable send/expect testing with pass/fail:
656
+
657
+ ```toml
658
+ name = "Modbus Register Test"
659
+ frame_gap = "20ms"
660
+
661
+ [[test]]
662
+ name = "Read 1 register"
663
+ send = "01 03 00 00 00 01 84 0A"
664
+ expect = "01 03 02 00 07 F9 86"
665
+
666
+ [[test]]
667
+ name = "Write register 5 = 1234"
668
+ send = "01 06 00 05 04 D2 1B 56"
669
+ expect = "01 06 00 05 04 D2 1B 56"
670
+ ```
671
+
672
+ Run with `/proto.run <file>` or from the proto debug screen, which adds repeat count, delay between runs, stop-on-error, scrolling results, and visualizer column data.
673
+
674
+ <!-- TODO: screenshot — proto debug screen showing test results with pass/fail coloring and visualizer columns -->
675
+
676
+ ### Inline Format Specs
677
+
678
+ Add `send_fmt` and `expect_fmt` to any test step to decode raw bytes into named columns. The proto debug screen displays the decoded values side by side with pass/fail highlighting — turning opaque hex into readable fields.
679
+
680
+ ```toml
681
+ [[test]]
682
+ name = "Read 2 registers"
683
+ send = "01 03 00 00 00 02 C4 0B"
684
+ send_fmt = "Title:Modbus_TX Slave:H1 Func:H2 Addr:U3-4 Count:U5-6 CRC:crc16-modbus_le"
685
+ expect = "01 03 04 00 07 00 14 4B FD"
686
+ expect_fmt = "Title:Modbus_Response Slave:H1 Func:H2 Bytes:U3 R0:U4-5 R1:U6-7 CRC:crc16-modbus_le"
687
+ ```
688
+
689
+ Each column is `Name:TypeBytes` where the type controls how bytes are displayed:
690
+
691
+ | Type | Description | Example | Display |
692
+ | ------ | ------------------------- | --------------------- | ------------------ |
693
+ | `H` | Hex (uppercase) | `H1` / `H3-4` | `0A` / `0A 2B` |
694
+ | `h` | Hex (lowercase) | `h1` | `0a` |
695
+ | `U` | Unsigned int (big-endian) | `U3-4` | `7` |
696
+ | `I` | Signed int (big-endian) | `I3-4` | `-1` |
697
+ | `S` | ASCII string | `S3-10` | `HELLO` |
698
+ | `B` | Bit field (integer) | `B4-5.0-2` | `3` |
699
+ | `b` | Bit field (binary string) | `b4-5.0-15` | `0000101000101011` |
700
+ | `F` | IEEE 754 float | `F3-6` | `3.14` |
701
+ | `_` | Padding (skip bytes) | `_3-4` | *(hidden)* |
702
+ | `crc*` | CRC auto-check | `CRC:crc16-modbus_le` | `OK` / `FAIL` |
703
+
704
+ Byte indices are 1-based. Ranges use `-` (e.g. `U3-4` = bytes 3–4). **Byte order is controlled by the index direction** — this is how you handle big-endian vs little-endian protocols:
705
+
706
+ - `U3-4` — big-endian (byte 3 is MSB, byte 4 is LSB)
707
+ - `U4-3` — little-endian (byte 4 is LSB, byte 3 is MSB)
708
+ - `U5-8` — 32-bit big-endian (4 bytes, MSB first)
709
+ - `U8-5` — 32-bit little-endian (4 bytes, LSB first)
710
+
711
+ This works for all multi-byte types (`U`, `I`, `H`, `F`, `B`). CRC columns auto-compute and verify the checksum over the preceding bytes. Append `_le` or `_be` to the CRC algorithm name for the byte order of the checksum itself:
712
+
713
+ - `CRC:crc16-modbus_le` — CRC-16/Modbus stored little-endian (low byte first, as Modbus RTU requires)
714
+ - `CRC:crc16-modbus_be` — same algorithm but stored big-endian (high byte first)
715
+
716
+ The demo project includes two `.pro` files that exercise inline format specs: `modbus_inline.pro` (register reads/writes with Modbus decoding) and `bitfield_inline.pro` (bit field extraction and binary display). Run them from the Proto button in `--demo` mode.
717
+
718
+ Here is the test that generates the Modbus response shown below:
719
+
720
+ ```toml
721
+ [[test]]
722
+ name = "Read 5 registers from addr 100"
723
+ send = "01 03 00 64 00 05 C4 16"
724
+ send_fmt = "Title:Modbus_TX Slave:H1 Func:H2 Addr:U3-4 Count:U5-6 CRC:crc16-modbus_le"
725
+ expect = "01 03 0A 05 1B 05 28 05 35 05 42 05 4F 8C 46"
726
+ expect_fmt = "Title:Modbus_Response Slave:H1 Func:H2 Bytes:U3 R0:U4-5 R1:U6-7 R2:U8-9 R3:U10-11 R4:U12-13 CRC:crc16-modbus_le"
727
+ ```
728
+
729
+ ![Inline format spec — decoded Modbus columns in proto debug screen](img/proto_inline_fmt.png)
730
+
731
+
732
+ Finally here is the serial log output of a protocol test:
733
+
734
+ ```text
735
+ ================================================================================
736
+ [2026-03-12 22:28:14] Script: Demo AT Command Test | Tests: 4 | Repeat: 1
737
+ ================================================================================
738
+ [PASS] AT basic
739
+ TX: 41 54 0D
740
+ EXP: 4F 4B 0D 0A
741
+ RX: 4F 4B 0D 0A
742
+ Time: 77ms
743
+ [Hex] TX spec: Hex:h1-*
744
+ [Hex] TX: Hex=41 54 0D
745
+ [Hex] RX spec: Hex:h1-*
746
+ [Hex] RX: Hex=4F 4B 0D 0A
747
+ [PASS] LED on
748
+ TX: 41 54 2B 4C 45 44 20 6F 6E 0D
749
+ EXP: 4F 4B 0D 0A
750
+ RX: 4F 4B 0D 0A
751
+ Time: 128ms
752
+ [Hex] TX spec: Hex:h1-*
753
+ [Hex] TX: Hex=41 54 2B 4C 45 44 20 6F 6E 0D
754
+ [Hex] RX spec: Hex:h1-*
755
+ [Hex] RX: Hex=4F 4B 0D 0A
756
+ [PASS] LED off
757
+ TX: 41 54 2B 4C 45 44 20 6F 66 66 0D
758
+ EXP: 4F 4B 0D 0A
759
+ RX: 4F 4B 0D 0A
760
+ Time: 99ms
761
+ [Hex] TX spec: Hex:h1-*
762
+ [Hex] TX: Hex=41 54 2B 4C 45 44 20 6F 66 66 0D
763
+ [Hex] RX spec: Hex:h1-*
764
+ [Hex] RX: Hex=4F 4B 0D 0A
765
+ [PASS] Unknown command
766
+ TX: 49 4E 56 41 4C 49 44 0D
767
+ EXP: 45 52 52 4F 52 3A 20 55 6E 6B 6E 6F 77 6E 20 63 6F 6D 6D 61 6E 64 20 27 49 4E 56 41 4C 49 44 27 0D 0A
768
+ RX: 45 52 52 4F 52 3A 20 55 6E 6B 6E 6F 77 6E 20 63 6F 6D 6D 61 6E 64 20 27 49 4E 56 41 4C 49 44 27 0D 0A
769
+ Time: 72ms
770
+ [Hex] TX spec: Hex:h1-*
771
+ [Hex] TX: Hex=49 4E 56 41 4C 49 44 0D
772
+ [Hex] RX spec: Hex:h1-*
773
+ [Hex] RX: Hex=45 52 52 4F 52 3A 20 55 6E 6B 6E 6F 77 6E 20 63 6F 6D 6D 61 6E 64 20 27 49 4E 56 41 4C 49 44 27 0D 0A
774
+ Summary: 4/4 PASS (4 tests)
775
+ ```
776
+
777
+ ### CRC Algorithms
778
+
779
+ 62 named CRC algorithms from the [reveng catalogue](https://reveng.sourceforge.io/crc-catalogue/all.htm) are built in. Browse with `/proto.crc.list`, inspect with `/proto.crc.help <name>`, compute with `/proto.crc.calc`.
780
+
781
+ </details>
782
+
783
+ <details>
784
+ <summary><strong>Demo Mode</strong> — simulated device for trying everything without hardware</summary>
785
+
786
+ `termapy --demo` launches a completely simulated COM port — no hardware needed. The simulated device (`BASSOMATIC-77`) responds to AT commands, NMEA/GPS sentences, and binary Modbus RTU frames, so you can exercise every termapy feature: serial I/O, scripting, protocol testing, and plugins.
787
+
788
+ > **Note:** The demo command sets (AT, NMEA, Modbus) are not validated protocol implementations. They simulate familiar interfaces so you can explore termapy's features without hardware.
789
+
790
+ <!-- TODO: screenshot — demo mode showing AT command output with the device responding -->
791
+
792
+ #### ASCII Commands
793
+
794
+ The device supports a full AT command set — type commands and get responses just like a real device:
795
+
796
+ | Command | Description |
797
+ | ------------------ | ------------------------------------------------- |
798
+ | `AT` | Connection test (returns `OK`) |
799
+ | `AT+PROD-ID` | Product identifier (returns `BASSOMATIC-77`) |
800
+ | `AT+INFO` | Device info (version, uptime, free memory) |
801
+ | `AT+TEMP` | Read temperature sensor |
802
+ | `AT+LED on\|off` | Control LED |
803
+ | `AT+NAME?` | Query device name |
804
+ | `AT+NAME=val` | Set device name (max 32 chars) |
805
+ | `AT+BAUD?` | Query baud rate |
806
+ | `AT+BAUD=val` | Set baud rate (9600, 19200, 38400, 57600, 115200) |
807
+ | `AT+STATUS` | Device status (LED, uptime, connections) |
808
+ | `AT+RESET` | Reset device (simulates boot sequence) |
809
+ | `mem <addr> [len]` | Hex memory dump (deterministic, max 256 bytes) |
810
+ | `help` | List all commands |
811
+
812
+ #### GPS / NMEA Commands
813
+
814
+ The device responds to standard NMEA queries and PMTK configuration commands. Position is fixed at the 50-yard line of Lumen Field, Seattle.
815
+
816
+ | Command | Description |
817
+ | --------------- | --------------------------------------------- |
818
+ | `$GPGGA` | Position fix (lat, lon, altitude, satellites) |
819
+ | `$GPRMC` | Recommended minimum nav (pos, speed, date) |
820
+ | `$GPGSA` | DOP and active satellites |
821
+ | `$GPGSV` | Satellites in view (elevation, azimuth, SNR) |
822
+ | `$PMTK220,1000` | Set update rate (acknowledged, no effect) |
823
+ | `$PMTK314,...` | Configure sentence output (acknowledged) |
824
+
825
+ #### Binary Protocol Testing
826
+
827
+ The device also speaks Modbus RTU (binary), so you can try protocol test files and visualizers. Use `/proto.send` with hex bytes (CRC included):
828
+
829
+ ```sh
830
+ /proto.send 01 03 00 00 00 01 84 0A # read 1 register from addr 0
831
+ /proto.send 01 06 00 05 04 D2 1B 56 # write register 5 = 1234
832
+ /proto.send 01 03 00 05 00 01 94 0B # read back register 5
833
+ ```
834
+
835
+ Modbus RTU supports function 0x03 (read holding registers) and 0x06 (write single register) with CRC16 enforced.
836
+
837
+ #### Bundled Scripts, Tests, and Plugins
838
+
839
+ The demo comes with everything wired up so you can try each feature:
840
+
841
+ - **Scripts** — `at_demo.run`, `smoke_test.run`, `status_check.run` — run via the Scripts button or `/run`
842
+ - **Proto test files** — `at_test.pro`, `bitfield_inline.pro`, `modbus_inline.pro` — run via the Proto button for pass/fail results
843
+ - **Plugins** — `/probe` sends a command sequence and reports results; `/cmd` adds a custom shortcut
844
+
845
+ </details>
846
+
847
+ <details>
848
+ <summary><strong>CLI Mode</strong> — plain-text terminal, no TUI</summary>
849
+
850
+ `termapy --cli` runs a plain-text serial terminal in your existing terminal window - no Textual UI, no mouse, just keyboard input and text output. All built-in plugins, scripting, and serial I/O work the same as the TUI.
851
+
852
+ ```sh
853
+ termapy --cli my_device # interactive terminal
854
+ termapy --cli --demo # demo device, no hardware needed
855
+ termapy --cli smoke_test.run # run a .run script and exit
856
+ termapy --cli my_device --no-color # strip ANSI color codes
857
+ ```
858
+
859
+ Passing a `.run` file to `--cli` automatically infers the config from the file's location and runs it. Passing a config name or path opens an interactive session.
860
+
861
+ **Features:**
862
+
863
+ - Rich colored output (toggle with `/color on|off` or `--no-color`)
864
+ - Command history shared with TUI (up/down arrows, persisted across sessions)
865
+ - Tab completion for REPL commands
866
+ - Script execution with `/run` (same scripts work in both TUI and CLI)
867
+ - `/delay` with progress bar for waits over 3 seconds (Ctrl+C to cancel)
868
+ - All `/port`, `/cfg`, `/var`, `/env`, `/proto.crc`, `/edit` commands work
869
+
870
+ **TUI-only features** (not available in CLI mode):
871
+
872
+ - `/ss.svg`, `/ss.txt` - screenshots (prints "not supported" message)
873
+ - `/grep` - scrollback search (no scrollback buffer in CLI)
874
+ - `/edit.cfg` - opens in system editor instead of built-in config editor
875
+ - Mouse interaction, modal dialogs, custom buttons
876
+
877
+ **Exit:** `/exit`, `/quit`, or Ctrl+C.
878
+
879
+ </details>
880
+
881
+ <details>
882
+ <summary><strong>Extending Termapy</strong> — plugins, subcommands, visualizers</summary>
883
+
884
+ ### Plugins
885
+
886
+ Add custom REPL commands by dropping a `.py` file in a plugin folder. No classes to subclass, no registration:
887
+
888
+ ```python
889
+ # hello.py — drop into termapy_cfg/plugin/ or termapy_cfg/<config>/plugin/
890
+ from termapy.plugins import Command, PluginContext
891
+
892
+ def _handler(ctx: PluginContext, args: str):
893
+ name = args.strip() or "world"
894
+ ctx.write(f"Hello, {name}!")
895
+
896
+ # ── COMMAND (must be at end of file) ──────────────────────────────────────────
897
+ COMMAND = Command(
898
+ name="hello",
899
+ args="{name}", # {braces} = optional, <angle> = required, "" = no args
900
+ help="Say hello.",
901
+ handler=_handler,
902
+ )
903
+ ```
904
+
905
+ **Plugin locations** (loaded in order, later overrides earlier):
906
+
907
+ 1. **Built-in** — shipped with termapy, always available
908
+ 2. **Global** — `termapy_cfg/plugin/*.py`, shared across all configs
909
+ 3. **Per-config** — `termapy_cfg/<name>/plugin/*.py`, specific to one config
910
+ 4. **App hooks** — frontend-specific commands (`/ss`, `/delay`, `/run`, etc.)
911
+
912
+ <details>
913
+ <summary>Subcommands</summary>
914
+
915
+ Use `sub_commands` for related operations. Users invoke them with dot notation (`/tool.run`):
916
+
917
+ ```python
918
+ from termapy.plugins import Command
919
+
920
+ def _run(ctx, args):
921
+ ctx.write(f"Running {args}...")
922
+
923
+ def _status(ctx, args):
924
+ ctx.write("All good.")
925
+
926
+ # ── COMMAND (must be at end of file) ──────────────────────────────────────────
927
+ COMMAND = Command(
928
+ name="tool",
929
+ help="A tool with subcommands.",
930
+ sub_commands={
931
+ "run": Command(args="<file>", help="Run a file.", handler=_run),
932
+ "status": Command(help="Show status.", handler=_status),
933
+ },
934
+ )
935
+ ```
936
+
937
+ The user types `/tool.run myfile` or `/tool.status`.
938
+
939
+ </details>
940
+
941
+ <details>
942
+ <summary>PluginContext API</summary>
943
+
944
+ The `ctx` object passed to every handler:
945
+
946
+ | Method / Attribute | Description |
947
+ | --------------------------- | ------------------------------------------------------------------ |
948
+ | `ctx.write(text, color)` | Print to the terminal (color is optional) |
949
+ | `ctx.write_markup(text)` | Print Rich markup text (e.g. `[bold red]Warning![/]`) |
950
+ | `ctx.cfg` | Current config dict (read-only access) |
951
+ | `ctx.config_path` | Path to the current `.cfg` config file |
952
+ | `ctx.port()` | The raw pyserial object, or `None` when disconnected |
953
+ | `ctx.is_connected()` | Check if the serial port is open |
954
+ | `ctx.log(prefix, text)` | Write to session log: `">"` TX, `"<"` RX, `"#"` status |
955
+ | `ctx.serial_write(data)` | Send bytes to the serial port (auto-logged as TX to session log) |
956
+ | `ctx.serial_wait_idle()` | Wait until serial output settles |
957
+ | `ctx.serial_read_raw()` | Read raw bytes with timeout framing (returns `bytes`) |
958
+ | `ctx.serial_io()` | Context manager for exclusive serial I/O (`with ctx.serial_io():`) |
959
+ | `ctx.ss_dir` | Screenshot directory (`Path`) |
960
+ | `ctx.scripts_dir` | Scripts directory (`Path`) |
961
+ | `ctx.confirm(message)` | Show Yes/Cancel dialog, return `bool` (scripts only) |
962
+ | `ctx.notify(text)` | Show a toast notification |
963
+ | `ctx.clear_screen()` | Clear the terminal output |
964
+ | `ctx.save_screenshot(path)` | Save an SVG screenshot to a file |
965
+ | `ctx.get_screen_text()` | Get terminal content as plain text |
966
+ | `ctx.open_file(path)` | Open a file or folder in the system viewer/editor |
967
+
968
+ There is also `ctx.engine` which exposes internal engine state (sequence counters, echo, config save, etc.). This is used by built-in commands and may change between versions — external plugins should avoid it.
969
+
970
+ </details>
971
+
972
+ <details>
973
+ <summary>Example plugins</summary>
974
+
975
+ See `examples/plugins/` for working examples:
976
+
977
+ - **hello.py** — minimal greeting command
978
+ - **at_test.py** — send AT commands over serial
979
+ - **timestamp.py** — print the current date/time
980
+ - **ping.py** — send a command and measure response time
981
+
982
+ A more complete example ships with `--demo`: the `probe.py` plugin demonstrates the drain → write → read → parse cycle for device interaction. Run `/help probe` or `/help.dev probe` to see its documentation.
983
+
984
+ </details>
985
+
986
+ ### Binary Format Specs
987
+
988
+ Embedded protocols send raw bytes. Format specs decode them into human-readable fields - so you see "Temp: 200" and "CRC: OK" instead of `00 C8 ... XX XX`. Used in protocol testing (`.pro` files), data capture (`/cap.struct`, `/cap.hex`), and the proto debug screen.
989
+
990
+ A format spec is a one-line definition of your packet layout. Each field has a name, a type, and a byte range:
991
+
992
+ ```text
993
+ "ID:H1 Temp:U2-3 Signed:I4-5 Status:H6"
994
+ ```
995
+
996
+ Given the bytes `01 00 C8 FF FE 0A`, this decodes to:
997
+
998
+ ```text
999
+ ID = 01 (byte 1 as hex)
1000
+ Temp = 200 (bytes 2-3 as unsigned int, big-endian)
1001
+ Signed = -2 (bytes 4-5 as signed int, big-endian)
1002
+ Status = 0A (byte 6 as hex)
1003
+ ```
1004
+
1005
+ In protocol tests, termapy decodes both expected and actual bytes using your spec, then shows per-column pass/fail:
1006
+
1007
+ ```text
1008
+ Expected: 01 00 C8 FF FE 0A -> ID:01 Temp:200 Signed:-2 Status:0A
1009
+ Actual: 01 00 C9 FF FE 0A -> ID:01 Temp:201 Signed:-2 Status:0A
1010
+ match MISMATCH match match
1011
+ ```
1012
+
1013
+ <details>
1014
+ <summary>Supported types</summary>
1015
+
1016
+ | Code | Meaning | Example | Output |
1017
+ | ------ | ---------------- | ------------------- | ------------- |
1018
+ | `H` | Hex bytes | `H1`, `H3-4` | `0A`, `01FF` |
1019
+ | `U` | Unsigned integer | `U1`, `U3-4` | `10`, `256` |
1020
+ | `I` | Signed integer | `I1`, `I3-4` | `-1`, `+127` |
1021
+ | `S` | ASCII string | `S5-12` | `Hello...` |
1022
+ | `F` | IEEE 754 float | `F1-4` | `3.14` |
1023
+ | `B` | Bit field | `B1.3`, `B1-2.7-9` | `1`, `5` |
1024
+ | `_` | Padding (hidden) | `_:_3-4` | *(skipped)* |
1025
+ | `crc*` | CRC verify | `CRC:crc16m_le` | pass/fail |
1026
+
1027
+ Integers support 1, 2, 3, 4, and 8 byte widths. Floats are 4-byte (F32) or 8-byte (F64).
1028
+
1029
+ </details>
1030
+
1031
+ <details>
1032
+ <summary>Endianness</summary>
1033
+
1034
+ Byte order in the spec IS the endianness - no flags needed:
1035
+
1036
+ - `U2-3` = bytes 2 then 3 = big-endian: `00 C8` = 200
1037
+ - `U3-2` = bytes 3 then 2 = little-endian: `C8 00` = 51200
1038
+ - `I4-5` = big-endian signed: `FF FE` = -2
1039
+ - `I5-4` = little-endian signed: `FE FF` = -257
1040
+
1041
+ You read the spec the same way you read the protocol datasheet. Modbus devices are big-endian (`U2-3`), x86-based devices are little-endian (`U3-2`).
1042
+
1043
+ </details>
1044
+
1045
+ <details>
1046
+ <summary>Real-world examples</summary>
1047
+
1048
+ **Modbus RTU response** (read 2 holding registers):
1049
+
1050
+ ```text
1051
+ "Slave:H1 Func:H2 Len:U3 Reg0:U4-5 Reg1:U6-7 CRC:crc16-modbus_le"
1052
+ ```
1053
+
1054
+ Decodes: `01 03 04 00 C8 01 F4 XX XX` -> Slave:01 Func:03 Len:4 Reg0:200 Reg1:500 CRC:pass
1055
+
1056
+ **GPS binary packet** (mixed types):
1057
+
1058
+ ```text
1059
+ "Sync:H1-2 MsgID:U3 Lat:F4-7 Lon:F8-11 Alt:F12-15 Sats:U16 _:_17 CRC:crc8-maxim"
1060
+ ```
1061
+
1062
+ **Sensor with bit flags** (status byte with packed bits):
1063
+
1064
+ ```text
1065
+ "Temp:U1-2 Humid:U3 MotorOn:B4.0 AlarmHi:B4.1 AlarmLo:B4.2 Mode:B4.5-7"
1066
+ ```
1067
+
1068
+ Decodes byte 4 into individual bit fields: MotorOn=1, AlarmHi=0, AlarmLo=0, Mode=3
1069
+
1070
+ **Simple checksum** (not CRC - custom sum):
1071
+
1072
+ ```text
1073
+ "Header:H1 Payload:H2-9 Sum:H10"
1074
+ ```
1075
+
1076
+ For non-standard checksums, add a CRC plugin (3 lines of Python):
1077
+
1078
+ ```python
1079
+ NAME = "sum8"
1080
+ WIDTH = 1
1081
+
1082
+ def compute(data: bytes) -> int:
1083
+ return sum(data) & 0xFF
1084
+ ```
1085
+
1086
+ Drop into `builtins/crc/` or `termapy_cfg/<name>/crc/`.
1087
+
1088
+ </details>
1089
+
1090
+ <details>
1091
+ <summary>CRC support</summary>
1092
+
1093
+ 62 built-in algorithms covering CRC-8, CRC-16, CRC-32 families (Modbus, XMODEM, CCITT, USB, and more).
1094
+
1095
+ In format specs, CRC columns verify data integrity automatically:
1096
+
1097
+ - `CRC:crc16-modbus_le` - little-endian Modbus CRC-16
1098
+ - `CRC:crc16-xmodem_be` - big-endian XMODEM CRC-16
1099
+ - `CRC:crc8-maxim` - 1-byte CRC (no endianness needed)
1100
+
1101
+ From the REPL:
1102
+
1103
+ - `/proto.crc.list` - show all 62 algorithms
1104
+ - `/proto.crc.help crc16-modbus` - show parameters
1105
+ - `/proto.crc.calc crc16-modbus 01 03 00 00 00 0A` - compute CRC
1106
+
1107
+ </details>
1108
+
1109
+ </details>
1110
+
1111
+ <details>
1112
+ <summary><strong>Portability</strong></summary>
1113
+
1114
+ Developed and tested on **Windows**. Basic usage verified on **macOS** (serial, ANSI rendering, screenshots). macOS support is **alpha** until further testing. Linux has not been tested.
1115
+
1116
+ </details>
1117
+
1118
+ <details>
1119
+ <summary><strong>Architecture</strong> — threading model</summary>
1120
+
1121
+ Textual runs on a single async event loop. Termapy uses `@work(thread=True)` for blocking operations, posting UI updates via `call_from_thread()`.
1122
+
1123
+ | Worker | Lifetime | Purpose |
1124
+ | ------------------- | ----------- | ------------------------------------------------------- |
1125
+ | `read_serial()` | Long-lived | Reads serial data in a loop, posts lines to the RichLog |
1126
+ | `_auto_reconnect()` | Short-lived | Retries serial connection every 2.5s until success |
1127
+ | `_run_lines()` | Short-lived | Sends multiple commands with inter-command delay |
1128
+ | `_run_script()` | Short-lived | Executes a `.run` script file line by line |
1129
+ | `_send_test()` | Short-lived | Runs a single protocol test case (send/receive/match) |
1130
+ | `_run_cmds()` | Short-lived | Sends setup/teardown commands for protocol tests |
1131
+
1132
+ Only `read_serial()` is long-lived. At most two workers run concurrently: the serial reader plus one command/script/test worker.
1133
+
1134
+ </details>
1135
+
1136
+ <details>
1137
+ <summary><strong>Test Coverage</strong> — 1191 tests, 68% overall</summary>
1138
+
1139
+ 1191 tests across 27 test files. Run with `uv run pytest`.
1140
+
1141
+ **Core logic** (serial engine, capture, REPL, protocol, config):
1142
+
1143
+ | Module | Coverage | Test file |
1144
+ | ------------------ | -------- | ------------------------------------ |
1145
+ | `defaults.py` | 97% | `test_defaults.py` |
1146
+ | `scripting.py` | 97% | `test_scripting.py` |
1147
+ | `migration.py` | 98% | `test_migration.py` |
1148
+ | `plugins.py` | 94% | `test_plugins.py` |
1149
+ | `capture.py` | 92% | `test_capture.py` |
1150
+ | `serial_engine.py` | 93% | `test_serial_engine.py` |
1151
+ | `serial_port.py` | 92% | `test_serial_port.py` |
1152
+ | `protocol.py` | 89% | `test_protocol.py` |
1153
+ | `config.py` | 87% | `test_app_config.py` |
1154
+ | `port_control.py` | 80% | `test_port_control.py` |
1155
+ | `repl.py` | 72% | `test_engine.py`, `test_repl_cfg.py` |
1156
+ | `demo.py` | 73% | `test_demo.py` |
1157
+ | `cli.py` | 53% | `test_cli.py` |
1158
+
1159
+ **Built-in plugins** — 15 of 18 plugins tested via mock `PluginContext` in `test_builtins.py`.
1160
+
1161
+ **UI code** — `app.py` (~3400 lines), `proto_debug.py` (~1160 lines), `dialogs.py` (~1200 lines) are Textual UI and tested manually. The 66% overall figure reflects these large untested UI files. Core logic coverage is higher — the focus has been on extracting business logic into testable modules and keeping UI as thin delegation.
1162
+
1163
+ </details>
1164
+
1165
+ <details>
1166
+ <summary><strong>Continuous Integration</strong> — GitHub Actions</summary>
1167
+
1168
+ All tests run automatically on push to `main` and on pull requests via GitHub Actions.
1169
+
1170
+ | Job | What it does |
1171
+ | --- | --- |
1172
+ | **test** | Runs `pytest` across Python 3.11, 3.12, 3.13, and 3.14 |
1173
+ | **coverage** | Runs `pytest --cov` on Python 3.14 and uploads to [Codecov](https://codecov.io/gh/hucker/termapy) |
1174
+ | **audit** | Runs `pip-audit` to check for known vulnerabilities in dependencies |
1175
+
1176
+ The CI badge at the top of this README reflects the current status of the test workflow. See [`.github/workflows/tests.yml`](.github/workflows/tests.yml) for the full configuration.
1177
+
1178
+ </details>