From 43fbd7a496a07b1f41e3e4a7c1c7ef5ae4379480 Mon Sep 17 00:00:00 2001 From: smpdl Date: Tue, 2 Jun 2026 15:14:45 -0400 Subject: [PATCH 1/6] feat(dynio): added group I/O features: sync read/write + bulk read/writeadded - added control tables for xc330-m288t, xl430w250t, xm430w350t - added new public functions for reading some essential information from the control table - updated the docs for all the new changes --- docs.md | 161 +++++++++++++- .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 354 bytes .../__pycache__/port_handler.cpython-313.pyc | Bin 0 -> 5648 bytes dynio/DynamixelJSON/XC330M288T.json | 77 +++++++ dynio/DynamixelJSON/XL430W250T.json | 86 ++++++++ dynio/DynamixelJSON/XM430W350T.json | 88 ++++++++ dynio/__init__.py | 14 ++ dynio/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 440 bytes .../dynamixel_controller.cpython-313.pyc | Bin 0 -> 25957 bytes dynio/dynamixel_controller.py | 200 +++++++++++++++++- dynio/group_io.py | 180 ++++++++++++++++ 11 files changed, 804 insertions(+), 2 deletions(-) create mode 100644 dynamixel_sdk/__pycache__/__init__.cpython-313.pyc create mode 100644 dynamixel_sdk/__pycache__/port_handler.cpython-313.pyc create mode 100644 dynio/DynamixelJSON/XC330M288T.json create mode 100644 dynio/DynamixelJSON/XL430W250T.json create mode 100644 dynio/DynamixelJSON/XM430W350T.json create mode 100644 dynio/__pycache__/__init__.cpython-313.pyc create mode 100644 dynio/__pycache__/dynamixel_controller.cpython-313.pyc create mode 100644 dynio/group_io.py diff --git a/docs.md b/docs.md index 5a4554e..c7b820e 100644 --- a/docs.md +++ b/docs.md @@ -1,10 +1,24 @@ # dynio +Public exports from `dynio`: + +- `DynamixelIO`, `DynamixelMotor` — motor bus and per-motor API +- `DynamixelCommError` — raised when group transactions or connection checks fail +- `control_table_path` — resolve bundled control-table JSON paths +- `dxl` — alias for `dynio.dynamixel_controller` + # dynio.dynamixel_controller +## control_table_path +```python +control_table_path(filename) +``` +Returns the filesystem path to a control-table JSON file bundled in `dynio/DynamixelJSON/`. Raises `FileNotFoundError` if the file is missing. + + ## DynamixelIO ```python DynamixelIO(self, device_name='/dev/ttyUSB0', baud_rate=57600) @@ -62,6 +76,111 @@ DynamixelIO.new_mx106(dxl_id, protocol=1, control_table_protocol=None) ``` Returns a new DynamixelMotor object for an MX106 +### new_xc330m288t +```python +DynamixelIO.new_xc330m288t(dxl_id, protocol=2, control_table_protocol=None) +``` +Returns a new DynamixelMotor object for an XC330-M288-T (Protocol 2) + +### new_xl430w250t +```python +DynamixelIO.new_xl430w250t(dxl_id, protocol=2, control_table_protocol=None) +``` +Returns a new DynamixelMotor object for an XL430-W250-T (Protocol 2) + +### new_xm430w350t +```python +DynamixelIO.new_xm430w350t(dxl_id, protocol=2, control_table_protocol=None) +``` +Returns a new DynamixelMotor object for an XM430-W350-T (Protocol 2) + +### sync_read +```python +DynamixelIO.sync_read(motors, register_name, protocol=None) +``` +Reads the same register from multiple motors in one Sync Read transaction (Protocol 2 only). All motors must share the same protocol and register layout. Returns a dict mapping {`dxl_id`: raw register value}. Raises `DynamixelCommError` on bus failure. + +Example usage: +```python +from dynio import DynamixelIO + +dxl_io = DynamixelIO('/dev/ttyUSB0', 57600) +m1 = dxl_io.new_xm430w350t(1) +m2 = dxl_io.new_xm430w350t(2) +m3 = dxl_io.new_xm430w350t(3) + +# One transaction reads the same register from every motor (Protocol 2 only) +positions = dxl_io.sync_read([m1, m2, m3], "Present_Position") +# {1: 2048, 2: 1024, 3: 3072} — raw register values keyed by motor ID +``` + +### sync_write +```python +DynamixelIO.sync_write(writes, register_name=None, protocol=None) +``` +Writes the same register on multiple motors in one Sync Write transaction. `writes` maps each `DynamixelMotor` to an integer value, or to `(register_name, value)` when `register_name` is omitted. Raises `DynamixelCommError` on bus failure. + +Example usage: +```python +# All motors, same register — pass values keyed by motor object +dxl_io.sync_write( + {m1: 2048, m2: 1024, m3: 3072}, + register_name="Goal_Position", +) + +# Or omit register_name and give (register_name, value) per motor +dxl_io.sync_write({ + m1: ("Goal_Position", 2048), + m2: ("Goal_Position", 1024), + m3: ("Goal_Position", 3072), +}) +``` + +### bulk_read +```python +DynamixelIO.bulk_read(specs, protocol=None) +``` +Reads registers via Fast Bulk Read; each motor may use a different address and length. Each entry in `specs` is `(motor, register_name)` or `(motor, address, size)`. Returns a dict mapping `dxl_id` → raw register value. Raises `DynamixelCommError` on bus failure. + +Example usage: +```python +# Each motor can read a different register in one transaction +readings = dxl_io.bulk_read([ + (m1, "Present_Position"), + (m2, "Present_Current"), + (m3, "Hardware_Error_Status"), +]) +# {1: 2048, 2: 42, 3: 0} + +# Raw address/size when you already know the control-table layout +readings = dxl_io.bulk_read([ + (m1, 132, 4), # Present_Position on XM430-W350-T + (m2, 126, 2), # Present_Current +]) +``` + +### bulk_write +```python +DynamixelIO.bulk_write(specs, protocol=None) +``` +Writes registers via Bulk Write (Protocol 2 only). Each entry is `(motor, register_name, value)` or `(motor, address, size, value)`. Raises `DynamixelCommError` on bus failure. + +Example usage: +```python +# Mixed registers across motors in one transaction (Protocol 2 only) +dxl_io.bulk_write([ + (m1, "Goal_Position", 2048), + (m2, "Goal_Position", 1024), + (m3, "LED", 1), +]) + +# Raw address/size form +dxl_io.bulk_write([ + (m1, 116, 4, 2048), # Goal_Position on XM430-W350-T + (m2, 116, 4, 1024), +]) +``` + ### new_ax12_1 ```python DynamixelIO.new_ax12_1(*args, **kwargs) @@ -200,7 +319,7 @@ Returns the motor position as an angle in degrees ```python DynamixelMotor.get_current() ``` -Returns the current motor load +Returns the current motor load. On Protocol 2, reads `Present_Current` when available, otherwise `Present_Load` (e.g. XL430-W250-T). ### torque_enable ```python @@ -213,3 +332,43 @@ Enables motor torque DynamixelMotor.torque_disable() ``` Disables motor torque + +### get_model_number +```python +DynamixelMotor.get_model_number() +``` +Returns the model number of the motor + +### get_id +```python +DynamixelMotor.get_id() +``` +Returns the ID of the motor + +### get_baud_rate +```python +DynamixelMotor.get_baud_rate() +``` +Returns the baud rate of the motor + +### get_present_temperature +```python +DynamixelMotor.get_present_temperature() +``` +Returns the present temperature of the motor + +### hardware_error_status +```python +DynamixelMotor.hardware_error_status() +``` +Returns the hardware error status of the motor + + +# dynio.group_io + + +## DynamixelCommError +```python +DynamixelCommError(message, comm_result=None) +``` +Exception raised when a group transaction fails on the bus or when `DynamixelIO` is used without an open serial port. The `comm_result` attribute holds the SDK communication result code when available. diff --git a/dynamixel_sdk/__pycache__/__init__.cpython-313.pyc b/dynamixel_sdk/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f37ee090c7363ee68e274cb2865eda4187367973 GIT binary patch literal 354 zcmY+9!Ait15Qb-36>+V471VmIs5yEO5d<%Sm&J34O;~HQNl03>58^xMD}{RUQf|NQ*SbV;%Xp`J(S{YMP&)(^iG&7nIF@C0KR;}x7l`#*#OMFYhHEe*8X z({V&0rsT^s5el~XGi_gjC22U}Fa&n>sL zEdN<%!p!mlUH^fVv&zg*C|oRV!7zrLg;)>xjpv3J6(-IsY7<@7-a8#%2v*vQhid78^w`$a{p#Olig&Je9pl!P1;4o{64;%s<%j)s$}QMnY3e5587JsGA+C8;f{MtN=u z2Z`mRQIWjG7L+BGDzmrYB@s?u*0p%lC|v@@DDg17=FnC8f;XJH8k?t;z=w{~1_H+W zz0M?$AEy0l5b0DNlPso&S&1I z-i*|f5_@PZtQGK&d@=sVp~Lux=fO55OMrnf zcF;y3`LdLUe8l|AeBOpY+bw!VmiEQRq1=0Ok;7xmuYPS3U`A zNyr^V{8l1ckY4oRn_(>$o{gzSQ$E~_mM7;%_F8R9mX z1T#R*b{N-ywMT$8us4Ktr?qqcIcX>1{kdrytsVcq8HRHfl1yu9T;1yYs}eg{KS87N0J@9N+95$oK}o?i|?QAB{fYzdWDr97y{HQ=Y+p zeeE06(O7@<`iFzgfbN0p+Da?g`xiwswGuGC;GkNGAo~g@LS4uFclPJFnf9u z-qbO1+bG1S_hddJ1yf?M7>KqIOYqK7q0h>^P~qhVxZgn?a4SCbMxj^q~h7 zPO#r%c{^(EBWD321QJf;w?x^r|v-@$%Rt1w3N9WLom( z>O&JV1hA#o)wKqs zopj5&zci(;UQ1uRmTtbD`Y@7ij;uPr5gUq?Yv)-=72iv_kWuV>ZIuTO`aYauDkWb5 z&P82~6v$n3P_oNfET-v3+uNi!_gqCC)E%LN<~CbO8wRk|mhuV={yT&Yi*PKy!R3Kn zR_wE*eAE$gn%Qz{9O_k!$!JPhi$_!$?+6SEMX_7b^g_Eg1^?Xch?2K#$3cEx`V5>oB3)h~-zN zmsll&%y+v9VTFQ5zWVI|bCH$1J;R^@fhLyYbA}UdLkGoX_%Q18MJ;rwIh3@CN%~r< zel)dy^^2>&nc3)1`@5l7e_5B7`cq>aSAVr4# zES1@Mf@5=S|CGE)!+|dN35xj#yx#T#D*}|EG8DJ~M$~*api{8FC}$VK2UV-1bArqJ z>$dz|X@6J7f8zPnR`+Nce*V$bvaH+x#F23~WP_ot;Hh-*R3_MuaA@0>>QZ7|vDcTt zc?=Hkf`CU=Cs@;B3q$?{mErya%~C~x%pky6o3sO~HACQ&2E1=yy!Yo}7L&^OC#H+rpeX1HIXl@ARQ_L^O*|oD~hjg3`m6`XxR$#_(!7We=#|)Rup5}c9 zWj0F3E}p+IHF9xEHY<=SszStsl4NN)smw0V&#RQB9UEYgc_`5pR)M20SOxeG@vwq4 z49A$YdmNVZkAVQN+|t_M+F;gGu{LpUV$;+3cVFH5&=*75O8Si|sFG+acbkc}5$q(|AO74;wC(6gSSTGLJO76E!phamk_9f> z3nv)02P2ksA^Ih(TFJ=mdqcR|F>a4=o-uCQ*RcB2ZJ(bB8xqYM2cn&VJLTm_)96@~ zGlNcqj^Q^O{6GZXY|0d6F)_Cs!@fsRK3)#Tax0Re%xhFn#e5p{oTf+&xyDna6+7&sWZr8;L?MRw;U8O=7t~TZ7 zoWjtqhZ8z+^BlJ;@~}}91pjV1Ck*Vm1fhPn+$U6jcR~IsvKxdH*0x`s; zSU3r_`yvIXz!y!Nb0ubPsF9?}W+Cf_IA|*Q4Ao&PL`0&a(%v{Yj@x?&toe#~Uy-s` dMB1*XTJdIm^(j*EtxMy$n$J7GBlu+O{0GwgOnd+U literal 0 HcmV?d00001 diff --git a/dynio/DynamixelJSON/XC330M288T.json b/dynio/DynamixelJSON/XC330M288T.json new file mode 100644 index 0000000..a78c36d --- /dev/null +++ b/dynio/DynamixelJSON/XC330M288T.json @@ -0,0 +1,77 @@ +{ + "Protocol_2": { + "Control_Table": { + "Model_Number": [0, 2], + "Model_Information": [2, 4], + "Firmware_Version": [6, 1], + "ID": [7, 1], + "Baud_Rate": [8, 1], + "Return_Delay_Time": [9, 1], + "Drive_Mode": [10, 1], + "Operating_Mode": [11, 1], + "Secondary_Shadow_ID": [12, 1], + "Protocol_Type": [13, 1], + "Homing_Offset": [20, 4], + "Moving_Threshold": [24, 4], + "Temperature_Limit": [31, 1], + "Max_Voltage_Limit": [32, 2], + "Min_Voltage_Limit": [34, 2], + "PWM_Limit": [36, 2], + "Current_Limit": [38, 2], + "Velocity_Limit": [44, 4], + "Max_Position_Limit": [48, 4], + "Min_Position_Limit": [52, 4], + "Startup_Configuration": [60, 1], + "PWM_Slope": [62, 1], + "Shutdown": [63, 1], + "Torque_Enable": [64, 1], + "LED": [65, 1], + "Status_Return_Level": [68, 1], + "Registered_Instruction": [69, 1], + "Hardware_Error_Status": [70, 1], + "Velocity_I_Gain": [76, 2], + "Velocity_P_Gain": [78, 2], + "Position_D_Gain": [80, 2], + "Position_I_Gain": [82, 2], + "Position_P_Gain": [84, 2], + "Feedforward_2nd_Gain": [88, 2], + "Feedforward_1st_Gain": [90, 2], + "Bus_Watchdog": [98, 1], + "Goal_PWM": [100, 2], + "Goal_Current": [102, 2], + "Goal_Velocity": [104, 4], + "Profile_Acceleration": [108, 4], + "Profile_Velocity": [112, 4], + "Goal_Position": [116, 4], + "Realtime_Tick": [120, 2], + "Moving": [122, 1], + "Moving_Status": [123, 1], + "Present_PWM": [124, 2], + "Present_Current": [126, 2], + "Present_Velocity": [128, 4], + "Present_Position": [132, 4], + "Velocity_Trajectory": [136, 4], + "Position_Trajectory": [140, 4], + "Present_Input_Voltage": [144, 2], + "Present_Temperature": [146, 1], + "Backup_Ready": [147, 1], + "Indirect_Address_1": [168, 2], + "Indirect_Address_2": [170, 2], + "Indirect_Address_3": [172, 2], + "Indirect_Address_26": [218, 2], + "Indirect_Address_27": [220, 2], + "Indirect_Address_28": [222, 2], + "Indirect_Data_1": [224, 1], + "Indirect_Data_2": [225, 1], + "Indirect_Data_3": [226, 1], + "Indirect_Data_26": [249, 1], + "Indirect_Data_27": [250, 1], + "Indirect_Data_28": [251, 1] + }, + "Values": { + "Min_Position": 0, + "Max_Position": 4095, + "Max_Angle": 360 + } + } +} \ No newline at end of file diff --git a/dynio/DynamixelJSON/XL430W250T.json b/dynio/DynamixelJSON/XL430W250T.json new file mode 100644 index 0000000..2dea37b --- /dev/null +++ b/dynio/DynamixelJSON/XL430W250T.json @@ -0,0 +1,86 @@ +{ + "Protocol_2": { + "Control_Table": { + "Model_Number": [0, 2], + "Model_Information": [2, 4], + "Firmware_Version": [6, 1], + "ID": [7, 1], + "Baud_Rate": [8, 1], + "Return_Delay_Time": [9, 1], + "Drive_Mode": [10, 1], + "Operating_Mode": [11, 1], + "Secondary_Shadow_ID": [12, 1], + "Protocol_Type": [13, 1], + "Homing_Offset": [20, 4], + "Moving_Threshold": [24, 4], + "Temperature_Limit": [31, 1], + "Max_Voltage_Limit": [32, 2], + "Min_Voltage_Limit": [34, 2], + "PWM_Limit": [36, 2], + "Velocity_Limit": [44, 4], + "Max_Position_Limit": [48, 4], + "Min_Position_Limit": [52, 4], + "Startup_Configuration": [60, 1], + "Shutdown": [63, 1], + "Torque_Enable": [64, 1], + "LED": [65, 1], + "Status_Return_Level": [68, 1], + "Registered_Instruction": [69, 1], + "Hardware_Error_Status": [70, 1], + "Velocity_I_Gain": [76, 2], + "Velocity_P_Gain": [78, 2], + "Position_D_Gain": [80, 2], + "Position_I_Gain": [82, 2], + "Position_P_Gain": [84, 2], + "Feedforward_2nd_Gain": [88, 2], + "Feedforward_1st_Gain": [90, 2], + "Bus_Watchdog": [98, 1], + "Goal_PWM": [100, 2], + "Goal_Velocity": [104, 4], + "Profile_Acceleration": [108, 4], + "Profile_Velocity": [112, 4], + "Goal_Position": [116, 4], + "Realtime_Tick": [120, 2], + "Moving": [122, 1], + "Moving_Status": [123, 1], + "Present_PWM": [124, 2], + "Present_Load": [126, 2], + "Present_Velocity": [128, 4], + "Present_Position": [132, 4], + "Velocity_Trajectory": [136, 4], + "Position_Trajectory": [140, 4], + "Present_Input_Voltage": [144, 2], + "Present_Temperature": [146, 1], + "Backup_Ready": [147, 1], + "Indirect_Address_1": [168, 2], + "Indirect_Address_2": [170, 2], + "Indirect_Address_3": [172, 2], + "Indirect_Address_26": [218, 2], + "Indirect_Address_27": [220, 2], + "Indirect_Address_28": [222, 2], + "Indirect_Data_1": [224, 1], + "Indirect_Data_2": [225, 1], + "Indirect_Data_3": [226, 1], + "Indirect_Data_26": [249, 1], + "Indirect_Data_27": [250, 1], + "Indirect_Data_28": [251, 1], + "Indirect_Address_29": [578, 2], + "Indirect_Address_30": [580, 2], + "Indirect_Address_31": [582, 2], + "Indirect_Address_54": [628, 2], + "Indirect_Address_55": [630, 2], + "Indirect_Address_56": [632, 2], + "Indirect_Data_29": [634, 1], + "Indirect_Data_30": [635, 1], + "Indirect_Data_31": [636, 1], + "Indirect_Data_54": [659, 1], + "Indirect_Data_55": [660, 1], + "Indirect_Data_56": [661, 1] + }, + "Values": { + "Min_Position": 0, + "Max_Position": 4095, + "Max_Angle": 360 + } + } +} \ No newline at end of file diff --git a/dynio/DynamixelJSON/XM430W350T.json b/dynio/DynamixelJSON/XM430W350T.json new file mode 100644 index 0000000..cf374c7 --- /dev/null +++ b/dynio/DynamixelJSON/XM430W350T.json @@ -0,0 +1,88 @@ +{ + "Protocol_2": { + "Control_Table": { + "Model_Number": [0, 2], + "Model_Information": [2, 4], + "Firmware_Version": [6, 1], + "ID": [7, 1], + "Baud_Rate": [8, 1], + "Return_Delay_Time": [9, 1], + "Drive_Mode": [10, 1], + "Operating_Mode": [11, 1], + "Secondary_Shadow_ID": [12, 1], + "Protocol_Type": [13, 1], + "Homing_Offset": [20, 4], + "Moving_Threshold": [24, 4], + "Temperature_Limit": [31, 1], + "Max_Voltage_Limit": [32, 2], + "Min_Voltage_Limit": [34, 2], + "PWM_Limit": [36, 2], + "Current_Limit": [38, 2], + "Velocity_Limit": [44, 4], + "Max_Position_Limit": [48, 4], + "Min_Position_Limit": [52, 4], + "Startup_Configuration": [60, 1], + "Shutdown": [63, 1], + "Torque_Enable": [64, 1], + "LED": [65, 1], + "Status_Return_Level": [68, 1], + "Registered_Instruction": [69, 1], + "Hardware_Error_Status": [70, 1], + "Velocity_I_Gain": [76, 2], + "Velocity_P_Gain": [78, 2], + "Position_D_Gain": [80, 2], + "Position_I_Gain": [82, 2], + "Position_P_Gain": [84, 2], + "Feedforward_2nd_Gain": [88, 2], + "Feedforward_1st_Gain": [90, 2], + "Bus_Watchdog": [98, 1], + "Goal_PWM": [100, 2], + "Goal_Current": [102, 2], + "Goal_Velocity": [104, 4], + "Profile_Acceleration": [108, 4], + "Profile_Velocity": [112, 4], + "Goal_Position": [116, 4], + "Realtime_Tick": [120, 2], + "Moving": [122, 1], + "Moving_Status": [123, 1], + "Present_PWM": [124, 2], + "Present_Current": [126, 2], + "Present_Velocity": [128, 4], + "Present_Position": [132, 4], + "Velocity_Trajectory": [136, 4], + "Position_Trajectory": [140, 4], + "Present_Input_Voltage": [144, 2], + "Present_Temperature": [146, 1], + "Backup_Ready": [147, 1], + "Indirect_Address_1": [168, 2], + "Indirect_Address_2": [170, 2], + "Indirect_Address_3": [172, 2], + "Indirect_Address_26": [218, 2], + "Indirect_Address_27": [220, 2], + "Indirect_Address_28": [222, 2], + "Indirect_Data_1": [224, 1], + "Indirect_Data_2": [225, 1], + "Indirect_Data_3": [226, 1], + "Indirect_Data_26": [249, 1], + "Indirect_Data_27": [250, 1], + "Indirect_Data_28": [251, 1], + "Indirect_Address_29": [578, 2], + "Indirect_Address_30": [580, 2], + "Indirect_Address_31": [582, 2], + "Indirect_Address_54": [628, 2], + "Indirect_Address_55": [630, 2], + "Indirect_Address_56": [632, 2], + "Indirect_Data_29": [634, 1], + "Indirect_Data_30": [635, 1], + "Indirect_Data_31": [636, 1], + "Indirect_Data_54": [659, 1], + "Indirect_Data_55": [660, 1], + "Indirect_Data_56": [661, 1] + }, + "Values": { + "Min_Position": 0, + "Max_Position": 4095, + "Max_Angle": 360 + } + } +} \ No newline at end of file diff --git a/dynio/__init__.py b/dynio/__init__.py index 060cef5..ebab739 100644 --- a/dynio/__init__.py +++ b/dynio/__init__.py @@ -17,3 +17,17 @@ # Author: Hunter Halloran (Jyumpp) import dynio.dynamixel_controller as dxl +from dynio.dynamixel_controller import ( + DynamixelIO, + DynamixelMotor, + control_table_path, +) +from dynio.group_io import DynamixelCommError + +__all__ = [ + "DynamixelIO", + "DynamixelMotor", + "DynamixelCommError", + "control_table_path", + "dxl", +] diff --git a/dynio/__pycache__/__init__.cpython-313.pyc b/dynio/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1d573464d55a42a4cfa5362e741908f4df2a8a86 GIT binary patch literal 440 zcmZvW!AiqG5Qb;7NmE-|p?LCQ@F3)1a}|+Zss~e4BzRt;S!&8A8zw2VM<2q6@Rj!J z$(ve=S0`<4FAnU_Kf}!T@1oOb0oD2O(PUx+yw}ZNSsSxBBl8LoL~H~hL(8kZMV=!M zZL~uNoz;ww8lj7>2LVL(J;Uah`OZg;x%IheU(WM5OJ`DD-m&51epJ0+06O^l4T26|J!b~trr(NwDb#- Ct9ax9 literal 0 HcmV?d00001 diff --git a/dynio/__pycache__/dynamixel_controller.cpython-313.pyc b/dynio/__pycache__/dynamixel_controller.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f0d7f343646fb4640069a30f0c66484764b4f74b GIT binary patch literal 25957 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 c<5pALbdO^{KR@CW%% raw register value. + """ + self._require_connected() + motors = normalize_motor_list(motors) + resolved_protocol, address, size = resolve_sync_register(motors, register_name) + if protocol is not None and protocol != resolved_protocol: + raise ValueError( + f"Requested protocol {protocol} does not match motors (use {resolved_protocol})." + ) + protocol = resolved_protocol + if protocol != 2: + raise ValueError("sync_read requires Protocol 2 motors.") + + motor_ids = tuple(motor.dxl_id for motor in motors) + cache_key = (protocol, address, size, motor_ids) + group = self._sync_read_groups.get(cache_key) + if group is None: + handler = self.packet_handler[protocol - 1] + group = GroupSyncRead(self.port_handler, handler, address, size) + self._sync_read_groups[cache_key] = group + + group.clearParam() + for motor in motors: + group.addParam(motor.dxl_id) + + comm_result = group.txRxPacket() + self._raise_on_comm_failure(protocol, comm_result) + + return { + motor.dxl_id: group.getData(motor.dxl_id, address, size) for motor in motors + } + + def sync_write(self, writes, register_name=None, protocol=None): + """Write the same register on multiple motors in one Sync Write. + + ``writes`` maps each :class:`DynamixelMotor` to an integer value, or to + ``(register_name, value)`` when ``register_name`` is omitted. + """ + self._require_connected() + motors, values, register_name = normalize_sync_write_targets(writes, register_name) + resolved_protocol, address, size = resolve_sync_register(motors, register_name) + if protocol is not None and protocol != resolved_protocol: + raise ValueError( + f"Requested protocol {protocol} does not match motors (use {resolved_protocol})." + ) + protocol = resolved_protocol + handler = self.packet_handler[protocol - 1] + + group = GroupSyncWrite(self.port_handler, handler, address, size) + for motor, value in zip(motors, values): + if not group.addParam(motor.dxl_id, encode_register_value(value, size)): + raise DynamixelCommError( + f"Failed to add sync write param for motor id={motor.dxl_id}." + ) + + comm_result = group.txPacket() + self._raise_on_comm_failure(protocol, comm_result) + + def bulk_read(self, specs, protocol=None): + """Read registers via Fast Bulk Read; each motor may use a different address/length. + + Each entry in ``specs`` is ``(motor, register_name)`` or ``(motor, address, size)``. + Returns a dict mapping dxl_id -> raw register value. + """ + self._require_connected() + parsed = parse_bulk_read_specs(specs) + if protocol is None: + protocol = parsed[0][0].PROTOCOL + for motor, address, size in parsed: + if motor.PROTOCOL != protocol: + raise ValueError( + f"All motors must use protocol {protocol} (id={motor.dxl_id} uses " + f"{motor.PROTOCOL})." + ) + + handler = self.packet_handler[protocol - 1] + group = GroupBulkRead(self.port_handler, handler) + read_targets: list[tuple[int, int, int]] = [] + + for motor, address, size in parsed: + if not group.addParam(motor.dxl_id, address, size): + raise DynamixelCommError( + f"Failed to add bulk read param for motor id={motor.dxl_id}." + ) + read_targets.append((motor.dxl_id, address, size)) + + comm_result = group.txRxPacket() + self._raise_on_comm_failure(protocol, comm_result) + + return { + dxl_id: group.getData(dxl_id, address, size) + for dxl_id, address, size in read_targets + } + + def bulk_write(self, specs, protocol=None): + """Write registers via Bulk Write; each motor may use a different address/length. + + Each entry is ``(motor, register_name, value)`` or ``(motor, address, size, value)``. + """ + self._require_connected() + parsed = parse_bulk_write_specs(specs) + if protocol is None: + protocol = parsed[0][0].PROTOCOL + if protocol != 2: + raise ValueError("bulk_write requires Protocol 2.") + + for motor, _, _ in parsed: + if motor.PROTOCOL != protocol: + raise ValueError( + f"All motors must use protocol {protocol} (id={motor.dxl_id} uses " + f"{motor.PROTOCOL})." + ) + + handler = self.packet_handler[protocol - 1] + group = GroupBulkWrite(self.port_handler, handler) + for motor, address, size, data in parsed: + if not group.addParam(motor.dxl_id, address, size, data): + raise DynamixelCommError( + f"Failed to add bulk write param for motor id={motor.dxl_id}." + ) + + comm_result = group.txPacket() + self._raise_on_comm_failure(protocol, comm_result) # the following functions are deprecated and will be removed in version 1.0 release. They have been restructured # to continue to function for the time being, but are the result of an older system of JSON config files which # initially stored less information about each motor, causing a different initialization function to be needed @@ -305,7 +478,12 @@ def get_current(self): current *= -1 return current elif self.CONTROL_TABLE_PROTOCOL == 2: - return self.read_control_table("Present_Current") + table = self.CONTROL_TABLE + if "Present_Current" in table: + return self.read_control_table("Present_Current") + if "Present_Load" in table: + # XL430-W250-T reports load at address 126 (0.1% units, signed). + return self.read_control_table("Present_Load") def torque_enable(self): """Enables motor torque""" @@ -314,3 +492,23 @@ def torque_enable(self): def torque_disable(self): """Disables motor torque""" self.write_control_table("Torque_Enable", 0) + + def get_model_number(self): + """Returns the model number of the motor""" + return self.read_control_table("Model_Number") + + def get_id(self): + """Returns the ID of the motor""" + return self.read_control_table("ID") + + def get_baud_rate(self): + """Returns the baud rate of the motor""" + return self.read_control_table("Baud_Rate") + + def get_present_temperature(self): + """Returns the present temperature of the motor""" + return self.read_control_table("Present_Temperature") + + def hardware_error_status(self): + """Returns the hardware error status of the motor""" + return self.read_control_table("Hardware_Error_Status") \ No newline at end of file diff --git a/dynio/group_io.py b/dynio/group_io.py new file mode 100644 index 0000000..9d1f3d8 --- /dev/null +++ b/dynio/group_io.py @@ -0,0 +1,180 @@ +"""Helpers for Dynamixel group (sync/bulk) communication.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Iterable, Union + +from dynamixel_sdk import ( + DXL_HIBYTE, + DXL_HIWORD, + DXL_LOBYTE, + DXL_LOWORD, +) + +if TYPE_CHECKING: + from dynio.dynamixel_controller import DynamixelMotor + +BulkReadSpec = Union[ + "DynamixelMotor", + tuple["DynamixelMotor", str], + tuple["DynamixelMotor", int, int], +] + +BulkWriteSpec = Union[ + tuple["DynamixelMotor", str, int], + tuple["DynamixelMotor", int, int, int], +] + +WriteTarget = Union["DynamixelMotor", int] + + +class DynamixelCommError(Exception): + """Raised when a group transaction fails on the bus.""" + + def __init__(self, message: str, comm_result: int | None = None): + super().__init__(message) + self.comm_result = comm_result + + +def motor_register(motor: DynamixelMotor, register_name: str) -> tuple[int, int]: + """Return (address, size) for a named control-table entry.""" + table = motor.CONTROL_TABLE + if register_name not in table: + raise ValueError( + f"Register '{register_name}' is not in the control table for motor id={motor.dxl_id}." + ) + address, size = table[register_name] + return int(address), int(size) + + +def encode_register_value(value: int, size: int) -> list[int]: + """Encode an integer as little-endian register bytes for sync/bulk write.""" + value = int(value) + # just get the lower 8 bits for a 1-byte register + if size == 1: + return [value & 0xFF] + if size == 2: + return [DXL_LOBYTE(value), DXL_HIBYTE(value)] + if size == 4: + return [ + DXL_LOBYTE(DXL_LOWORD(value)), + DXL_HIBYTE(DXL_LOWORD(value)), + DXL_LOBYTE(DXL_HIWORD(value)), + DXL_HIBYTE(DXL_HIWORD(value)), + ] + raise ValueError(f"Unsupported register size: {size}") + + +def normalize_motor_list(motors: Iterable[DynamixelMotor]) -> list[DynamixelMotor]: + """Normalize a list of motors to a list of DynamixelMotor objects.""" + motors = list(motors) + if not motors: + raise ValueError("At least one motor is required.") + return motors + + +def resolve_sync_register( + motors: list[DynamixelMotor], register_name: str +) -> tuple[int, int, int]: + """Validate motors share protocol and register layout; return (protocol, address, size).""" + protocol = motors[0].PROTOCOL + address, size = motor_register(motors[0], register_name) + for motor in motors[1:]: + if motor.PROTOCOL != protocol: + raise ValueError( + f"All motors must use the same bus protocol (id={motors[0].dxl_id} uses " + f"{protocol}, id={motor.dxl_id} uses {motor.PROTOCOL})." + ) + other_address, other_size = motor_register(motor, register_name) + if (other_address, other_size) != (address, size): + raise ValueError( + f"Register '{register_name}' must share the same address and size for sync " + f"operations (id={motors[0].dxl_id}: {address}/{size}, " + f"id={motor.dxl_id}: {other_address}/{other_size})." + ) + return protocol, address, size + + +def parse_bulk_read_specs( + specs: Iterable[BulkReadSpec], +) -> list[tuple[DynamixelMotor, int, int]]: + parsed: list[tuple[DynamixelMotor, int, int]] = [] + for spec in specs: + if isinstance(spec, tuple): + if len(spec) == 2: + motor, register_name = spec + parsed.append((motor, *motor_register(motor, register_name))) + elif len(spec) == 3: + motor, address, size = spec + parsed.append((motor, int(address), int(size))) + else: + raise ValueError(f"Invalid bulk read spec: {spec!r}") + else: + raise ValueError( + "Bulk read requires (motor, register_name) or (motor, address, size) specs." + ) + if not parsed: + raise ValueError("At least one bulk read spec is required.") + return parsed + + +def parse_bulk_write_specs( + specs: Iterable[BulkWriteSpec], +) -> list[tuple[DynamixelMotor, int, int, list[int]]]: + parsed: list[tuple[DynamixelMotor, int, int, list[int]]] = [] + for spec in specs: + if len(spec) == 3: + motor, register_name, value = spec + address, size = motor_register(motor, register_name) + parsed.append((motor, address, size, encode_register_value(value, size))) + elif len(spec) == 4: + motor, address, size, value = spec + size = int(size) + parsed.append( + (motor, int(address), size, encode_register_value(value, size)) + ) + else: + raise ValueError(f"Invalid bulk write spec: {spec!r}") + if not parsed: + raise ValueError("At least one bulk write spec is required.") + return parsed + + +def normalize_sync_write_targets( + writes: dict[WriteTarget, Any], + register_name: str | None, +) -> tuple[list[DynamixelMotor], list[int], str | None]: + """Return (motors, values, register_name) from a sync_write mapping.""" + if not writes: + raise ValueError("writes must not be empty.") + + motors: list[DynamixelMotor] = [] + values: list[int] = [] + resolved_register: str | None = register_name + + for target, payload in writes.items(): + motor = target if hasattr(target, "CONTROL_TABLE") else None + if motor is None: + raise ValueError("sync_write keys must be DynamixelMotor instances.") + + if isinstance(payload, tuple) and len(payload) == 2: + reg_name, value = payload + value = int(value) + if resolved_register is None: + resolved_register = reg_name + elif resolved_register != reg_name: + raise ValueError("All sync_write entries must use the same register.") + else: + if resolved_register is None and register_name is None: + raise ValueError( + "register_name is required when write values are plain integers." + ) + value = int(payload) + + motors.append(motor) + values.append(value) + + if resolved_register is None: + raise ValueError("register_name could not be determined from writes.") + + return motors, values, resolved_register From b5ca991e51006b31d8d6d0c4d13fd21736162c8a Mon Sep 17 00:00:00 2001 From: smpdl Date: Tue, 2 Jun 2026 15:22:04 -0400 Subject: [PATCH 2/6] chore: update the readme --- README.md | 139 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 115 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index cae61ce..ccc3901 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,84 @@ # dynamixel-controller -dynamixel-controller is a Python library designed from the ground up to work with any Dynamixel motor on the market with few to no modifications. +Python library for controlling Dynamixel motors with JSON-defined control tables and a simple per-motor API. + +**This repository is a fork of [UGA-BSAIL/dynamixel-controller](https://github.com/UGA-BSAIL/dynamixel-controller)** by Hunter Halloran and the [University of Georgia Bio-Sensing and Instrumentation Lab](https://github.com/UGA-BSAIL). The original project is licensed under [Apache 2.0](LICENSE). This fork extends it with Protocol 2 group I/O (sync/bulk read and write), additional motor control tables, and related API updates. See [docs.md](docs.md) for the full API reference. ## Installation -Use the package manager [pip](https://pip.pypa.io/en/stable/) to install dynamixel-controller. +### From this repository (recommended for this fork) + +Clone and install in editable mode so local changes are picked up immediately: + +```bash +git clone https://github.com/smpdl/dynamixel-controller.git +cd dynamixel-controller +pip install -e . +``` + +Or install directly from GitHub without cloning: + +```bash +pip install git+https://github.com/smpdl/dynamixel-controller.git +``` + +**Requirements:** Python 3, [pyserial](https://pypi.org/project/pyserial/), and [deprecation](https://pypi.org/project/deprecation/) (installed automatically). + +### Original package on PyPI + +The upstream project is also published as `dynamixel-controller`: ```bash pip install dynamixel-controller ``` +That release does not include the fork-specific features below (group I/O, newer control tables). Use the install options above for this codebase. + ## Usage -#### Import: + +### Import + ```python -from dynio import * +from dynio import DynamixelIO, DynamixelMotor, DynamixelCommError, control_table_path, dxl ``` -#### Port handling: +`dxl` is an alias for `dynio.dynamixel_controller` if you prefer the original import style. + +### Open the bus + +```python +dxl_io = DynamixelIO('/dev/ttyUSB0', 57600) # port and baud rate for U2D2 or other adapter +``` + +On Windows, use a COM port (for example `COM3`) instead of `/dev/ttyUSB0`. + +### Create motors + +Pre-defined helpers load bundled control-table JSON from `dynio/DynamixelJSON/`: + ```python -dxl_io = dxl.DynamixelIO('portname') # your port for U2D2 or other serial device +ax_12 = dxl_io.new_ax12(1) # AX-12, Protocol 1 +mx_64_1 = dxl_io.new_mx64(2, protocol=1) # MX-64, Protocol 1 +mx_64_2 = dxl_io.new_mx64(3, protocol=2) # MX-64, Protocol 2 + +# Protocol 2 models added in this fork +xc330 = dxl_io.new_xc330m288t(4) +xl430 = dxl_io.new_xl430w250t(5) +xm430 = dxl_io.new_xm430w350t(6) ``` -#### Pre-made motor declarations: -See [documentation](https://github.com/UGA-BSAIL/dynamixel-controller/blob/moreJSON/docs.md) for full list of motors. -See below for sample: +Custom models can use any JSON control table: + ```python -ax_12 = dxl_io.new_ax12(1) # AX-12 protocol 1 with ID 1 -mx_64_1 = dxl_io.new_mx64(2, 1) # MX-64 protocol 1 with ID 2 -mx_64_2 = dxl_io.new_mx64(3, 2) # MX-64 protocol 2 with ID 3 +motor = dxl_io.new_motor(7, control_table_path('MyMotor.json'), protocol=2) ``` -#### Motor Control: -See [documentation](https://github.com/UGA-BSAIL/dynamixel-controller/blob/moreJSON/docs.md) for full list of functions -and their usage. +See [docs.md](docs.md) for all `new_*` helpers and motor APIs. + +### Per-motor control + +Common operations are methods on `DynamixelMotor`: -The most prevalent functions are pre-implemented as methods for the motor objects. -These include: ```python motor.torque_enable() motor.torque_disable() @@ -44,23 +87,71 @@ motor.set_velocity_mode() motor.set_velocity(velocity) motor.set_position_mode() motor.set_position(position) -motor.set_angle(angle) +motor.set_angle(angle) motor.set_extended_position_mode() -motor.set_position(position) position = motor.get_position() angle = motor.get_angle() current = motor.get_current() ``` -All other values can be easily read from or written to using their [control table](http://emanual.robotis.com/) name. Example: + +Any control-table field can be read or written by name (see [ROBOTIS e-Manual](http://emanual.robotis.com/) for register definitions): + ```python -motor.write_control_table("LED", 1) # Turns the LED on -speed = motor.read_control_table("Present_Speed") # Returns present velocity +motor.write_control_table("LED", 1) +speed = motor.read_control_table("Present_Speed") ``` +### Group I/O (Protocol 2, this fork) + +Read or write the same register on multiple motors in one bus transaction with **sync** operations, or mix different registers per motor with **bulk** operations. All motors in a group transaction must use the same protocol (typically Protocol 2). + +```python +m1 = dxl_io.new_xm430w350t(1) +m2 = dxl_io.new_xm430w350t(2) +m3 = dxl_io.new_xm430w350t(3) + +# Sync read: one register, many motors +positions = dxl_io.sync_read([m1, m2, m3], "Present_Position") +# {1: 2048, 2: 1024, 3: 3072} + +# Sync write: one register, many motors +dxl_io.sync_write( + {m1: 2048, m2: 1024, m3: 3072}, + register_name="Goal_Position", +) + +# Bulk read: different registers per motor in one transaction +readings = dxl_io.bulk_read([ + (m1, "Present_Position"), + (m2, "Present_Current"), + (m3, "Hardware_Error_Status"), +]) + +# Bulk write: mixed registers (Protocol 2 only) +dxl_io.bulk_write([ + (m1, "Goal_Position", 2048), + (m2, "Goal_Position", 1024), + (m3, "LED", 1), +]) +``` + +Bus failures raise `DynamixelCommError` (for example port not open or a failed group transaction). + +## Documentation + +Full API documentation: [docs.md](docs.md) + +## Acknowledgments + +- **Original library:** [UGA-BSAIL/dynamixel-controller](https://github.com/UGA-BSAIL/dynamixel-controller) — Hunter Halloran (Jyumpp), University of Georgia Bio-Sensing and Instrumentation Lab. Copyright 2020 UGA BSAIL, Apache License 2.0. +- **Dynamixel SDK:** vendored from [ROBOTIS-GIT/DynamixelSDK](https://github.com/ROBOTIS-GIT/DynamixelSDK) (see [NOTICE.md](NOTICE.md)). + ## Contributing + Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. -Especially encouraged is new control tables to be published as part of the package. +Especially encouraged: new control-table JSON files under `dynio/DynamixelJSON/`. ## License -[Apache 2.0](https://choosealicense.com/licenses/apache-2.0/) \ No newline at end of file + +[Apache 2.0](https://choosealicense.com/licenses/apache-2.0/) — see [LICENSE](LICENSE). From e522642483c0d88d0413f2a27b73f83af2d341ba Mon Sep 17 00:00:00 2001 From: smpdl Date: Tue, 2 Jun 2026 15:31:23 -0400 Subject: [PATCH 3/6] chore: add .gitignore and remove bytecode artifacts --- .gitignore | 220 ++++++++++++++++++ .../__pycache__/__init__.cpython-313.pyc | Bin 354 -> 0 bytes .../__pycache__/port_handler.cpython-313.pyc | Bin 5648 -> 0 bytes dynio/__pycache__/__init__.cpython-313.pyc | Bin 440 -> 0 bytes .../dynamixel_controller.cpython-313.pyc | Bin 25957 -> 0 bytes 5 files changed, 220 insertions(+) create mode 100644 .gitignore delete mode 100644 dynamixel_sdk/__pycache__/__init__.cpython-313.pyc delete mode 100644 dynamixel_sdk/__pycache__/port_handler.cpython-313.pyc delete mode 100644 dynio/__pycache__/__init__.cpython-313.pyc delete mode 100644 dynio/__pycache__/dynamixel_controller.cpython-313.pyc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b3ec7d5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,220 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +*.lcov +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +# Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +# poetry.lock +# poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +# pdm.lock +# pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +# pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi/* +!.pixi/config.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule* +celerybeat.pid + +# Redis +*.rdb +*.aof +*.pid + +# RabbitMQ +mnesia/ +rabbitmq/ +rabbitmq-data/ + +# ActiveMQ +activemq-data/ + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +# .idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ +# Temporary file for partial code execution +tempCodeRunnerFile.py + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml diff --git a/dynamixel_sdk/__pycache__/__init__.cpython-313.pyc b/dynamixel_sdk/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index f37ee090c7363ee68e274cb2865eda4187367973..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 354 zcmY+9!Ait15Qb-36>+V471VmIs5yEO5d<%Sm&J34O;~HQNl03>58^xMD}{RUQf|NQ*SbV;%Xp`J(S{YMP&)(^iG&7nIF@C0KR;}x7l`#*#OMFYhHEe*8X z({V&0rsT^s5el~XGi_gjC22U}Fa&n>sL zEdN<%!p!mlUH^fVv&zg*C|oRV!7zrLg;)>xjpv3J6(-IsY7<@7-a8#%2v*vQhid78^w`$a{p#Olig&Je9pl!P1;4o{64;%s<%j)s$}QMnY3e5587JsGA+C8;f{MtN=u z2Z`mRQIWjG7L+BGDzmrYB@s?u*0p%lC|v@@DDg17=FnC8f;XJH8k?t;z=w{~1_H+W zz0M?$AEy0l5b0DNlPso&S&1I z-i*|f5_@PZtQGK&d@=sVp~Lux=fO55OMrnf zcF;y3`LdLUe8l|AeBOpY+bw!VmiEQRq1=0Ok;7xmuYPS3U`A zNyr^V{8l1ckY4oRn_(>$o{gzSQ$E~_mM7;%_F8R9mX z1T#R*b{N-ywMT$8us4Ktr?qqcIcX>1{kdrytsVcq8HRHfl1yu9T;1yYs}eg{KS87N0J@9N+95$oK}o?i|?QAB{fYzdWDr97y{HQ=Y+p zeeE06(O7@<`iFzgfbN0p+Da?g`xiwswGuGC;GkNGAo~g@LS4uFclPJFnf9u z-qbO1+bG1S_hddJ1yf?M7>KqIOYqK7q0h>^P~qhVxZgn?a4SCbMxj^q~h7 zPO#r%c{^(EBWD321QJf;w?x^r|v-@$%Rt1w3N9WLom( z>O&JV1hA#o)wKqs zopj5&zci(;UQ1uRmTtbD`Y@7ij;uPr5gUq?Yv)-=72iv_kWuV>ZIuTO`aYauDkWb5 z&P82~6v$n3P_oNfET-v3+uNi!_gqCC)E%LN<~CbO8wRk|mhuV={yT&Yi*PKy!R3Kn zR_wE*eAE$gn%Qz{9O_k!$!JPhi$_!$?+6SEMX_7b^g_Eg1^?Xch?2K#$3cEx`V5>oB3)h~-zN zmsll&%y+v9VTFQ5zWVI|bCH$1J;R^@fhLyYbA}UdLkGoX_%Q18MJ;rwIh3@CN%~r< zel)dy^^2>&nc3)1`@5l7e_5B7`cq>aSAVr4# zES1@Mf@5=S|CGE)!+|dN35xj#yx#T#D*}|EG8DJ~M$~*api{8FC}$VK2UV-1bArqJ z>$dz|X@6J7f8zPnR`+Nce*V$bvaH+x#F23~WP_ot;Hh-*R3_MuaA@0>>QZ7|vDcTt zc?=Hkf`CU=Cs@;B3q$?{mErya%~C~x%pky6o3sO~HACQ&2E1=yy!Yo}7L&^OC#H+rpeX1HIXl@ARQ_L^O*|oD~hjg3`m6`XxR$#_(!7We=#|)Rup5}c9 zWj0F3E}p+IHF9xEHY<=SszStsl4NN)smw0V&#RQB9UEYgc_`5pR)M20SOxeG@vwq4 z49A$YdmNVZkAVQN+|t_M+F;gGu{LpUV$;+3cVFH5&=*75O8Si|sFG+acbkc}5$q(|AO74;wC(6gSSTGLJO76E!phamk_9f> z3nv)02P2ksA^Ih(TFJ=mdqcR|F>a4=o-uCQ*RcB2ZJ(bB8xqYM2cn&VJLTm_)96@~ zGlNcqj^Q^O{6GZXY|0d6F)_Cs!@fsRK3)#Tax0Re%xhFn#e5p{oTf+&xyDna6+7&sWZr8;L?MRw;U8O=7t~TZ7 zoWjtqhZ8z+^BlJ;@~}}91pjV1Ck*Vm1fhPn+$U6jcR~IsvKxdH*0x`s; zSU3r_`yvIXz!y!Nb0ubPsF9?}W+Cf_IA|*Q4Ao&PL`0&a(%v{Yj@x?&toe#~Uy-s` dMB1*XTJdIm^(j*EtxMy$n$J7GBlu+O{0GwgOnd+U diff --git a/dynio/__pycache__/__init__.cpython-313.pyc b/dynio/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 1d573464d55a42a4cfa5362e741908f4df2a8a86..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 440 zcmZvW!AiqG5Qb;7NmE-|p?LCQ@F3)1a}|+Zss~e4BzRt;S!&8A8zw2VM<2q6@Rj!J z$(ve=S0`<4FAnU_Kf}!T@1oOb0oD2O(PUx+yw}ZNSsSxBBl8LoL~H~hL(8kZMV=!M zZL~uNoz;ww8lj7>2LVL(J;Uah`OZg;x%IheU(WM5OJ`DD-m&51epJ0+06O^l4T26|J!b~trr(NwDb#- Ct9ax9 diff --git a/dynio/__pycache__/dynamixel_controller.cpython-313.pyc b/dynio/__pycache__/dynamixel_controller.cpython-313.pyc deleted file mode 100644 index f0d7f343646fb4640069a30f0c66484764b4f74b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25957 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 c<5pALbdO^{KR@CW%% Date: Tue, 2 Jun 2026 15:55:33 -0400 Subject: [PATCH 4/6] feat: added new_xm540w270t --- README.md | 1 + docs.md | 6 ++++++ dynio/dynamixel_controller.py | 8 +++++++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ccc3901..00c02cd 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ mx_64_2 = dxl_io.new_mx64(3, protocol=2) # MX-64, Protocol 2 xc330 = dxl_io.new_xc330m288t(4) xl430 = dxl_io.new_xl430w250t(5) xm430 = dxl_io.new_xm430w350t(6) +xm540 = dxl_io.new_xm540w270t(7) ``` Custom models can use any JSON control table: diff --git a/docs.md b/docs.md index c7b820e..3ba2218 100644 --- a/docs.md +++ b/docs.md @@ -94,6 +94,12 @@ DynamixelIO.new_xm430w350t(dxl_id, protocol=2, control_table_protocol=None) ``` Returns a new DynamixelMotor object for an XM430-W350-T (Protocol 2) +### new_xm540w270t +```python +DynamixelIO.new_xm540w270t(dxl_id, protocol=2, control_table_protocol=None) +``` +Returns a new DynamixelMotor object for an XM540-W270-T (Protocol 2) + ### sync_read ```python DynamixelIO.sync_read(motors, register_name, protocol=None) diff --git a/dynio/dynamixel_controller.py b/dynio/dynamixel_controller.py index 3aeac16..a0f3ad3 100644 --- a/dynio/dynamixel_controller.py +++ b/dynio/dynamixel_controller.py @@ -163,7 +163,13 @@ def new_xm430w350t(self, dxl_id, protocol=2, control_table_protocol=None): return self.new_motor( dxl_id, pkg_resources.resource_filename(__name__, "DynamixelJSON/XM430W350T.json"), protocol, control_table_protocol ) - + + def new_xm540w270t(self, dxl_id, protocol=2, control_table_protocol=None): + """Returns a new DynamixelMotor object for an XM540-W270-T (Protocol 2).""" + return self.new_motor( + dxl_id, pkg_resources.resource_filename(__name__, "DynamixelJSON/XM540W270.json"), protocol, control_table_protocol + ) + def sync_read(self, motors, register_name, protocol=None): """Read the same register from multiple motors in one Sync Read (Protocol 2). From 54d7c9bff49d651e68d60f88ebd65ad7566e793d Mon Sep 17 00:00:00 2001 From: Samip Paudel <111848794+smpdl@users.noreply.github.com> Date: Fri, 5 Jun 2026 14:04:19 -0400 Subject: [PATCH 5/6] Fix formatting issue in XM540W270.json --- dynio/DynamixelJSON/XM540W270.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dynio/DynamixelJSON/XM540W270.json b/dynio/DynamixelJSON/XM540W270.json index ff344ac..4b88d0d 100644 --- a/dynio/DynamixelJSON/XM540W270.json +++ b/dynio/DynamixelJSON/XM540W270.json @@ -62,7 +62,7 @@ "External_Port_Data_3": [156, 2], "Indirect_Address_1": [168, 2], "Indirect_Address_2": [170, 2], - "Indirect_Address_3": [172, 2], + "Indirect_Address_3": [172, 2] } } } From 49fa4ebf9675eeed9c7f4e7edea265ebfae0a8df Mon Sep 17 00:00:00 2001 From: Samip Paudel <111848794+smpdl@users.noreply.github.com> Date: Fri, 5 Jun 2026 14:13:41 -0400 Subject: [PATCH 6/6] Add Max_Angle field to XM540W270.json Added Max_Angle to Values section in JSON. --- dynio/DynamixelJSON/XM540W270.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dynio/DynamixelJSON/XM540W270.json b/dynio/DynamixelJSON/XM540W270.json index 4b88d0d..458e1d0 100644 --- a/dynio/DynamixelJSON/XM540W270.json +++ b/dynio/DynamixelJSON/XM540W270.json @@ -63,6 +63,11 @@ "Indirect_Address_1": [168, 2], "Indirect_Address_2": [170, 2], "Indirect_Address_3": [172, 2] + }, + "Values": { + "Min_Position": 0, + "Max_Position": 4095, + "Max_Angle": 360 } } }