benchlab-pytools 3.3.1__tar.gz → 3.4.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.4.0}/CHANGELOG.md +17 -0
  2. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/PKG-INFO +1 -1
  3. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/__init__.py +1 -1
  4. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/core/datasource.py +13 -0
  5. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/flash/flash_manager.py +8 -2
  6. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/flash/flash_tool.py +27 -4
  7. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/flash/test_flash_manager.py +27 -0
  8. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/tui/README.md +7 -2
  9. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/tui/config.py +14 -1
  10. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/tui/test_tui_core.py +53 -4
  11. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/tui/tui_core.py +111 -2
  12. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/.flake8 +0 -0
  13. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/.gitignore +0 -0
  14. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/CONTRIBUTING.md +0 -0
  15. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/LICENSE +0 -0
  16. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/README.md +0 -0
  17. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/__main__.py +0 -0
  18. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/bootstrap.py +0 -0
  19. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/config/README.md +0 -0
  20. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/config/__init__.py +0 -0
  21. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/config/config_client.py +0 -0
  22. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/config/config_manager.py +0 -0
  23. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/config/config_tool.py +0 -0
  24. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/config/diff.py +0 -0
  25. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/config/examples/complete_config.json +0 -0
  26. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/config/examples/rgb_lighting.json +0 -0
  27. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/config/examples/simple_fan.json +0 -0
  28. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/config/http_dto.py +0 -0
  29. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/config/schema.py +0 -0
  30. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/config/test_config_client.py +0 -0
  31. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/config/test_config_manager.py +0 -0
  32. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/config/test_diff.py +0 -0
  33. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/config/test_http_config_client.py +0 -0
  34. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/config/test_http_dto.py +0 -0
  35. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/core/README.md +0 -0
  36. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/core/__init__.py +0 -0
  37. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/core/config.py +0 -0
  38. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/core/datasource_manager.py +0 -0
  39. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/core/device_registry.py +0 -0
  40. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/core/discovery.py +0 -0
  41. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/core/infrastructure.py +0 -0
  42. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/core/process_manager.py +0 -0
  43. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/core/retry.py +0 -0
  44. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/core/shared_serial.py +0 -0
  45. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/core/statistics.py +0 -0
  46. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/csv_log/README.md +0 -0
  47. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/csv_log/__init__.py +0 -0
  48. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/csv_log/csv_logger_enhanced.py +0 -0
  49. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/csv_log/message_batcher.py +0 -0
  50. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/csv_log/requirements.txt +0 -0
  51. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/csv_log/smart_retry.py +0 -0
  52. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/flash/README.md +0 -0
  53. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/flash/__init__.py +0 -0
  54. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/flash/dfu.py +0 -0
  55. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/flash/image.py +0 -0
  56. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/flash/requirements.txt +0 -0
  57. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/flash/test_dfu.py +0 -0
  58. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/flash/test_flash_tool.py +0 -0
  59. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/flash/test_image.py +0 -0
  60. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/graph/README.md +0 -0
  61. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/graph/__init__.py +0 -0
  62. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/graph/app.py +0 -0
  63. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/graph/requirements.txt +0 -0
  64. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/graph/runner.py +0 -0
  65. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/graph/test_graph.py +0 -0
  66. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/graph/ui.py +0 -0
  67. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/hwinfo/README.md +0 -0
  68. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/hwinfo/__init__.py +0 -0
  69. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/hwinfo/hwinfo_export.py +0 -0
  70. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/hwinfo/requirements.txt +0 -0
  71. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/hwinfo/test_hwinfo_export.py +0 -0
  72. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/launcher.py +0 -0
  73. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/link/.env.EXAMPLE +0 -0
  74. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/link/README.md +0 -0
  75. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/link/__init__.py +0 -0
  76. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/link/link_main.py +0 -0
  77. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/link/requirements.txt +0 -0
  78. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/link/test_link_main.py +0 -0
  79. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/main.py +0 -0
  80. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/menu.py +0 -0
  81. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/menu_classic.py +0 -0
  82. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/mqtt/README.md +0 -0
  83. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/mqtt/__init__.py +0 -0
  84. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/mqtt/mqtt.config_template +0 -0
  85. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/mqtt/mqtt_publisher.py +0 -0
  86. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/mqtt/requirements.txt +0 -0
  87. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/mqtt/test_mqtt_publisher.py +0 -0
  88. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/prefs.py +0 -0
  89. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/requirements.txt +0 -0
  90. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/restapi/.env.example +0 -0
  91. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/restapi/__init__.py +0 -0
  92. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/restapi/favicon.ico +0 -0
  93. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/restapi/readme.md +0 -0
  94. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/restapi/requirements.txt +0 -0
  95. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/restapi/telemetry_api.py +0 -0
  96. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/restapi/test_read_recovery.py +0 -0
  97. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/restapi/test_server.py +0 -0
  98. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/sources.py +0 -0
  99. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/tools.py +0 -0
  100. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/tui/__init__.py +0 -0
  101. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/tui/requirements.txt +0 -0
  102. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/tui/test_tui_main.py +0 -0
  103. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/tui/tui_main.py +0 -0
  104. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/README.md +0 -0
  105. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/NOTICE.md +0 -0
  106. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/README.md +0 -0
  107. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/Running_from_source_on_linux.md +0 -0
  108. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/__init__.py +0 -0
  109. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/assets/vu1_hello_world.png +0 -0
  110. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/community_applications.md +0 -0
  111. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/config.yaml +0 -0
  112. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/database.py +0 -0
  113. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/dial_driver.py +0 -0
  114. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/dials/Comms_Hub_Gauge.py +0 -0
  115. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/dials/Comms_Hub_Server.py +0 -0
  116. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/dials/__init__.py +0 -0
  117. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/dials/base_logger.py +0 -0
  118. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/installer/inc/icon.ico +0 -0
  119. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/installer/install.nsi +0 -0
  120. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/make_version.py +0 -0
  121. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/pylintrc +0 -0
  122. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/requirements.txt +0 -0
  123. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/serial_driver.py +0 -0
  124. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/server.py +0 -0
  125. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/server_config.py +0 -0
  126. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/server_dial_handler.py +0 -0
  127. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/vu_notifications.py +0 -0
  128. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/android-chrome-192x192.png +0 -0
  129. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/android-chrome-512x512.png +0 -0
  130. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/apple-touch-icon.png +0 -0
  131. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/css/demo.css +0 -0
  132. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/css/demo.min.css +0 -0
  133. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/css/demo.rtl.css +0 -0
  134. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/css/demo.rtl.min.css +0 -0
  135. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/css/tabler-flags.css +0 -0
  136. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/css/tabler-flags.min.css +0 -0
  137. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/css/tabler-flags.rtl.css +0 -0
  138. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/css/tabler-flags.rtl.min.css +0 -0
  139. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/css/tabler-payments.css +0 -0
  140. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/css/tabler-payments.min.css +0 -0
  141. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/css/tabler-payments.rtl.css +0 -0
  142. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/css/tabler-payments.rtl.min.css +0 -0
  143. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/css/tabler-social.css +0 -0
  144. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/css/tabler-social.min.css +0 -0
  145. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/css/tabler-social.rtl.css +0 -0
  146. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/css/tabler-social.rtl.min.css +0 -0
  147. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/css/tabler-vendors.css +0 -0
  148. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/css/tabler-vendors.min.css +0 -0
  149. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/css/tabler-vendors.rtl.css +0 -0
  150. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/css/tabler-vendors.rtl.min.css +0 -0
  151. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/css/tabler.css +0 -0
  152. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/css/tabler.min.css +0 -0
  153. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/css/tabler.rtl.css +0 -0
  154. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/css/tabler.rtl.min.css +0 -0
  155. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/css/theme.min.css +0 -0
  156. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/img/header.png +0 -0
  157. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/img/logo.png +0 -0
  158. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/js/dark-theme.js +0 -0
  159. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/js/dark-theme.min.js +0 -0
  160. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/js/demo.js +0 -0
  161. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/js/demo.min.js +0 -0
  162. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/js/jquery.min.js +0 -0
  163. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/js/tabler.esm.js +0 -0
  164. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/js/tabler.esm.min.js +0 -0
  165. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/js/tabler.js +0 -0
  166. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/js/tabler.min.js +0 -0
  167. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/js/vu1_api.js +0 -0
  168. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_api_key_settings.js +0 -0
  169. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_api_keys.js +0 -0
  170. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_dial.js +0 -0
  171. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_home.js +0 -0
  172. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_root.js +0 -0
  173. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/favicon-16x16.png +0 -0
  174. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/favicon-32x32.png +0 -0
  175. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/favicon.ico +0 -0
  176. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/favicon.png +0 -0
  177. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/index.html +0 -0
  178. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/views/api_keys.html +0 -0
  179. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/views/dial.html +0 -0
  180. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/views/footer.html +0 -0
  181. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/views/header.html +0 -0
  182. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/views/key_settings.html +0 -0
  183. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/VU-Server/www/views/start.html +0 -0
  184. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/__init__.py +0 -0
  185. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/assets/Barlow-Bold.ttf +0 -0
  186. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/assets/bl_dial_144x200.png +0 -0
  187. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/assets/bl_logo_144x200.png +0 -0
  188. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/devices.py +0 -0
  189. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/requirements.txt +0 -0
  190. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/sensors.py +0 -0
  191. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/test_devices.py +0 -0
  192. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/test_vu_server_config.py +0 -0
  193. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/test_vu_server_manager.py +0 -0
  194. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/test_vu_tui.py +0 -0
  195. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/vu_dial.config_template +0 -0
  196. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/vu_logo_gen.py +0 -0
  197. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/vu_server.config_template +0 -0
  198. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/vu_server_config.py +0 -0
  199. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/vu_server_manager.py +0 -0
  200. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/vu_tui.py +0 -0
  201. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/vu/vu_updater.py +0 -0
  202. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/wigidash/README.md +0 -0
  203. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/wigidash/__init__.py +0 -0
  204. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/wigidash/assets/Inter-Bold.ttf +0 -0
  205. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/wigidash/assets/Inter.ttf +0 -0
  206. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/wigidash/assets/benchlab.png +0 -0
  207. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/wigidash/benchlab_fleet.py +0 -0
  208. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/wigidash/benchlab_graph.py +0 -0
  209. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/wigidash/benchlab_overview.py +0 -0
  210. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/wigidash/benchlab_telemetry.py +0 -0
  211. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/wigidash/benchlab_ui.py +0 -0
  212. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/wigidash/benchlab_utils.py +0 -0
  213. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/wigidash/requirements.txt +0 -0
  214. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/wigidash/test_wigidash.py +0 -0
  215. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/wigidash/wigidash_device.py +0 -0
  216. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/wigidash/wigidash_manager.py +0 -0
  217. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/wigidash/wigidash_session.py +0 -0
  218. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/wigidash/wigidash_usb.py +0 -0
  219. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab/wigidash/wigidash_widget.py +0 -0
  220. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/benchlab.py +0 -0
  221. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/pyproject.toml +0 -0
  222. {benchlab_pytools-3.3.1 → benchlab_pytools-3.4.0}/pytest.ini +0 -0
@@ -4,6 +4,23 @@ 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.4.0] - 2026-10-22
8
+
9
+ ### Added
10
+ - TUI: BENCHLAB2 fault status is now surfaced. The Device tab shows a
11
+ quick-glance warning when a fault is currently active, and a new Faults
12
+ tab lists all 34 known BL2 faults with their live (`FaultStatus`) and
13
+ latched (`FaultLog`) state. Requires BL2 firmware 05+; ORIGINAL devices
14
+ and earlier BL2 firmware show an explanatory message instead. (#95)
15
+
16
+ ### Fixed
17
+ - `-flash`: confirmed against real hardware that a device doesn't always
18
+ re-enumerate as CDC on its own after leaving DFU mode, even though the
19
+ flash and verify already succeeded. The tool now says so plainly instead
20
+ of a vague "may just be slow re-enumeration" warning, and the
21
+ interactive flow prompts to replug the USB cable and re-checks the
22
+ device before exiting instead of leaving the user to guess. (#97)
23
+
7
24
  ## [3.3.1] - 2026-10-17
8
25
 
9
26
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: benchlab-pytools
3
- Version: 3.3.1
3
+ Version: 3.4.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
@@ -2,4 +2,4 @@
2
2
  BENCHLAB Telemetry Package
3
3
  """
4
4
 
5
- __version__ = "3.3.1"
5
+ __version__ = "3.4.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
 
@@ -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",
@@ -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,18 @@ 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 Fault enum members fit on one screen under MIN_TERMINAL_ROWS/COLS.
329
+ FAULT_COLS = {
330
+ 'NAME': 2, # Fault name column (left grid column)
331
+ 'ACTIVE': 26, # Active status column (left grid column)
332
+ 'LOGGED': 36, # Logged status column (left grid column)
333
+ 'NAME2': 48, # Fault name column (right grid column)
334
+ 'ACTIVE2': 72, # Active status column (right grid column)
335
+ 'LOGGED2': 82, # Logged status column (right grid column)
336
+ }
337
+ FAULT_ROWS_PER_COL = 17 # Rows per grid column (34 faults / 2)
338
+
326
339
 
327
340
  # ═══════════════════════════════════════════════════════════════════════════════
328
341
  # 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,53 @@ 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) == []
241
+ assert _decode_faults(0) == []
242
+ assert _decode_faults(1 << 9) == ["OCP_CPU1"]
243
+ decoded = _decode_faults((1 << 0) | (1 << 33))
244
+ assert set(decoded) == {"OTP_TCHIP", "CURRENT_IMBALANCE_HPWR2"}
245
+
246
+
198
247
  def test_help_modal_renders_without_stomping_main_refresh():
199
248
  """Regression test: help modal must be composited, not overwritten
200
249
  every tick.
@@ -13,10 +13,22 @@ from typing import Dict, Any, List, Optional
13
13
 
14
14
  from .config import Config
15
15
  from benchlab.core.statistics import ChannelStats, StatsFormatter
16
+ from benchlab_pycore.core import Fault
16
17
 
17
18
  logger = logging.getLogger("benchlab.tui.tui_core")
18
19
 
19
20
 
21
+ def _decode_faults(bitmask: Optional[float]) -> List[str]:
22
+ """Decode a FaultStatus/FaultLog bitmask into a list of Fault names.
23
+
24
+ Returns an empty list for None/0/falsy bitmasks (no faults set).
25
+ """
26
+ if not bitmask:
27
+ return []
28
+ bitmask = int(bitmask)
29
+ return [f.name for f in Fault if bitmask & (1 << f.value)]
30
+
31
+
20
32
  class TUICore:
21
33
  """
22
34
  Core TUI class handling rendering, navigation, and user input.
@@ -290,6 +302,8 @@ class TUICore:
290
302
  self._render_temperature_tab(snapshot, stats)
291
303
  elif self.current_tab == 7:
292
304
  self._render_fans_tab(snapshot, stats, height, width)
305
+ elif self.current_tab == 8:
306
+ self._render_faults_tab(snapshot)
293
307
  except curses.error:
294
308
  pass
295
309
 
@@ -603,9 +617,27 @@ class TUICore:
603
617
  self.stdscr.addstr(13, 4, f"{'Device UID':<22} {uid or 'N/A'}")
604
618
  self.stdscr.addstr(14, 4, f"{'Firmware Version':<22} {fw_str}")
605
619
 
606
- self._draw_section(16, 2, "TUI")
620
+ # Quick-glance active-fault warning (BL2 fw05+ only; nothing is
621
+ # drawn for ORIGINAL devices or pre-fw05 BL2, where FaultStatus
622
+ # is simply absent from sensor_data). Full per-fault detail,
623
+ # including latched history, lives on the Faults tab.
624
+ sd = snapshot.get('sensor_data') or {}
625
+ if variant == 'BL2' and sd.get('FaultStatus') is not None:
626
+ active_faults = _decode_faults(sd.get('FaultStatus'))
627
+ if active_faults:
628
+ names = ", ".join(active_faults)
629
+ msg = f"Active Faults: {names}"
630
+ width = self.stdscr.getmaxyx()[1]
631
+ if len(msg) > width - 5:
632
+ msg = (f"{len(active_faults)} active "
633
+ f"fault(s) — see Faults tab")
634
+ self.stdscr.addstr(
635
+ 16, 4, msg,
636
+ curses.color_pair(Config.COLOR_PAIRS['error']))
637
+
638
+ self._draw_section(18, 2, "TUI")
607
639
  self.stdscr.addstr(
608
- 17, 4, f"{'Refresh Interval':<22} {refresh_interval} s")
640
+ 19, 4, f"{'Refresh Interval':<22} {refresh_interval} s")
609
641
 
610
642
  except curses.error:
611
643
  pass
@@ -1216,6 +1248,83 @@ class TUICore:
1216
1248
  except curses.error:
1217
1249
  pass
1218
1250
 
1251
+ def _render_faults_tab(self, snapshot: Dict[str, Any]):
1252
+ """Render Faults tab — all 34 BL2 Fault enum members, live/latched."""
1253
+ self._draw_section(4, 2, "Fault Monitoring (BL2)")
1254
+
1255
+ if not snapshot.get('connected'):
1256
+ self._draw_disconnected("Fault")
1257
+ return
1258
+
1259
+ sd = snapshot.get('sensor_data') or {}
1260
+ device_info = snapshot.get('device_info') or {}
1261
+ product_id = device_info.get('ProductId')
1262
+ variant = device_info.get('variant')
1263
+ if variant is None:
1264
+ variant = 'BL2' if product_id == 0x11 else 'ORIGINAL'
1265
+
1266
+ if variant != 'BL2' or sd.get('FaultStatus') is None:
1267
+ try:
1268
+ self.stdscr.addstr(
1269
+ 6, 4,
1270
+ "Fault data unavailable for this device "
1271
+ "(BL2 firmware 05+ required).",
1272
+ curses.color_pair(Config.COLOR_PAIRS['default']))
1273
+ except curses.error:
1274
+ pass
1275
+ return
1276
+
1277
+ fault_status = int(sd.get('FaultStatus') or 0)
1278
+ fault_log = int(sd.get('FaultLog') or 0)
1279
+ cols = Config.Layout.FAULT_COLS
1280
+ rows_per_col = Config.Layout.FAULT_ROWS_PER_COL
1281
+
1282
+ try:
1283
+ hdr = f"{'Fault':<22} {'Active':<8} {'Logged':<8}"
1284
+ self.stdscr.addstr(
1285
+ 5, cols['NAME'], hdr,
1286
+ curses.A_UNDERLINE | curses.color_pair(
1287
+ Config.COLOR_PAIRS['default']))
1288
+ self.stdscr.addstr(
1289
+ 5, cols['NAME2'], hdr,
1290
+ curses.A_UNDERLINE | curses.color_pair(
1291
+ Config.COLOR_PAIRS['default']))
1292
+ except curses.error:
1293
+ pass
1294
+
1295
+ faults = list(Fault)
1296
+ for i, fault in enumerate(faults):
1297
+ col_idx = i // rows_per_col
1298
+ row = 6 + (i % rows_per_col)
1299
+ name_col = cols['NAME'] if col_idx == 0 else cols['NAME2']
1300
+ active_col = cols['ACTIVE'] if col_idx == 0 else cols['ACTIVE2']
1301
+ logged_col = cols['LOGGED'] if col_idx == 0 else cols['LOGGED2']
1302
+
1303
+ is_active = bool(fault_status & (1 << fault.value))
1304
+ is_logged = bool(fault_log & (1 << fault.value))
1305
+
1306
+ active_str = "ACTIVE" if is_active else "OK"
1307
+ active_color = (
1308
+ Config.COLOR_PAIRS['error'] if is_active
1309
+ else Config.COLOR_PAIRS['ok'])
1310
+ logged_str = "YES" if is_logged else "-"
1311
+ logged_color = (
1312
+ Config.COLOR_PAIRS['caution'] if is_logged
1313
+ else Config.COLOR_PAIRS['default'])
1314
+
1315
+ try:
1316
+ self.stdscr.addstr(
1317
+ row, name_col, f"{fault.name:<22}",
1318
+ curses.color_pair(Config.COLOR_PAIRS['default']))
1319
+ self.stdscr.addstr(
1320
+ row, active_col, f"{active_str:<8}",
1321
+ curses.color_pair(active_color))
1322
+ self.stdscr.addstr(
1323
+ row, logged_col, f"{logged_str:<8}",
1324
+ curses.color_pair(logged_color))
1325
+ except curses.error:
1326
+ pass
1327
+
1219
1328
  # ═══════════════════════════════════════════════════════════════════════════════
1220
1329
  # Helper Methods
1221
1330
  # ═══════════════════════════════════════════════════════════════════════════════