Skip to content

Commit 8c8335d

Browse files
Physical e-stop support for MacOS (#3800)
* Find estop device path on macos using usb id * Add list of possible vid and pid pairs * [pre-commit.ci lite] apply automatic fixes --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
1 parent c189c27 commit 8c8335d

9 files changed

Lines changed: 76 additions & 19 deletions

File tree

src/shared/constants.h

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -220,11 +220,22 @@ static const unsigned int NUM_TIMES_SEND_STOP = 10;
220220
// disconnected
221221
static const double DISCONNECT_DURATION_MS = 1 * MILLISECONDS_PER_SECOND;
222222

223-
// product and vendor id for Arduino Uno Rev3 (retrieved from
224-
// http://www.linux-usb.org/usb.ids )
225-
#define ARDUINO_ID_LENGTH 5
226-
static const char ARDUINO_VENDOR_ID[ARDUINO_ID_LENGTH] = "2341";
227-
static const char ARDUINO_PRODUCT_ID[ARDUINO_ID_LENGTH] = "0043";
223+
// Vendor and product id pairs for the USB-to-serial adapters used by the
224+
// physical estop. Multiple pairs are supported since estop units may use
225+
// different adapters. IDs retrieved from http://www.linux-usb.org/usb.ids
226+
struct EstopUsbId
227+
{
228+
const char* vendor_id;
229+
const char* product_id;
230+
};
231+
232+
constexpr EstopUsbId ESTOP_USB_DEVICE_IDS[] = {
233+
{"2341", "0043"}, // Arduino Uno Rev3
234+
{"1a86", "7523"}, // CH340-based Arduino clones
235+
};
236+
237+
constexpr int NUM_ESTOP_USB_DEVICE_IDS =
238+
sizeof(ESTOP_USB_DEVICE_IDS) / sizeof(ESTOP_USB_DEVICE_IDS[0]);
228239

229240
// Number of times thunderloop should tick per second
230241
static const unsigned THUNDERLOOP_HZ = 300u;

src/software/estop/arduino_util.cpp

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,14 @@ std::optional<std::string> ArduinoUtil::getArduinoPort()
2121
std::optional<HwInfo> hwInfo = getInfo(device);
2222
if (hwInfo.has_value())
2323
{
24-
if (hwInfo.value().vendor == ARDUINO_VENDOR_ID &&
25-
hwInfo.value().product == ARDUINO_PRODUCT_ID)
24+
for (const auto& id : ESTOP_USB_DEVICE_IDS)
2625
{
27-
std::string device_path = (boost::format("/dev/%1%") % device).str();
28-
return device_path;
26+
if (hwInfo.value().vendor == id.vendor_id &&
27+
hwInfo.value().product == id.product_id)
28+
{
29+
std::string device_path = (boost::format("/dev/%1%") % device).str();
30+
return device_path;
31+
}
2932
}
3033
}
3134
}

src/software/py_constants.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include <pybind11/pybind11.h>
2+
#include <pybind11/stl.h>
23

34
#include "shared/constants.h"
45
#include "software/constants.h"
@@ -169,4 +170,13 @@ PYBIND11_MODULE(py_constants, m)
169170
m.attr("AUTO_CHIP_DISTANCE_DEFAULT_M") = AUTO_CHIP_DISTANCE_DEFAULT_M;
170171
m.attr("AUTO_KICK_SPEED_DEFAULT_M_PER_S") = AUTO_KICK_SPEED_DEFAULT_M_PER_S;
171172
m.attr("WHEEL_ROTATION_MAX_SPEED_M_PER_S") = WHEEL_ROTATION_MAX_SPEED_M_PER_S;
173+
174+
// Estop USB-to-serial adapter identifiers, as a list of
175+
// (vendor_id, product_id) pairs
176+
std::vector<std::pair<std::string, std::string>> estop_usb_device_ids;
177+
for (const auto& id : ESTOP_USB_DEVICE_IDS)
178+
{
179+
estop_usb_device_ids.emplace_back(id.vendor_id, id.product_id);
180+
}
181+
m.attr("ESTOP_USB_DEVICE_IDS") = estop_usb_device_ids;
172182
}

src/software/thunderscope/BUILD

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,9 @@ py_library(
190190
py_library(
191191
name = "estop_helpers",
192192
srcs = ["estop_helpers.py"],
193+
data = ["//software:py_constants.so"],
193194
deps = [
194195
"//software/thunderscope:constants",
196+
requirement("pyserial"),
195197
],
196198
)

src/software/thunderscope/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ class EstopMode(IntEnum):
107107
LogLevel.CONTRACT: "CONTRACT",
108108
}
109109

