diff --git a/README.md b/README.md index ea1e82e..0fea48a 100755 --- a/README.md +++ b/README.md @@ -71,6 +71,28 @@ In order to install this application, Please see the file winbuild.bat in this directory, or use the MSI file build by github actions on any commit +### USB2CAN (8devices Korlan) Support on Windows + +The DroneCAN GUI Tool now includes support for 8devices USB2CAN (Korlan) adapters on Windows. + +**Setup:** +1. Connect your 8devices USB2CAN adapter +2. Run the setup script to install the required DLL: + ``` + python setup_usb2can.py + ``` +3. The USB2CAN adapter will now appear in the interface selection dropdown as "8devices USB2CAN (channel_id)" + +**Features:** +- Automatic detection of USB2CAN adapters +- Native integration with DroneCAN protocol +- Full support for all GUI tool features + +**Requirements:** +- 8devices USB2CAN adapter (Korlan) +- Windows 10/11 (x64 or x86) +- DLL files included in `bin/usb2can_canal_v2.0.0/` + ## Installing on macOS OSX support is a bit lacking in the way that installation doesn't create an entry in the applications menu, diff --git a/bin/dronecan_gui_tool_launcher b/bin/dronecan_gui_tool_launcher new file mode 100644 index 0000000..f6b73d6 --- /dev/null +++ b/bin/dronecan_gui_tool_launcher @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2016 UAVCAN Development Team +# +# This software is distributed under the terms of the MIT License. +# +# Author: Pavel Kirienko +# + +import os +import sys +import multiprocessing + +# +# When frozen, stdout/stderr are None, causing nasty exceptions. This workaround silences them. +# +class SupermassiveBlackHole: + def write(self, *_): + pass + + def read(self, *_): + pass + + def flush(self): + pass + + def close(self): + pass + +try: + sys.stdout.flush() + sys.stderr.flush() +except AttributeError: + sys.__stdout__ = sys.stdout = SupermassiveBlackHole() + sys.__stderr__ = sys.stderr = SupermassiveBlackHole() + sys.__stdin__ = sys.stdin = SupermassiveBlackHole() + +# +# Calling main directly. +# The 'if' wrapper is absolutely needed because we're spawning new processes with 'multiprocessing'; refer +# to the Python docs for more info. +# +if __name__ == '__main__': + multiprocessing.freeze_support() + + # Import and run main - avoid relative imports by running as absolute import + import dronecan_gui_tool.main + dronecan_gui_tool.main.main() \ No newline at end of file diff --git a/bin/usb2can_canal_v2.0.0/x64/Release/usb2can.dll b/bin/usb2can_canal_v2.0.0/x64/Release/usb2can.dll new file mode 100644 index 0000000..6d756b3 Binary files /dev/null and b/bin/usb2can_canal_v2.0.0/x64/Release/usb2can.dll differ diff --git a/bin/usb2can_canal_v2.0.0/x64/Release/usb2can.lib b/bin/usb2can_canal_v2.0.0/x64/Release/usb2can.lib new file mode 100644 index 0000000..ffeb392 Binary files /dev/null and b/bin/usb2can_canal_v2.0.0/x64/Release/usb2can.lib differ diff --git a/bin/usb2can_canal_v2.0.0/x86/Release/usb2can.dll b/bin/usb2can_canal_v2.0.0/x86/Release/usb2can.dll new file mode 100644 index 0000000..d58f036 Binary files /dev/null and b/bin/usb2can_canal_v2.0.0/x86/Release/usb2can.dll differ diff --git a/bin/usb2can_canal_v2.0.0/x86/Release/usb2can.lib b/bin/usb2can_canal_v2.0.0/x86/Release/usb2can.lib new file mode 100644 index 0000000..7d8c1da Binary files /dev/null and b/bin/usb2can_canal_v2.0.0/x86/Release/usb2can.lib differ diff --git a/dronecan_gui_tool/main.py b/dronecan_gui_tool/main.py index 611ffc2..57fdf8e 100644 --- a/dronecan_gui_tool/main.py +++ b/dronecan_gui_tool/main.py @@ -70,6 +70,11 @@ import dronecan +# Ensure can.__version__ is defined (cx_Freeze builds lack importlib.metadata) +import can +if not hasattr(can, '__version__'): + can.__version__ = '4.0.0' + from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QSplitter, QAction from PyQt5.QtGui import QKeySequence, QDesktopServices from PyQt5.QtCore import QTimer, Qt, QUrl @@ -629,11 +634,67 @@ def main(): node_info.software_version.major = __version__[0] node_info.software_version.minor = __version__[1] - node = dronecan.make_node(iface, + # Handle USB2CAN interface specification (format: "usb2can:channel") + actual_iface = iface + if iface and ':' in iface: + bustype, channel = iface.split(':', 1) + if bustype == 'usb2can': + # For USB2CAN interfaces, specify the bustype and DLL path + import platform + import os + + actual_iface = channel + iface_kwargs['bustype'] = 'usb2can' + + # Determine the correct DLL path based on architecture + # Handle both development environment and frozen executable + if getattr(sys, 'frozen', False): + # Running as cx_Freeze executable + gui_tool_dir = os.path.dirname(sys.executable) + else: + # Running in development environment + current_dir = os.path.dirname(os.path.abspath(__file__)) + gui_tool_dir = os.path.dirname(current_dir) + + if platform.machine().lower() in ['amd64', 'x86_64', 'x64']: + dll_path = os.path.join(gui_tool_dir, 'bin', 'usb2can_canal_v2.0.0', 'x64', 'Release', 'usb2can.dll') + else: + dll_path = os.path.join(gui_tool_dir, 'bin', 'usb2can_canal_v2.0.0', 'x86', 'Release', 'usb2can.dll') + + iface_kwargs['dll'] = dll_path + + # Add the DLL directory to the system PATH and DLL search directories + # so that python-can's usb2can backend can find usb2can.dll by name + dll_dir = os.path.dirname(dll_path) + if dll_dir not in os.environ.get('PATH', ''): + os.environ['PATH'] = dll_dir + ';' + os.environ.get('PATH', '') + if hasattr(os, 'add_dll_directory'): + os.add_dll_directory(dll_dir) + + logger.info('Using USB2CAN interface: channel=%s, dll=%s (frozen=%s)', channel, dll_path, getattr(sys, 'frozen', False)) + elif bustype == 'pcan': + # PCAN interfaces should also specify bustype + actual_iface = channel + iface_kwargs['bustype'] = 'pcan' + logger.info('Using PCAN interface: channel=%s', channel) + + node = dronecan.make_node(actual_iface, node_info=node_info, mode=dronecan.uavcan.protocol.NodeStatus().MODE_OPERATIONAL, **iface_kwargs) + # Monkey-patch flush_tx_buffer for bus drivers that don't implement it + # (e.g. usb2can). The dronecan PythonCAN writer thread calls flush_tx_buffer() + # after every send, but not all python-can backends provide it. + try: + can_bus = node._can_driver._bus + can_bus.flush_tx_buffer() + except NotImplementedError: + can_bus.flush_tx_buffer = lambda: None + logger.info('Patched flush_tx_buffer for %s backend', type(can_bus).__name__) + except AttributeError: + pass + if iface_kwargs["filtered"]: setup_filtering(node) diff --git a/dronecan_gui_tool/setup_window.py b/dronecan_gui_tool/setup_window.py index 227eedf..f9708fb 100644 --- a/dronecan_gui_tool/setup_window.py +++ b/dronecan_gui_tool/setup_window.py @@ -96,18 +96,34 @@ def list_ifaces(): # Windows, Mac, whatever from PyQt5 import QtSerialPort - out = OrderedDict() + # Collect ports with priority handling for USB2CAN adapters + priority_ports = [] # USB2CAN adapters go first + regular_ports = [] + for port in QtSerialPort.QSerialPortInfo.availablePorts(): if sys.platform == 'darwin': if 'tty' in port.systemLocation(): if port.systemLocation() not in MACOS_SERIAL_PORTS_FILTER: - out[port.systemLocation()] = port.systemLocation() + regular_ports.append((port.systemLocation(), port.systemLocation())) else: sys_name = port.systemLocation() sys_alpha = re.sub(r'[^a-zA-Z0-9]', '', sys_name) description = port.description() - # show the COM port in parentheses to make it clearer which port it is - out["%s (%s)" % (description, sys_alpha)] = sys_name + + # Special handling for 8devices Korlan USB2CAN adapter + # The Korlan appears in Windows as "USB2CAN converter" + if "USB2CAN converter" in description: + display_name = "8devices Korlan USB2CAN (%s)" % sys_alpha + priority_ports.append((display_name, sys_name)) + else: + # show the COM port in parentheses to make it clearer which port it is + display_name = "%s (%s)" % (description, sys_alpha) + regular_ports.append((display_name, sys_name)) + + # Build output with priority ports first + out = OrderedDict() + for display_name, sys_name in priority_ports + regular_ports: + out[display_name] = sys_name mifaces = _mavcan_interfaces() mifaces += ["mcast:0", "mcast:1"] @@ -125,6 +141,13 @@ def list_ifaces(): for interface in detect_available_configs(): if interface['interface'] == "pcan": out[interface['channel']] = interface['channel'] + elif interface['interface'] == "usb2can": + # Add USB2CAN (8devices Korlan) interfaces with descriptive name + # Store as "usb2can:channel" to specify the bustype + display_name = "8devices USB2CAN (%s)" % interface['channel'] + interface_spec = "usb2can:%s" % interface['channel'] + out[display_name] = interface_spec + logger.info('Added USB2CAN interface: %s -> %s', display_name, interface_spec) except Exception as ex: logger.warning('Could not load can interfaces: %s', ex, exc_info=True) diff --git a/setup.py b/setup.py index dea9620..1079328 100755 --- a/setup.py +++ b/setup.py @@ -122,6 +122,7 @@ 'qtpy', 'qtconsole', 'easywebdav', + 'python-can', # Add python-can to unpacked eggs (package name is python-can, import name is can) ] unpacked_eggs_dir = os.path.join('build', 'hatched_eggs') sys.path.insert(0, unpacked_eggs_dir) @@ -144,6 +145,7 @@ import jupyter_client import traitlets import numpy + import can # Add python-can import # Oh, Windows, never change. missing_dlls = glob.glob(os.path.join(os.path.dirname(numpy.core.__file__), '*.dll')) @@ -157,6 +159,21 @@ 'zmq', 'pygments', 'jupyter_client', + # Add python-can and related packages + 'can', + 'can.interfaces', + 'can.interfaces.usb2can', + 'can.interfaces.usb2can.usb2canabstractionlayer', + 'can.interfaces.usb2can.usb2canInterface', + 'can.interfaces.usb2can.serial_selector', + 'can.interfaces.serial', + 'can.interfaces.pcan', + 'can.interfaces.vector', + 'can.interfaces.kvaser', + 'can.interfaces.ixxat', + 'can.interfaces.socketcan', + 'can.interfaces.socketcand', + 'can.interfaces.virtual', ], 'include_msvcr': True, 'include_files': [ @@ -172,6 +189,12 @@ os.path.join(unpacked_eggs_dir, os.path.dirname(jupyter_client.__file__)), os.path.join(unpacked_eggs_dir, os.path.dirname(traitlets.__file__)), os.path.join(unpacked_eggs_dir, os.path.dirname(numpy.__file__)), + os.path.join(unpacked_eggs_dir, os.path.dirname(can.__file__)), # Include python-can package files + # Explicitly include USB2CAN interface files + (os.path.join(unpacked_eggs_dir, os.path.dirname(can.__file__), 'interfaces', 'usb2can'), + 'can/interfaces/usb2can'), + # Include USB2CAN DLL files + ('bin/usb2can_canal_v2.0.0', 'bin/usb2can_canal_v2.0.0'), ] + missing_dlls, }, 'bdist_msi': { @@ -180,8 +203,9 @@ }, } args['executables'] = [ - cx_Freeze.Executable(os.path.join('bin', PACKAGE_NAME), + cx_Freeze.Executable(os.path.join('bin', PACKAGE_NAME + '_launcher'), base='Win32GUI', + target_name=PACKAGE_NAME + '.exe', icon='icons/logo.ico', shortcut_name=HUMAN_FRIENDLY_NAME, shortcut_dir='ProgramMenuFolder'), diff --git a/setup_usb2can.py b/setup_usb2can.py new file mode 100644 index 0000000..a44b26e --- /dev/null +++ b/setup_usb2can.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +""" +USB2CAN setup script for DroneCAN GUI Tool + +This script sets up the USB2CAN DLL for use with the DroneCAN GUI Tool. +It copies the appropriate DLL to a location where it can be found by python-can. +""" + +import os +import shutil +import platform +import sys + +def setup_usb2can_dll(): + """Copy the USB2CAN DLL to an accessible location""" + print("Setting up USB2CAN DLL for DroneCAN GUI Tool...") + + # Get the current directory (should be the gui_tool root) + current_dir = os.path.dirname(os.path.abspath(__file__)) + + # Determine the correct DLL based on architecture + if platform.machine().lower() in ['amd64', 'x86_64', 'x64']: + dll_source = os.path.join(current_dir, 'bin', 'usb2can_canal_v2.0.0', 'x64', 'Release', 'usb2can.dll') + arch = 'x64' + else: + dll_source = os.path.join(current_dir, 'bin', 'usb2can_canal_v2.0.0', 'x86', 'Release', 'usb2can.dll') + arch = 'x86' + + if not os.path.exists(dll_source): + print(f"Error: USB2CAN DLL not found at {dll_source}") + return False + + # Copy to the current directory (where the script is run from) + dll_dest = os.path.join(current_dir, 'usb2can.dll') + + try: + shutil.copy2(dll_source, dll_dest) + print(f"Successfully copied {arch} USB2CAN DLL to {dll_dest}") + return True + except Exception as e: + print(f"Error copying DLL: {e}") + return False + +if __name__ == "__main__": + success = setup_usb2can_dll() + if success: + print("\n✅ USB2CAN setup completed successfully!") + print("You can now use 8devices USB2CAN adapters with the DroneCAN GUI Tool.") + else: + print("\n❌ USB2CAN setup failed!") + sys.exit(1) \ No newline at end of file diff --git a/test_usb2can_detection.py b/test_usb2can_detection.py new file mode 100644 index 0000000..ed6e3c5 --- /dev/null +++ b/test_usb2can_detection.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +""" +Test script to specifically check USB2CAN interface detection +""" + +import sys +import logging + +# Setup logging to see warnings and errors +logging.basicConfig(level=logging.WARNING) +logger = logging.getLogger(__name__) + +print("=== Testing USB2CAN Interface Detection ===") +print(f"Python version: {sys.version}") + +try: + # Import the setup window module which handles interface detection + sys.path.insert(0, r'C:\Users\bluea\OneDrive\Documents\GitHub\gui_tool') + from dronecan_gui_tool.setup_window import list_ifaces + + print("\n=== Calling list_ifaces() ===") + interfaces = list_ifaces() + + print(f"Found {len(interfaces)} interfaces:") + usb2can_found = False + + for i, (display_name, interface_spec) in enumerate(interfaces.items()): + print(f" {i+1}. '{display_name}' -> '{interface_spec}'") + if 'usb2can' in display_name.lower() or 'usb2can:' in interface_spec: + print(f" *** USB2CAN INTERFACE FOUND! ***") + usb2can_found = True + + if not usb2can_found: + print("\n⚠ No USB2CAN interfaces were detected!") + print(" This means the 8devices USB2CAN adapter is not showing up.") + else: + print("\n✅ USB2CAN interface detection is working!") + +except Exception as e: + print(f"✗ Error during interface detection: {e}") + import traceback + traceback.print_exc() + +print("\n=== Test completed ===") \ No newline at end of file diff --git a/winbuild_venv.bat b/winbuild_venv.bat new file mode 100644 index 0000000..e4a054d --- /dev/null +++ b/winbuild_venv.bat @@ -0,0 +1,33 @@ +@echo on + +rem - this is a sample build script for building gui_tool MSI file under windows, it assumes the following: +rem - you already have a python.org python installed (at least 3.10) tested on 3.10.2 +rem - you have a git checkout of [the correct] gui_tool [release] here + +rem - how to use: +rem - step 1 - edit the script to change the PATH below to include your python 3.10 install directory +rem - step 2 - open a command prompt +rem - step 3 - run winbuild_venv.bat in the gui_tool directory + +rem NOTE: you need visual studio installed, with the C++ build tools + +rem Use the virtual environment Python instead of system Python for compatibility +SET VENV_PYTHON=C:\Users\bluea\OneDrive\Documents\GitHub\gui_tool\.venv\Scripts\python.exe + +%VENV_PYTHON% --version + +%VENV_PYTHON% -m pip install -U cx_Freeze +%VENV_PYTHON% -m pip install -U pymavlink +%VENV_PYTHON% -m pip install -U pywin32 +%VENV_PYTHON% -m pip install -U python-can +%VENV_PYTHON% -m pip install -U . + +rem show pip sizes for debug +%VENV_PYTHON% pip_sizes.py + +rem make the .msi +%VENV_PYTHON% setup.py install +%VENV_PYTHON% setup.py bdist_msi + +rem find the binary in 'dist' folder +dir dist \ No newline at end of file