benchlab-pytools 3.0.4__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 (208) hide show
  1. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/CHANGELOG.md +60 -1
  2. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/PKG-INFO +28 -10
  3. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/README.md +15 -3
  4. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/__init__.py +1 -1
  5. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/config/README.md +15 -0
  6. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/config/config_client.py +61 -6
  7. benchlab_pytools-3.2.0/benchlab/config/test_config_client.py +252 -0
  8. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/core/README.md +5 -3
  9. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/core/config.py +49 -0
  10. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/core/datasource.py +315 -16
  11. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/core/datasource_manager.py +14 -1
  12. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/csv_log/README.md +1 -1
  13. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/csv_log/requirements.txt +1 -1
  14. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/graph/README.md +1 -1
  15. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/hwinfo/README.md +1 -1
  16. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/main.py +30 -2
  17. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/menu.py +16 -2
  18. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/menu_classic.py +13 -1
  19. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/requirements.txt +3 -4
  20. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/restapi/readme.md +11 -0
  21. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/restapi/requirements.txt +1 -1
  22. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/restapi/telemetry_api.py +122 -0
  23. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/restapi/test_server.py +1 -0
  24. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/sources.py +27 -0
  25. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/tui/README.md +3 -1
  26. benchlab_pytools-3.2.0/benchlab/tui/requirements.txt +4 -0
  27. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/tui/test_tui_core.py +23 -0
  28. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/tui/test_tui_main.py +37 -0
  29. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/tui/tui_core.py +2 -0
  30. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/tui/tui_main.py +26 -4
  31. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/pyproject.toml +10 -5
  32. benchlab_pytools-3.0.4/benchlab/config/test_config_client.py +0 -107
  33. benchlab_pytools-3.0.4/benchlab/tui/requirements.txt +0 -2
  34. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/.flake8 +0 -0
  35. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/.gitignore +0 -0
  36. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/CONTRIBUTING.md +0 -0
  37. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/LICENSE +0 -0
  38. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/__main__.py +0 -0
  39. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/bootstrap.py +0 -0
  40. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/config/__init__.py +0 -0
  41. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/config/config_manager.py +0 -0
  42. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/config/config_tool.py +0 -0
  43. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/config/diff.py +0 -0
  44. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/config/examples/complete_config.json +0 -0
  45. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/config/examples/rgb_lighting.json +0 -0
  46. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/config/examples/simple_fan.json +0 -0
  47. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/config/schema.py +0 -0
  48. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/config/test_config_manager.py +0 -0
  49. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/config/test_diff.py +0 -0
  50. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/core/__init__.py +0 -0
  51. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/core/device_registry.py +0 -0
  52. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/core/discovery.py +0 -0
  53. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/core/infrastructure.py +0 -0
  54. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/core/process_manager.py +0 -0
  55. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/core/retry.py +0 -0
  56. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/core/shared_serial.py +0 -0
  57. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/core/statistics.py +0 -0
  58. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/csv_log/__init__.py +0 -0
  59. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/csv_log/csv_logger_enhanced.py +0 -0
  60. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/csv_log/message_batcher.py +0 -0
  61. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/csv_log/smart_retry.py +0 -0
  62. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/graph/__init__.py +0 -0
  63. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/graph/app.py +0 -0
  64. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/graph/requirements.txt +0 -0
  65. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/graph/runner.py +0 -0
  66. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/graph/test_graph.py +0 -0
  67. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/graph/ui.py +0 -0
  68. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/hwinfo/__init__.py +0 -0
  69. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/hwinfo/hwinfo_export.py +0 -0
  70. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/hwinfo/requirements.txt +0 -0
  71. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/hwinfo/test_hwinfo_export.py +0 -0
  72. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/launcher.py +0 -0
  73. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/link/.env.EXAMPLE +0 -0
  74. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/link/README.md +0 -0
  75. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/link/__init__.py +0 -0
  76. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/link/link_main.py +0 -0
  77. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/link/test_link_main.py +0 -0
  78. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/mqtt/README.md +0 -0
  79. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/mqtt/__init__.py +0 -0
  80. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/mqtt/mqtt.config_template +0 -0
  81. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/mqtt/mqtt_publisher.py +0 -0
  82. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/mqtt/requirements.txt +0 -0
  83. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/mqtt/test_mqtt_publisher.py +0 -0
  84. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/prefs.py +0 -0
  85. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/restapi/.env.example +0 -0
  86. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/restapi/__init__.py +0 -0
  87. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/restapi/favicon.ico +0 -0
  88. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/tools.py +0 -0
  89. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/tui/__init__.py +0 -0
  90. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/tui/config.py +0 -0
  91. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/README.md +0 -0
  92. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/NOTICE.md +0 -0
  93. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/README.md +0 -0
  94. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/Running_from_source_on_linux.md +0 -0
  95. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/__init__.py +0 -0
  96. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/assets/vu1_hello_world.png +0 -0
  97. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/community_applications.md +0 -0
  98. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/config.yaml +0 -0
  99. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/database.py +0 -0
  100. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/dial_driver.py +0 -0
  101. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/dials/Comms_Hub_Gauge.py +0 -0
  102. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/dials/Comms_Hub_Server.py +0 -0
  103. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/dials/__init__.py +0 -0
  104. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/dials/base_logger.py +0 -0
  105. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/installer/inc/icon.ico +0 -0
  106. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/installer/install.nsi +0 -0
  107. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/make_version.py +0 -0
  108. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/pylintrc +0 -0
  109. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/requirements.txt +0 -0
  110. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/serial_driver.py +0 -0
  111. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/server.py +0 -0
  112. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/server_config.py +0 -0
  113. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/server_dial_handler.py +0 -0
  114. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/vu_notifications.py +0 -0
  115. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/android-chrome-192x192.png +0 -0
  116. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/android-chrome-512x512.png +0 -0
  117. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/apple-touch-icon.png +0 -0
  118. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/demo.css +0 -0
  119. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/demo.min.css +0 -0
  120. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/demo.rtl.css +0 -0
  121. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/demo.rtl.min.css +0 -0
  122. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-flags.css +0 -0
  123. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-flags.min.css +0 -0
  124. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-flags.rtl.css +0 -0
  125. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-flags.rtl.min.css +0 -0
  126. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-payments.css +0 -0
  127. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-payments.min.css +0 -0
  128. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-payments.rtl.css +0 -0
  129. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-payments.rtl.min.css +0 -0
  130. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-social.css +0 -0
  131. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-social.min.css +0 -0
  132. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-social.rtl.css +0 -0
  133. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-social.rtl.min.css +0 -0
  134. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-vendors.css +0 -0
  135. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-vendors.min.css +0 -0
  136. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-vendors.rtl.css +0 -0
  137. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-vendors.rtl.min.css +0 -0
  138. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler.css +0 -0
  139. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler.min.css +0 -0
  140. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler.rtl.css +0 -0
  141. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler.rtl.min.css +0 -0
  142. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/theme.min.css +0 -0
  143. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/img/header.png +0 -0
  144. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/img/logo.png +0 -0
  145. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/dark-theme.js +0 -0
  146. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/dark-theme.min.js +0 -0
  147. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/demo.js +0 -0
  148. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/demo.min.js +0 -0
  149. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/jquery.min.js +0 -0
  150. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/tabler.esm.js +0 -0
  151. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/tabler.esm.min.js +0 -0
  152. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/tabler.js +0 -0
  153. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/tabler.min.js +0 -0
  154. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/vu1_api.js +0 -0
  155. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_api_key_settings.js +0 -0
  156. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_api_keys.js +0 -0
  157. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_dial.js +0 -0
  158. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_home.js +0 -0
  159. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_root.js +0 -0
  160. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/favicon-16x16.png +0 -0
  161. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/favicon-32x32.png +0 -0
  162. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/favicon.ico +0 -0
  163. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/favicon.png +0 -0
  164. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/index.html +0 -0
  165. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/views/api_keys.html +0 -0
  166. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/views/dial.html +0 -0
  167. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/views/footer.html +0 -0
  168. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/views/header.html +0 -0
  169. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/views/key_settings.html +0 -0
  170. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/views/start.html +0 -0
  171. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/__init__.py +0 -0
  172. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/assets/Barlow-Bold.ttf +0 -0
  173. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/assets/bl_dial_144x200.png +0 -0
  174. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/assets/bl_logo_144x200.png +0 -0
  175. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/devices.py +0 -0
  176. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/requirements.txt +0 -0
  177. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/sensors.py +0 -0
  178. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/test_devices.py +0 -0
  179. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/test_vu_server_config.py +0 -0
  180. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/test_vu_server_manager.py +0 -0
  181. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/test_vu_tui.py +0 -0
  182. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/vu_dial.config_template +0 -0
  183. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/vu_logo_gen.py +0 -0
  184. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/vu_server.config_template +0 -0
  185. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/vu_server_config.py +0 -0
  186. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/vu_server_manager.py +0 -0
  187. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/vu_tui.py +0 -0
  188. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/vu/vu_updater.py +0 -0
  189. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/wigidash/README.md +0 -0
  190. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/wigidash/__init__.py +0 -0
  191. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/wigidash/assets/Inter-Bold.ttf +0 -0
  192. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/wigidash/assets/Inter.ttf +0 -0
  193. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/wigidash/assets/benchlab.png +0 -0
  194. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/wigidash/benchlab_fleet.py +0 -0
  195. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/wigidash/benchlab_graph.py +0 -0
  196. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/wigidash/benchlab_overview.py +0 -0
  197. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/wigidash/benchlab_telemetry.py +0 -0
  198. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/wigidash/benchlab_ui.py +0 -0
  199. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/wigidash/benchlab_utils.py +0 -0
  200. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/wigidash/requirements.txt +0 -0
  201. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/wigidash/test_wigidash.py +0 -0
  202. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/wigidash/wigidash_device.py +0 -0
  203. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/wigidash/wigidash_manager.py +0 -0
  204. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/wigidash/wigidash_session.py +0 -0
  205. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/wigidash/wigidash_usb.py +0 -0
  206. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab/wigidash/wigidash_widget.py +0 -0
  207. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/benchlab.py +0 -0
  208. {benchlab_pytools-3.0.4 → benchlab_pytools-3.2.0}/pytest.ini +0 -0
