Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
48 changes: 48 additions & 0 deletions bin/dronecan_gui_tool_launcher
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env python3
#
# Copyright (C) 2016 UAVCAN Development Team <uavcan.org>
#
# This software is distributed under the terms of the MIT License.
#
# Author: Pavel Kirienko <pavel.kirienko@zubax.com>
#

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()
Binary file added bin/usb2can_canal_v2.0.0/x64/Release/usb2can.dll
Binary file not shown.
Binary file added bin/usb2can_canal_v2.0.0/x64/Release/usb2can.lib
Binary file not shown.
Binary file added bin/usb2can_canal_v2.0.0/x86/Release/usb2can.dll
Binary file not shown.
Binary file added bin/usb2can_canal_v2.0.0/x86/Release/usb2can.lib
Binary file not shown.
63 changes: 62 additions & 1 deletion dronecan_gui_tool/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
31 changes: 27 additions & 4 deletions dronecan_gui_tool/setup_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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)

Expand Down
26 changes: 25 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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'))
Expand All @@ -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': [
Expand All @@ -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': {
Expand All @@ -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'),
Expand Down
51 changes: 51 additions & 0 deletions setup_usb2can.py
Original file line number Diff line number Diff line change
@@ -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)
44 changes: 44 additions & 0 deletions test_usb2can_detection.py
Original file line number Diff line number Diff line change
@@ -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 ===")
Loading