Skip to content

Conversation

@X9X0
Copy link
Owner

@X9X0 X9X0 commented Nov 28, 2025

Summary

This PR implements asynchronous equipment discovery to eliminate UI freezing and fixes critical connection workflow issues that prevented discovered equipment from appearing in the client interface.

Problem Statement

The equipment discovery workflow had two critical UX issues:

  1. UI Freezing: The client became completely unresponsive during equipment discovery (10-30 seconds), showing "Python3 has stopped responding" warnings. VISA enumeration blocked the Qt event loop with synchronous HTTP calls.

  2. Connection Failures: Equipment connected successfully on the server but didn't appear in the client UI due to field name mismatches between the server API (id, resource_string) and client models (equipment_id, resource_name).

  3. Missing Endpoints: No /readings endpoint existed to retrieve current equipment readings, causing 404 errors when users clicked "Refresh Readings".

Changes

🚀 Async Discovery Implementation

Client (client/api/client.py)

  • Added async HTTP methods using aiohttp:
    • discover_equipment_async() - Non-blocking discovery
    • connect_device_async() - Non-blocking connection
    • update_discovery_settings_async() - Non-blocking settings updates
  • Boolean parameter conversion for aiohttp compatibility (True"true")

Client (client/ui/equipment_panel.py)

  • Refactored discover_equipment() to use asyncio.create_task()
  • Used QTimer.singleShot() to schedule modal dialogs on Qt main thread
  • Prevents Qt event loop blocking from async coroutines
  • Removed blocking "Discovery Started" message

Impact: UI remains fully responsive during discovery. No more "Python3 has stopped responding" errors.

🔧 Equipment Connection Fixes

Client (client/models/equipment.py)

  • Fixed field name mapping in from_api_dict():
    • Server id → Client equipment_id
    • Server resource_string → Client resource_name
    • Server nickname → Client name (with fallback to model)
  • Default connection_status to "connected" for listed equipment
  • Map serial_number to idn when needed

Server (server/api/equipment.py)

  • Added GET /equipment/{equipment_id}/readings endpoint
  • Returns voltage, current, power, and output status
  • Includes debug logging for equipment ID diagnostics

Impact: Discovered equipment now appears correctly in the client UI with proper connection status.

🔌 BK Precision 1902B Support

