diff --git a/local-tool/server/scripts/kneron_bridge.py b/local-tool/server/scripts/kneron_bridge.py index 4f4bd95..32357ec 100644 --- a/local-tool/server/scripts/kneron_bridge.py +++ b/local-tool/server/scripts/kneron_bridge.py @@ -38,6 +38,24 @@ except ImportError: # ── Global state ────────────────────────────────────────────────────── _device_group = None + + +def _clear_device_group(): + """Safely disconnect and clear the global _device_group. + + KneronPLUS SDK's DeviceGroup.__del__ calls kp_disconnect_devices on the + native handle, but if the handle is already invalid (failed connect / stale + state) it causes 'OSError: access violation'. By explicitly disconnecting + before setting None, __del__ becomes a no-op on an already-disconnected + handle. All errors are silenced — this is best-effort cleanup. + """ + global _device_group + if _device_group is not None: + try: + kp.core.disconnect_devices(_device_group) + except Exception: + pass + _device_group = None _model_id = None _model_nef = None _model_input_size = 224 # updated on model load @@ -732,7 +750,7 @@ def handle_connect(params): _log(f"KL720: Reconnected after firmware load, pid=0x{target_dev.product_id:04X}, fw={fw_str}") else: _log("WARNING: KL720 firmware files not found. Cannot operate with KDP legacy device.") - _device_group = None + _clear_device_group() return {"error": "KL720 has legacy KDP firmware but KDP2 firmware files not found. " "Run update_kl720_firmware.py to flash KDP2 permanently."} @@ -756,9 +774,8 @@ def handle_connect(params): last_err = None for attempt in range(max_retries): try: - # Clear any stale device group from previous failed attempt - # to prevent DeviceGroup.__del__ access violation during GC. - _device_group = None + # Clear any stale device group from previous failed attempt. + _clear_device_group() if use_without_check: _log(f"{_device_chip}: connect_devices_without_check(usb_port_id={target_dev.usb_port_id}, connectable={target_dev.is_connectable}) attempt {attempt+1}/{max_retries}...") @@ -774,7 +791,7 @@ def handle_connect(params): last_err = None break except Exception as conn_err: - _device_group = None # prevent __del__ crash on stale handle + _clear_device_group() last_err = conn_err _log(f"connect attempt {attempt+1} failed: {conn_err}") if attempt < max_retries - 1: @@ -822,7 +839,7 @@ def handle_connect(params): time.sleep(5) # Reconnect after firmware load (with retry) - _device_group = None + _clear_device_group() for retry in range(3): try: descs = kp.core.scan_devices() @@ -861,7 +878,7 @@ def handle_connect(params): } except Exception as e: - _device_group = None + _clear_device_group() return {"error": str(e)} @@ -870,7 +887,7 @@ def handle_disconnect(params): global _device_group, _model_id, _model_nef, _firmware_loaded global _model_type, _model_input_size, _device_chip - _device_group = None + _clear_device_group() _model_id = None _model_nef = None _model_type = "tiny_yolov3" @@ -906,7 +923,7 @@ def handle_reset(params): # Even if it throws, the device usually does reset. # Clear all state — the device is gone until it re-enumerates. - _device_group = None + _clear_device_group() _model_id = None _model_nef = None _model_type = "tiny_yolov3"