benchlab-pytools 3.2.0__tar.gz → 3.2.1__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.2.0 → benchlab_pytools-3.2.1}/CHANGELOG.md +15 -0
  2. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/PKG-INFO +1 -1
  3. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/__init__.py +1 -1
  4. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/restapi/telemetry_api.py +28 -24
  5. benchlab_pytools-3.2.1/benchlab/restapi/test_read_recovery.py +235 -0
  6. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/.flake8 +0 -0
  7. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/.gitignore +0 -0
  8. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/CONTRIBUTING.md +0 -0
  9. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/LICENSE +0 -0
  10. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/README.md +0 -0
  11. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/__main__.py +0 -0
  12. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/bootstrap.py +0 -0
  13. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/config/README.md +0 -0
  14. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/config/__init__.py +0 -0
  15. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/config/config_client.py +0 -0
  16. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/config/config_manager.py +0 -0
  17. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/config/config_tool.py +0 -0
  18. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/config/diff.py +0 -0
  19. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/config/examples/complete_config.json +0 -0
  20. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/config/examples/rgb_lighting.json +0 -0
  21. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/config/examples/simple_fan.json +0 -0
  22. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/config/schema.py +0 -0
  23. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/config/test_config_client.py +0 -0
  24. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/config/test_config_manager.py +0 -0
  25. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/config/test_diff.py +0 -0
  26. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/core/README.md +0 -0
  27. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/core/__init__.py +0 -0
  28. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/core/config.py +0 -0
  29. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/core/datasource.py +0 -0
  30. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/core/datasource_manager.py +0 -0
  31. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/core/device_registry.py +0 -0
  32. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/core/discovery.py +0 -0
  33. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/core/infrastructure.py +0 -0
  34. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/core/process_manager.py +0 -0
  35. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/core/retry.py +0 -0
  36. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/core/shared_serial.py +0 -0
  37. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/core/statistics.py +0 -0
  38. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/csv_log/README.md +0 -0
  39. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/csv_log/__init__.py +0 -0
  40. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/csv_log/csv_logger_enhanced.py +0 -0
  41. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/csv_log/message_batcher.py +0 -0
  42. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/csv_log/requirements.txt +0 -0
  43. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/csv_log/smart_retry.py +0 -0
  44. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/graph/README.md +0 -0
  45. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/graph/__init__.py +0 -0
  46. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/graph/app.py +0 -0
  47. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/graph/requirements.txt +0 -0
  48. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/graph/runner.py +0 -0
  49. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/graph/test_graph.py +0 -0
  50. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/graph/ui.py +0 -0
  51. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/hwinfo/README.md +0 -0
  52. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/hwinfo/__init__.py +0 -0
  53. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/hwinfo/hwinfo_export.py +0 -0
  54. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/hwinfo/requirements.txt +0 -0
  55. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/hwinfo/test_hwinfo_export.py +0 -0
  56. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/launcher.py +0 -0
  57. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/link/.env.EXAMPLE +0 -0
  58. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/link/README.md +0 -0
  59. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/link/__init__.py +0 -0
  60. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/link/link_main.py +0 -0
  61. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/link/test_link_main.py +0 -0
  62. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/main.py +0 -0
  63. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/menu.py +0 -0
  64. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/menu_classic.py +0 -0
  65. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/mqtt/README.md +0 -0
  66. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/mqtt/__init__.py +0 -0
  67. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/mqtt/mqtt.config_template +0 -0
  68. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/mqtt/mqtt_publisher.py +0 -0
  69. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/mqtt/requirements.txt +0 -0
  70. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/mqtt/test_mqtt_publisher.py +0 -0
  71. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/prefs.py +0 -0
  72. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/requirements.txt +0 -0
  73. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/restapi/.env.example +0 -0
  74. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/restapi/__init__.py +0 -0
  75. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/restapi/favicon.ico +0 -0
  76. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/restapi/readme.md +0 -0
  77. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/restapi/requirements.txt +0 -0
  78. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/restapi/test_server.py +0 -0
  79. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/sources.py +0 -0
  80. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/tools.py +0 -0
  81. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/tui/README.md +0 -0
  82. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/tui/__init__.py +0 -0
  83. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/tui/config.py +0 -0
  84. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/tui/requirements.txt +0 -0
  85. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/tui/test_tui_core.py +0 -0
  86. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/tui/test_tui_main.py +0 -0
  87. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/tui/tui_core.py +0 -0
  88. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/tui/tui_main.py +0 -0
  89. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/README.md +0 -0
  90. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/NOTICE.md +0 -0
  91. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/README.md +0 -0
  92. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/Running_from_source_on_linux.md +0 -0
  93. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/__init__.py +0 -0
  94. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/assets/vu1_hello_world.png +0 -0
  95. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/community_applications.md +0 -0
  96. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/config.yaml +0 -0
  97. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/database.py +0 -0
  98. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/dial_driver.py +0 -0
  99. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/dials/Comms_Hub_Gauge.py +0 -0
  100. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/dials/Comms_Hub_Server.py +0 -0
  101. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/dials/__init__.py +0 -0
  102. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/dials/base_logger.py +0 -0
  103. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/installer/inc/icon.ico +0 -0
  104. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/installer/install.nsi +0 -0
  105. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/make_version.py +0 -0
  106. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/pylintrc +0 -0
  107. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/requirements.txt +0 -0
  108. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/serial_driver.py +0 -0
  109. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/server.py +0 -0
  110. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/server_config.py +0 -0
  111. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/server_dial_handler.py +0 -0
  112. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/vu_notifications.py +0 -0
  113. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/android-chrome-192x192.png +0 -0
  114. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/android-chrome-512x512.png +0 -0
  115. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/apple-touch-icon.png +0 -0
  116. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/demo.css +0 -0
  117. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/demo.min.css +0 -0
  118. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/demo.rtl.css +0 -0
  119. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/demo.rtl.min.css +0 -0
  120. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler-flags.css +0 -0
  121. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler-flags.min.css +0 -0
  122. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler-flags.rtl.css +0 -0
  123. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler-flags.rtl.min.css +0 -0
  124. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler-payments.css +0 -0
  125. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler-payments.min.css +0 -0
  126. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler-payments.rtl.css +0 -0
  127. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler-payments.rtl.min.css +0 -0
  128. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler-social.css +0 -0
  129. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler-social.min.css +0 -0
  130. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler-social.rtl.css +0 -0
  131. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler-social.rtl.min.css +0 -0
  132. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler-vendors.css +0 -0
  133. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler-vendors.min.css +0 -0
  134. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler-vendors.rtl.css +0 -0
  135. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler-vendors.rtl.min.css +0 -0
  136. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler.css +0 -0
  137. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler.min.css +0 -0
  138. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler.rtl.css +0 -0
  139. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler.rtl.min.css +0 -0
  140. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/theme.min.css +0 -0
  141. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/img/header.png +0 -0
  142. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/img/logo.png +0 -0
  143. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/js/dark-theme.js +0 -0
  144. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/js/dark-theme.min.js +0 -0
  145. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/js/demo.js +0 -0
  146. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/js/demo.min.js +0 -0
  147. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/js/jquery.min.js +0 -0
  148. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/js/tabler.esm.js +0 -0
  149. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/js/tabler.esm.min.js +0 -0
  150. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/js/tabler.js +0 -0
  151. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/js/tabler.min.js +0 -0
  152. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/js/vu1_api.js +0 -0
  153. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_api_key_settings.js +0 -0
  154. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_api_keys.js +0 -0
  155. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_dial.js +0 -0
  156. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_home.js +0 -0
  157. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_root.js +0 -0
  158. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/favicon-16x16.png +0 -0
  159. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/favicon-32x32.png +0 -0
  160. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/favicon.ico +0 -0
  161. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/favicon.png +0 -0
  162. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/index.html +0 -0
  163. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/views/api_keys.html +0 -0
  164. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/views/dial.html +0 -0
  165. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/views/footer.html +0 -0
  166. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/views/header.html +0 -0
  167. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/views/key_settings.html +0 -0
  168. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/views/start.html +0 -0
  169. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/__init__.py +0 -0
  170. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/assets/Barlow-Bold.ttf +0 -0
  171. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/assets/bl_dial_144x200.png +0 -0
  172. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/assets/bl_logo_144x200.png +0 -0
  173. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/devices.py +0 -0
  174. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/requirements.txt +0 -0
  175. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/sensors.py +0 -0
  176. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/test_devices.py +0 -0
  177. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/test_vu_server_config.py +0 -0
  178. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/test_vu_server_manager.py +0 -0
  179. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/test_vu_tui.py +0 -0
  180. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/vu_dial.config_template +0 -0
  181. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/vu_logo_gen.py +0 -0
  182. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/vu_server.config_template +0 -0
  183. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/vu_server_config.py +0 -0
  184. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/vu_server_manager.py +0 -0
  185. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/vu_tui.py +0 -0
  186. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/vu/vu_updater.py +0 -0
  187. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/README.md +0 -0
  188. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/__init__.py +0 -0
  189. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/assets/Inter-Bold.ttf +0 -0
  190. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/assets/Inter.ttf +0 -0
  191. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/assets/benchlab.png +0 -0
  192. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/benchlab_fleet.py +0 -0
  193. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/benchlab_graph.py +0 -0
  194. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/benchlab_overview.py +0 -0
  195. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/benchlab_telemetry.py +0 -0
  196. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/benchlab_ui.py +0 -0
  197. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/benchlab_utils.py +0 -0
  198. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/requirements.txt +0 -0
  199. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/test_wigidash.py +0 -0
  200. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/wigidash_device.py +0 -0
  201. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/wigidash_manager.py +0 -0
  202. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/wigidash_session.py +0 -0
  203. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/wigidash_usb.py +0 -0
  204. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/wigidash_widget.py +0 -0
  205. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/benchlab.py +0 -0
  206. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/pyproject.toml +0 -0
  207. {benchlab_pytools-3.2.0 → benchlab_pytools-3.2.1}/pytest.ini +0 -0
