benchlab-pytools 3.1.0__tar.gz → 3.2.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 (207) hide show
  1. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/CHANGELOG.md +46 -0
  2. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/PKG-INFO +11 -4
  3. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/README.md +4 -0
  4. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/__init__.py +1 -1
  5. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/config/README.md +15 -0
  6. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/config/config_client.py +46 -0
  7. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/config/test_config_client.py +86 -0
  8. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/core/README.md +1 -1
  9. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/csv_log/requirements.txt +1 -1
  10. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/requirements.txt +1 -1
  11. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/restapi/readme.md +11 -0
  12. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/restapi/telemetry_api.py +122 -0
  13. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/restapi/test_server.py +1 -0
  14. benchlab_pytools-3.2.0/benchlab/tui/requirements.txt +4 -0
  15. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/tui/test_tui_core.py +23 -0
  16. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/tui/test_tui_main.py +37 -0
  17. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/tui/tui_core.py +2 -0
  18. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/tui/tui_main.py +6 -3
  19. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/pyproject.toml +4 -2
  20. benchlab_pytools-3.1.0/benchlab/tui/requirements.txt +0 -2
  21. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/.flake8 +0 -0
  22. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/.gitignore +0 -0
  23. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/CONTRIBUTING.md +0 -0
  24. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/LICENSE +0 -0
  25. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/__main__.py +0 -0
  26. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/bootstrap.py +0 -0
  27. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/config/__init__.py +0 -0
  28. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/config/config_manager.py +0 -0
  29. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/config/config_tool.py +0 -0
  30. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/config/diff.py +0 -0
  31. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/config/examples/complete_config.json +0 -0
  32. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/config/examples/rgb_lighting.json +0 -0
  33. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/config/examples/simple_fan.json +0 -0
  34. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/config/schema.py +0 -0
  35. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/config/test_config_manager.py +0 -0
  36. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/config/test_diff.py +0 -0
  37. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/core/__init__.py +0 -0
  38. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/core/config.py +0 -0
  39. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/core/datasource.py +0 -0
  40. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/core/datasource_manager.py +0 -0
  41. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/core/device_registry.py +0 -0
  42. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/core/discovery.py +0 -0
  43. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/core/infrastructure.py +0 -0
  44. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/core/process_manager.py +0 -0
  45. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/core/retry.py +0 -0
  46. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/core/shared_serial.py +0 -0
  47. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/core/statistics.py +0 -0
  48. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/csv_log/README.md +0 -0
  49. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/csv_log/__init__.py +0 -0
  50. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/csv_log/csv_logger_enhanced.py +0 -0
  51. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/csv_log/message_batcher.py +0 -0
  52. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/csv_log/smart_retry.py +0 -0
  53. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/graph/README.md +0 -0
  54. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/graph/__init__.py +0 -0
  55. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/graph/app.py +0 -0
  56. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/graph/requirements.txt +0 -0
  57. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/graph/runner.py +0 -0
  58. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/graph/test_graph.py +0 -0
  59. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/graph/ui.py +0 -0
  60. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/hwinfo/README.md +0 -0
  61. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/hwinfo/__init__.py +0 -0
  62. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/hwinfo/hwinfo_export.py +0 -0
  63. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/hwinfo/requirements.txt +0 -0
  64. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/hwinfo/test_hwinfo_export.py +0 -0
  65. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/launcher.py +0 -0
  66. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/link/.env.EXAMPLE +0 -0
  67. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/link/README.md +0 -0
  68. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/link/__init__.py +0 -0
  69. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/link/link_main.py +0 -0
  70. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/link/test_link_main.py +0 -0
  71. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/main.py +0 -0
  72. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/menu.py +0 -0
  73. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/menu_classic.py +0 -0
  74. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/mqtt/README.md +0 -0
  75. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/mqtt/__init__.py +0 -0
  76. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/mqtt/mqtt.config_template +0 -0
  77. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/mqtt/mqtt_publisher.py +0 -0
  78. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/mqtt/requirements.txt +0 -0
  79. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/mqtt/test_mqtt_publisher.py +0 -0
  80. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/prefs.py +0 -0
  81. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/restapi/.env.example +0 -0
  82. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/restapi/__init__.py +0 -0
  83. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/restapi/favicon.ico +0 -0
  84. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/restapi/requirements.txt +0 -0
  85. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/sources.py +0 -0
  86. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/tools.py +0 -0
  87. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/tui/README.md +0 -0
  88. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/tui/__init__.py +0 -0
  89. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/tui/config.py +0 -0
  90. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/README.md +0 -0
  91. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/NOTICE.md +0 -0
  92. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/README.md +0 -0
  93. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/Running_from_source_on_linux.md +0 -0
  94. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/__init__.py +0 -0
  95. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/assets/vu1_hello_world.png +0 -0
  96. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/community_applications.md +0 -0
  97. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/config.yaml +0 -0
  98. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/database.py +0 -0
  99. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/dial_driver.py +0 -0
  100. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/dials/Comms_Hub_Gauge.py +0 -0
  101. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/dials/Comms_Hub_Server.py +0 -0
  102. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/dials/__init__.py +0 -0
  103. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/dials/base_logger.py +0 -0
  104. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/installer/inc/icon.ico +0 -0
  105. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/installer/install.nsi +0 -0
  106. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/make_version.py +0 -0
  107. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/pylintrc +0 -0
  108. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/requirements.txt +0 -0
  109. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/serial_driver.py +0 -0
  110. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/server.py +0 -0
  111. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/server_config.py +0 -0
  112. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/server_dial_handler.py +0 -0
  113. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/vu_notifications.py +0 -0
  114. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/android-chrome-192x192.png +0 -0
  115. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/android-chrome-512x512.png +0 -0
  116. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/apple-touch-icon.png +0 -0
  117. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/demo.css +0 -0
  118. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/demo.min.css +0 -0
  119. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/demo.rtl.css +0 -0
  120. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/demo.rtl.min.css +0 -0
  121. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-flags.css +0 -0
  122. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-flags.min.css +0 -0
  123. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-flags.rtl.css +0 -0
  124. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-flags.rtl.min.css +0 -0
  125. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-payments.css +0 -0
  126. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-payments.min.css +0 -0
  127. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-payments.rtl.css +0 -0
  128. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-payments.rtl.min.css +0 -0
  129. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-social.css +0 -0
  130. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-social.min.css +0 -0
  131. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-social.rtl.css +0 -0
  132. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-social.rtl.min.css +0 -0
  133. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-vendors.css +0 -0
  134. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-vendors.min.css +0 -0
  135. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-vendors.rtl.css +0 -0
  136. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-vendors.rtl.min.css +0 -0
  137. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler.css +0 -0
  138. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler.min.css +0 -0
  139. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler.rtl.css +0 -0
  140. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler.rtl.min.css +0 -0
  141. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/theme.min.css +0 -0
  142. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/img/header.png +0 -0
  143. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/img/logo.png +0 -0
  144. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/dark-theme.js +0 -0
  145. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/dark-theme.min.js +0 -0
  146. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/demo.js +0 -0
  147. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/demo.min.js +0 -0
  148. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/jquery.min.js +0 -0
  149. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/tabler.esm.js +0 -0
  150. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/tabler.esm.min.js +0 -0
  151. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/tabler.js +0 -0
  152. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/tabler.min.js +0 -0
  153. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/vu1_api.js +0 -0
  154. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_api_key_settings.js +0 -0
  155. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_api_keys.js +0 -0
  156. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_dial.js +0 -0
  157. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_home.js +0 -0
  158. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_root.js +0 -0
  159. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/favicon-16x16.png +0 -0
  160. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/favicon-32x32.png +0 -0
  161. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/favicon.ico +0 -0
  162. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/favicon.png +0 -0
  163. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/index.html +0 -0
  164. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/views/api_keys.html +0 -0
  165. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/views/dial.html +0 -0
  166. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/views/footer.html +0 -0
  167. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/views/header.html +0 -0
  168. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/views/key_settings.html +0 -0
  169. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/views/start.html +0 -0
  170. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/__init__.py +0 -0
  171. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/assets/Barlow-Bold.ttf +0 -0
  172. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/assets/bl_dial_144x200.png +0 -0
  173. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/assets/bl_logo_144x200.png +0 -0
  174. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/devices.py +0 -0
  175. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/requirements.txt +0 -0
  176. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/sensors.py +0 -0
  177. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/test_devices.py +0 -0
  178. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/test_vu_server_config.py +0 -0
  179. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/test_vu_server_manager.py +0 -0
  180. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/test_vu_tui.py +0 -0
  181. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/vu_dial.config_template +0 -0
  182. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/vu_logo_gen.py +0 -0
  183. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/vu_server.config_template +0 -0
  184. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/vu_server_config.py +0 -0
  185. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/vu_server_manager.py +0 -0
  186. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/vu_tui.py +0 -0
  187. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/vu_updater.py +0 -0
  188. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/README.md +0 -0
  189. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/__init__.py +0 -0
  190. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/assets/Inter-Bold.ttf +0 -0
  191. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/assets/Inter.ttf +0 -0
  192. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/assets/benchlab.png +0 -0
  193. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/benchlab_fleet.py +0 -0
  194. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/benchlab_graph.py +0 -0
  195. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/benchlab_overview.py +0 -0
  196. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/benchlab_telemetry.py +0 -0
  197. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/benchlab_ui.py +0 -0
  198. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/benchlab_utils.py +0 -0
  199. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/requirements.txt +0 -0
  200. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/test_wigidash.py +0 -0
  201. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/wigidash_device.py +0 -0
  202. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/wigidash_manager.py +0 -0
  203. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/wigidash_session.py +0 -0
  204. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/wigidash_usb.py +0 -0
  205. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/wigidash_widget.py +0 -0
  206. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab.py +0 -0
  207. {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/pytest.ini +0 -0
@@ -4,6 +4,51 @@ 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.2.0] - 2026-09-29
8
+
9
+ ### Added
10
+ - `GET /events` WebSocket on the FastAPI server (`benchlab.restapi.telemetry_api`):
11
+ a multiplexed event stream matching the frame envelope of the C# BenchLab
12
+ service's `/events` (`hello`/`telemetry`/`device` frames), so
13
+ `ServiceWsDataSource` (`--source service_ws`) can point at either server.
14
+ `telemetry.v` keeps pytools' own TUI-style sensor-key names (e.g.
15
+ `EPS1_Power`) rather than the C# service's `ShortName` convention (e.g.
16
+ `EPS1_P`) -- there's no pycore equivalent of `ShortName` to translate
17
+ into, and `ServiceWsDataSource`'s key mapping already passes unrecognised
18
+ keys through unchanged, so no client-side changes are needed. The
19
+ existing per-device `/device/{uid}/stream` WebSocket is unchanged.
20
+ - `ConfigClient.factory_cal_unlock()` (`direct` and `named_pipe` sources):
21
+ temporarily lifts write-protection on BL2's factory calibration slot
22
+ (BL2 firmware 7+ only). Disruptive -- a successful call resets the
23
+ device immediately; reconnect before issuing further commands.
24
+
25
+ ### Changed
26
+ - Bumped the `benchlab-pycore` floor to `>=0.8.0` (from `>=0.6.0`).
27
+ 0.8.0 fixes `write_calibration()`'s wire format on BL1 firmware 6+ /
28
+ BL2 firmware 7+, where `WRITE_CALIBRATION` changed from an
29
+ offset-addressed partial write to a whole-struct replace; sending the
30
+ old format there silently corrupts calibration data.
31
+ `DirectConfigClient.write_calibration()` was already firmware-agnostic
32
+ (it doesn't pass `fw_version`, so pycore auto-probes it), so no code
33
+ change was needed here beyond the floor bump. Companion release:
34
+ BENCHLAB_Service v2.6.0, which adds the same `FACTORY_CAL_UNLOCK`
35
+ command plus BL1/BL2 firmware compatibility and a stuck-disconnect
36
+ reconnect fix.
37
+
38
+ ### Fixed
39
+ - TUI: a disconnected remote data source (`fastapi_custom`, `mqtt_custom`,
40
+ `named_pipe`, `service_http`, `service_ws`, etc.) fell back to scanning
41
+ local serial ports for the Fleet view, which could probe ports already
42
+ owned by another collector and show unrelated local devices instead of
43
+ an empty fleet. (#74)
44
+ - TUI: the terminal-too-small warning was drawn but never flushed to the
45
+ screen before `render()` returned, leaving the terminal blank instead
46
+ of showing the warning. (#75)
47
+ - Packaging: `benchlab-pytools[tui]` was missing `pydantic` and `requests`,
48
+ both required to initialize an HTTP-backed TUI (`fastapi`/`service_http`
49
+ sources); a clean `[tui]`-only install crashed with `ModuleNotFoundError`
50
+ unless the server/all extras happened to already pull them in. (#76)
51
+
7
52
  ## [3.1.0] - 2026-09-08
8
53
 
9
54
  ### Added
@@ -114,6 +159,7 @@ export, MQTT publisher, VU dials, WigiDash, and config import/export tools,
114
159
  sharing a common data-source layer (direct serial, FastAPI, MQTT, named
115
160
  pipe, service HTTP, service WebSocket).
116
161
 
162
+ [3.2.0]: https://github.com/BenchLab-io/benchlab-pytools/compare/v3.1.0...v3.2.0
117
163
  [3.1.0]: https://github.com/BenchLab-io/benchlab-pytools/compare/v3.0.4...v3.1.0
118
164
  [3.0.4]: https://github.com/BenchLab-io/benchlab-pytools/compare/v3.0.3...v3.0.4
119
165
  [3.0.3]: https://github.com/BenchLab-io/benchlab-pytools/compare/v3.0.2...v3.0.3
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: benchlab-pytools
3
- Version: 3.1.0
3
+ Version: 3.2.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.6.0
25
+ Requires-Dist: benchlab-pycore>=0.8.0
26
26
  Requires-Dist: packaging<26.0,>=23.0
27
27
  Requires-Dist: python-dotenv<2.0,>=1.2.2
28
28
  Requires-Dist: pywin32>=306; platform_system == 'Windows'
@@ -30,7 +30,7 @@ Requires-Dist: textual<9.0,>=0.60
30
30
  Requires-Dist: windows-curses>=2.4.1; platform_system == 'Windows'
31
31
  Provides-Extra: all
32
32
  Requires-Dist: amqtt<0.13.0,>=0.12.0; extra == 'all'
33
- Requires-Dist: benchlab-pycore>=0.6.0; extra == 'all'
33
+ Requires-Dist: benchlab-pycore>=0.8.0; extra == 'all'
34
34
  Requires-Dist: blessed>=1.20.0; extra == 'all'
35
35
  Requires-Dist: dearpygui>=2.1.1; extra == 'all'
36
36
  Requires-Dist: fastapi==0.111.1; extra == 'all'
@@ -48,12 +48,13 @@ Requires-Dist: pyusb>=1.3.1; extra == 'all'
48
48
  Requires-Dist: pyyaml<7.0,>=6.0; extra == 'all'
49
49
  Requires-Dist: pyyaml>=6.0; extra == 'all'
50
50
  Requires-Dist: requests>=2.31.0; extra == 'all'
51
+ Requires-Dist: requests>=2.33.1; extra == 'all'
51
52
  Requires-Dist: ruamel-yaml; extra == 'all'
52
53
  Requires-Dist: tornado; extra == 'all'
53
54
  Requires-Dist: uvicorn[standard]==0.24.0; extra == 'all'
54
55
  Requires-Dist: websockets<16.0,>=13.0; extra == 'all'
55
56
  Provides-Extra: csv-log
56
- Requires-Dist: benchlab-pycore>=0.6.0; extra == 'csv-log'
57
+ Requires-Dist: benchlab-pycore>=0.8.0; extra == 'csv-log'
57
58
  Requires-Dist: pyserial>=3.5; extra == 'csv-log'
58
59
  Provides-Extra: graph
59
60
  Requires-Dist: dearpygui>=2.1.1; extra == 'graph'
@@ -73,7 +74,9 @@ Requires-Dist: uvicorn[standard]==0.24.0; extra == 'restapi'
73
74
  Provides-Extra: service-ws
74
75
  Requires-Dist: websockets<16.0,>=13.0; extra == 'service-ws'
75
76
  Provides-Extra: tui
77
+ Requires-Dist: pydantic>=2.0.0; extra == 'tui'
76
78
  Requires-Dist: pyserial>=3.5; extra == 'tui'
79
+ Requires-Dist: requests>=2.33.1; extra == 'tui'
77
80
  Provides-Extra: vu
78
81
  Requires-Dist: blessed>=1.20.0; extra == 'vu'
79
82
  Requires-Dist: numpy; extra == 'vu'
@@ -196,6 +199,10 @@ Most tools accept `--source` to choose where telemetry comes from:
196
199
  `/events` WebSocket: telemetry is pushed at the service's own cadence instead of
197
200
  polled, and device connect/disconnect is reflected without a manual rescan.
198
201
  Install its dependency with `pip install "benchlab-pytools[service_ws]"`.
202
+ The pytools FastAPI server (`fastapi`/`fastapi_custom`) also exposes an
203
+ `/events` endpoint with the same frame envelope, so `--source service_ws
204
+ --service-ws-url ws://<fastapi-host>:<port>/events` works against it too —
205
+ see `benchlab/restapi/readme.md`.
199
206
 
200
207
  Common connection flags:
201
208
 
@@ -102,6 +102,10 @@ Most tools accept `--source` to choose where telemetry comes from:
102
102
  `/events` WebSocket: telemetry is pushed at the service's own cadence instead of
103
103
  polled, and device connect/disconnect is reflected without a manual rescan.
104
104
  Install its dependency with `pip install "benchlab-pytools[service_ws]"`.
105
+ The pytools FastAPI server (`fastapi`/`fastapi_custom`) also exposes an
106
+ `/events` endpoint with the same frame envelope, so `--source service_ws
107
+ --service-ws-url ws://<fastapi-host>:<port>/events` works against it too —
108
+ see `benchlab/restapi/readme.md`.
105
109
 
106
110
  Common connection flags:
107
111
 
@@ -2,4 +2,4 @@
2
2
  BENCHLAB Telemetry Package
3
3
  """
4
4
 
5
- __version__ = "3.1.0"
5
+ __version__ = "3.2.0"
@@ -315,6 +315,21 @@ manager.export_config('COM4', 'output.json')
315
315
  manager.import_config('input.json', dry_run=False)
316
316
  ```
317
317
 
318
+ For lower-level access (used internally by `ConfigManager`), `create_config_client()`
319
+ returns a `ConfigClient` with `read_calibration()`/`write_calibration()`,
320
+ `save_config()`/`load_config()`/`reset_config()`, and (BL2 firmware 7+ only)
321
+ `factory_cal_unlock()`, which temporarily lifts write-protection on the factory
322
+ calibration slot. There is no CLI/JSON-config surface for `factory_cal_unlock()` —
323
+ it's a niche factory-calibration tool, not a user-facing config option, and a
324
+ successful call resets the device immediately:
325
+
326
+ ```python
327
+ from benchlab.config.config_client import create_config_client
328
+
329
+ client = create_config_client('direct', 'COM4')
330
+ client.factory_cal_unlock() # BL2 fw7+ only; device resets on success
331
+ ```
332
+
318
333
  ## See Also
319
334
 
320
335
  - [BENCHLAB PyTools Documentation](../../README.md) - Main PyTools documentation
@@ -149,6 +149,17 @@ class ConfigClient(ABC):
149
149
  """Reset configuration to factory defaults."""
150
150
  pass
151
151
 
152
+ @abstractmethod
153
+ def factory_cal_unlock(self) -> bool:
154
+ """Temporarily lift write-protection on BL2's factory calibration
155
+ slot (BL2 fw07+ only; rejected on ORIGINAL or older BL2 firmware).
156
+
157
+ Disruptive: a successful call resets the device immediately -- the
158
+ connection drops and must be reestablished before sending further
159
+ commands.
160
+ """
161
+ pass
162
+
152
163
  @abstractmethod
153
164
  def close(self):
154
165
  """Close connection and cleanup resources."""
@@ -438,6 +449,30 @@ class DirectConfigClient(ConfigClient):
438
449
  from benchlab_pycore.core import send_action
439
450
  return send_action(self.ser, action=self._CONFIG_ACTION_RESET)
440
451
 
452
+ def factory_cal_unlock(self) -> bool:
453
+ """Temporarily lift write-protection on BL2's factory calibration
454
+ slot. BL2 fw07+ only -- rejected on ORIGINAL or older BL2 firmware.
455
+
456
+ Disruptive: a successful call resets the device immediately -- the
457
+ USB CDC port drops and re-enumerates, so callers must reconnect
458
+ before sending any further command.
459
+ """
460
+ from benchlab_pycore.core import (
461
+ factory_cal_unlock, BENCHLAB_BL2_PRODUCT_ID)
462
+
463
+ # pycore's factory_cal_unlock() only reports whether the passphrase
464
+ # bytes were written to the serial port, not whether the firmware
465
+ # accepted them -- ORIGINAL and older BL2 firmware silently ignore
466
+ # the unknown opcode with no ACK either way. Gate on product_id
467
+ # ourselves so callers get an honest False instead of a misleading
468
+ # True on hardware where the unlock can never actually take effect.
469
+ if self.product_id != BENCHLAB_BL2_PRODUCT_ID:
470
+ logger.warning(
471
+ "factory_cal_unlock: not a BL2 device, refusing to send "
472
+ "(unknown opcode on ORIGINAL firmware)")
473
+ return False
474
+ return factory_cal_unlock(self.ser)
475
+
441
476
  def close(self):
442
477
  """Close serial connection."""
443
478
  if self.ser and self.ser.is_open:
@@ -611,6 +646,17 @@ class NamedPipeConfigClient(ConfigClient):
611
646
  result = self._send_command("ResetConfig")
612
647
  return result and result.get('success', False)
613
648
 
649
+ def factory_cal_unlock(self) -> bool:
650
+ """Temporarily lift write-protection on BL2's factory calibration
651
+ slot. BL2 fw07+ only -- rejected on ORIGINAL or older BL2 firmware.
652
+
653
+ Disruptive: a successful call resets the device immediately -- the
654
+ pipe and COM port disconnect, so callers must reconnect before
655
+ sending any further command.
656
+ """
657
+ result = self._send_command("SendFactoryCalUnlock", payload="benchlab")
658
+ return result and result.get('success', False)
659
+
614
660
  def close(self):
615
661
  """Close pipe connection."""
616
662
  self._close_pipe()
@@ -164,3 +164,89 @@ def test_save_config_propagates_a_failed_send(monkeypatch):
164
164
  "benchlab_pycore.core.send_action", lambda ser, action: False)
165
165
 
166
166
  assert client.save_config() is False
167
+
168
+
169
+ # ---------------------------------------------------------------------------
170
+ # factory_cal_unlock -- benchlab-pycore 0.8.0 / BENCHLAB_Service 2.6.0
171
+ #
172
+ # New BL2 fw07+ only command that temporarily lifts write-protection on the
173
+ # factory calibration slot. Disruptive (resets the device on success), but
174
+ # from this client's perspective it's a thin pass-through: DirectConfigClient
175
+ # calls pycore's factory_cal_unlock(ser); NamedPipeConfigClient sends the
176
+ # SendFactoryCalUnlock pipe command with the fixed passphrase payload.
177
+ # ---------------------------------------------------------------------------
178
+
179
+ def test_direct_factory_cal_unlock_calls_pycore(monkeypatch):
180
+ client = _make_client(BENCHLAB_BL2_PRODUCT_ID)
181
+ captured = {}
182
+
183
+ def fake_factory_cal_unlock(ser):
184
+ captured["called"] = True
185
+ return True
186
+ monkeypatch.setattr(
187
+ "benchlab_pycore.core.factory_cal_unlock", fake_factory_cal_unlock)
188
+
189
+ assert client.factory_cal_unlock() is True
190
+ assert captured["called"] is True
191
+
192
+
193
+ def test_direct_factory_cal_unlock_refuses_non_bl2_device(monkeypatch):
194
+ """pycore's factory_cal_unlock() only reports whether bytes were
195
+ written, not whether the firmware accepted them -- confirmed against
196
+ real BL1 hardware, which returns True even though the opcode is
197
+ unknown on ORIGINAL firmware and nothing happens. Gate on product_id
198
+ ourselves so callers get an honest False."""
199
+ client = _make_client(BENCHLAB_ORIGINAL_PRODUCT_ID)
200
+ captured = {}
201
+
202
+ def fake_factory_cal_unlock(ser):
203
+ captured["called"] = True
204
+ return True
205
+ monkeypatch.setattr(
206
+ "benchlab_pycore.core.factory_cal_unlock", fake_factory_cal_unlock)
207
+
208
+ assert client.factory_cal_unlock() is False
209
+ assert "called" not in captured
210
+
211
+
212
+ def test_direct_factory_cal_unlock_propagates_a_failed_call(monkeypatch):
213
+ client = _make_client(BENCHLAB_BL2_PRODUCT_ID)
214
+ monkeypatch.setattr(
215
+ "benchlab_pycore.core.factory_cal_unlock", lambda ser: False)
216
+
217
+ assert client.factory_cal_unlock() is False
218
+
219
+
220
+ def test_named_pipe_factory_cal_unlock_sends_command_and_passphrase(
221
+ monkeypatch):
222
+ from benchlab.config.config_client import NamedPipeConfigClient
223
+
224
+ client = NamedPipeConfigClient.__new__(NamedPipeConfigClient)
225
+ client.pipe_name = "BenchlabSensorPipe_11_TEST"
226
+ client.handle = None
227
+
228
+ captured = {}
229
+
230
+ def fake_send_command(command, payload=None):
231
+ captured["command"] = command
232
+ captured["payload"] = payload
233
+ return {"success": True}
234
+ monkeypatch.setattr(client, "_send_command", fake_send_command)
235
+
236
+ assert client.factory_cal_unlock() is True
237
+ assert captured["command"] == "SendFactoryCalUnlock"
238
+ assert captured["payload"] == "benchlab"
239
+
240
+
241
+ def test_named_pipe_factory_cal_unlock_propagates_a_failed_call(monkeypatch):
242
+ from benchlab.config.config_client import NamedPipeConfigClient
243
+
244
+ client = NamedPipeConfigClient.__new__(NamedPipeConfigClient)
245
+ client.pipe_name = "BenchlabSensorPipe_11_TEST"
246
+ client.handle = None
247
+
248
+ monkeypatch.setattr(
249
+ client, "_send_command",
250
+ lambda command, payload=None: {"success": False})
251
+
252
+ assert client.factory_cal_unlock() is False
@@ -27,7 +27,7 @@ A **DataSource abstraction layer** (`datasource.py`) with multiple implementatio
27
27
  | `MQTTDataSource` | `mqtt` / `mqtt_custom` | Subscribes to `<topic_prefix>/+/telemetry` and `<topic_prefix>/+/info` on an MQTT broker via `paho-mqtt`. Resolves `MQTT_PROTOCOL` (v3.1 / v3.1.1 / v5) via the module-level `resolve_mqtt_protocol()` helper. |
28
28
  | `NamedPipeDataSource` | `named_pipe` | Windows-only. Talks to the C# BENCHLAB Windows service over named pipes (`BenchlabDiscovery` for device listing, per-device `BenchlabSensorPipe_XX_YYY` pipes for telemetry). Normalizes the C# service's `ShortName`-keyed sensor payloads into the same flat key names Python tools expect, via the module-level `_normalise_cs_telemetry()` helper. |
29
29
  | `ServiceHttpDataSource` | `service_http` | REST client for the C# BENCHLAB service's HTTP API (default `http://localhost:8585`). Thin client only — does not start/manage the service. Same telemetry normalization as `NamedPipeDataSource`. |
30
- | `ServiceWsDataSource` | `service_ws` | Client for the C# BENCHLAB service's `/events` WebSocket stream (default `ws://localhost:8585/events`). Runs an asyncio event loop on a background daemon thread; consumes pushed `hello` / `telemetry` / `device` frames (no poll loop). Same telemetry normalization as `ServiceHttpDataSource` (shared `CS_SHORT_NAME_MAP` / `_normalise_cs_v()`). Requires the `websockets` package (`benchlab-pytools[service_ws]`). |
30
+ | `ServiceWsDataSource` | `service_ws` | Client for the C# BENCHLAB service's `/events` WebSocket stream (default `ws://localhost:8585/events`). Runs an asyncio event loop on a background daemon thread; consumes pushed `hello` / `telemetry` / `device` frames (no poll loop). Same telemetry normalization as `ServiceHttpDataSource` (shared `CS_SHORT_NAME_MAP` / `_normalise_cs_v()`). Requires the `websockets` package (`benchlab-pytools[service_ws]`). The pytools FastAPI server (`benchlab.restapi.telemetry_api`) exposes the same `/events` frame envelope, so this class also works unmodified against it — its `telemetry.v` keys are unmapped by `_normalise_cs_v()` since they're already in the target key convention, not the C# `ShortName` one. |
31
31
 
32
32
  Each implementation validates its constructor kwargs through a Pydantic model in `config.py` (`SerialConfig`, `FastAPIConfig`, `MQTTConfig`, `NamedPipeConfig`, `ServiceHttpConfig`, `ServiceWsConfig`).
33
33
 
@@ -1,3 +1,3 @@
1
1
  # benchlab/csv_log/requirements.txt
2
2
  pyserial>=3.5
3
- benchlab-pycore>=0.6.0
3
+ benchlab-pycore>=0.8.0
@@ -4,7 +4,7 @@ python-dotenv>=1.2.2,<2.0
4
4
  textual>=0.60,<9.0
5
5
 
6
6
  # BENCHLAB Serial Handler
7
- benchlab-pycore>=0.6.0
7
+ benchlab-pycore>=0.8.0
8
8
 
9
9
  # FastAPI Data Source
10
10
  fastapi==0.111.1
@@ -61,6 +61,7 @@ The server will start on `http://0.0.0.0:8000` by default (configurable via `API
61
61
 
62
62
  ### Real-time Streaming
63
63
  - `WebSocket /device/{uid}/stream` - Real-time telemetry updates, pushed as each device's reader thread reads new sensor data
64
+ - `WebSocket /events` - Multiplexed event stream for all devices: a `hello` frame on connect (service version, poll interval, device list), then `telemetry` and `device` (connected/disconnected) frames as they occur. Matches the frame envelope of the C# BENCHLAB service's `/events` (see `ServiceWsDataSource`, `--source service_ws`), so the same client works against either server — except `telemetry.v` keeps this server's own long-form sensor-key names (e.g. `EPS1_Power`) rather than the C# service's `ShortName` convention (e.g. `EPS1_P`).
64
65
 
65
66
  ### Monitoring
66
67
  - `GET /health` - Basic health check (status, platform, timestamp, connected_devices, total_clients)
@@ -131,6 +132,16 @@ ws.onmessage = (event) => {
131
132
  };
132
133
  ```
133
134
 
135
+ ### Multiplexed Event Stream (WebSocket)
136
+ ```javascript
137
+ const ws = new WebSocket('ws://localhost:8000/events');
138
+ ws.onmessage = (event) => {
139
+ const frame = JSON.parse(event.data);
140
+ // frame.type is "hello", "telemetry", or "device"
141
+ console.log(frame);
142
+ };
143
+ ```
144
+
134
145
  ## Production Deployment
135
146
 
136
147
  ### Docker Support
@@ -14,6 +14,7 @@ from fastapi.middleware.cors import CORSMiddleware
14
14
  from fastapi.responses import FileResponse, JSONResponse
15
15
  from pathlib import Path
16
16
 
17
+ import benchlab
17
18
  from benchlab_pycore.core import (
18
19
  read_sensors, read_device, translate_sensor_struct)
19
20
  from benchlab.core import (
@@ -86,6 +87,7 @@ api_port = Config.API_PORT
86
87
  # { uid: { "port": str, "latest": dict, "history": deque, "connected": bool } }
87
88
  devices_data = {}
88
89
  clients = {} # { uid: set([WebSocket, ...]) }
90
+ event_clients = set() # WebSockets subscribed to the /events stream
89
91
  main_loop = None # Will store main asyncio loop
90
92
  shutdown_event = threading.Event() # Graceful shutdown flag
91
93
  device_threads = {} # { uid: threading.Thread } - device reader threads
@@ -142,9 +144,12 @@ def device_scanner_loop():
142
144
  logger.info(
143
145
  "Device %s appears disconnected "
144
146
  "(port %s not found)", uid, port)
147
+ was_connected = data.get("connected", False)
145
148
  with data_lock:
146
149
  if uid in devices_data:
147
150
  devices_data[uid]["connected"] = False
151
+ if was_connected:
152
+ schedule_device_event(uid, "disconnected")
148
153
 
149
154
  logger.debug("Scanner: scan complete")
150
155
 
@@ -277,10 +282,77 @@ async def send_updates(uid, data):
277
282
  clients[uid].discard(ws)
278
283
 
279
284
 
285
+ async def _broadcast_event(frame):
286
+ """Push a hello/telemetry/device frame to every /events subscriber."""
287
+ with data_lock:
288
+ ws_list = list(event_clients)
289
+ dead_clients = set()
290
+ for ws in ws_list:
291
+ try:
292
+ await ws.send_json(frame)
293
+ except Exception:
294
+ dead_clients.add(ws)
295
+ if dead_clients:
296
+ with data_lock:
297
+ event_clients.difference_update(dead_clients)
298
+
299
+
300
+ async def send_event_telemetry(uid, data):
301
+ """Push a /events "telemetry" frame for this UID's latest reading.
302
+
303
+ ``v`` keeps pytools' own TUI-style sensor keys (e.g. "EPS1_Power")
304
+ rather than the C# service's ShortName convention (e.g. "EPS1_P") --
305
+ there's no pycore equivalent of ShortName to translate into, and
306
+ ServiceWsDataSource's ShortName->TUI-key mapping already passes
307
+ unrecognised keys through unchanged, so this is a no-op there rather
308
+ than a translation, but no client-side change is required.
309
+ """
310
+ await _broadcast_event({
311
+ "type": "telemetry",
312
+ "uid": uid,
313
+ "ts": data.get("timestamp"),
314
+ "v": {k: v for k, v in data.items() if k != "timestamp"},
315
+ })
316
+
317
+
280
318
  def schedule_update(uid, data):
281
319
  """Thread-safe schedule to send telemetry to WebSocket clients."""
282
320
  if main_loop is not None and not main_loop.is_closed():
283
321
  asyncio.run_coroutine_threadsafe(send_updates(uid, data), main_loop)
322
+ asyncio.run_coroutine_threadsafe(
323
+ send_event_telemetry(uid, data), main_loop)
324
+
325
+
326
+ def _device_to_wire_shape(uid, info=None):
327
+ """Shape a device's info the way /events' hello/device frames expect
328
+ (mirrors the C# service's device shape), independent of GET /devices'
329
+ own response shape."""
330
+ if info is None:
331
+ with data_lock:
332
+ info = devices_data.get(uid, {})
333
+ device_info = info.get("info", {}) or {}
334
+ return {
335
+ "uid": uid,
336
+ "name": device_info.get("deviceName", ""),
337
+ "productId": device_info.get("ProductId", 0),
338
+ "port": info.get("port", "unknown"),
339
+ "status": "CONNECTED" if info.get("connected", False) else
340
+ "DISCONNECTED",
341
+ "sensorCount": len(info.get("latest", {}) or {}),
342
+ "firmwareVersion": device_info.get("FwVersion", 0),
343
+ }
344
+
345
+
346
+ def schedule_device_event(uid, event):
347
+ """Thread-safe schedule to broadcast a /events "device" frame."""
348
+ if main_loop is not None and not main_loop.is_closed():
349
+ frame = {
350
+ "type": "device",
351
+ "uid": uid,
352
+ "event": event,
353
+ "device": _device_to_wire_shape(uid),
354
+ }
355
+ asyncio.run_coroutine_threadsafe(_broadcast_event(frame), main_loop)
284
356
 
285
357
 
286
358
  RECONNECT_DELAY = 2.0 # seconds
@@ -383,6 +455,7 @@ def read_device_loop(port, uid):
383
455
  if uid in devices_data:
384
456
  devices_data[uid]["connected"] = False
385
457
  devices_data[uid]["latest"] = create_empty_telemetry()
458
+ schedule_device_event(uid, "disconnected")
386
459
  logger.info("[%s] Device disconnected", uid)
387
460
 
388
461
  try:
@@ -412,6 +485,7 @@ def read_device_loop(port, uid):
412
485
  with data_lock:
413
486
  if uid in devices_data:
414
487
  devices_data[uid]["connected"] = True
488
+ schedule_device_event(uid, "connected")
415
489
  logger.info("Connected to device %s on %s", uid, port)
416
490
  except Exception as exc:
417
491
  ser = None # Ensure ser is None on error
@@ -490,6 +564,7 @@ def read_device_loop(port, uid):
490
564
  if uid in devices_data:
491
565
  devices_data[uid]["connected"] = False
492
566
  devices_data[uid]["latest"] = create_empty_telemetry()
567
+ schedule_device_event(uid, "disconnected")
493
568
 
494
569
  if ser:
495
570
  try:
@@ -627,6 +702,50 @@ async def stream_device(uid: str, ws: WebSocket):
627
702
  logger.info("[%s] Client disconnected (%d total)", uid, client_count)
628
703
 
629
704
 
705
+ def build_hello_frame():
706
+ """Build the /events "hello" frame sent on connect: service version,
707
+ poll interval, and the current device list, matching the C# service's
708
+ /events wire shape."""
709
+ with data_lock:
710
+ uids = list(devices_data.keys())
711
+ return {
712
+ "type": "hello",
713
+ "serviceVersion": benchlab.__version__,
714
+ "pollIntervalMs": int(Config.POLL_INTERVAL * 1000),
715
+ "devices": [_device_to_wire_shape(uid) for uid in uids],
716
+ }
717
+
718
+
719
+ @app.websocket("/events")
720
+ async def events_stream(ws: WebSocket):
721
+ """Multiplexed event stream: hello on connect, then telemetry/device
722
+ frames for every device, matching the C# BenchLab service's /events
723
+ protocol (see ServiceWsDataSource in benchlab.core.datasource, the
724
+ consumer this endpoint is meant to be compatible with)."""
725
+ await ws.accept()
726
+ with data_lock:
727
+ event_clients.add(ws)
728
+ await ws.send_json(build_hello_frame())
729
+ logger.info("[/events] Client connected (%d total)", len(event_clients))
730
+ try:
731
+ while True:
732
+ msg = await ws.receive_json()
733
+ if isinstance(msg, dict) and msg.get("type") == "ping":
734
+ await ws.send_json({
735
+ "type": "pong",
736
+ "ts": datetime.now().strftime("%Y-%m-%dT%H:%M:%S"),
737
+ })
738
+ # "subscribe" and any other client message: accepted, no-op --
739
+ # this endpoint always broadcasts every device to every client.
740
+ except WebSocketDisconnect:
741
+ pass
742
+ finally:
743
+ with data_lock:
744
+ event_clients.discard(ws)
745
+ logger.info(
746
+ "[/events] Client disconnected (%d total)", len(event_clients))
747
+
748
+
630
749
  @app.get("/favicon.ico")
631
750
  def favicon():
632
751
  return FileResponse(Path(__file__).parent / "favicon.ico")
@@ -766,9 +885,12 @@ def scan_for_devices():
766
885
  disconnected.append({"uid": uid, "port": port})
767
886
  logger.info(
768
887
  "Device %s appears to be disconnected from %s", uid, port)
888
+ was_connected = data.get("connected", False)
769
889
  with data_lock:
770
890
  if uid in devices_data:
771
891
  devices_data[uid]["connected"] = False
892
+ if was_connected:
893
+ schedule_device_event(uid, "disconnected")
772
894
 
773
895
  result = {
774
896
  "scan_time": datetime.now().isoformat(),
@@ -53,6 +53,7 @@ def test_api_endpoints():
53
53
  "/device/{uid}/history",
54
54
  "/device/{uid}/sensors",
55
55
  "/device/{uid}/stream",
56
+ "/events",
56
57
  "/favicon.ico",
57
58
  "/health",
58
59
  "/status",
@@ -0,0 +1,4 @@
1
+ pyserial>=3.5
2
+ windows-curses>=2.4.1; platform_system=="Windows"
3
+ requests>=2.33.1
4
+ pydantic>=2.0.0
@@ -20,6 +20,7 @@ except ImportError:
20
20
  HAS_CURSES = False
21
21
 
22
22
  import pytest
23
+ from unittest.mock import MagicMock
23
24
 
24
25
  from benchlab.core.statistics import ChannelStats
25
26
 
@@ -239,3 +240,25 @@ def test_help_modal_renders_without_stomping_main_refresh():
239
240
  curses.wrapper(_run)
240
241
  except curses.error:
241
242
  pytest.skip("curses screen unavailable in this environment")
243
+
244
+
245
+ @pytest.mark.parametrize("size", [(47, 100), (48, 99), (12, 60)])
246
+ def test_size_warning_is_drawn_and_flushed(monkeypatch, size):
247
+ from benchlab.tui.tui_core import TUICore
248
+
249
+ for name in ("curs_set", "start_color", "use_default_colors", "init_pair"):
250
+ monkeypatch.setattr(curses, name, MagicMock())
251
+ monkeypatch.setattr(curses, "color_pair", lambda _: 0)
252
+ screen = MagicMock()
253
+ screen.getmaxyx.return_value = size
254
+ core = TUICore(screen)
255
+ calls = MagicMock()
256
+ calls.attach_mock(screen.addstr, "draw")
257
+ calls.attach_mock(screen.noutrefresh, "stage")
258
+ monkeypatch.setattr(curses, "doupdate", calls.update)
259
+
260
+ assert core.render(_snapshot(), ChannelStats(), [], 1.0) is False
261
+
262
+ assert "Terminal too small" in screen.addstr.call_args.args[2]
263
+ assert [call[0] for call in calls.mock_calls] == [
264
+ "draw", "stage", "update"]
@@ -5,6 +5,8 @@ DataSourceManager directly.
5
5
  """
6
6
 
7
7
  import types
8
+
9
+ import pytest
8
10
  from unittest.mock import MagicMock
9
11
 
10
12
  from benchlab.tui.tui_main import TUIApplication
@@ -74,3 +76,38 @@ def test_scan_local_fleet_survives_none_port_when_disconnected():
74
76
  fleet = app._scan_local_fleet()
75
77
 
76
78
  assert fleet == []
79
+
80
+
81
+ @pytest.mark.parametrize("source", [
82
+ "fastapi", "fastapi_custom", "mqtt", "mqtt_custom",
83
+ "named_pipe", "service_http", "service_ws",
84
+ ])
85
+ def test_remote_fleet_refresh_never_probes_serial(source):
86
+ app = _make_app()
87
+ app.source_type = source
88
+ app.datasource_manager.is_connected.return_value = True
89
+ app.datasource_manager.list_devices.return_value = {
90
+ "REMOTE-UID": {"port": "remote", "variant": "BL2"},
91
+ }
92
+ app._refresh_fleet_cache()
93
+ assert [device["uid"] for device in app.fleet_cache] == ["REMOTE-UID"]
94
+ app.datasource_manager.list_devices.assert_called_once_with()
95
+
96
+ # A disconnect clears the remote list without discovering local devices.
97
+ app.datasource_manager.is_connected.return_value = False
98
+ app.last_fleet_refresh = 0
99
+ app._refresh_fleet_cache()
100
+ assert app.fleet_cache == []
101
+ app.datasource_manager.discover_devices.assert_not_called()
102
+ app.datasource_manager.list_devices.assert_called_once_with()
103
+
104
+
105
+ def test_direct_fleet_refresh_still_discovers_devices():
106
+ app = _make_app()
107
+ app.datasource_manager.is_connected.return_value = False
108
+ app.datasource_manager.discover_devices.return_value = [
109
+ {"uid": "LOCAL-UID", "port": "COM5", "fw": 2},
110
+ ]
111
+ app._refresh_fleet_cache()
112
+ app.datasource_manager.discover_devices.assert_called_once_with()
113
+ assert [device["uid"] for device in app.fleet_cache] == ["LOCAL-UID"]