@@ -4,6 +4,63 @@ 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
+
52
+ ## [3.1.0] - 2026-09-08
53
+
54
+ ### Added
55
+ - `service_ws` data source: consumes the BENCHLAB service `/events`
56
+ WebSocket event stream (added in the C# service's PR #76). Telemetry is
57
+ pushed at the service's own poll cadence rather than polled, and device
58
+ connect/disconnect is reflected without a manual rescan. Output is
59
+ normalized to the same shape as `service_http`. New CLI flags
60
+ `--service-ws-url` (default `ws://localhost:8585/events`) and
61
+ `--service-token`; new optional-dependency group
62
+ `benchlab-pytools[service_ws]` (`websockets`).
63
+
7
64
  ## [3.0.4] - 2026-08-18
8
65
 
9
66
  ### Fixed
@@ -100,8 +157,10 @@ Snapshot of the codebase at the point packaging work began. Interactive
100
157
  menu (prompt_toolkit-based), TUI, CSV logger, FastAPI server, graph, HWiNFO
101
158
  export, MQTT publisher, VU dials, WigiDash, and config import/export tools,
102
159
  sharing a common data-source layer (direct serial, FastAPI, MQTT, named
103
- pipe, service HTTP).
160
+ pipe, service HTTP, service WebSocket).
104
161
 