@@ -4,6 +4,20 @@ 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.1] - 2026-10-01
8
+
9
+ ### Fixed
10
+ - Collector recovery after incomplete sensor reads
11
+ (`benchlab.restapi.telemetry_api.read_device_loop`): incomplete serial
12
+ responses (`read_sensors()` returning `None`) are now treated as read
13
+ failures instead of being silently ignored, so the collector reconnects
14
+ instead of continuing to publish stale telemetry. The read-error
15
+ backoff counter now survives serial port reopen attempts and resets
16
+ only after a successfully decoded sample, restoring the intended
17
+ retry/backoff policy. A device is marked `connected` only once a
18
+ sample has actually been decoded, and stale telemetry is cleared
19
+ before the collector waits to retry. (#77)
20
+
7
21
  ## [3.2.0] - 2026-09-29
8
22
 
9
23
  ### Added
@@ -159,6 +173,7 @@ export, MQTT publisher, VU dials, WigiDash, and config import/export tools,
159
173
  sharing a common data-source layer (direct serial, FastAPI, MQTT, named
160
174
  pipe, service HTTP, service WebSocket).
161
175
 
176
+ [3.2.1]: https://github.com/BenchLab-io/benchlab-pytools/compare/v3.2.0...v3.2.1
162
177
  [3.2.0]: https://github.com/BenchLab-io/benchlab-pytools/compare/v3.1.0...v3.2.0
163
178
  [3.1.0]: https://github.com/BenchLab-io/benchlab-pytools/compare/v3.0.4...v3.1.0
164
179
  [3.0.4]: https://github.com/BenchLab-io/benchlab-pytools/compare/v3.0.3...v3.0.4
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: benchlab-pytools
3
- Version: 3.2.0
3
+ Version: 3.2.1
4
4
  Summary: Python control suite for BENCHLAB telemetry devices
5
5
  Project-URL: Homepage, https://benchlab.io/
6
6
  Project-URL: Repository, https://github.com/BenchLab-io/benchlab-pytools
@@ -2,4 +2,4 @@
2
2
  BENCHLAB Telemetry Package
3
3
  """
4
4
 
5
- __version__ = "3.2.0"
5
+ __version__ = "3.2.1"
@@ -441,30 +441,25 @@ def read_device_loop(port, uid):
441
441
  reconnection logic."""
442
442
  ser = None
443
443
  consecutive_errors = 0
444
- max_consecutive_errors = 10 # After this many errors, attempt reconnect
444
+ max_consecutive_errors = 10 # Back off after repeated read failures
445
445
  product_id = None # Will be set once we read device info
446
- is_connected = False # Track connection status
446
+ is_connected = False # Only valid telemetry establishes a connection
447
+ with data_lock:
448
+ was_connected = devices_data.get(uid, {}).get("connected", False)
449
+ if uid in devices_data:
450
+ devices_data[uid]["connected"] = False
451
+ devices_data[uid]["latest"] = create_empty_telemetry()
452
+ if was_connected:
453
+ schedule_device_event(uid, "disconnected")
447
454
 
448
455
  while not shutdown_event.is_set():
449
456
  # --- (Re)connect ---
450
457
  if ser is None:
451
- # Mark as disconnected
452
- if is_connected:
453
- is_connected = False
454
- with data_lock:
455
- if uid in devices_data:
456
- devices_data[uid]["connected"] = False
457
- devices_data[uid]["latest"] = create_empty_telemetry()
458
- schedule_device_event(uid, "disconnected")
459
- logger.info("[%s] Device disconnected", uid)
460
-
461
458
  try:
462
459
  new_ser = open_serial_connection(port)
463
460
  if new_ser is None:
464
461
  raise OSError("open_serial_connection returned None")
465
462
  ser = new_ser
466
- # Reset error counter on successful connect
467
- consecutive_errors = 0
468
463
 
469
464
  # Read device info to get product_id for sensor reading
470
465
  try:
@@ -481,18 +476,12 @@ def read_device_loop(port, uid):
481
476
  except Exception:
482
477
  pass
483
478
 
484
- is_connected = True
485
- with data_lock:
486
- if uid in devices_data:
487
- devices_data[uid]["connected"] = True
488
- schedule_device_event(uid, "connected")
489
- logger.info("Connected to device %s on %s", uid, port)
490
479
  except Exception as exc:
491
480
  ser = None # Ensure ser is None on error
492
481
  consecutive_errors += 1
493
482
  delay = min(
494
483
  RECONNECT_DELAY * consecutive_errors,
495
- 30) # Exponential backoff, capped
484
+ 30) # Linear backoff, capped
496
485
  logger.warning(
497
486
  "[%s] Failed to open serial port %s: %s (retry in %.1fs)",
498
487
  uid,
@@ -509,6 +498,8 @@ def read_device_loop(port, uid):
509
498
  shutdown_event.wait(1)
510
499
  continue
511
500
  sensors = read_sensors(ser, product_id=product_id)
501
+ if sensors is None:
502
+ raise OSError("No complete sensor response received")
512
503
  if sensors:
513
504
  translated = translate_sensor_struct(sensors)
514
505
  translated["timestamp"] = datetime.now().strftime(
@@ -518,12 +509,15 @@ def read_device_loop(port, uid):
518
509
 
519
510
  with data_lock:
520
511
  if uid in devices_data:
512
+ devices_data[uid]["connected"] = True
521
513
  devices_data[uid]["latest"] = translated
522
514
  devices_data[uid]["history"].append(translated)
523
515
 
516
+ if not is_connected:
517
+ is_connected = True
518
+ schedule_device_event(uid, "connected")
519
+ logger.info("Connected to device %s on %s", uid, port)
524
520
  schedule_update(uid, translated)
525
- else:
526
- logger.debug("[%s] No sensor data read", uid)
527
521
  except Exception as e:
528
522
  consecutive_errors += 1
529
523
  # Specific debug logging for unsupported commands
@@ -547,8 +541,18 @@ def read_device_loop(port, uid):
547
541
  except Exception:
548
542
  pass
549
543
  ser = None
544
+ was_connected = is_connected
545
+ if is_connected:
546
+ logger.info("[%s] Device disconnected", uid)
547
+ is_connected = False
548
+ with data_lock:
549
+ if uid in devices_data:
550
+ devices_data[uid]["connected"] = False
551
+ devices_data[uid]["latest"] = create_empty_telemetry()
552
+ if was_connected:
553
+ schedule_device_event(uid, "disconnected")
550
554
 
551
- # Exponential backoff with cap
555
+ # Linear backoff with cap
552
556
  if consecutive_errors >= max_consecutive_errors:
553
557
  delay = min(RECONNECT_DELAY * consecutive_errors, 30)
554
558
  logger.warning(
@@ -0,0 +1,235 @@
1
+ """Fault injection for the collector's serial recovery; no hardware needed."""
2
+ from collections import deque
3
+ from unittest.mock import MagicMock, call
4
+
5
+ import pytest
6
+
7
+ from benchlab.restapi import telemetry_api as api
8
+
9
+
10
+ class StopEvent:
11
+ def __init__(self):
12
+ self.stopped = False
13
+ self.waits = []
14
+ self.on_wait = lambda _: None
15
+
16
+ def is_set(self):
17
+ return self.stopped
18
+
19
+ def wait(self, delay):
20
+ self.waits.append(delay)
21
+ self.on_wait(delay)
22
+ return self.stopped
23
+
24
+
25
+ @pytest.fixture
26
+ def reader(monkeypatch):
27
+ stop = StopEvent()
28
+ device = {"connected": False, "latest": {}, "history": deque()}
29
+ connections = []
30
+
31
+ def open_port(_):
32
+ ser = MagicMock()
33
+ connections.append(ser)
34
+ if len(connections) > 30:
35
+ stop.stopped = True # Bound regressions that never reach backoff.
36
+ return ser
37
+
38
+ monkeypatch.setattr(api, "shutdown_event", stop)
39
+ monkeypatch.setattr(api, "devices_data", {"test": device})
40
+ monkeypatch.setattr(api, "open_serial_connection", open_port)
41
+ monkeypatch.setattr(api, "read_device", lambda _: {"ProductId": 16})
42
+ monkeypatch.setattr(
43
+ api, "translate_sensor_struct", lambda _: {"TS_2": 24.0})
44
+ trace = []
45
+ snapshots = []
46
+
47
+ def device_event(uid, event):
48
+ # Record rather than assert here: the reader catches exceptions.
49
+ unlocked = api.data_lock.acquire(blocking=False)
50
+ if unlocked:
51
+ api.data_lock.release()
52
+ wire = api._device_to_wire_shape(uid) if unlocked else {}
53
+ snapshots.append((event, device["connected"],
54
+ device["latest"].copy(), unlocked, wire))
55
+ trace.append(event)
56
+
57
+ events = MagicMock(side_effect=device_event)
58
+ publish = MagicMock(side_effect=lambda *_: trace.append("telemetry"))
59
+ monkeypatch.setattr(api, "schedule_update", publish)
60
+ monkeypatch.setattr(api, "schedule_device_event", events)
61
+ yield stop, device, connections, publish, events, trace
62
+
63
+ for event, connected, latest, unlocked, wire in snapshots:
64
+ assert unlocked, "Device event scheduled while holding data_lock"
65
+ assert connected == (event == "connected")
66
+ assert wire["status"] == event.upper()
67
+ if connected:
68
+ assert latest["TS_2"] == 24.0
69
+ else:
70
+ assert latest["connected"] is False
71
+ assert latest["TS_2"] == 0
72
+
73
+
74
+ @pytest.mark.parametrize("failure", [None, OSError("USB disconnected")])
75
+ def test_failed_read_discards_stale_data_and_reconnects(
76
+ monkeypatch, reader, failure):
77
+ stop, device, connections, publish, events, trace = reader
78
+ reads = 0
79
+
80
+ def read(*args, **kwargs):
81
+ nonlocal reads
82
+ reads += 1
83
+ if reads >= 3:
84
+ stop.stopped = True
85
+ if reads == 2:
86
+ assert device["connected"]
87
+ if isinstance(failure, Exception):
88
+ raise failure
89
+ return failure
90
+ if reads == 3:
91
+ assert not device["connected"]
92
+ assert device["latest"]["connected"] is False
93
+ assert len(connections) == 2
94
+ stop.stopped = True
95
+ return object()
96
+
97
+ monkeypatch.setattr(api, "read_sensors", read)
98
+ api.read_device_loop("/fake", "test")
99
+ assert publish.call_count == 2
100
+ assert trace == ["connected", "telemetry", "disconnected",
101
+ "connected", "telemetry", "disconnected"]
102
+ assert events.call_args_list == [
103
+ call("test", "connected"), call("test", "disconnected"),
104
+ call("test", "connected"), call("test", "disconnected"),
105
+ ]
106
+ assert len(device["history"]) == 2
107
+ assert all(s.close.call_count == 1 for s in connections)
108
+
109
+
110
+ @pytest.mark.parametrize("failure", [None, OSError("read failed")])
111
+ def test_repeated_read_failures_back_off_even_when_open_succeeds(
112
+ monkeypatch, reader, failure):
113
+ stop, device, connections, publish, events, trace = reader
114
+
115
+ def read(*args, **kwargs):
116
+ assert not device["connected"]
117
+ if isinstance(failure, Exception):
118
+ raise failure
119
+ return failure
120
+
121
+ def waited(delay):
122
+ assert not device["connected"]
123
+ assert device["latest"]["connected"] is False
124
+ stop.stopped = True
125
+
126
+ stop.on_wait = waited
127
+ monkeypatch.setattr(api, "read_sensors", read)
128
+ api.read_device_loop("/fake", "test")
129
+ assert len(connections) == 10
130
+ assert stop.waits == [min(api.RECONNECT_DELAY * 10, 30)]
131
+ publish.assert_not_called()
132
+ assert not device["history"]
133
+ events.assert_not_called()
134
+ assert all(s.close.call_count == 1 for s in connections)
135
+
136
+
137
+ def test_valid_sample_resets_failure_streak(monkeypatch, reader):
138
+ stop, device, connections, publish, events, trace = reader
139
+ reads = 0
140
+
141
+ def read(*args, **kwargs):
142
+ nonlocal reads
143
+ reads += 1
144
+ if reads > 30:
145
+ stop.stopped = True
146
+ if reads == 10:
147
+ return object()
148
+ return None
149
+
150
+ def waited(delay):
151
+ if delay == api.poll_interval:
152
+ assert device["connected"]
153
+ else:
154
+ assert reads == 20
155
+ stop.stopped = True
156
+
157
+ stop.on_wait = waited
158
+ monkeypatch.setattr(api, "read_sensors", read)
159
+ api.read_device_loop("/fake", "test")
160
+ assert stop.waits == [api.poll_interval, min(api.RECONNECT_DELAY * 10, 30)]
161
+ publish.assert_called_once()
162
+ assert len(device["history"]) == 1
163
+ assert trace == ["connected", "telemetry", "disconnected"]
164
+
165
+
166
+ @pytest.mark.parametrize("initially_connected", [False, True])
167
+ def test_failed_initial_open_clears_discovery_connected_flag(
168
+ monkeypatch, reader, initially_connected):
169
+ stop, device, connections, publish, events, trace = reader
170
+ device.update(connected=initially_connected, latest={"TS_2": 24.0})
171
+ monkeypatch.setattr(api, "open_serial_connection", lambda _: None)
172
+
173
+ def waited(delay):
174
+ assert not device["connected"]
175
+ assert device["latest"]["connected"] is False
176
+ stop.stopped = True
177
+
178
+ stop.on_wait = waited
179
+ api.read_device_loop("/fake", "test")
180
+ assert stop.waits == [min(api.RECONNECT_DELAY, 30)]
181
+ publish.assert_not_called()
182
+ assert trace == (["disconnected"] if initially_connected else [])
183
+
184
+
185
+ def test_steady_samples_emit_one_connection_and_shutdown_event(
186
+ monkeypatch, reader):
187
+ stop, device, connections, publish, events, trace = reader
188
+ reads = 0
189
+
190
+ def read(*args, **kwargs):
191
+ nonlocal reads
192
+ reads += 1
193
+ if reads >= 3:
194
+ stop.stopped = True
195
+ return object()
196
+
197
+ monkeypatch.setattr(api, "read_sensors", read)
198
+ api.read_device_loop("/fake", "test")
199
+ assert trace == ["connected", "telemetry", "telemetry", "telemetry",
200
+ "disconnected"]
201
+ assert len(connections) == 1
202
+ assert publish.call_count == 3
203
+ assert not device["connected"]
204
+ assert connections[0].close.call_count == 1
205
+
206
+
207
+ @pytest.mark.parametrize("failure", [None, OSError("read failed")])
208
+ def test_disconnect_event_precedes_backoff(monkeypatch, reader, failure):
209
+ stop, device, connections, publish, events, trace = reader
210
+ reads = 0
211
+ backoff_traces = []
212
+
213
+ def read(*args, **kwargs):
214
+ nonlocal reads
215
+ reads += 1
216
+ if reads > 15:
217
+ stop.stopped = True
218
+ if reads == 1:
219
+ return object()
220
+ if isinstance(failure, Exception):
221
+ raise failure
222
+ return failure
223
+
224
+ def waited(delay):
225
+ if delay != api.poll_interval:
226
+ backoff_traces.append(trace.copy())
227
+ stop.stopped = True
228
+
229
+ stop.on_wait = waited
230
+ monkeypatch.setattr(api, "read_sensors", read)
231
+ api.read_device_loop("/fake", "test")
232
+ assert reads == 11
233
+ assert backoff_traces == [["connected", "telemetry", "disconnected"]]
234
+ assert trace == backoff_traces[0]
235
+ assert stop.waits == [api.poll_interval, min(api.RECONNECT_DELAY * 10, 30)]