benchlab-pytools 3.1.0__tar.gz → 3.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/CHANGELOG.md +46 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/PKG-INFO +11 -4
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/README.md +4 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/__init__.py +1 -1
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/config/README.md +15 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/config/config_client.py +46 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/config/test_config_client.py +86 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/core/README.md +1 -1
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/csv_log/requirements.txt +1 -1
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/requirements.txt +1 -1
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/restapi/readme.md +11 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/restapi/telemetry_api.py +122 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/restapi/test_server.py +1 -0
- benchlab_pytools-3.2.0/benchlab/tui/requirements.txt +4 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/tui/test_tui_core.py +23 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/tui/test_tui_main.py +37 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/tui/tui_core.py +2 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/tui/tui_main.py +6 -3
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/pyproject.toml +4 -2
- benchlab_pytools-3.1.0/benchlab/tui/requirements.txt +0 -2
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/.flake8 +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/.gitignore +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/CONTRIBUTING.md +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/LICENSE +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/__main__.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/bootstrap.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/config/__init__.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/config/config_manager.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/config/config_tool.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/config/diff.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/config/examples/complete_config.json +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/config/examples/rgb_lighting.json +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/config/examples/simple_fan.json +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/config/schema.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/config/test_config_manager.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/config/test_diff.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/core/__init__.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/core/config.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/core/datasource.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/core/datasource_manager.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/core/device_registry.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/core/discovery.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/core/infrastructure.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/core/process_manager.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/core/retry.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/core/shared_serial.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/core/statistics.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/csv_log/README.md +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/csv_log/__init__.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/csv_log/csv_logger_enhanced.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/csv_log/message_batcher.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/csv_log/smart_retry.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/graph/README.md +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/graph/__init__.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/graph/app.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/graph/requirements.txt +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/graph/runner.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/graph/test_graph.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/graph/ui.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/hwinfo/README.md +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/hwinfo/__init__.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/hwinfo/hwinfo_export.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/hwinfo/requirements.txt +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/hwinfo/test_hwinfo_export.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/launcher.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/link/.env.EXAMPLE +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/link/README.md +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/link/__init__.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/link/link_main.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/link/test_link_main.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/main.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/menu.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/menu_classic.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/mqtt/README.md +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/mqtt/__init__.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/mqtt/mqtt.config_template +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/mqtt/mqtt_publisher.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/mqtt/requirements.txt +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/mqtt/test_mqtt_publisher.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/prefs.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/restapi/.env.example +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/restapi/__init__.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/restapi/favicon.ico +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/restapi/requirements.txt +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/sources.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/tools.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/tui/README.md +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/tui/__init__.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/tui/config.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/README.md +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/NOTICE.md +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/README.md +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/Running_from_source_on_linux.md +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/__init__.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/assets/vu1_hello_world.png +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/community_applications.md +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/config.yaml +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/database.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/dial_driver.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/dials/Comms_Hub_Gauge.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/dials/Comms_Hub_Server.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/dials/__init__.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/dials/base_logger.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/installer/inc/icon.ico +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/installer/install.nsi +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/make_version.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/pylintrc +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/requirements.txt +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/serial_driver.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/server.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/server_config.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/server_dial_handler.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/vu_notifications.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/android-chrome-192x192.png +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/android-chrome-512x512.png +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/apple-touch-icon.png +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/demo.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/demo.min.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/demo.rtl.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/demo.rtl.min.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-flags.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-flags.min.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-flags.rtl.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-flags.rtl.min.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-payments.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-payments.min.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-payments.rtl.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-payments.rtl.min.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-social.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-social.min.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-social.rtl.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-social.rtl.min.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-vendors.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-vendors.min.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-vendors.rtl.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler-vendors.rtl.min.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler.min.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler.rtl.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/tabler.rtl.min.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/css/theme.min.css +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/img/header.png +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/img/logo.png +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/dark-theme.js +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/dark-theme.min.js +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/demo.js +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/demo.min.js +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/jquery.min.js +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/tabler.esm.js +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/tabler.esm.min.js +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/tabler.js +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/tabler.min.js +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/vu1_api.js +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_api_key_settings.js +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_api_keys.js +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_dial.js +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_home.js +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/assets/js/vu1_gui_root.js +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/favicon-16x16.png +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/favicon-32x32.png +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/favicon.ico +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/favicon.png +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/index.html +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/views/api_keys.html +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/views/dial.html +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/views/footer.html +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/views/header.html +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/views/key_settings.html +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/VU-Server/www/views/start.html +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/__init__.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/assets/Barlow-Bold.ttf +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/assets/bl_dial_144x200.png +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/assets/bl_logo_144x200.png +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/devices.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/requirements.txt +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/sensors.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/test_devices.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/test_vu_server_config.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/test_vu_server_manager.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/test_vu_tui.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/vu_dial.config_template +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/vu_logo_gen.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/vu_server.config_template +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/vu_server_config.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/vu_server_manager.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/vu_tui.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/vu/vu_updater.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/README.md +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/__init__.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/assets/Inter-Bold.ttf +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/assets/Inter.ttf +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/assets/benchlab.png +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/benchlab_fleet.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/benchlab_graph.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/benchlab_overview.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/benchlab_telemetry.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/benchlab_ui.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/benchlab_utils.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/requirements.txt +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/test_wigidash.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/wigidash_device.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/wigidash_manager.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/wigidash_session.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/wigidash_usb.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab/wigidash/wigidash_widget.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/benchlab.py +0 -0
- {benchlab_pytools-3.1.0 → benchlab_pytools-3.2.0}/pytest.ini +0 -0
|
@@ -4,6 +4,51 @@ All notable changes to BENCHLAB PyTools are documented here. Format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows
|
|
5
5
|
[SemVer](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [3.2.0] - 2026-09-29
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- `GET /events` WebSocket on the FastAPI server (`benchlab.restapi.telemetry_api`):
|
|
11
|
+
a multiplexed event stream matching the frame envelope of the C# BenchLab
|
|
12
|
+
service's `/events` (`hello`/`telemetry`/`device` frames), so
|
|
13
|
+
`ServiceWsDataSource` (`--source service_ws`) can point at either server.
|
|
14
|
+
`telemetry.v` keeps pytools' own TUI-style sensor-key names (e.g.
|
|
15
|
+
`EPS1_Power`) rather than the C# service's `ShortName` convention (e.g.
|
|
16
|
+
`EPS1_P`) -- there's no pycore equivalent of `ShortName` to translate
|
|
17
|
+
into, and `ServiceWsDataSource`'s key mapping already passes unrecognised
|
|
18
|
+
keys through unchanged, so no client-side changes are needed. The
|
|
19
|
+
existing per-device `/device/{uid}/stream` WebSocket is unchanged.
|
|
20
|
+
- `ConfigClient.factory_cal_unlock()` (`direct` and `named_pipe` sources):
|
|
21
|
+
temporarily lifts write-protection on BL2's factory calibration slot
|
|
22
|
+
(BL2 firmware 7+ only). Disruptive -- a successful call resets the
|
|
23
|
+
device immediately; reconnect before issuing further commands.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- Bumped the `benchlab-pycore` floor to `>=0.8.0` (from `>=0.6.0`).
|
|
27
|
+
0.8.0 fixes `write_calibration()`'s wire format on BL1 firmware 6+ /
|
|
28
|
+
BL2 firmware 7+, where `WRITE_CALIBRATION` changed from an
|
|
29
|
+
offset-addressed partial write to a whole-struct replace; sending the
|
|
30
|
+
old format there silently corrupts calibration data.
|
|
31
|
+
`DirectConfigClient.write_calibration()` was already firmware-agnostic
|
|
32
|
+
(it doesn't pass `fw_version`, so pycore auto-probes it), so no code
|
|
33
|
+
change was needed here beyond the floor bump. Companion release:
|
|
34
|
+
BENCHLAB_Service v2.6.0, which adds the same `FACTORY_CAL_UNLOCK`
|
|
35
|
+
command plus BL1/BL2 firmware compatibility and a stuck-disconnect
|
|
36
|
+
reconnect fix.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
- TUI: a disconnected remote data source (`fastapi_custom`, `mqtt_custom`,
|
|
40
|
+
`named_pipe`, `service_http`, `service_ws`, etc.) fell back to scanning
|
|
41
|
+
local serial ports for the Fleet view, which could probe ports already
|
|
42
|
+
owned by another collector and show unrelated local devices instead of
|
|
43
|
+
an empty fleet. (#74)
|
|
44
|
+
- TUI: the terminal-too-small warning was drawn but never flushed to the
|
|
45
|
+
screen before `render()` returned, leaving the terminal blank instead
|
|
46
|
+
of showing the warning. (#75)
|
|
47
|
+
- Packaging: `benchlab-pytools[tui]` was missing `pydantic` and `requests`,
|
|
48
|
+
both required to initialize an HTTP-backed TUI (`fastapi`/`service_http`
|
|
49
|
+
sources); a clean `[tui]`-only install crashed with `ModuleNotFoundError`
|
|
50
|
+
unless the server/all extras happened to already pull them in. (#76)
|
|
51
|
+
|
|
7
52
|
## [3.1.0] - 2026-09-08
|
|
8
53
|
|
|
9
54
|
### Added
|
|
@@ -114,6 +159,7 @@ export, MQTT publisher, VU dials, WigiDash, and config import/export tools,
|
|
|
114
159
|
sharing a common data-source layer (direct serial, FastAPI, MQTT, named
|
|
115
160
|
pipe, service HTTP, service WebSocket).
|
|
116
161
|
|
|
162
|
+
[3.2.0]: https://github.com/BenchLab-io/benchlab-pytools/compare/v3.1.0...v3.2.0
|
|
117
163
|
[3.1.0]: https://github.com/BenchLab-io/benchlab-pytools/compare/v3.0.4...v3.1.0
|
|
118
164
|
[3.0.4]: https://github.com/BenchLab-io/benchlab-pytools/compare/v3.0.3...v3.0.4
|
|
119
165
|
[3.0.3]: https://github.com/BenchLab-io/benchlab-pytools/compare/v3.0.2...v3.0.3
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: benchlab-pytools
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.0
|
|
4
4
|
Summary: Python control suite for BENCHLAB telemetry devices
|
|
5
5
|
Project-URL: Homepage, https://benchlab.io/
|
|
6
6
|
Project-URL: Repository, https://github.com/BenchLab-io/benchlab-pytools
|
|
@@ -22,7 +22,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
22
22
|
Classifier: Topic :: System :: Hardware
|
|
23
23
|
Classifier: Topic :: System :: Monitoring
|
|
24
24
|
Requires-Python: >=3.10
|
|
25
|
-
Requires-Dist: benchlab-pycore>=0.
|
|
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
|
|
@@ -383,6 +455,7 @@ def read_device_loop(port, uid):
|
|
|
383
455
|
if uid in devices_data:
|
|
384
456
|
devices_data[uid]["connected"] = False
|
|
385
457
|
devices_data[uid]["latest"] = create_empty_telemetry()
|
|
458
|
+
schedule_device_event(uid, "disconnected")
|
|
386
459
|
logger.info("[%s] Device disconnected", uid)
|
|
387
460
|
|
|
388
461
|
try:
|
|
@@ -412,6 +485,7 @@ def read_device_loop(port, uid):
|
|
|
412
485
|
with data_lock:
|
|
413
486
|
if uid in devices_data:
|
|
414
487
|
devices_data[uid]["connected"] = True
|
|
488
|
+
schedule_device_event(uid, "connected")
|
|
415
489
|
logger.info("Connected to device %s on %s", uid, port)
|
|
416
490
|
except Exception as exc:
|
|
417
491
|
ser = None # Ensure ser is None on error
|
|
@@ -490,6 +564,7 @@ def read_device_loop(port, uid):
|
|
|
490
564
|
if uid in devices_data:
|
|
491
565
|
devices_data[uid]["connected"] = False
|
|
492
566
|
devices_data[uid]["latest"] = create_empty_telemetry()
|
|
567
|
+
schedule_device_event(uid, "disconnected")
|
|
493
568
|
|
|
494
569
|
if ser:
|
|
495
570
|
try:
|
|
@@ -627,6 +702,50 @@ async def stream_device(uid: str, ws: WebSocket):
|
|
|
627
702
|
logger.info("[%s] Client disconnected (%d total)", uid, client_count)
|
|
628
703
|
|
|
629
704
|
|
|
705
|
+
def build_hello_frame():
|
|
706
|
+
"""Build the /events "hello" frame sent on connect: service version,
|
|
707
|
+
poll interval, and the current device list, matching the C# service's
|
|
708
|
+
/events wire shape."""
|
|
709
|
+
with data_lock:
|
|
710
|
+
uids = list(devices_data.keys())
|
|
711
|
+
return {
|
|
712
|
+
"type": "hello",
|
|
713
|
+
"serviceVersion": benchlab.__version__,
|
|
714
|
+
"pollIntervalMs": int(Config.POLL_INTERVAL * 1000),
|
|
715
|
+
"devices": [_device_to_wire_shape(uid) for uid in uids],
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
@app.websocket("/events")
|
|
720
|
+
async def events_stream(ws: WebSocket):
|
|
721
|
+
"""Multiplexed event stream: hello on connect, then telemetry/device
|
|
722
|
+
frames for every device, matching the C# BenchLab service's /events
|
|
723
|
+
protocol (see ServiceWsDataSource in benchlab.core.datasource, the
|
|
724
|
+
consumer this endpoint is meant to be compatible with)."""
|
|
725
|
+
await ws.accept()
|
|
726
|
+
with data_lock:
|
|
727
|
+
event_clients.add(ws)
|
|
728
|
+
await ws.send_json(build_hello_frame())
|
|
729
|
+
logger.info("[/events] Client connected (%d total)", len(event_clients))
|
|
730
|
+
try:
|
|
731
|
+
while True:
|
|
732
|
+
msg = await ws.receive_json()
|
|
733
|
+
if isinstance(msg, dict) and msg.get("type") == "ping":
|
|
734
|
+
await ws.send_json({
|
|
735
|
+
"type": "pong",
|
|
736
|
+
"ts": datetime.now().strftime("%Y-%m-%dT%H:%M:%S"),
|
|
737
|
+
})
|
|
738
|
+
# "subscribe" and any other client message: accepted, no-op --
|
|
739
|
+
# this endpoint always broadcasts every device to every client.
|
|
740
|
+
except WebSocketDisconnect:
|
|
741
|
+
pass
|
|
742
|
+
finally:
|
|
743
|
+
with data_lock:
|
|
744
|
+
event_clients.discard(ws)
|
|
745
|
+
logger.info(
|
|
746
|
+
"[/events] Client disconnected (%d total)", len(event_clients))
|
|
747
|
+
|
|
748
|
+
|
|
630
749
|
@app.get("/favicon.ico")
|
|
631
750
|
def favicon():
|
|
632
751
|
return FileResponse(Path(__file__).parent / "favicon.ico")
|
|
@@ -766,9 +885,12 @@ def scan_for_devices():
|
|
|
766
885
|
disconnected.append({"uid": uid, "port": port})
|
|
767
886
|
logger.info(
|
|
768
887
|
"Device %s appears to be disconnected from %s", uid, port)
|
|
888
|
+
was_connected = data.get("connected", False)
|
|
769
889
|
with data_lock:
|
|
770
890
|
if uid in devices_data:
|
|
771
891
|
devices_data[uid]["connected"] = False
|
|
892
|
+
if was_connected:
|
|
893
|
+
schedule_device_event(uid, "disconnected")
|
|
772
894
|
|
|
773
895
|
result = {
|
|
774
896
|
"scan_time": datetime.now().isoformat(),
|
|
@@ -20,6 +20,7 @@ except ImportError:
|
|
|
20
20
|
HAS_CURSES = False
|
|
21
21
|
|
|
22
22
|
import pytest
|
|
23
|
+
from unittest.mock import MagicMock
|
|
23
24
|
|
|
24
25
|
from benchlab.core.statistics import ChannelStats
|
|
25
26
|
|
|
@@ -239,3 +240,25 @@ def test_help_modal_renders_without_stomping_main_refresh():
|
|
|
239
240
|
curses.wrapper(_run)
|
|
240
241
|
except curses.error:
|
|
241
242
|
pytest.skip("curses screen unavailable in this environment")
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@pytest.mark.parametrize("size", [(47, 100), (48, 99), (12, 60)])
|
|
246
|
+
def test_size_warning_is_drawn_and_flushed(monkeypatch, size):
|
|
247
|
+
from benchlab.tui.tui_core import TUICore
|
|
248
|
+
|
|
249
|
+
for name in ("curs_set", "start_color", "use_default_colors", "init_pair"):
|
|
250
|
+
monkeypatch.setattr(curses, name, MagicMock())
|
|
251
|
+
monkeypatch.setattr(curses, "color_pair", lambda _: 0)
|
|
252
|
+
screen = MagicMock()
|
|
253
|
+
screen.getmaxyx.return_value = size
|
|
254
|
+
core = TUICore(screen)
|
|
255
|
+
calls = MagicMock()
|
|
256
|
+
calls.attach_mock(screen.addstr, "draw")
|
|
257
|
+
calls.attach_mock(screen.noutrefresh, "stage")
|
|
258
|
+
monkeypatch.setattr(curses, "doupdate", calls.update)
|
|
259
|
+
|
|
260
|
+
assert core.render(_snapshot(), ChannelStats(), [], 1.0) is False
|
|
261
|
+
|
|
262
|
+
assert "Terminal too small" in screen.addstr.call_args.args[2]
|
|
263
|
+
assert [call[0] for call in calls.mock_calls] == [
|
|
264
|
+
"draw", "stage", "update"]
|
|
@@ -5,6 +5,8 @@ DataSourceManager directly.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import types
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
8
10
|
from unittest.mock import MagicMock
|
|
9
11
|
|
|
10
12
|
from benchlab.tui.tui_main import TUIApplication
|
|
@@ -74,3 +76,38 @@ def test_scan_local_fleet_survives_none_port_when_disconnected():
|
|
|
74
76
|
fleet = app._scan_local_fleet()
|
|
75
77
|
|
|
76
78
|
assert fleet == []
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@pytest.mark.parametrize("source", [
|
|
82
|
+
"fastapi", "fastapi_custom", "mqtt", "mqtt_custom",
|
|
83
|
+
"named_pipe", "service_http", "service_ws",
|
|
84
|
+
])
|
|
85
|
+
def test_remote_fleet_refresh_never_probes_serial(source):
|
|
86
|
+
app = _make_app()
|
|
87
|
+
app.source_type = source
|
|
88
|
+
app.datasource_manager.is_connected.return_value = True
|
|
89
|
+
app.datasource_manager.list_devices.return_value = {
|
|
90
|
+
"REMOTE-UID": {"port": "remote", "variant": "BL2"},
|
|
91
|
+
}
|
|
92
|
+
app._refresh_fleet_cache()
|
|
93
|
+
assert [device["uid"] for device in app.fleet_cache] == ["REMOTE-UID"]
|
|
94
|
+
app.datasource_manager.list_devices.assert_called_once_with()
|
|
95
|
+
|
|
96
|
+
# A disconnect clears the remote list without discovering local devices.
|
|
97
|
+
app.datasource_manager.is_connected.return_value = False
|
|
98
|
+
app.last_fleet_refresh = 0
|
|
99
|
+
app._refresh_fleet_cache()
|
|
100
|
+
assert app.fleet_cache == []
|
|
101
|
+
app.datasource_manager.discover_devices.assert_not_called()
|
|
102
|
+
app.datasource_manager.list_devices.assert_called_once_with()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_direct_fleet_refresh_still_discovers_devices():
|
|
106
|
+
app = _make_app()
|
|
107
|
+
app.datasource_manager.is_connected.return_value = False
|
|
108
|
+
app.datasource_manager.discover_devices.return_value = [
|
|
109
|
+
{"uid": "LOCAL-UID", "port": "COM5", "fw": 2},
|
|
110
|
+
]
|
|
111
|
+
app._refresh_fleet_cache()
|
|
112
|
+
app.datasource_manager.discover_devices.assert_called_once_with()
|
|
113
|
+
assert [device["uid"] for device in app.fleet_cache] == ["LOCAL-UID"]
|