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.
Files changed (50) hide show
  1. plexus/__init__.py +31 -0
  2. plexus/__main__.py +4 -0
  3. plexus/adapters/__init__.py +122 -0
  4. plexus/adapters/base.py +409 -0
  5. plexus/adapters/ble.py +257 -0
  6. plexus/adapters/can.py +439 -0
  7. plexus/adapters/can_detect.py +174 -0
  8. plexus/adapters/mavlink.py +642 -0
  9. plexus/adapters/mavlink_detect.py +192 -0
  10. plexus/adapters/modbus.py +622 -0
  11. plexus/adapters/mqtt.py +350 -0
  12. plexus/adapters/opcua.py +607 -0
  13. plexus/adapters/registry.py +206 -0
  14. plexus/adapters/serial_adapter.py +547 -0
  15. plexus/buffer.py +257 -0
  16. plexus/cameras/__init__.py +57 -0
  17. plexus/cameras/auto.py +239 -0
  18. plexus/cameras/base.py +189 -0
  19. plexus/cameras/picamera.py +171 -0
  20. plexus/cameras/usb.py +143 -0
  21. plexus/cli.py +783 -0
  22. plexus/client.py +465 -0
  23. plexus/config.py +169 -0
  24. plexus/connector.py +666 -0
  25. plexus/deps.py +246 -0
  26. plexus/detect.py +1238 -0
  27. plexus/importers/__init__.py +25 -0
  28. plexus/importers/rosbag.py +778 -0
  29. plexus/sensors/__init__.py +118 -0
  30. plexus/sensors/ads1115.py +164 -0
  31. plexus/sensors/adxl345.py +179 -0
  32. plexus/sensors/auto.py +290 -0
  33. plexus/sensors/base.py +412 -0
  34. plexus/sensors/bh1750.py +102 -0
  35. plexus/sensors/bme280.py +241 -0
  36. plexus/sensors/gps.py +317 -0
  37. plexus/sensors/ina219.py +149 -0
  38. plexus/sensors/magnetometer.py +239 -0
  39. plexus/sensors/mpu6050.py +162 -0
  40. plexus/sensors/sht3x.py +139 -0
  41. plexus/sensors/spi_scan.py +164 -0
  42. plexus/sensors/system.py +261 -0
  43. plexus/sensors/vl53l0x.py +109 -0
  44. plexus/streaming.py +743 -0
  45. plexus/tui.py +642 -0
  46. plexus_python-0.1.0.dist-info/METADATA +470 -0
  47. plexus_python-0.1.0.dist-info/RECORD +50 -0
  48. plexus_python-0.1.0.dist-info/WHEEL +4 -0
  49. plexus_python-0.1.0.dist-info/entry_points.txt +2 -0
  50. 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