162
+ [3.2.0]: https://github.com/BenchLab-io/benchlab-pytools/compare/v3.1.0...v3.2.0
163
+ [3.1.0]: https://github.com/BenchLab-io/benchlab-pytools/compare/v3.0.4...v3.1.0
105
164
  [3.0.4]: https://github.com/BenchLab-io/benchlab-pytools/compare/v3.0.3...v3.0.4
106
165
  [3.0.3]: https://github.com/BenchLab-io/benchlab-pytools/compare/v3.0.2...v3.0.3
107
166
  [3.0.2]: https://github.com/BenchLab-io/benchlab-pytools/compare/v3.0.1...v3.0.2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: benchlab-pytools
3
- Version: 3.0.4
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,15 +22,15 @@ 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.5.1
25
+ Requires-Dist: benchlab-pycore>=0.8.0
26
26
  Requires-Dist: packaging<26.0,>=23.0
27
- Requires-Dist: python-dotenv<2.0,>=1.0.0
27
+ Requires-Dist: python-dotenv<2.0,>=1.2.2
28
28
  Requires-Dist: pywin32>=306; platform_system == 'Windows'
29
29
  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.5.1; 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'
@@ -43,16 +43,18 @@ Requires-Dist: pillow>=10.0.0; extra == 'all'
43
43
  Requires-Dist: pillow>=11.3.0; extra == 'all'
