benchlab-pytools 3.3.1__tar.gz → 3.5.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 (222) hide show
  1. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/CHANGELOG.md +38 -0
  2. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/PKG-INFO +2 -2
  3. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/__init__.py +1 -1
  4. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/core/datasource.py +13 -0
  5. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/core/datasource_manager.py +15 -2
  6. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/core/discovery.py +4 -2
  7. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/flash/flash_manager.py +8 -2
  8. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/flash/flash_tool.py +27 -4
  9. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/flash/test_flash_manager.py +27 -0
  10. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/main.py +33 -0
  11. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/mqtt/mqtt_publisher.py +3 -0
  12. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/restapi/telemetry_api.py +1 -0
  13. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/tui/README.md +7 -2
  14. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/tui/config.py +15 -1
  15. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/tui/test_tui_core.py +56 -4
  16. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/tui/tui_core.py +127 -2
  17. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/pyproject.toml +1 -1
  18. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/.flake8 +0 -0
  19. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/.gitignore +0 -0
  20. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/CONTRIBUTING.md +0 -0
  21. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/LICENSE +0 -0
  22. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/README.md +0 -0
  23. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/__main__.py +0 -0
  24. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/bootstrap.py +0 -0
  25. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/config/README.md +0 -0
  26. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/config/__init__.py +0 -0
  27. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/config/config_client.py +0 -0
  28. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/config/config_manager.py +0 -0
  29. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/config/config_tool.py +0 -0
  30. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/config/diff.py +0 -0
  31. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/config/examples/complete_config.json +0 -0
  32. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/config/examples/rgb_lighting.json +0 -0
  33. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/config/examples/simple_fan.json +0 -0
  34. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/config/http_dto.py +0 -0
  35. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/config/schema.py +0 -0
  36. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/config/test_config_client.py +0 -0
  37. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/config/test_config_manager.py +0 -0
  38. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/config/test_diff.py +0 -0
  39. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/config/test_http_config_client.py +0 -0
  40. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/config/test_http_dto.py +0 -0
  41. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/core/README.md +0 -0
  42. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/core/__init__.py +0 -0
  43. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/core/config.py +0 -0
  44. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/core/device_registry.py +0 -0
  45. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/core/infrastructure.py +0 -0
  46. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/core/process_manager.py +0 -0
  47. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/core/retry.py +0 -0
  48. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/core/shared_serial.py +0 -0
  49. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/core/statistics.py +0 -0
  50. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/csv_log/README.md +0 -0
  51. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/csv_log/__init__.py +0 -0
  52. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/csv_log/csv_logger_enhanced.py +0 -0
  53. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/csv_log/message_batcher.py +0 -0
  54. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/csv_log/requirements.txt +0 -0
  55. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/csv_log/smart_retry.py +0 -0
  56. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/flash/README.md +0 -0
  57. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/flash/__init__.py +0 -0
  58. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/flash/dfu.py +0 -0
  59. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/flash/image.py +0 -0
  60. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/flash/requirements.txt +0 -0
  61. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/flash/test_dfu.py +0 -0
  62. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/flash/test_flash_tool.py +0 -0
  63. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/flash/test_image.py +0 -0
  64. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/graph/README.md +0 -0
  65. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/graph/__init__.py +0 -0
  66. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/graph/app.py +0 -0
  67. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/graph/requirements.txt +0 -0
  68. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/graph/runner.py +0 -0
  69. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/graph/test_graph.py +0 -0
  70. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/graph/ui.py +0 -0
  71. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/hwinfo/README.md +0 -0
  72. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/hwinfo/__init__.py +0 -0
  73. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/hwinfo/hwinfo_export.py +0 -0
  74. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/hwinfo/requirements.txt +0 -0
  75. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/hwinfo/test_hwinfo_export.py +0 -0
  76. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/launcher.py +0 -0
  77. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/link/.env.EXAMPLE +0 -0
  78. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/link/README.md +0 -0
  79. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/link/__init__.py +0 -0
  80. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/link/link_main.py +0 -0
  81. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/link/requirements.txt +0 -0
  82. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/link/test_link_main.py +0 -0
  83. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/menu.py +0 -0
  84. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/menu_classic.py +0 -0
  85. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/mqtt/README.md +0 -0
  86. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/mqtt/__init__.py +0 -0
  87. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/mqtt/mqtt.config_template +0 -0
  88. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/mqtt/requirements.txt +0 -0
  89. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/mqtt/test_mqtt_publisher.py +0 -0
  90. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/prefs.py +0 -0
  91. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/requirements.txt +0 -0
  92. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/restapi/.env.example +0 -0
  93. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/restapi/__init__.py +0 -0
  94. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/restapi/favicon.ico +0 -0
  95. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/restapi/readme.md +0 -0
  96. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/restapi/requirements.txt +0 -0
  97. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/restapi/test_read_recovery.py +0 -0
  98. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/restapi/test_server.py +0 -0
  99. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/sources.py +0 -0
  100. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/tools.py +0 -0
  101. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/tui/__init__.py +0 -0
  102. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/tui/requirements.txt +0 -0
  103. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/tui/test_tui_main.py +0 -0
  104. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/tui/tui_main.py +0 -0
  105. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/README.md +0 -0
  106. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/NOTICE.md +0 -0
  107. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/README.md +0 -0
  108. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/Running_from_source_on_linux.md +0 -0
  109. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/__init__.py +0 -0
  110. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/assets/vu1_hello_world.png +0 -0
  111. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/community_applications.md +0 -0
  112. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/config.yaml +0 -0
  113. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/database.py +0 -0
  114. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/dial_driver.py +0 -0
  115. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/dials/Comms_Hub_Gauge.py +0 -0
  116. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/dials/Comms_Hub_Server.py +0 -0
  117. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/dials/__init__.py +0 -0
  118. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/dials/base_logger.py +0 -0
  119. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/installer/inc/icon.ico +0 -0
  120. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/installer/install.nsi +0 -0
  121. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/make_version.py +0 -0
  122. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/pylintrc +0 -0
  123. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/requirements.txt +0 -0
  124. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/serial_driver.py +0 -0
  125. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/server.py +0 -0
  126. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/server_config.py +0 -0
  127. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/server_dial_handler.py +0 -0
  128. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/vu_notifications.py +0 -0
  129. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/android-chrome-192x192.png +0 -0
  130. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/android-chrome-512x512.png +0 -0
  131. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/apple-touch-icon.png +0 -0
  132. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/css/demo.css +0 -0
  133. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/css/demo.min.css +0 -0
  134. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/css/demo.rtl.css +0 -0
  135. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/css/demo.rtl.min.css +0 -0
  136. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/css/tabler-flags.css +0 -0
  137. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/css/tabler-flags.min.css +0 -0
  138. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/css/tabler-flags.rtl.css +0 -0
  139. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/css/tabler-flags.rtl.min.css +0 -0
  140. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/css/tabler-payments.css +0 -0
  141. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/css/tabler-payments.min.css +0 -0
  142. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/css/tabler-payments.rtl.css +0 -0
  143. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/css/tabler-payments.rtl.min.css +0 -0
  144. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/css/tabler-social.css +0 -0
  145. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/css/tabler-social.min.css +0 -0
  146. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/css/tabler-social.rtl.css +0 -0
  147. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/css/tabler-social.rtl.min.css +0 -0
  148. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/css/tabler-vendors.css +0 -0
  149. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/css/tabler-vendors.min.css +0 -0
  150. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/css/tabler-vendors.rtl.css +0 -0
  151. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/css/tabler-vendors.rtl.min.css +0 -0
  152. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/css/tabler.css +0 -0
  153. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/css/tabler.min.css +0 -0
  154. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/css/tabler.rtl.css +0 -0
  155. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/css/tabler.rtl.min.css +0 -0
  156. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/css/theme.min.css +0 -0
  157. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/img/header.png +0 -0
  158. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/img/logo.png +0 -0
  159. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/js/dark-theme.js +0 -0
  160. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/js/dark-theme.min.js +0 -0
  161. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/js/demo.js +0 -0
  162. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/js/demo.min.js +0 -0
  163. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/js/jquery.min.js +0 -0
  164. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/js/tabler.esm.js +0 -0
  165. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/js/tabler.esm.min.js +0 -0
  166. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/js/tabler.js +0 -0
  167. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/js/tabler.min.js +0 -0
  168. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/js/vu1_api.js +0 -0
  169. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_api_key_settings.js +0 -0
  170. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_api_keys.js +0 -0
  171. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_dial.js +0 -0
  172. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_home.js +0 -0
  173. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_root.js +0 -0
  174. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/favicon-16x16.png +0 -0
  175. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/favicon-32x32.png +0 -0
  176. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/favicon.ico +0 -0
  177. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/favicon.png +0 -0
  178. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/index.html +0 -0
  179. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/views/api_keys.html +0 -0
  180. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/views/dial.html +0 -0
  181. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/views/footer.html +0 -0
  182. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/views/header.html +0 -0
  183. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/views/key_settings.html +0 -0
  184. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/VU-Server/www/views/start.html +0 -0
  185. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/__init__.py +0 -0
  186. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/assets/Barlow-Bold.ttf +0 -0
  187. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/assets/bl_dial_144x200.png +0 -0
  188. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/assets/bl_logo_144x200.png +0 -0
  189. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/devices.py +0 -0
  190. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/requirements.txt +0 -0
  191. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/sensors.py +0 -0
  192. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/test_devices.py +0 -0
  193. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/test_vu_server_config.py +0 -0
  194. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/test_vu_server_manager.py +0 -0
  195. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/test_vu_tui.py +0 -0
  196. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/vu_dial.config_template +0 -0
  197. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/vu_logo_gen.py +0 -0
  198. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/vu_server.config_template +0 -0
  199. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/vu_server_config.py +0 -0
  200. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/vu_server_manager.py +0 -0
  201. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/vu_tui.py +0 -0
  202. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/vu/vu_updater.py +0 -0
  203. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/wigidash/README.md +0 -0
  204. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/wigidash/__init__.py +0 -0
  205. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/wigidash/assets/Inter-Bold.ttf +0 -0
  206. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/wigidash/assets/Inter.ttf +0 -0
  207. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/wigidash/assets/benchlab.png +0 -0
  208. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/wigidash/benchlab_fleet.py +0 -0
  209. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/wigidash/benchlab_graph.py +0 -0
  210. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/wigidash/benchlab_overview.py +0 -0
  211. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/wigidash/benchlab_telemetry.py +0 -0
  212. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/wigidash/benchlab_ui.py +0 -0
  213. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/wigidash/benchlab_utils.py +0 -0
  214. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/wigidash/requirements.txt +0 -0
  215. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/wigidash/test_wigidash.py +0 -0
  216. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/wigidash/wigidash_device.py +0 -0
  217. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/wigidash/wigidash_manager.py +0 -0
  218. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/wigidash/wigidash_session.py +0 -0
  219. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/wigidash/wigidash_usb.py +0 -0
  220. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab/wigidash/wigidash_widget.py +0 -0
  221. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/benchlab.py +0 -0
  222. {benchlab_pytools-3.3.1 → benchlab_pytools-3.5.0}/pytest.ini +0 -0