Server (server/equipment/bk_power_supply.py, server/equipment/manager.py)

  • Added BK1902B power supply driver (60V/15A, 900W)
  • Serial port configuration (9600 baud, 8N1, CR+LF termination)
  • Optional *IDN? query (device doesn't support standard SCPI)
  • Model mapping in equipment manager

Testing

Manual Testing Completed:

  • ✅ Discovery runs without freezing UI
  • ✅ Equipment appears in list after connection
  • ✅ Connection status shows correctly
  • ✅ BK1902B connects via USB-to-Serial (/dev/ttyUSB0)

Known Issues:

  • Readings endpoint may return 404 if equipment ID doesn't match (debug logging added)
  • Further testing needed with actual hardware

Breaking Changes

None. All changes are additive or fix existing bugs.

Documentation

  • Updated README.md with Equipment Discovery & Connection features
  • Updated ROADMAP.md with November 2025 UX improvements
  • Added inline code documentation for async methods

Commits

  • fix: Prevent UI freezing during equipment discovery by using async HTTP
  • fix: Map server API field names correctly in Equipment model
  • feat: Add GET /equipment/{equipment_id}/readings endpoint
  • fix: Prevent UI blocking by using QTimer for modal dialogs in async context
  • fix: Convert boolean values to strings for aiohttp query parameters
  • fix: Revert connection to synchronous (only discovery needs async)
  • debug: Add logging to diagnose equipment not found in readings endpoint
  • docs: Update README and ROADMAP with async discovery improvements

Migration Guide

Server Update:

cd ~/LabLink
git pull origin <branch>
docker compose build --no-cache
sudo systemctl restart lablink

The equipment discovery endpoint was defined as GET on the server but
called with POST by the client, causing 405 Method Not Allowed errors.
Since discovery is an action that triggers hardware scanning, POST is
more semantically correct for REST principles.

Changes:
- Updated server endpoint from @router.get to @router.post
- Updated all test files to use POST instead of GET
- Updated API documentation to reflect POST method
- Updated curl examples in getting started guide

Fixes USB equipment discovery for the GUI client.
Added ability to configure discovery settings from the client GUI, making
it easy to enable serial port scanning for equipment like BK Precision
power supplies that connect via USB-to-Serial adapters.

Changes:
- Added GET/POST /api/discovery/settings endpoints to read/update scan types
- Added client methods get_discovery_settings() and update_discovery_settings()
- Added DiscoverySettingsDialog to equipment panel with checkboxes for:
  * TCPIP/Network devices
  * USB devices
  * Serial devices (COM/ttyUSB) - enables serial port scanning
  * GPIB devices
- Modified discover_equipment() to show settings dialog before scanning
- Settings are loaded from server and applied before each discovery scan
- Improved discovery feedback showing found devices or helpful message if none

This allows users to easily enable serial scanning without SSH/config edits.
The discovery manager and VISA scanner store configuration objects at
initialization time. When the API endpoint updates settings, it was only
updating the global settings object, not the discovery manager's config.

This caused serial/GPIB scan toggles in the UI to have no effect because
the VISA scanner was still filtering based on the old config values.

Now the /api/discovery/settings endpoint updates:
- Global settings object (for persistence across restarts)
- Discovery manager's config object (for immediate effect)
- VISA scanner's config object (for scan filtering)

This allows the UI toggle for serial scanning to work without requiring
a server restart, fixing the issue where the BK Precision power supply
connected via USB-to-Serial wasn't being discovered even when serial
scanning was enabled in the UI.
…VISA

The equipment manager's discover_devices() method was bypassing the
discovery manager and calling list_resources() directly, which ignored
all discovery configuration settings including scan_serial.

Now it uses the discovery manager's scan() method which:
- Respects scan type configuration (TCPIP, USB, Serial, GPIB)
- Uses the real-time config updates from the API
- Returns discovered devices with proper filtering

This fixes the issue where enabling serial scanning in the UI had no
effect because the equipment discovery endpoint was using a different
code path.

Falls back to direct VISA scanning if discovery manager is unavailable.
After discovering equipment, users can now immediately connect to devices
via a new dialog that:
- Shows all discovered resources in a selectable list
- Allows choosing equipment type (power_supply, oscilloscope, etc.)
- Allows selecting or entering the model name
- Defaults to power_supply/BK Precision 9206B for convenience
- Calls the connect API and shows success/failure messages

This eliminates the confusing workflow where discovered devices didn't
appear in the equipment list (they need to be connected first).

Now the workflow is:
1. Scan for Equipment (with serial enabled)
2. Dialog shows discovered devices
3. Select device and click Connect
4. Device appears in equipment list as connected
The BK Precision 1902B is a DC power supply (60V/15A, 900W), not an electronic load.
- Added BK1902B class to bk_power_supply.py with correct specs
- Updated equipment manager to import from bk_power_supply module
- Moved model mapping from electronic loads to power supplies section

This resolves the connection error when trying to connect to the discovered device.
BK1902B was incorrectly defined in both bk_electronic_load.py and bk_power_supply.py,
causing a name conflict. Removed the incorrect electronic load version since BK1902B
is actually a DC power supply (60V/15A, 900W), not an electronic load.
Import BK1902B from bk_power_supply module instead of bk_electronic_load
since it's a power supply, not an electronic load.
The resource_manager was being closed during shutdown but not set to None,
causing it to remain in an unusable state. This fix:
1. Sets resource_manager to None after closing during shutdown
2. Adds lazy initialization in connect_device to re-create if needed

This resolves connection failures when resource_manager hasn't been
initialized or was closed during a previous shutdown.
Fixed chicken-and-egg problem where connect() calls _query("*IDN?") to verify
connection, but _query() checks self.connected which is only set to True AFTER
the *IDN? query succeeds.

Now _query(), _write(), and _query_binary() only check if self.instrument exists,
allowing them to be used during the connection verification process.
The lazy initialization was checking if resource_manager is None, but a closed
ResourceManager object still exists (not None) and causes "Invalid session handle"
errors. Now we:
1. Test if the resource_manager is valid before use
2. Recreate it if it's in a closed/invalid state

This ensures we always have a valid ResourceManager when connecting devices.
BK Precision devices require specific serial settings to communicate properly:
- Baud rate: 9600
- Data bits: 8
- Parity: None
- Stop bits: 1
- Flow control: None
- Termination: \n

Added connect() override in BKPowerSupplyBase to configure these settings
before querying the device, resolving timeout errors with BK1902B over serial.
BK Precision devices typically use CR+LF (\r\n) line termination instead of
just LF (\n). Updated both read and write termination to use \r\n to match
the expected protocol format.
Some BK Precision models may not respond to *IDN? immediately or at all.
Updated connect() and get_info() to gracefully handle *IDN? failures:
- Connection proceeds even if *IDN? times out
- Uses default manufacturer/model values from __init__ if query fails
- Logs warnings instead of failing completely

This allows connection to BK1902B and similar models that may have
limited SCPI command support.
The validation was checking self.resource_manager.list_resources (attribute)
instead of calling list_resources() as a method. This meant it never detected
closed resource managers. Now properly calls the method to trigger an exception
on closed/invalid resource managers.
The get_status() method was setting connected=False when *IDN? timed out,
causing the UI to show the device as disconnected even though it was successfully
connected. Now get_status() tries to get firmware info from *IDN? but gracefully
continues without it if the query fails, relying on self.connected for the
actual connection status.
This resolves the "Python3 has stopped responding" warnings that occurred
during equipment discovery.

Changes:
- Add async discovery methods to LabLinkClient using aiohttp
  - discover_equipment_async()
  - connect_device_async()
  - update_discovery_settings_async()
- Update equipment_panel.py to use asyncio.create_task() for non-blocking discovery
- Update connect_dialog.py to use async connection
- Add user feedback message informing that discovery is running in background

The UI now remains responsive during the 10-30 second discovery process.
Discovery and connection operations no longer block the Qt event loop.
This resolves the issue where serial scanning settings had to be re-enabled
every time discovery was run.

Changes:
- Create server/config/persistence.py for runtime settings persistence
  - save_discovery_settings(): Saves settings to config/runtime_settings.json
  - load_discovery_settings(): Loads persisted settings from JSON file
  - apply_persisted_settings(): Applies persisted settings to Settings object
- Update discovery API endpoint to persist settings changes
  - Calls save_discovery_settings() after updating settings
  - Settings now survive server restarts
- Load persisted settings on server startup in main.py lifespan handler
  - Loads after config validation but before equipment initialization
  - Overrides .env defaults with runtime_settings.json values

Discovery settings (scan_serial, scan_gpib, etc.) now persist across server
restarts. Users no longer need to re-enable serial scanning each time.
Changed from ./config to ./data directory to avoid permission issues
and ensure the settings file is stored in a writable location that
already exists (data directory is created on server startup).
Changed from 'server.config.persistence' to 'config.persistence' to match
the server's module structure. Added separate ImportError handling to better
diagnose import issues.
Wait 500ms after successful device connection before refreshing the
equipment list to ensure the server has fully registered the equipment.
Reverted connect_dialog.py back to yesterday's synchronous approach.
Connection is fast (~1s) so it doesn't need async. Only discovery is
slow (10-30s) and needs to be async to prevent UI freezing.

This aligns with yesterday's working code while keeping the async
discovery improvement for the UI freezing issue.
aiohttp is stricter than requests library and doesn't allow boolean
values in query parameters. Convert True/False to 'true'/'false'
strings before passing to aiohttp.
Server API returns 'id' but client expected 'equipment_id', causing
connected equipment to not appear in the list.

Fixed field mappings:
- 'id' → 'equipment_id'
- 'resource_string' → 'resource_name'
- Use 'nickname' or 'model' for 'name'
- Default 'connection_status' to 'connected' for listed equipment
- Map 'serial_number' to 'idn' if needed

This fixes the issue where successful connections didn't show equipment
in the main window.
Added missing endpoint to retrieve current readings from connected equipment.
This endpoint:
- Calls equipment's get_readings() method
- Converts response to JSON dict
- Returns 404 if equipment not found
- Returns 501 if equipment doesn't support readings

Fixes the 'refresh readings' button that was getting 404 errors.
…ontext

The issue was calling connect_dialog.exec() (a blocking modal dialog) from
within an async coroutine, which froze the Qt event loop.

Solution:
- Use QTimer.singleShot(0, ...) to schedule dialogs on main Qt thread
- Move connect dialog logic to separate _show_connect_dialog() method
- This ensures dialogs run on main thread, not in async context
- Removed the blocking progress message before async discovery

This fixes UI freezing during discovery and connection phases.
Added debug logging to show:
- Requested equipment_id
- All available equipment IDs in the manager
- Enhanced error message with available IDs

This will help diagnose why equipment shows as connected in UI
but returns 404 when requesting readings.
- Added Equipment Discovery & Connection section to README
- Documented async discovery implementation in ROADMAP
- Highlighted UI responsiveness improvements and bug fixes
- Wrapped aiohttp import in try/except block
- Added runtime checks in async methods with informative error messages
- Prevents ImportError during test collection when aiohttp not installed
- Async methods will raise clear ImportError if aiohttp missing at runtime
@X9X0 X9X0 merged commit 8c61d76 into main Nov 28, 2025
25 checks passed
@X9X0 X9X0 deleted the claude/work-on-equip-01QQBuPfzhcFYPGSrUY5w9xW branch November 28, 2025 03:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants