From 4b3d6b5e43782cd8c55e3bdb832f8b3c40c6cd8d Mon Sep 17 00:00:00 2001 From: Lei Date: Sat, 1 Aug 2026 11:46:10 -0700 Subject: [PATCH] Fix: join the control thread before closing the CAN socket MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DMChainCanInterface.close() sets running = False and closes the CAN socket in the same breath, without waiting for the control loop to notice: def close(self) -> None: self.running = False self.motor_interface.close() A thread already inside _set_commands then sends on a socket that has just been closed, and dies on every clean shutdown: Robot closed with all torques set to zero. ERROR:root:1th motor at DMChainCanInterface(channel=can0) failed with info [2, 'DM4340'] DM Error in control loop: file descriptor cannot be a negative integer (-1) Exception in thread Thread-1 (_set_torques_and_update_state): File "i2rt/motor_drivers/can_interface.py", line 54, in _send_message_get_response self.bus.send(message) ValueError: file descriptor cannot be a negative integer (-1) It could not be joined, because start_thread() assigned the handle to a local and dropped it. Keep it, and join it in close() with a timeout so close() stays bounded if the loop is wedged — in that case behaviour is unchanged. MotorChainRobot.close() already does this for its own thread one level up, so this brings the motor chain in line with the pattern beside it. Cosmetic: it fires after torques are zeroed, so the arm is safe either way. But it is noisy enough to bury anything else printed around a shutdown, and it reproduces on every disconnect — including a plain connect/read/disconnect with no motion commanded. Co-Authored-By: Claude Opus 5 (1M context) --- i2rt/motor_drivers/dm_driver.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/i2rt/motor_drivers/dm_driver.py b/i2rt/motor_drivers/dm_driver.py index 1ef69541..3d22024e 100644 --- a/i2rt/motor_drivers/dm_driver.py +++ b/i2rt/motor_drivers/dm_driver.py @@ -464,6 +464,7 @@ def __init__( self.command_lock = threading.RLock() self.start_thread_flag = False + self._control_thread: Optional[threading.Thread] = None if start_thread: self.start_thread() @@ -532,8 +533,8 @@ def start_thread(self) -> None: if self.start_thread_flag: return logging.info("starting separate thread for control loop") - thread = threading.Thread(target=self._set_torques_and_update_state) - thread.start() + self._control_thread = threading.Thread(target=self._set_torques_and_update_state) + self._control_thread.start() self.start_thread_flag = True time.sleep(0.1) while self.state is None: @@ -748,6 +749,13 @@ def get_same_bus_device_states(self) -> Any: def close(self) -> None: self.running = False + # Give the control loop a chance to see running=False and fall out + # before its CAN socket is closed underneath it. Without this it can + # be inside _set_commands when the socket goes, and dies with + # "file descriptor cannot be a negative integer (-1)" on every + # shutdown. The timeout keeps close() bounded if the loop is wedged. + if self._control_thread is not None: + self._control_thread.join(timeout=1.0) self.motor_interface.close()