@@ -4,6 +4,44 @@ All notable changes to BENCHLAB PyTools are documented here. Format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows
5
5
  [SemVer](https://semver.org/).
6
6
 
7
+ ## [3.5.0] - 2026-11-04
8
+
9
+ ### Added
10
+ - BL1 (ORIGINAL, fw07+) fault status is now supported, matching BL2:
11
+ the TUI is variant-aware and decodes faults with the correct enum
12
+ for each device instead of always assuming BL2's (which previously
13
+ meant BL1 faults were silently never shown). (#100)
14
+ - `PatchVersion` is now surfaced end-to-end (TUI Device tab, discovery,
15
+ FastAPI `/devices`, MQTT publisher), including a fix for a
16
+ worker-loop bug that dropped it on every periodic device-list
17
+ refresh. (#100)
18
+ - A startup check now verifies the installed `benchlab-pycore` version
19
+ and fails with a clear message instead of an `ImportError` traceback
20
+ when it's missing or too old. (#100)
21
+
22
+ ### Changed
23
+ - Raised the `benchlab-pycore` floor to `>=0.9.1` after finding and
24
+ reporting a `read_device()` short-read bug in 0.9.0
25
+ (benchlab-pycore#22), fixed upstream in 0.9.1 (benchlab-pycore#23).
26
+ (#100)
27
+
28
+ ## [3.4.0] - 2026-10-22
29
+
30
+ ### Added
31
+ - TUI: BENCHLAB2 fault status is now surfaced. The Device tab shows a
32
+ quick-glance warning when a fault is currently active, and a new Faults
33
+ tab lists all 34 known BL2 faults with their live (`FaultStatus`) and
34
+ latched (`FaultLog`) state. Requires BL2 firmware 05+; ORIGINAL devices
35
+ and earlier BL2 firmware show an explanatory message instead. (#95)
36
+
37
+ ### Fixed
38
+ - `-flash`: confirmed against real hardware that a device doesn't always
39
+ re-enumerate as CDC on its own after leaving DFU mode, even though the
40
+ flash and verify already succeeded. The tool now says so plainly instead
41
+ of a vague "may just be slow re-enumeration" warning, and the
42
+ interactive flow prompts to replug the USB cable and re-checks the
43
+ device before exiting instead of leaving the user to guess. (#97)
44
+
7
45
  ## [3.3.1] - 2026-10-17
8
46
 
9
47
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: benchlab-pytools
3
- Version: 3.3.1
3
+ Version: 3.5.0
4
4
  Summary: Python control suite for BENCHLAB telemetry devices
5
5
  Project-URL: Homepage, https://benchlab.io/
6
6
  Project-URL: Repository, https://github.com/BenchLab-io/benchlab-pytools
@@ -22,7 +22,7 @@ Classifier: Programming Language :: Python :: 3.13
22
22
  Classifier: Topic :: System :: Hardware
23
23
  Classifier: Topic :: System :: Monitoring
24
24
  Requires-Python: >=3.10
25
- Requires-Dist: benchlab-pycore>=0.8.0
25
+ Requires-Dist: benchlab-pycore>=0.9.1
26
26
  Requires-Dist: packaging<26.0,>=23.0
27
27
  Requires-Dist: pydantic>=2.0.0
28
28
  Requires-Dist: python-dotenv<2.0,>=1.2.2
@@ -2,4 +2,4 @@
2
2
  BENCHLAB Telemetry Package
3
3
  """
4
4
 
5
- __version__ = "3.3.1"
5
+ __version__ = "3.5.0"
@@ -312,6 +312,15 @@ class DirectDataSource(DataSource):
312
312
  ser, product_id=product_id)
313
313
  if sensors:
314
314
  data = self._pycore['translate_sensor_struct'](sensors)
315
+ # translate_sensor_struct_bl2() doesn't read these
316
+ # off the raw struct (BL2 fw05+ only), so pull them
317
+ # directly here.
318
+ fault_status = getattr(sensors, 'FaultStatus', None)
319
+ fault_log = getattr(sensors, 'FaultLog', None)
320
+ if fault_status is not None:
321
+ data['FaultStatus'] = fault_status
322
+ if fault_log is not None:
323
+ data['FaultLog'] = fault_log
315
324
  data['timestamp'] = datetime.now(UTC).isoformat()
316
325
  with self._lock:
317
326
  self._latest_data[uid] = data
@@ -722,6 +731,10 @@ CS_SHORT_NAME_MAP: Dict[str, str] = {
722
731
  **{f"FAN{i}_T": f"Fan{i}_RPM" for i in range(1, 10)},
723
732
  **{f"FAN{i}_D": f"Fan{i}_Duty" for i in range(1, 10)},
724
733
  "FAN_EXT": "FanExtDuty",
734
+
735
+ # BL2 fault bitmasks (fw05+ only) — see Fault enum in benchlab_pycore
736
+ "FAULT_STATUS": "FaultStatus",
737
+ "FAULT_LOG": "FaultLog",
725
738
  }
726
739
 
727
740
 
@@ -372,7 +372,11 @@ class DataSourceManager:
372
372
  time.sleep(2.0)
373
373
  continue
374
374
 
375
- # Update device list periodically
375
+ # Update device list periodically. list_devices() only
376
+ # returns a summary shape; merge in get_device_info()'s
377
+ # fuller dict (as connect() does) so fields it alone
378
+ # provides -- e.g. PatchVersion -- don't get dropped on
379
+ # every refresh.
376
380
  try:
377
381
  devices = self._datasource.list_devices()
378
382
  with self._lock:
@@ -380,7 +384,16 @@ class DataSourceManager:
380
384
  for device in devices:
381
385
  uid = device.get('uid')
382
386
  if uid:
383
- self._devices[uid] = device
387
+ device_entry = device.copy()
388
+ try:
389
+ full_info = (
390
+ self._datasource.get_device_info(
391
+ uid))
392
+ if full_info:
393
+ device_entry.update(full_info)
394
+ except Exception:
395
+ pass
396
+ self._devices[uid] = device_entry
384
397
  except Exception as e:
385
398
  logger.debug(f"Error updating device list: {e}")
386
399
 
@@ -68,13 +68,15 @@ def discover_devices() -> List[Dict[str, Any]]:
68
68
  ser.close()
69
69
  if uid:
70
70
  fw = info.get("FwVersion", "?")
71
+ patch = info.get("PatchVersion", 0xFF)
71
72
  product_id = info.get(
72
73
  "ProductId", BENCHLAB_ORIGINAL_PRODUCT_ID)
73
74
  variant = (
74
75
  "BL2" if product_id == BENCHLAB_BL2_PRODUCT_ID
75
76
  else "ORIGINAL")
76
- devices.append({"uid": uid, "port": port,
77
- "fw": fw, "variant": variant})
77
+ devices.append({
78
+ "uid": uid, "port": port, "fw": fw,
79
+ "PatchVersion": patch, "variant": variant})
78
80
  logger.info(
79
81
  "Discovered BenchLab device UID=%s on %s "
80
82
  "(FW=%s, Variant=%s)",
@@ -325,7 +325,9 @@ class FlashManager:
325
325
 
326
326
  logger.warning(
327
327
  "%s did not reappear within %.0fs after leaving DFU mode -- "
328
- "this may just be slow re-enumeration, verify manually.",
328
+ "flash/verify already succeeded, but it didn't re-enumerate "
329
+ "as CDC on its own. Unplug and replug the USB cable to "
330
+ "reconnect it.",
329
331
  port, timeout)
330
332
  return None
331
333
 
@@ -353,7 +355,11 @@ class FlashManager:
353
355
  "Flash completed but verification failed")
354
356
 
355
357
  if port is not None:
356
- self.leave_and_reboot(usb_dev, port)
358
+ reboot_info = self.leave_and_reboot(usb_dev, port)
359
+ if reboot_info is None and self.source == "direct":
360
+ message += (
361
+ f" -- {port} did not come back on its own; unplug and "
362
+ "replug the USB cable to reconnect it")
357
363
  else:
358
364
  # No CDC port to wait for -- just trigger the DfuSe leave
359
365
  # sequence so the device reboots into the newly-flashed app.
@@ -226,8 +226,9 @@ def cmd_flash(args):
226
226
 
227
227
  def progress_cb(port, written, total):
228
228
  pct = (written / total * 100) if total else 100
229
+ done = total and written >= total
229
230
  print(f"\r {port}: {written}/{total} bytes ({pct:.0f}%)",
230
- end="", flush=True)
231
+ end="\n" if done else "", flush=True)
231
232
 
232
233
  results = manager.flash_many(
233
234
  [port for port, _ in targets], args.file,
@@ -316,8 +317,9 @@ def cmd_flash_bare_dfu(args):
316
317
 
317
318
  def progress_cb(written, total):
318
319
  pct = (written / total * 100) if total else 100
320
+ done = total and written >= total
319
321
  print(f"\r {written}/{total} bytes ({pct:.0f}%)",
320
- end="", flush=True)
322
+ end="\n" if done else "", flush=True)
321
323
 
322
324
  result = manager.flash_bare_dfu(
323
325
  product_id, data, verify_only=args.verify_only,
@@ -453,24 +455,45 @@ def interactive_mode(args):
453
455
 
454
456
  def progress_cb(port, written, total):
455
457
  pct = (written / total * 100) if total else 100
458
+ done = total and written >= total
456
459
  print(f"\r {port}: {written}/{total} bytes ({pct:.0f}%)",
457
- end="", flush=True)
460
+ end="\n" if done else "", flush=True)
458
461
 
459
462
  print()
460
463
  results = manager.flash_many(
461
464
  [_device_identifier(args, device) for device in targets],
462
465
  file_path, progress_cb=progress_cb,
463
466
  on_unsupported_firmware=_make_boot0_fallback_prompt())
464
- print()
465
467
 
466
468
  print()
467
469
  print("Results:")
468
470
  any_failed = False
471
+ needs_reconnect_check = []
469
472
  for result in results:
470
473
  status = "OK" if result.ok else "FAILED"
471
474
  if not result.ok:
472
475
  any_failed = True
473
476
  print(f" {result.port} [{status}] {result.message}")
477
+ if result.ok and "unplug and replug" in result.message:
478
+ needs_reconnect_check.append(result.port)
479
+
480
+ for port in needs_reconnect_check:
481
+ print()
482
+ while True:
483
+ choice = input(
484
+ f"Replug {port}'s USB cable, then press Enter to check "
485
+ "it's back (or type 'skip' to continue without "
486
+ "checking): ").strip().lower()
487
+ if choice == "skip":
488
+ break
489
+ info = manager.identify_device(port)
490
+ if info is not None:
491
+ fw = info.get('FwVersion', 0)
492
+ print(f" {port} is back (FW: 0x{fw:02X})")
493
+ break
494
+ print(f" {port} not detected yet -- check the cable and try "
495
+ "again.")
496
+
474
497
  print()
475
498
  input("Press Enter to exit...")
476
499
 
@@ -365,6 +365,33 @@ def test_flash_bare_dfu_flashes_without_cdc_round_trip(
365
365
  assert "Flashed and verified successfully" in result.message
366
366
 
367
367
 
368
+ def test_erase_verify_leave_hints_replug_when_port_does_not_reappear(
369
+ manager, monkeypatch):
370
+ """leave_and_reboot() returning None for a 'direct' flash (the CDC
371
+ port never came back on its own) must not be silently swallowed --
372
+ append an actionable unplug/replug hint to the result message rather
373
+ than reporting a bare success. Confirmed against real hardware: the
374
+ flash+verify succeed but the device needs a manual USB replug to
375
+ re-enumerate as CDC (see PR discussion)."""
376
+ fake_usb_dev = MagicMock()
377
+ monkeypatch.setattr(
378
+ manager, "verify_image", lambda usb_dev, d, pid: True)
379
+ monkeypatch.setattr(
380
+ manager, "erase_and_flash",
381
+ lambda usb_dev, d, pid, progress_cb=None: None)
382
+ monkeypatch.setattr(
383
+ manager, "leave_and_reboot", lambda usb_dev, port: None)
384
+
385
+ ok, message = manager._erase_verify_leave(
386
+ fake_usb_dev, b"\x00" * 16, BENCHLAB_ORIGINAL_PRODUCT_ID,
387
+ "COM3", verify_only=False)
388
+
389
+ assert ok
390
+ assert "Flashed and verified successfully" in message
391
+ assert "unplug and replug" in message.lower()
392
+ assert "COM3" in message
393
+
394
+
368
395
  def test_flash_bare_dfu_catches_exceptions(manager, monkeypatch):
369
396
  monkeypatch.setattr(
370
397
  manager, "wait_for_dfu_device",
@@ -17,6 +17,9 @@ import logging
17
17
  import os
18
18
  import sys
19
19
  import traceback
20
+ from importlib.metadata import PackageNotFoundError, version
21
+
22
+ from packaging.version import Version
20
23
 
21
24
  from .tools import CONSUMER_TOOLS, LAUNCH_PROFILES # noqa: F401
22
25
  # CONSUMER_TOOLS re-exported for benchlab.main.CONSUMER_TOOLS consumers
@@ -25,6 +28,34 @@ from .launcher import launch_tools_concurrent
25
28
 
26
29
  logger = logging.getLogger("benchlab.launcher")
27
30
 
31
+ # Keep in sync with the "benchlab-pycore>=X.Y.Z" pin in pyproject.toml --
32
+ # this is pytools' own belt-and-suspenders check so a stale pycore install
33
+ # (e.g. an env pip never upgraded) fails with a clear message instead of an
34
+ # ImportError traceback deep in a tool module.
35
+ MIN_PYCORE_VERSION = "0.9.1"
36
+
37
+
38
+ def _check_pycore_version() -> None:
39
+ try:
40
+ installed = version("benchlab-pycore")
41
+ except PackageNotFoundError:
42
+ print(
43
+ "ERROR: benchlab-pycore is not installed.\n"
44
+ "Install it with: pip install "
45
+ f"\"benchlab-pycore>={MIN_PYCORE_VERSION}\"",
46
+ file=sys.stderr)
47
+ sys.exit(1)
48
+
49
+ if Version(installed) < Version(MIN_PYCORE_VERSION):
50
+ print(
51
+ f"ERROR: benchlab-pycore {installed} is installed, but "
52
+ f"pytools requires >={MIN_PYCORE_VERSION}.\n"
53
+ "Upgrade it with: pip install "
54
+ f"\"benchlab-pycore>={MIN_PYCORE_VERSION}\"",
55
+ file=sys.stderr)
56
+ sys.exit(1)
57
+
58
+
28
59
  try:
29
60
  from .menu import interactive_loop
30
61
  except ImportError as e:
@@ -511,6 +542,8 @@ def launch_mode() -> None:
511
542
 
512
543
  def main() -> None:
513
544
  """Entry point."""
545
+ if not ({"-h", "--help", "--version"} & set(sys.argv[1:])):
546
+ _check_pycore_version()
514
547
  launch_mode()
515
548
 
516
549
 
@@ -351,6 +351,7 @@ def device_thread(device, cfg, publish_interval=1):
351
351
  vendor_id = 0
352
352
  product_id = BENCHLAB_ORIGINAL_PRODUCT_ID
353
353
  fw_version = device.get("firmware")
354
+ patch_version = 0xFF
354
355
  try:
355
356
  dev_info = read_device(ser)
356
357
  if dev_info:
@@ -358,6 +359,7 @@ def device_thread(device, cfg, publish_interval=1):
358
359
  product_id = dev_info.get(
359
360
  'ProductId', BENCHLAB_ORIGINAL_PRODUCT_ID)
360
361
  fw_version = dev_info.get('FwVersion', fw_version)
362
+ patch_version = dev_info.get('PatchVersion', 0xFF)
361
363
  except Exception:
362
364
  pass
363
365
  variant = (
@@ -373,6 +375,7 @@ def device_thread(device, cfg, publish_interval=1):
373
375
  "VendorId": vendor_id,
374
376
  "ProductId": product_id,
375
377
  "FwVersion": fw_version,
378
+ "PatchVersion": patch_version,
376
379
  "variant": variant,
377
380
  }
378
381
  topic_info = f"{cfg['topic_prefix']}/{uid}/info"
@@ -606,6 +606,7 @@ def list_devices():
606
606
  "uid": uid,
607
607
  "port": info.get("port", "unknown"),
608
608
  "firmware": device_info.get("FwVersion", "?"),
609
+ "PatchVersion": device_info.get("PatchVersion", 0xFF),
609
610
  "variant": device_info.get("variant", "ORIGINAL"),
610
611
  "VendorId": device_info.get("VendorId", 0),
611
612
  "ProductId": device_info.get("ProductId", 0),
@@ -33,7 +33,7 @@ The BENCHLAB Enhanced TUI provides a modern, feature-rich curses-based interface
33
33
 
34
34
  ## 📊 Telemetry Tabs
35
35
 
36
- The TUI has 8 tabs: `Fleet`, `Device`, `System`, `Motherboard`, `12VHPWR`, `Voltage`, `Temperature`, `Fans` (in this order; see `Config.TAB_NAMES` in `benchlab/tui/config.py`). Tabs can also be jumped to directly with the number keys `0`-`7`.
36
+ The TUI has 9 tabs: `Fleet`, `Device`, `System`, `Motherboard`, `12VHPWR`, `Voltage`, `Temperature`, `Fans`, `Faults` (in this order; see `Config.TAB_NAMES` in `benchlab/tui/config.py`). Tabs can also be jumped to directly with the number keys `0`-`8`.
37
37
 
38
38
  ### **Fleet Tab (Tab 0)**
39
39
  - Device list with connection status indicators
@@ -46,6 +46,7 @@ The TUI has 8 tabs: `Fleet`, `Device`, `System`, `Motherboard`, `12VHPWR`, `Volt
46
46
  - Device identification (Model, Vendor ID, Product ID, UID)
47
47
  - Firmware version display
48
48
  - TUI refresh interval
49
+ - Quick-glance active-fault warning (BL2 fw05+ only); see the Faults tab for full detail
49
50
 
50
51
  ### **System / Motherboard / 12VHPWR Tabs (Tabs 2-4)**
51
52
  - Real-time power consumption with progress bars
@@ -76,12 +77,16 @@ The TUI has 8 tabs: `Fleet`, `Device`, `System`, `Motherboard`, `12VHPWR`, `Volt
76
77
  - External fan duty monitoring
77
78
  - Fan statistics (average duty, max RPM, active count)
78
79
 
80
+ ### **Faults Tab (Tab 8)**
81
+ - Per-fault Active/Logged status for all BENCHLAB 2 fault conditions
82
+ - BL2 firmware 05+ only; shows an unavailable message on ORIGINAL devices or older BL2 firmware
83
+
79
84
  ## 🎮 Keyboard Shortcuts
80
85
 
81
86
  ### **Navigation**
82
87
  - `h`, `←` - Move to previous tab
83
88
  - `l`, `→` - Move to next tab
84
- - `0`-`7` - Jump directly to a tab by index
89
+ - `0`-`8` - Jump directly to a tab by index
85
90
  - `q`, `Q` - Quit application
86
91
  - `?` - Show/hide help modal
87
92
 
@@ -29,7 +29,8 @@ TAB_NAMES = [
29
29
  "12VHPWR",
30
30
  "Voltage",
31
31
  "Temperature",
32
- "Fans"]
32
+ "Fans",
33
+ "Faults"]
33
34
 
34
35
  # Statistics display width
35
36
  STAT_COLUMN_WIDTH = 10
@@ -323,6 +324,19 @@ class Layout:
323
324
  'STATS': 52, # Statistics column
324
325
  }
325
326
 
327
+ # Faults tab column positions — two-column grid (17 rows each) so all
328
+ # 34 BL2 Fault enum members (or 16 for ORIGINAL fw07+) fit on one
329
+ # screen under MIN_TERMINAL_ROWS/COLS.
330
+ FAULT_COLS = {
331
+ 'NAME': 2, # Fault name column (left grid column)
332
+ 'ACTIVE': 26, # Active status column (left grid column)
333
+ 'LOGGED': 36, # Logged status column (left grid column)
334
+ 'NAME2': 48, # Fault name column (right grid column)
335
+ 'ACTIVE2': 72, # Active status column (right grid column)
336
+ 'LOGGED2': 82, # Logged status column (right grid column)
337
+ }
338
+ FAULT_ROWS_PER_COL = 17 # Rows per grid column (34 faults / 2)
339
+
326
340
 
327
341
  # ═══════════════════════════════════════════════════════════════════════════════
328
342
  # Help Text
@@ -106,7 +106,7 @@ def _render_tab(tab_index, snapshot, fleet_devices=None):
106
106
 
107
107
 
108
108
  @pytest.mark.parametrize("tab_index",
109
- range(8),
109
+ range(9),
110
110
  ids=["fleet",
111
111
  "device",
112
112
  "system",
@@ -114,7 +114,8 @@ def _render_tab(tab_index, snapshot, fleet_devices=None):
114
114
  "hpwr",
115
115
  "voltage",
116
116
  "temperature",
117
- "fans"],
117
+ "fans",
118
+ "faults"],
118
119
  )
119
120
  def test_render_tab_survives_none_sensor_values(tab_index):
120
121
  """Every tab must render without raising when sensor_data values are None.
@@ -148,7 +149,7 @@ def test_fleet_tab_survives_disconnected_and_empty_fleet():
148
149
 
149
150
 
150
151
  @pytest.mark.parametrize("tab_index",
151
- range(8),
152
+ range(9),
152
153
  ids=["fleet",
153
154
  "device",
154
155
  "system",
@@ -156,7 +157,8 @@ def test_fleet_tab_survives_disconnected_and_empty_fleet():
156
157
  "hpwr",
157
158
  "voltage",
158
159
  "temperature",
159
- "fans"],
160
+ "fans",
161
+ "faults"],
160
162
  )
161
163
  def test_render_tab_survives_disconnected(tab_index):
162
164
  """All tabs must render their disconnected state without raising."""
@@ -195,6 +197,56 @@ def test_fans_tab_survives_many_fans_on_small_terminal():
195
197
  pytest.skip("curses screen unavailable in this environment")
196
198
 
197
199
 
200
+ def test_faults_tab_shows_no_warning_when_no_faults_active():
201
+ """Device tab draws no fault warning line when FaultStatus is all-clear."""
202
+ bl2_info = {"VendorId": 0xEE, "ProductId": 0x11, "FwVersion": 0x07,
203
+ "variant": "BL2"}
204
+ sensor_data = dict(SENSOR_DATA_ALL_NONE)
205
+ sensor_data["FaultStatus"] = 0
206
+ sensor_data["FaultLog"] = 0
207
+ snapshot = _snapshot(device_info=bl2_info, sensor_data=sensor_data)
208
+ _render_tab(1, snapshot) # Device tab
209
+ _render_tab(8, snapshot) # Faults tab
210
+
211
+
212
+ def test_faults_tab_decodes_active_and_logged_bits():
213
+ """Faults tab and Device tab warning must render without raising when
214
+ specific fault bits are set, for a BL2 fw05+ device."""
215
+ bl2_info = {"VendorId": 0xEE, "ProductId": 0x11, "FwVersion": 0x07,
216
+ "variant": "BL2"}
217
+ sensor_data = dict(SENSOR_DATA_ALL_NONE)
218
+ # Bit 9 = OCP_CPU1 (active), bit 32 = CURRENT_IMBALANCE_HPWR1 (logged only)
219
+ sensor_data["FaultStatus"] = 1 << 9
220
+ sensor_data["FaultLog"] = (1 << 9) | (1 << 32)
221
+ snapshot = _snapshot(device_info=bl2_info, sensor_data=sensor_data)
222
+ _render_tab(1, snapshot) # Device tab warning line
223
+ _render_tab(8, snapshot) # Faults tab full grid
224
+
225
+
226
+ def test_faults_tab_handles_device_without_fault_data():
227
+ """ORIGINAL devices and pre-fw05 BL2 firmware have no FaultStatus key —
228
+ the Faults tab must show an explanatory message, not crash."""
229
+ original_info = {
230
+ "VendorId": 0x1234, "ProductId": 0x10, "FwVersion": 0x01,
231
+ "variant": "ORIGINAL"}
232
+ snapshot = _snapshot(device_info=original_info) # no FaultStatus key
233
+ _render_tab(1, snapshot) # Device tab
234
+ _render_tab(8, snapshot) # Faults tab
235
+
236
+
237
+ def test_decode_faults_helper():
238
+ from benchlab.tui.tui_core import _decode_faults
239
+
240
+ assert _decode_faults(None, "BL2") == []
241
+ assert _decode_faults(0, "BL2") == []
242
+ assert _decode_faults(1 << 9, "BL2") == ["OCP_CPU1"]
243
+ decoded = _decode_faults((1 << 0) | (1 << 33), "BL2")
244
+ assert set(decoded) == {"OTP_TCHIP", "CURRENT_IMBALANCE_HPWR2"}
245
+
246
+ decoded_original = _decode_faults((1 << 0) | (1 << 10), "ORIGINAL")
247
+ assert set(decoded_original) == {"OTP_TCHIP", "OCP_ATX12V"}
248
+
249
+
198
250
  def test_help_modal_renders_without_stomping_main_refresh():
199
251
  """Regression test: help modal must be composited, not overwritten
200
252
  every tick.