plexus-python 0.1.0__py3-none-any.whl
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.
- plexus/__init__.py +31 -0
- plexus/__main__.py +4 -0
- plexus/adapters/__init__.py +122 -0
- plexus/adapters/base.py +409 -0
- plexus/adapters/ble.py +257 -0
- plexus/adapters/can.py +439 -0
- plexus/adapters/can_detect.py +174 -0
- plexus/adapters/mavlink.py +642 -0
- plexus/adapters/mavlink_detect.py +192 -0
- plexus/adapters/modbus.py +622 -0
- plexus/adapters/mqtt.py +350 -0
- plexus/adapters/opcua.py +607 -0
- plexus/adapters/registry.py +206 -0
- plexus/adapters/serial_adapter.py +547 -0
- plexus/buffer.py +257 -0
- plexus/cameras/__init__.py +57 -0
- plexus/cameras/auto.py +239 -0
- plexus/cameras/base.py +189 -0
- plexus/cameras/picamera.py +171 -0
- plexus/cameras/usb.py +143 -0
- plexus/cli.py +783 -0
- plexus/client.py +465 -0
- plexus/config.py +169 -0
- plexus/connector.py +666 -0
- plexus/deps.py +246 -0
- plexus/detect.py +1238 -0
- plexus/importers/__init__.py +25 -0
- plexus/importers/rosbag.py +778 -0
- plexus/sensors/__init__.py +118 -0
- plexus/sensors/ads1115.py +164 -0
- plexus/sensors/adxl345.py +179 -0
- plexus/sensors/auto.py +290 -0
- plexus/sensors/base.py +412 -0
- plexus/sensors/bh1750.py +102 -0
- plexus/sensors/bme280.py +241 -0
- plexus/sensors/gps.py +317 -0
- plexus/sensors/ina219.py +149 -0
- plexus/sensors/magnetometer.py +239 -0
- plexus/sensors/mpu6050.py +162 -0
- plexus/sensors/sht3x.py +139 -0
- plexus/sensors/spi_scan.py +164 -0
- plexus/sensors/system.py +261 -0
- plexus/sensors/vl53l0x.py +109 -0
- plexus/streaming.py +743 -0
- plexus/tui.py +642 -0
- plexus_python-0.1.0.dist-info/METADATA +470 -0
- plexus_python-0.1.0.dist-info/RECORD +50 -0
- plexus_python-0.1.0.dist-info/WHEEL +4 -0
- plexus_python-0.1.0.dist-info/entry_points.txt +2 -0
- plexus_python-0.1.0.dist-info/licenses/LICENSE +190 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAN bus interface auto-detection.
|
|
3
|
+
|
|
4
|
+
Scans for SocketCAN interfaces on Linux and common USB-serial CAN adapters.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from plexus.adapters.can_detect import scan_can
|
|
8
|
+
|
|
9
|
+
interfaces = scan_can()
|
|
10
|
+
for iface in interfaces:
|
|
11
|
+
print(f"{iface.interface} ({'up' if iface.is_up else 'down'})")
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import glob
|
|
15
|
+
import logging
|
|
16
|
+
import os
|
|
17
|
+
import subprocess
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import List, Optional
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# ARPHRD_CAN - Linux ARP hardware type for CAN interfaces
|
|
24
|
+
ARPHRD_CAN = 280
|
|
25
|
+
|
|
26
|
+
# Default bitrate suggestion when interface is down
|
|
27
|
+
DEFAULT_BITRATE = 500000
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class DetectedCAN:
|
|
32
|
+
"""Information about a detected CAN interface."""
|
|
33
|
+
interface: str
|
|
34
|
+
channel: str
|
|
35
|
+
is_up: bool
|
|
36
|
+
bitrate: Optional[int]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _read_sysfs(path: str) -> Optional[str]:
|
|
40
|
+
"""Read a sysfs file, returning None on failure."""
|
|
41
|
+
try:
|
|
42
|
+
with open(path, "r") as f:
|
|
43
|
+
return f.read().strip()
|
|
44
|
+
except (OSError, IOError):
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _scan_socketcan() -> List[DetectedCAN]:
|
|
49
|
+
"""Scan for SocketCAN interfaces via /sys/class/net/."""
|
|
50
|
+
detected = []
|
|
51
|
+
net_dir = "/sys/class/net"
|
|
52
|
+
|
|
53
|
+
if not os.path.isdir(net_dir):
|
|
54
|
+
return detected
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
interfaces = os.listdir(net_dir)
|
|
58
|
+
except OSError:
|
|
59
|
+
return detected
|
|
60
|
+
|
|
61
|
+
for iface in sorted(interfaces):
|
|
62
|
+
# Check if this is a CAN interface (type == 280)
|
|
63
|
+
iface_type = _read_sysfs(os.path.join(net_dir, iface, "type"))
|
|
64
|
+
if iface_type != str(ARPHRD_CAN):
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
# Check operational state
|
|
68
|
+
operstate = _read_sysfs(os.path.join(net_dir, iface, "operstate"))
|
|
69
|
+
is_up = operstate in ("up", "unknown")
|
|
70
|
+
|
|
71
|
+
# Try to read bitrate from /sys/class/net/{iface}/can_bittiming/bitrate
|
|
72
|
+
bitrate = None
|
|
73
|
+
if is_up:
|
|
74
|
+
bitrate_str = _read_sysfs(
|
|
75
|
+
os.path.join(net_dir, iface, "can_bittiming", "bitrate")
|
|
76
|
+
)
|
|
77
|
+
if bitrate_str and bitrate_str.isdigit():
|
|
78
|
+
bitrate = int(bitrate_str)
|
|
79
|
+
|
|
80
|
+
detected.append(DetectedCAN(
|
|
81
|
+
interface="socketcan",
|
|
82
|
+
channel=iface,
|
|
83
|
+
is_up=is_up,
|
|
84
|
+
bitrate=bitrate,
|
|
85
|
+
))
|
|
86
|
+
|
|
87
|
+
return detected
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _scan_usb_serial() -> List[DetectedCAN]:
|
|
91
|
+
"""Scan for USB-serial CAN adapters (slcan devices)."""
|
|
92
|
+
detected = []
|
|
93
|
+
|
|
94
|
+
# Common paths for USB-serial devices that may be slcan adapters
|
|
95
|
+
patterns = ["/dev/ttyUSB*", "/dev/ttyACM*"]
|
|
96
|
+
|
|
97
|
+
for pattern in patterns:
|
|
98
|
+
for device_path in sorted(glob.glob(pattern)):
|
|
99
|
+
detected.append(DetectedCAN(
|
|
100
|
+
interface="slcan",
|
|
101
|
+
channel=device_path,
|
|
102
|
+
is_up=False, # slcan needs manual setup
|
|
103
|
+
bitrate=None,
|
|
104
|
+
))
|
|
105
|
+
|
|
106
|
+
return detected
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def setup_can(iface: DetectedCAN, bitrate: int = DEFAULT_BITRATE) -> bool:
|
|
110
|
+
"""
|
|
111
|
+
Bring up a SocketCAN interface.
|
|
112
|
+
|
|
113
|
+
Runs: sudo ip link set {channel} up type can bitrate {bitrate}
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
iface: Detected CAN interface to configure
|
|
117
|
+
bitrate: CAN bitrate in bps (default: 500000)
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
True if the interface was brought up successfully
|
|
121
|
+
"""
|
|
122
|
+
if iface.interface != "socketcan":
|
|
123
|
+
logger.warning(f"Auto-setup not supported for {iface.interface} interfaces")
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
result = subprocess.run(
|
|
128
|
+
["sudo", "ip", "link", "set", iface.channel, "up",
|
|
129
|
+
"type", "can", "bitrate", str(bitrate)],
|
|
130
|
+
capture_output=True,
|
|
131
|
+
text=True,
|
|
132
|
+
timeout=10,
|
|
133
|
+
)
|
|
134
|
+
if result.returncode == 0:
|
|
135
|
+
logger.info(f"Brought up {iface.channel} at {bitrate} bps")
|
|
136
|
+
return True
|
|
137
|
+
else:
|
|
138
|
+
logger.error(f"Failed to bring up {iface.channel}: {result.stderr.strip()}")
|
|
139
|
+
return False
|
|
140
|
+
except subprocess.TimeoutExpired:
|
|
141
|
+
logger.error(f"Timed out configuring {iface.channel}")
|
|
142
|
+
return False
|
|
143
|
+
except Exception as e:
|
|
144
|
+
logger.error(f"Error configuring {iface.channel}: {e}")
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def scan_can() -> List[DetectedCAN]:
|
|
149
|
+
"""
|
|
150
|
+
Scan for CAN interfaces on the system.
|
|
151
|
+
|
|
152
|
+
Checks:
|
|
153
|
+
1. SocketCAN interfaces via /sys/class/net/ (e.g. can0, vcan0)
|
|
154
|
+
2. USB-serial devices that may be slcan adapters (e.g. /dev/ttyUSB0)
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
List of detected CAN interfaces
|
|
158
|
+
"""
|
|
159
|
+
detected = []
|
|
160
|
+
|
|
161
|
+
# Primary: SocketCAN interfaces
|
|
162
|
+
try:
|
|
163
|
+
detected.extend(_scan_socketcan())
|
|
164
|
+
except Exception as e:
|
|
165
|
+
logger.debug(f"Error scanning SocketCAN interfaces: {e}")
|
|
166
|
+
|
|
167
|
+
# Secondary: USB-serial CAN adapters (only if no socketcan found)
|
|
168
|
+
if not detected:
|
|
169
|
+
try:
|
|
170
|
+
detected.extend(_scan_usb_serial())
|
|
171
|
+
except Exception as e:
|
|
172
|
+
logger.debug(f"Error scanning USB-serial CAN adapters: {e}")
|
|
173
|
+
|
|
174
|
+
return detected
|