110-
# Paths to check for estop when running diagnostics
110+
# Paths to check for estop when running diagnostics, used as a fallback for Linux.
111111
ESTOP_PATH_1 = "/dev/ttyACM0"
112112
ESTOP_PATH_2 = "/dev/ttyUSB0"
113113

src/software/thunderscope/estop_helpers.py

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,38 @@
11
import os
2+
from serial.tools import list_ports
3+
4+
from software.py_constants import ESTOP_USB_DEVICE_IDS
25
from software.thunderscope.constants import EstopMode, ESTOP_PATH_1, ESTOP_PATH_2
36

47

8+
def get_estop_path() -> str | None:
9+
"""Find the serial port that the physical estop is connected to.
10+
11+
Falls back to the well-known Linux device paths if the estop cannot be
12+
matched by its USB IDs.
13+
14+
:return: the estop's serial port path, or None if no estop was found
15+
"""
16+
estop_usb_ids = {
17+
(vendor.lower(), product.lower()) for vendor, product in ESTOP_USB_DEVICE_IDS
18+
}
19+
for port in list_ports.comports():
20+
if port.vid is None or port.pid is None:
21+
continue
22+
if (f"{port.vid:04x}".lower(), f"{port.pid:04x}".lower()) in estop_usb_ids:
23+
return port.device
24+
25+
# Fall back to linux device paths
26+
for path in (ESTOP_PATH_1, ESTOP_PATH_2):
27+
if os.path.exists(path):
28+
return path
29+
30+
return None
31+
32+
533
def get_estop_config(
634
keyboard_estop: bool, disable_communication: bool
7-
) -> tuple[EstopMode, os.PathLike]:
35+
) -> tuple[EstopMode, str | None]:
836
"""Based on the estop mode argument provided, gets the corresponding
937
estop mode and estop path (defined for physical estop mode only)
1038
Defaults to Physical estop if the given args are both False
@@ -21,15 +49,9 @@ def get_estop_config(
2149
if disable_communication:
2250
mode = EstopMode.DISABLE_ESTOP
2351

24-
# use different estop based on what is plugged in for physical estop mode
52+
# locate the physical estop's serial port when in physical estop mode
2553
if mode == EstopMode.PHYSICAL_ESTOP:
26-
path = (
27-
ESTOP_PATH_1
28-
if os.path.exists(ESTOP_PATH_1)
29-
else ESTOP_PATH_2
30-
if os.path.exists(ESTOP_PATH_2)
31-
else None
32-
)
54+
path = get_estop_path()
3355
if not path:
3456
raise Exception(
3557
"Estop is not plugged into a valid port, plug one in or use a different estop mode"

src/software/thunderscope/requirements.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ netifaces==0.11.0
33
evdev==1.7.0; sys_platform == "linux"
44
numpy==1.26.4
55
protobuf==6.31.1
6+
pyserial==3.5
67
pyqtgraph==0.13.7
78
pyqtdarktheme-fork==2.3.2
89
PyQt6-Qt6==6.8.1

src/software/thunderscope/requirements_lock.darwin.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ pyqtgraph==0.13.7 \
120120
--hash=sha256:64f84f1935c6996d0e09b1ee66fe478a7771e3ca6f3aaa05f00f6e068321d9e3 \
121121
--hash=sha256:7754edbefb6c367fa0dfb176e2d0610da3ada20aa7a5318516c74af5fb72bf7a
122122
# via -r software/thunderscope/requirements.in
123+
pyserial==3.5 \
124+
--hash=sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb \
125+
--hash=sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0
126+
# via -r software/thunderscope/requirements.in
123127
qtawesome==1.4.0 \
124128
--hash=sha256:783e414d1317f3e978bf67ea8e8a1b1498bad9dbd305dec814027e3b50521be6 \
125129
--hash=sha256:a4d689fa071c595aa6184171ce1f0f847677cb8d2db45382c43129f1d72a3d93

src/software/thunderscope/requirements_lock.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ pyqtgraph==0.13.7 \
123123
--hash=sha256:64f84f1935c6996d0e09b1ee66fe478a7771e3ca6f3aaa05f00f6e068321d9e3 \
124124
--hash=sha256:7754edbefb6c367fa0dfb176e2d0610da3ada20aa7a5318516c74af5fb72bf7a
125125
# via -r software/thunderscope/requirements.in
126+
pyserial==3.5 \
127+
--hash=sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb \
128+
--hash=sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0
129+
# via -r software/thunderscope/requirements.in
126130
qtawesome==1.4.0 \
127131
--hash=sha256:783e414d1317f3e978bf67ea8e8a1b1498bad9dbd305dec814027e3b50521be6 \
128132
--hash=sha256:a4d689fa071c595aa6184171ce1f0f847677cb8d2db45382c43129f1d72a3d93

0 commit comments

Comments
 (0)