44
44
  Requires-Dist: pydantic>=2.0.0; extra == 'all'
45
45
  Requires-Dist: pyserial>=3.5; extra == 'all'
46
- Requires-Dist: python-dotenv==1.1.1; extra == 'all'
46
+ Requires-Dist: python-dotenv==1.2.2; extra == 'all'
47
47
  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'
55
+ Requires-Dist: websockets<16.0,>=13.0; extra == 'all'
54
56
  Provides-Extra: csv-log
55
- Requires-Dist: benchlab-pycore>=0.5.1; extra == 'csv-log'
57
+ Requires-Dist: benchlab-pycore>=0.8.0; extra == 'csv-log'
56
58
  Requires-Dist: pyserial>=3.5; extra == 'csv-log'
57
59
  Provides-Extra: graph
58
60
  Requires-Dist: dearpygui>=2.1.1; extra == 'graph'
@@ -67,10 +69,14 @@ Provides-Extra: restapi
67
69
  Requires-Dist: fastapi==0.111.1; extra == 'restapi'
68
70
  Requires-Dist: pydantic>=2.0.0; extra == 'restapi'
69
71
  Requires-Dist: pyserial>=3.5; extra == 'restapi'
70
- Requires-Dist: python-dotenv==1.1.1; extra == 'restapi'
72
+ Requires-Dist: python-dotenv==1.2.2; extra == 'restapi'
71
73
  Requires-Dist: uvicorn[standard]==0.24.0; extra == 'restapi'
74
+ Provides-Extra: service-ws
75
+ Requires-Dist: websockets<16.0,>=13.0; extra == 'service-ws'
72
76
  Provides-Extra: tui
77
+ Requires-Dist: pydantic>=2.0.0; extra == 'tui'
73
78
  Requires-Dist: pyserial>=3.5; extra == 'tui'
79
+ Requires-Dist: requests>=2.33.1; extra == 'tui'
74
80
  Provides-Extra: vu
75
81
  Requires-Dist: blessed>=1.20.0; extra == 'vu'
76
82
  Requires-Dist: numpy; extra == 'vu'
@@ -186,17 +192,29 @@ Most tools accept `--source` to choose where telemetry comes from:
186
192
  | `mqtt` | Local MQTT broker + publisher, started automatically if needed |
187
193
  | `mqtt_custom` | Remote/existing MQTT broker — requires `--mqtt-broker`/`--mqtt-port` |
188
194
  | `named_pipe` | Windows BENCHLAB service (`BL_Service`) via named pipes — Windows only |
189
- | `service_http` | Windows BENCHLAB service HTTP API — requires `--service-url` (default `http://localhost:8585`) |
195
+ | `service_http` | BENCHLAB service HTTP API (polling) — requires `--service-url` (default `http://localhost:8585`) |
196
+ | `service_ws` | BENCHLAB service WebSocket event stream (`/events`, push) — requires `--service-ws-url` (default `ws://localhost:8585/events`); needs the `service_ws` extra |
197
+
198
+ `service_ws` consumes the same BENCHLAB service as `service_http` but over its
199
+ `/events` WebSocket: telemetry is pushed at the service's own cadence instead of
200
+ polled, and device connect/disconnect is reflected without a manual rescan.
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`.
190
206
 
191
207
  Common connection flags:
192
208
 
193
209
  ```
