benchlab-pytools 3.1.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.
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/CHANGELOG.md +61 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/PKG-INFO +11 -4
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/README.md +4 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/__init__.py +1 -1
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/config/README.md +15 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/config/config_client.py +46 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/config/test_config_client.py +86 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/core/README.md +1 -1
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/csv_log/requirements.txt +1 -1
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/requirements.txt +1 -1
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/restapi/readme.md +11 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/restapi/telemetry_api.py +148 -22
- benchlab_pytools-3.2.1/benchlab/restapi/test_read_recovery.py +235 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/restapi/test_server.py +1 -0
- benchlab_pytools-3.2.1/benchlab/tui/requirements.txt +4 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/tui/test_tui_core.py +23 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/tui/test_tui_main.py +37 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/tui/tui_core.py +2 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/tui/tui_main.py +6 -3
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/pyproject.toml +4 -2
- benchlab_pytools-3.1.0/benchlab/tui/requirements.txt +0 -2
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/.flake8 +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/.gitignore +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/CONTRIBUTING.md +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/LICENSE +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/__main__.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/bootstrap.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/config/__init__.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/config/config_manager.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/config/config_tool.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/config/diff.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/config/examples/complete_config.json +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/config/examples/rgb_lighting.json +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/config/examples/simple_fan.json +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/config/schema.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/config/test_config_manager.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/config/test_diff.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/core/__init__.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/core/config.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/core/datasource.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/core/datasource_manager.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/core/device_registry.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/core/discovery.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/core/infrastructure.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/core/process_manager.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/core/retry.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/core/shared_serial.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/core/statistics.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/csv_log/README.md +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/csv_log/__init__.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/csv_log/csv_logger_enhanced.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/csv_log/message_batcher.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/csv_log/smart_retry.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/graph/README.md +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/graph/__init__.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/graph/app.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/graph/requirements.txt +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/graph/runner.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/graph/test_graph.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/graph/ui.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/hwinfo/README.md +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/hwinfo/__init__.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/hwinfo/hwinfo_export.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/hwinfo/requirements.txt +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/hwinfo/test_hwinfo_export.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/launcher.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/link/.env.EXAMPLE +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/link/README.md +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/link/__init__.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/link/link_main.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/link/test_link_main.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/main.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/menu.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/menu_classic.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/mqtt/README.md +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/mqtt/__init__.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/mqtt/mqtt.config_template +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/mqtt/mqtt_publisher.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/mqtt/requirements.txt +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/mqtt/test_mqtt_publisher.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/prefs.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/restapi/.env.example +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/restapi/__init__.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/restapi/favicon.ico +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/restapi/requirements.txt +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/sources.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/tools.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/tui/README.md +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/tui/__init__.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/tui/config.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/README.md +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/NOTICE.md +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/README.md +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/Running_from_source_on_linux.md +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/__init__.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/assets/vu1_hello_world.png +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/community_applications.md +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/config.yaml +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/database.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/dial_driver.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/dials/Comms_Hub_Gauge.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/dials/Comms_Hub_Server.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/dials/__init__.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/dials/base_logger.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/installer/inc/icon.ico +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/installer/install.nsi +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/make_version.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/pylintrc +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/requirements.txt +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/serial_driver.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/server.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/server_config.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/server_dial_handler.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/vu_notifications.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/android-chrome-192x192.png +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/android-chrome-512x512.png +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/apple-touch-icon.png +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/demo.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/demo.min.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/demo.rtl.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/demo.rtl.min.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler-flags.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler-flags.min.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler-flags.rtl.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler-flags.rtl.min.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler-payments.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler-payments.min.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler-payments.rtl.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler-payments.rtl.min.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler-social.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler-social.min.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler-social.rtl.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler-social.rtl.min.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler-vendors.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler-vendors.min.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler-vendors.rtl.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler-vendors.rtl.min.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler.min.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler.rtl.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/tabler.rtl.min.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/css/theme.min.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/img/header.png +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/img/logo.png +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/js/dark-theme.js +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/js/dark-theme.min.js +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/js/demo.js +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/js/demo.min.js +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/js/jquery.min.js +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/js/tabler.esm.js +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/js/tabler.esm.min.js +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/js/tabler.js +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/js/tabler.min.js +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/js/vu1_api.js +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_api_key_settings.js +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_api_keys.js +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_dial.js +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_home.js +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_root.js +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/favicon-16x16.png +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/favicon-32x32.png +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/favicon.ico +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/favicon.png +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/index.html +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/views/api_keys.html +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/views/dial.html +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/views/footer.html +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/views/header.html +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/views/key_settings.html +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/VU-Server/www/views/start.html +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/__init__.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/assets/Barlow-Bold.ttf +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/assets/bl_dial_144x200.png +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/assets/bl_logo_144x200.png +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/devices.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/requirements.txt +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/sensors.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/test_devices.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/test_vu_server_config.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/test_vu_server_manager.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/test_vu_tui.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/vu_dial.config_template +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/vu_logo_gen.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/vu_server.config_template +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/vu_server_config.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/vu_server_manager.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/vu_tui.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/vu/vu_updater.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/README.md +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/__init__.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/assets/Inter-Bold.ttf +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/assets/Inter.ttf +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/assets/benchlab.png +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/benchlab_fleet.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/benchlab_graph.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/benchlab_overview.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/benchlab_telemetry.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/benchlab_ui.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/benchlab_utils.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/requirements.txt +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/test_wigidash.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/wigidash_device.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/wigidash_manager.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/wigidash_session.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/wigidash_usb.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab/wigidash/wigidash_widget.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/benchlab.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.1}/pytest.ini +0 -0
|
@@ -4,6 +4,65 @@ 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
|
+
|
|
21
|
+
## [3.2.0] - 2026-09-29
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- `GET /events` WebSocket on the FastAPI server (`benchlab.restapi.telemetry_api`):
|
|
25
|
+
a multiplexed event stream matching the frame envelope of the C# BenchLab
|
|
26
|
+
service's `/events` (`hello`/`telemetry`/`device` frames), so
|
|
27
|
+
`ServiceWsDataSource` (`--source service_ws`) can point at either server.
|
|
28
|
+
`telemetry.v` keeps pytools' own TUI-style sensor-key names (e.g.
|
|
29
|
+
`EPS1_Power`) rather than the C# service's `ShortName` convention (e.g.
|
|
30
|
+
`EPS1_P`) -- there's no pycore equivalent of `ShortName` to translate
|
|
31
|
+
into, and `ServiceWsDataSource`'s key mapping already passes unrecognised
|
|
32
|
+
keys through unchanged, so no client-side changes are needed. The
|
|
33
|
+
existing per-device `/device/{uid}/stream` WebSocket is unchanged.
|
|
34
|
+
- `ConfigClient.factory_cal_unlock()` (`direct` and `named_pipe` sources):
|
|
35
|
+
temporarily lifts write-protection on BL2's factory calibration slot
|
|
36
|
+
(BL2 firmware 7+ only). Disruptive -- a successful call resets the
|
|
37
|
+
device immediately; reconnect before issuing further commands.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
- Bumped the `benchlab-pycore` floor to `>=0.8.0` (from `>=0.6.0`).
|
|
41
|
+
0.8.0 fixes `write_calibration()`'s wire format on BL1 firmware 6+ /
|
|
42
|
+
BL2 firmware 7+, where `WRITE_CALIBRATION` changed from an
|
|
43
|
+
offset-addressed partial write to a whole-struct replace; sending the
|
|
44
|
+
old format there silently corrupts calibration data.
|
|
45
|
+
`DirectConfigClient.write_calibration()` was already firmware-agnostic
|
|
46
|
+
(it doesn't pass `fw_version`, so pycore auto-probes it), so no code
|
|
47
|
+
change was needed here beyond the floor bump. Companion release:
|
|
48
|
+
BENCHLAB_Service v2.6.0, which adds the same `FACTORY_CAL_UNLOCK`
|
|
49
|
+
command plus BL1/BL2 firmware compatibility and a stuck-disconnect
|
|
50
|
+
reconnect fix.
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
- TUI: a disconnected remote data source (`fastapi_custom`, `mqtt_custom`,
|
|
54
|
+
`named_pipe`, `service_http`, `service_ws`, etc.) fell back to scanning
|
|
55
|
+
local serial ports for the Fleet view, which could probe ports already
|
|
56
|
+
owned by another collector and show unrelated local devices instead of
|
|
57
|
+
an empty fleet. (#74)
|
|
58
|
+
- TUI: the terminal-too-small warning was drawn but never flushed to the
|
|
59
|
+
screen before `render()` returned, leaving the terminal blank instead
|
|
60
|
+
of showing the warning. (#75)
|
|
61
|
+
- Packaging: `benchlab-pytools[tui]` was missing `pydantic` and `requests`,
|
|
62
|
+
both required to initialize an HTTP-backed TUI (`fastapi`/`service_http`
|
|
63
|
+
sources); a clean `[tui]`-only install crashed with `ModuleNotFoundError`
|
|
64
|
+
unless the server/all extras happened to already pull them in. (#76)
|
|
65
|
+
|
|
7
66
|
## [3.1.0] - 2026-09-08
|
|
8
67
|
|
|
9
68
|
### Added
|
|
@@ -114,6 +173,8 @@ export, MQTT publisher, VU dials, WigiDash, and config import/export tools,
|
|
|
114
173
|
sharing a common data-source layer (direct serial, FastAPI, MQTT, named
|
|
115
174
|
pipe, service HTTP, service WebSocket).
|
|
116
175
|
|
|
176
|
+
[3.2.1]: https://github.com/BenchLab-io/benchlab-pytools/compare/v3.2.0...v3.2.1
|
|
177
|
+
[3.2.0]: https://github.com/BenchLab-io/benchlab-pytools/compare/v3.1.0...v3.2.0
|
|
117
178
|
[3.1.0]: https://github.com/BenchLab-io/benchlab-pytools/compare/v3.0.4...v3.1.0
|
|
118
179
|
[3.0.4]: https://github.com/BenchLab-io/benchlab-pytools/compare/v3.0.3...v3.0.4
|
|
119
180
|
[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
|
|
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
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -369,29 +441,25 @@ def read_device_loop(port, uid):
|
|
|
369
441
|
reconnection logic."""
|
|
370
442
|
ser = None
|
|
371
443
|
consecutive_errors = 0
|
|
372
|
-
max_consecutive_errors = 10 #
|
|
444
|
+
max_consecutive_errors = 10 # Back off after repeated read failures
|
|
373
445
|
product_id = None # Will be set once we read device info
|
|
374
|
-
is_connected = False #
|
|
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")
|
|
375
454
|
|
|
376
455
|
while not shutdown_event.is_set():
|
|
377
456
|
# --- (Re)connect ---
|
|
378
457
|
if ser is None:
|
|
379
|
-
# Mark as disconnected
|
|
380
|
-
if is_connected:
|
|
381
|
-
is_connected = False
|
|
382
|
-
with data_lock:
|
|
383
|
-
if uid in devices_data:
|
|
384
|
-
devices_data[uid]["connected"] = False
|
|
385
|
-
devices_data[uid]["latest"] = create_empty_telemetry()
|
|
386
|
-
logger.info("[%s] Device disconnected", uid)
|
|
387
|
-
|
|
388
458
|
try:
|
|
389
459
|
new_ser = open_serial_connection(port)
|
|
390
460
|
if new_ser is None:
|
|
391
461
|
raise OSError("open_serial_connection returned None")
|
|
392
462
|
ser = new_ser
|
|
393
|
-
# Reset error counter on successful connect
|
|
394
|
-
consecutive_errors = 0
|
|
395
463
|
|
|
396
464
|
# Read device info to get product_id for sensor reading
|
|
397
465
|
try:
|
|
@@ -408,17 +476,12 @@ def read_device_loop(port, uid):
|
|
|
408
476
|
except Exception:
|
|
409
477
|
pass
|
|
410
478
|
|
|
411
|
-
is_connected = True
|
|
412
|
-
with data_lock:
|
|
413
|
-
if uid in devices_data:
|
|
414
|
-
devices_data[uid]["connected"] = True
|
|
415
|
-
logger.info("Connected to device %s on %s", uid, port)
|
|
416
479
|
except Exception as exc:
|
|
417
480
|
ser = None # Ensure ser is None on error
|
|
418
481
|
consecutive_errors += 1
|
|
419
482
|
delay = min(
|
|
420
483
|
RECONNECT_DELAY * consecutive_errors,
|
|
421
|
-
30) #
|
|
484
|
+
30) # Linear backoff, capped
|
|
422
485
|
logger.warning(
|
|
423
486
|
"[%s] Failed to open serial port %s: %s (retry in %.1fs)",
|
|
424
487
|
uid,
|
|
@@ -435,6 +498,8 @@ def read_device_loop(port, uid):
|
|
|
435
498
|
shutdown_event.wait(1)
|
|
436
499
|
continue
|
|
437
500
|
sensors = read_sensors(ser, product_id=product_id)
|
|
501
|
+
if sensors is None:
|
|
502
|
+
raise OSError("No complete sensor response received")
|
|
438
503
|
if sensors:
|
|
439
504
|
translated = translate_sensor_struct(sensors)
|
|
440
505
|
translated["timestamp"] = datetime.now().strftime(
|
|
@@ -444,12 +509,15 @@ def read_device_loop(port, uid):
|
|
|
444
509
|
|
|
445
510
|
with data_lock:
|
|
446
511
|
if uid in devices_data:
|
|
512
|
+
devices_data[uid]["connected"] = True
|
|
447
513
|
devices_data[uid]["latest"] = translated
|
|
448
514
|
devices_data[uid]["history"].append(translated)
|
|
449
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)
|
|
450
520
|
schedule_update(uid, translated)
|
|
451
|
-
else:
|
|
452
|
-
logger.debug("[%s] No sensor data read", uid)
|
|
453
521
|
except Exception as e:
|
|
454
522
|
consecutive_errors += 1
|
|
455
523
|
# Specific debug logging for unsupported commands
|
|
@@ -473,8 +541,18 @@ def read_device_loop(port, uid):
|
|
|
473
541
|
except Exception:
|
|
474
542
|
pass
|
|
475
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")
|
|
476
554
|
|
|
477
|
-
#
|
|
555
|
+
# Linear backoff with cap
|
|
478
556
|
if consecutive_errors >= max_consecutive_errors:
|
|
479
557
|
delay = min(RECONNECT_DELAY * consecutive_errors, 30)
|
|
480
558
|
logger.warning(
|
|
@@ -490,6 +568,7 @@ def read_device_loop(port, uid):
|
|
|
490
568
|
if uid in devices_data:
|
|
491
569
|
devices_data[uid]["connected"] = False
|
|
492
570
|
devices_data[uid]["latest"] = create_empty_telemetry()
|
|
571
|
+
schedule_device_event(uid, "disconnected")
|
|
493
572
|
|
|
494
573
|
if ser:
|
|
495
574
|
try:
|
|
@@ -627,6 +706,50 @@ async def stream_device(uid: str, ws: WebSocket):
|
|
|
627
706
|
logger.info("[%s] Client disconnected (%d total)", uid, client_count)
|
|
628
707
|
|
|
629
708
|
|
|
709
|
+
def build_hello_frame():
|
|
710
|
+
"""Build the /events "hello" frame sent on connect: service version,
|
|
711
|
+
poll interval, and the current device list, matching the C# service's
|
|
712
|
+
/events wire shape."""
|
|
713
|
+
with data_lock:
|
|
714
|
+
uids = list(devices_data.keys())
|
|
715
|
+
return {
|
|
716
|
+
"type": "hello",
|
|
717
|
+
"serviceVersion": benchlab.__version__,
|
|
718
|
+
"pollIntervalMs": int(Config.POLL_INTERVAL * 1000),
|
|
719
|
+
"devices": [_device_to_wire_shape(uid) for uid in uids],
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
@app.websocket("/events")
|
|
724
|
+
async def events_stream(ws: WebSocket):
|
|
725
|
+
"""Multiplexed event stream: hello on connect, then telemetry/device
|
|
726
|
+
frames for every device, matching the C# BenchLab service's /events
|
|
727
|
+
protocol (see ServiceWsDataSource in benchlab.core.datasource, the
|
|
728
|
+
consumer this endpoint is meant to be compatible with)."""
|
|
729
|
+
await ws.accept()
|
|
730
|
+
with data_lock:
|
|
731
|
+
event_clients.add(ws)
|
|
732
|
+
await ws.send_json(build_hello_frame())
|
|
733
|
+
logger.info("[/events] Client connected (%d total)", len(event_clients))
|
|
734
|
+
try:
|
|
735
|
+
while True:
|
|
736
|
+
msg = await ws.receive_json()
|
|
737
|
+
if isinstance(msg, dict) and msg.get("type") == "ping":
|
|
738
|
+
await ws.send_json({
|
|
739
|
+
"type": "pong",
|
|
740
|
+
"ts": datetime.now().strftime("%Y-%m-%dT%H:%M:%S"),
|
|
741
|
+
})
|
|
742
|
+
# "subscribe" and any other client message: accepted, no-op --
|
|
743
|
+
# this endpoint always broadcasts every device to every client.
|
|
744
|
+
except WebSocketDisconnect:
|
|
745
|
+
pass
|
|
746
|
+
finally:
|
|
747
|
+
with data_lock:
|
|
748
|
+
event_clients.discard(ws)
|
|
749
|
+
logger.info(
|
|
750
|
+
"[/events] Client disconnected (%d total)", len(event_clients))
|
|
751
|
+
|
|
752
|
+
|
|
630
753
|
@app.get("/favicon.ico")
|
|
631
754
|
def favicon():
|
|
632
755
|
return FileResponse(Path(__file__).parent / "favicon.ico")
|
|
@@ -766,9 +889,12 @@ def scan_for_devices():
|
|
|
766
889
|
disconnected.append({"uid": uid, "port": port})
|
|
767
890
|
logger.info(
|
|
768
891
|
"Device %s appears to be disconnected from %s", uid, port)
|
|
892
|
+
was_connected = data.get("connected", False)
|
|
769
893
|
with data_lock:
|
|
770
894
|
if uid in devices_data:
|
|
771
895
|
devices_data[uid]["connected"] = False
|
|
896
|
+
if was_connected:
|
|
897
|
+
schedule_device_event(uid, "disconnected")
|
|
772
898
|
|
|
773
899
|
result = {
|
|
774
900
|
"scan_time": datetime.now().isoformat(),
|