194
- --source SOURCE direct | fastapi | fastapi_custom | mqtt | mqtt_custom | named_pipe | service_http
210
+ --source SOURCE direct | fastapi | fastapi_custom | mqtt | mqtt_custom | named_pipe | service_http | service_ws
195
211
  --api-url URL FastAPI base URL (default: http://127.0.0.1:8000)
196
212
  --api-port PORT FastAPI port (default: 8000)
197
213
  --mqtt-broker HOST MQTT broker host (default: localhost)
198
214
  --mqtt-port PORT MQTT broker port (default: 1883)
199
- --service-url URL BENCHLAB Windows service HTTP API URL (default: http://localhost:8585)
215
+ --service-url URL BENCHLAB service HTTP API URL (default: http://localhost:8585)
216
+ --service-ws-url URL BENCHLAB service WebSocket event stream URL (default: ws://localhost:8585/events)
217
+ --service-token TOKEN X-Benchlab-Token for the BENCHLAB service (only if token auth is enabled)
200
218
  -i, --interval SECONDS Refresh interval (default: 1.0)
201
219
  ```
202
220
 
@@ -95,17 +95,29 @@ Most tools accept `--source` to choose where telemetry comes from:
95
95
  | `mqtt` | Local MQTT broker + publisher, started automatically if needed |
96
96
  | `mqtt_custom` | Remote/existing MQTT broker — requires `--mqtt-broker`/`--mqtt-port` |
97
97
  | `named_pipe` | Windows BENCHLAB service (`BL_Service`) via named pipes — Windows only |
98
- | `service_http` | Windows BENCHLAB service HTTP API — requires `--service-url` (default `http://localhost:8585`) |
98
+ | `service_http` | BENCHLAB service HTTP API (polling) — requires `--service-url` (default `http://localhost:8585`) |
99
+ | `service_ws` | BENCHLAB service WebSocket event stream (`/events`, push) — requires `--service-ws-url` (default `ws://localhost:8585/events`); needs the `service_ws` extra |
100
+
101
+ `service_ws` consumes the same BENCHLAB service as `service_http` but over its
102
+ `/events` WebSocket: telemetry is pushed at the service's own cadence instead of
103
+ polled, and device connect/disconnect is reflected without a manual rescan.
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`.
99
109
 
100
110
  Common connection flags:
101
111
 
102
112
  ```
103
- --source SOURCE direct | fastapi | fastapi_custom | mqtt | mqtt_custom | named_pipe | service_http
113
+ --source SOURCE direct | fastapi | fastapi_custom | mqtt | mqtt_custom | named_pipe | service_http | service_ws
104
114
  --api-url URL FastAPI base URL (default: http://127.0.0.1:8000)
105
115
  --api-port PORT FastAPI port (default: 8000)
106
116
  --mqtt-broker HOST MQTT broker host (default: localhost)
107
117
  --mqtt-port PORT MQTT broker port (default: 1883)
108
- --service-url URL BENCHLAB Windows service HTTP API URL (default: http://localhost:8585)
118
+ --service-url URL BENCHLAB service HTTP API URL (default: http://localhost:8585)
119
+ --service-ws-url URL BENCHLAB service WebSocket event stream URL (default: ws://localhost:8585/events)
120
+ --service-token TOKEN X-Benchlab-Token for the BENCHLAB service (only if token auth is enabled)
109
121
  -i, --interval SECONDS Refresh interval (default: 1.0)
110
122
  ```
111
123
 
@@ -2,4 +2,4 @@
2
2
  BENCHLAB Telemetry Package
3
3
  """
4
4
 
5
- __version__ = "3.0.4"
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."""
@@ -414,20 +425,53 @@ class DirectConfigClient(ConfigClient):
414
425
  calibration=cal,
415
426
  product_id=self.product_id)
416
427
 
428
+ # CONFIG_ACTION_* codes for UART_CMD_ACTION (opcode 2), the command
429
+ # firmware actually implements. pycore's old save_config/load_config/
430
+ # reset_config sent UART_CMD_NVM_CONFIG (opcode 17) and friends -- opcodes
431
+ # no released or in-development firmware handles -- and were removed in
432
+ # pycore 0.6.0. See benchlab-pycore issue #11.
433
+ _CONFIG_ACTION_SAVE = 0
434
+ _CONFIG_ACTION_LOAD = 1
435
+ _CONFIG_ACTION_RESET = 2
436
+
417
437
  def save_config(self) -> bool:
418
438
  """Save configuration to device flash."""
419
- from benchlab_pycore.core.config_io import save_config
420
- return save_config(self.ser)
439
+ from benchlab_pycore.core import send_action
440
+ return send_action(self.ser, action=self._CONFIG_ACTION_SAVE)
421
441
 
422
442
  def load_config(self) -> bool:
423
443
  """Load configuration from device flash."""
424
- from benchlab_pycore.core.config_io import load_config
425
- return load_config(self.ser)
444
+ from benchlab_pycore.core import send_action
445
+ return send_action(self.ser, action=self._CONFIG_ACTION_LOAD)
426
446
 
427
447
  def reset_config(self) -> bool:
428
448
  """Reset configuration to factory defaults."""
429
- from benchlab_pycore.core.config_io import reset_config
430
- return reset_config(self.ser)
449
+ from benchlab_pycore.core import send_action
450
+ return send_action(self.ser, action=self._CONFIG_ACTION_RESET)
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)
431
475
 
432
476
  def close(self):
433
477
  """Close serial connection."""
@@ -602,6 +646,17 @@ class NamedPipeConfigClient(ConfigClient):
602
646
  result = self._send_command("ResetConfig")
603
647
  return result and result.get('success', False)
604
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
+
605
660
  def close(self):
606
661
  """Close pipe connection."""
607
662
  self._close_pipe()
@@ -0,0 +1,252 @@
1
+ """Non-hardware regression tests for benchlab.config.config_client.
2
+
3
+ Covers the BL2 vs Original calibration struct selection bug fixed in the
4
+ config bug sweep (issue #32): write_calibration used to always reconstruct
5
+ a plain CalibrationStruct (Original, 4 temp sensors) regardless of the
6
+ connected device's actual product_id, even though benchlab-pycore has a
7
+ distinct CalibrationStructBL2 (8 temp sensors) for BL2 devices and
8
+ read_calibration already selected the right one. Uses the real
9
+ benchlab-pycore struct types (a hard dependency of this module) but no
10
+ real serial device -- write_calibration is invoked directly on an instance
11
+ built without going through __init__'s serial connection.
12
+ """
13
+
14
+ from benchlab.config.config_client import DirectConfigClient
15
+ from benchlab_pycore.core import (
16
+ CalibrationStruct, CalibrationStructBL2,
17
+ BENCHLAB_BL2_PRODUCT_ID, BENCHLAB_ORIGINAL_PRODUCT_ID,
18
+ )
19
+ import pytest
20
+
21
+ pytest.importorskip("benchlab_pycore")
22
+
23
+
24
+ def _make_client(product_id):
25
+ """Construct a DirectConfigClient without running __init__ (which opens
26
+ a real serial connection) -- just set the attributes write_calibration
27
+ actually needs."""
28
+ client = DirectConfigClient.__new__(DirectConfigClient)
29
+ client.product_id = product_id
30
+ client.ser = None
31
+ return client
32
+
33
+
34
+ def test_write_calibration_selects_bl2_struct_for_bl2_device(monkeypatch):
35
+ """Regression test: BL2-shaped calibration data (8 temp sensors) used
36
+ to be forced into the Original 4-sensor struct, raising IndexError."""
37
+ client = _make_client(BENCHLAB_BL2_PRODUCT_ID)
38
+
39
+ captured = {}
40
+
41
+ def fake_write_calibration(ser, calibration, product_id=None):
42
+ captured["struct_type"] = type(calibration)
43
+ return True
44
+ monkeypatch.setattr(
45
+ "benchlab_pycore.core.write_calibration",
46
+ fake_write_calibration)
47
+
48
+ cal_dict = {
49
+ "Crc": 0,
50
+ # BL2 has 8 sensors
51
+ "Ts": [{"Offset": i, "GainOffset": 0} for i in range(8)],
52
+ }
53
+
54
+ result = client.write_calibration(cal_dict)
55
+
56
+ assert result is True
57
+ assert captured["struct_type"] is CalibrationStructBL2
58
+
59
+
60
+ def test_write_calibration_selects_original_struct_for_original_device(
61
+ monkeypatch):
62
+ client = _make_client(BENCHLAB_ORIGINAL_PRODUCT_ID)
63
+
64
+ captured = {}
65
+
66
+ def fake_write_calibration(ser, calibration, product_id=None):
67
+ captured["struct_type"] = type(calibration)
68
+ return True
69
+ monkeypatch.setattr(
70
+ "benchlab_pycore.core.write_calibration",
71
+ fake_write_calibration)
72
+
73
+ cal_dict = {
74
+ "Crc": 0,
75
+ # Original has 4 sensors
76
+ "Ts": [{"Offset": i, "GainOffset": 0} for i in range(4)],
77
+ }
78
+
79
+ result = client.write_calibration(cal_dict)
80
+
81
+ assert result is True
82
+ assert captured["struct_type"] is CalibrationStruct
83
+
84
+
85
+ def test_write_calibration_bl2_data_no_longer_raises_indexerror():
86
+ """Directly reproduces the original bug scenario: reconstructing
87
+ 8-sensor calibration data now succeeds when the client knows it's
88
+ talking to a BL2 device, instead of raising IndexError against the
89
+ 4-sensor Original struct."""
90
+ client = _make_client(BENCHLAB_BL2_PRODUCT_ID)
91
+
92
+ cal_dict = {
93
+ "Crc": 0,
94
+ "Ts": [{"Offset": i, "GainOffset": 0} for i in range(8)],
95
+ }
96
+
97
+ # _dict_to_struct is the piece that previously raised IndexError when
98
+ # forced into the wrong (4-sensor) struct type; call it directly with
99
+ # the now-correct struct type to confirm reconstruction succeeds.
100
+ struct_type = (
101
+ CalibrationStructBL2
102
+ if client.product_id == BENCHLAB_BL2_PRODUCT_ID
103
+ else CalibrationStruct
104
+ )
105
+ result = client._dict_to_struct(cal_dict, struct_type)
106
+
107
+ assert len(result.Ts) == 8
108
+
109
+
110
+ # ---------------------------------------------------------------------------
111
+ # save_config / load_config / reset_config -- issue #68
112
+ #
113
+ # benchlab-pycore 0.6.0 removed config_io.save_config/load_config/reset_config
114
+ # (they sent UART_CMD_NVM_CONFIG and friends, opcodes no released or
115
+ # in-development firmware implements -- see benchlab-pycore#11). These three
116
+ # methods now route through send_action(), which mirrors UART_CMD_ACTION
117
+ # (opcode 2), the command firmware actually implements for save/load/reset.
118
+ # ---------------------------------------------------------------------------
119
+
120
+ def test_save_config_sends_the_save_action(monkeypatch):
121
+ client = _make_client(BENCHLAB_ORIGINAL_PRODUCT_ID)
122
+ captured = {}
123
+
124
+ def fake_send_action(ser, action):
125
+ captured["action"] = action
126
+ return True
127
+ monkeypatch.setattr("benchlab_pycore.core.send_action", fake_send_action)
128
+
129
+ assert client.save_config() is True
130
+ assert captured["action"] == 0 # CONFIG_ACTION_SAVE
131
+
132
+
133
+ def test_load_config_sends_the_load_action(monkeypatch):
134
+ client = _make_client(BENCHLAB_ORIGINAL_PRODUCT_ID)
135
+ captured = {}
136
+
137
+ def fake_send_action(ser, action):
138
+ captured["action"] = action
139
+ return True
140
+ monkeypatch.setattr("benchlab_pycore.core.send_action", fake_send_action)
141
+
142
+ assert client.load_config() is True
143
+ assert captured["action"] == 1 # CONFIG_ACTION_LOAD
144
+
145
+
146
+ def test_reset_config_sends_the_reset_action(monkeypatch):
147
+ client = _make_client(BENCHLAB_ORIGINAL_PRODUCT_ID)
148
+ captured = {}
149
+
150
+ def fake_send_action(ser, action):
151
+ captured["action"] = action
152
+ return True
153
+ monkeypatch.setattr("benchlab_pycore.core.send_action", fake_send_action)
154
+
155
+ assert client.reset_config() is True
156
+ assert captured["action"] == 2 # CONFIG_ACTION_RESET
157
+
158
+
159
+ def test_save_config_propagates_a_failed_send(monkeypatch):
160
+ """send_action returning False (e.g. another action already pending on
161
+ the device) must surface as a failed save, not be swallowed."""
162
+ client = _make_client(BENCHLAB_ORIGINAL_PRODUCT_ID)
163
+ monkeypatch.setattr(
164
+ "benchlab_pycore.core.send_action", lambda ser, action: False)
165
+
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,8 +27,9 @@ 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]`). 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. |
30
31
 
31
- Each implementation validates its constructor kwargs through a Pydantic model in `config.py` (`SerialConfig`, `FastAPIConfig`, `MQTTConfig`, `NamedPipeConfig`, `ServiceHttpConfig`).
32
+ Each implementation validates its constructor kwargs through a Pydantic model in `config.py` (`SerialConfig`, `FastAPIConfig`, `MQTTConfig`, `NamedPipeConfig`, `ServiceHttpConfig`, `ServiceWsConfig`).
32
33
 
33
34
  The `create_datasource(source_type, **kwargs)` factory function builds the right class:
34
35
 
@@ -109,7 +110,7 @@ Also provides `SharedSerialManager`, a singleton connection pool keyed by port w
109
110
 
110
111
  ### 10. Config models (`config.py`)
111
112
 
112
- Pydantic models validating each DataSource's constructor kwargs: `SerialConfig`, `FastAPIConfig` (normalizes `base_url` to include a scheme and strips trailing slash), `MQTTConfig`, `NamedPipeConfig`, `ServiceHttpConfig`.
113
+ Pydantic models validating each DataSource's constructor kwargs: `SerialConfig`, `FastAPIConfig` (normalizes `base_url` to include a scheme and strips trailing slash), `MQTTConfig`, `NamedPipeConfig`, `ServiceHttpConfig`, `ServiceWsConfig` (normalizes `url` to a `ws://`/`wss://` scheme ending in `/events`).
113
114
 
114
115
  ## Package exports (`benchlab/core/__init__.py`)
115
116
 
@@ -124,7 +125,7 @@ from benchlab.core import (
124
125
  )
125
126
  ```
126
127
 
127
- `NamedPipeDataSource` and `ServiceHttpDataSource` live in `datasource.py` but aren't re-exported from `__init__.py`; import them directly from `benchlab.core.datasource` if needed. `BENCHLAB_ORIGINAL_PRODUCT_ID` / `BENCHLAB_BL2_PRODUCT_ID` are re-exported from `benchlab_pycore` (with a fallback to hardcoded `0x10`/`0x11` if pycore isn't installed) so tools can detect device variant without importing pycore directly.
128
+ `NamedPipeDataSource`, `ServiceHttpDataSource`, and `ServiceWsDataSource` live in `datasource.py` but aren't re-exported from `__init__.py`; import them directly from `benchlab.core.datasource` if needed. `BENCHLAB_ORIGINAL_PRODUCT_ID` / `BENCHLAB_BL2_PRODUCT_ID` are re-exported from `benchlab_pycore` (with a fallback to hardcoded `0x10`/`0x11` if pycore isn't installed) so tools can detect device variant without importing pycore directly.
128
129
 
129
130
  ## How tools use this
130
131
 
@@ -144,6 +145,7 @@ from benchlab.core import (
144
145
  | `mqtt` / `mqtt_custom` | Distributed/IoT integration | Pub/sub, works with existing MQTT infra | Requires a broker, more moving parts |
145
146
  | `named_pipe` | Windows, alongside the C# BENCHLAB service | Multiple tools, no direct serial management | Windows only, requires the service + pywin32 |
146
147
  | `service_http` | Windows, alongside the C# BENCHLAB service | HTTP-based, multi-client | Windows-oriented, requires the service running |
148
+ | `service_ws` | Alongside the C# BENCHLAB service, low-latency updates | Push-based (no poll loop), device connect/disconnect events, multi-client | Requires the service running + `websockets` |
147
149
 
148
150
  ## See Also
149
151