├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── pyproject.toml ├── requirements.txt ├── setup.cfg ├── src └── ticlib │ ├── __init__.py │ ├── ticlib.py │ └── ticlib.pyi └── tests ├── __init__.py ├── tests.py └── usb ├── __init__.py └── core.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Build artifacts 2 | build/** 3 | dist/** 4 | **/*.egg-info/** -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2021, Julien Phalip 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | `ticlib` is a pure-Python library to drive [Pololu Tic stepper motor controllers](https://www.pololu.com/category/212/tic-stepper-motor-controllers). 2 | 3 | This library supports serial, I²C, and USB connections for Python3; and serial and I²C for Micropython. 4 | 5 | # Example code 6 | 7 | ```python 8 | from ticlib import TicUSB 9 | from time import sleep 10 | 11 | tic = TicUSB() 12 | 13 | tic.halt_and_set_position(0) 14 | tic.energize() 15 | tic.exit_safe_start() 16 | 17 | positions = [500, 300, 800, 0] 18 | for position in positions: 19 | tic.set_target_position(position) 20 | while tic.get_current_position() != tic.get_target_position(): 21 | sleep(0.1) 22 | 23 | tic.deenergize() 24 | tic.enter_safe_start() 25 | ``` 26 | 27 | # Installation 28 | 29 | The `ticlib` library is hosted on PyPI and can be installed by running this command: 30 | 31 | ```shell 32 | pip install ticlib 33 | ``` 34 | 35 | # Available controllers 36 | 37 | ## Serial 38 | 39 | Example using Python 3 and the [pyserial](https://pypi.org/project/pyserial/) library: 40 | 41 | ```python 42 | import serial 43 | from ticlib import TicSerial 44 | 45 | port = serial.Serial("/dev/ttyS0", baud_rate=9600, timeout=0.1, write_timeout=0.1) 46 | tic = TicSerial(port) 47 | ``` 48 | 49 | Example using Micropython: 50 | 51 | ```python 52 | from machine import UART 53 | from ticlib import TicSerial 54 | 55 | port = UART(0, baudrate=9600, timeout=100) 56 | tic = TicSerial(port) 57 | ``` 58 | 59 | Instantiation parameters for `TicSerial`: 60 | 61 | - `port` (required): Serial port used to communicate with the Tic. 62 | - `device_number` (optional): Number of the device that you want to control. Use this if you have multiple devices 63 | connected to your serial line. Defaults to `None`. 64 | - `crc_for_commands` (optional): If `True`, the library will append a CRC byte to every command sent to the Tic. Set 65 | this to `True` if your Tic's "Enable CRC for commands" setting is turned on. Defaults to `False`. 66 | - `crc_for_responses` (optional): If `True`, the library will expect a CRC byte at the end of every response returned 67 | by the Tic. Set this to `True` if your Tic's "Enable CRC for responses" setting is turned on. Defaults to `False`. 68 | 69 | For more details, see Pololu's official documentation on [serial command encoding](https://www.pololu.com/docs/0J71/9). 70 | 71 | 72 | ## I²C 73 | 74 | Example using Python 3 and the [smbus2](https://pypi.org/project/smbus2/) library. 75 | 76 | ```python 77 | from smbus2 import SMBus 78 | from ticlib import TicI2C, SMBus2Backend 79 | 80 | bus = SMBus(3) # Represents /dev/i2c-3 81 | address = 14 # Address of the Tic, that is its device number 82 | backend = SMBus2Backend(bus) 83 | 84 | tic = TicI2C(backend) 85 | ``` 86 | 87 | Example using Micropython: 88 | 89 | ```python 90 | from machine import I2C 91 | from ticlib import TicI2C, MachineI2CBackend 92 | 93 | i2c = I2C(1) # ID of your I2C peripheral 94 | address = 14 # Address of the Tic, that is its device number 95 | backend = MachineI2CBackend(i2c, address) 96 | 97 | tic = TicI2C(backend) 98 | ``` 99 | 100 | Instantiation parameter for `TicI2C`: 101 | 102 | - `backend` (required): The I²C backend. Available options are `SMBus2Backend` for Python 3 and `MachineI2C` for 103 | Micropython. 104 | 105 | Note: If you use a Raspberry Pi, make sure to follow the workaround described in the [Pololu documentation](https://www.pololu.com/docs/0J71/12.8). 106 | 107 | For more details, see Pololu's official documentation on [I²C command encoding](https://www.pololu.com/docs/0J71/10). 108 | 109 | ## USB 110 | 111 | The USB controller has a dependency on the [pyusb](https://pypi.org/project/pyusb/) library. 112 | 113 | Example: 114 | 115 | ```python 116 | from ticlib import TicUSB 117 | 118 | tic = TicUSB() 119 | ``` 120 | 121 | Instantiation parameters for `TicUSB`: 122 | 123 | - `product` (optional): USB product ID for your Tic. If `None`, the device will be automatically detected. Use this if 124 | multiple Tic devices are connected to your computer. The available options are: `TIC_T825` (`0x00b3`), `TIC_T834` 125 | (`0x00b5`), `TIC_T500` (`0x00bd`), `TIC_N825` (`0x00c3`), `TIC_T249` (`0x00c9`), and `TIC_36v4` (`0x00cb`). Defaults 126 | to `None`. 127 | - `serial_number` (optional): The serial number (in string format) of your Tic. If `None`, the device will be 128 | automatically detected. Use this if multiple Tic devices are connected to your computer. Default to `None`. 129 | 130 | For more details, see Pololu's official documentation on [USB command encoding](https://www.pololu.com/docs/0J71/11). 131 | 132 | # Commands: 133 | 134 | Available commands: 135 | 136 | ```python 137 | tic.clear_driver_error() 138 | tic.deenergize() 139 | tic.energize() 140 | tic.enter_safe_start() 141 | tic.exit_safe_start() 142 | tic.go_home(value) 143 | tic.halt_and_hold() 144 | tic.halt_and_set_position(value) 145 | tic.reset() 146 | tic.reset_command_timeout() 147 | tic.set_agc_option(value) 148 | tic.set_current_limit(value) 149 | tic.set_decay_mode(value) 150 | tic.set_max_acceleration(value) 151 | tic.set_max_deceleration(value) 152 | tic.set_max_speed(value) 153 | tic.set_starting_speed(value) 154 | tic.set_step_mode(value) 155 | tic.set_target_position(value) 156 | tic.set_target_velocity(value) 157 | ``` 158 | 159 | For more details, see the official [command reference](https://www.pololu.com/docs/0J71/8). 160 | 161 | Please note: 162 | - `tic.set_current_limit(value)` is encoded as described [here](https://www.pololu.com/docs/0J71/6#setting-current-limit) rather than sent as a value in mA. 163 | - `tic.set_step_mode(value)` is encdoed as described [here](https://www.pololu.com/docs/0J71/8#cmd-set-step-mode) rather than sent as a fraction. 164 | 165 | # Variables 166 | 167 | Available variables: 168 | 169 | ```python 170 | # General status ------------------------------------- 171 | tic.get_error_occured() 172 | tic.get_error_status() 173 | tic.get_misc_flags() 174 | tic.get_operation_state() 175 | 176 | # Step planning --------------------------------------- 177 | tic.get_acting_target_position() 178 | tic.get_current_position() 179 | tic.get_current_velocity() 180 | tic.get_max_acceleration() 181 | tic.get_max_deceleration() 182 | tic.get_max_speed() 183 | tic.get_planning_mode() 184 | tic.get_starting_speed() 185 | tic.get_target_position() 186 | tic.get_target_velocity() 187 | tic.get_time_since_last_step() 188 | 189 | # Other ----------------------------------------------- 190 | tic.get_analog_reading_rx() 191 | tic.get_analog_reading_scl() 192 | tic.get_analog_reading_sda() 193 | tic.get_analog_reading_tx() 194 | tic.get_current_limit() 195 | tic.get_decay_mode() 196 | tic.get_device_reset() 197 | tic.get_digital_readings() 198 | tic.get_encoder_position() 199 | tic.get_input_after_averaging() 200 | tic.get_input_after_hysteresis() 201 | tic.get_input_after_scaling() 202 | tic.get_input_state() 203 | tic.get_pin_states() 204 | tic.get_rc_pulse() 205 | tic.get_step_mode() 206 | tic.get_vin_voltage() 207 | tic.get_uptime() 208 | 209 | # T249-only ------------------------------------------- 210 | tic.get_agc_bottom_current_limit() 211 | tic.get_agc_current_boost_steps() 212 | tic.get_agc_frequency_limit() 213 | tic.get_agc_mode() 214 | tic.get_last_motor_driver_error() 215 | 216 | # 36v4-only ------------------------------------------- 217 | tic.get_last_hp_driver_errors() 218 | ``` 219 | 220 | For more details, see the official [variable reference](https://www.pololu.com/docs/0J71/7). 221 | 222 | # Settings 223 | 224 | Available settings: 225 | 226 | ```python 227 | tic.settings.get_control_mode() 228 | 229 | # Miscellaneous ------------------------------------------------- 230 | tic.settings.get_auto_clear_driver_error() 231 | tic.settings.get_disable_safe_start() 232 | tic.settings.get_ignore_err_line_high() 233 | tic.settings.get_never_sleep() 234 | tic.settings.get_vin_calibration() 235 | 236 | # Soft error response ------------------------------------------- 237 | tic.settings.get_current_limit_during_error() 238 | tic.settings.get_soft_error_position() 239 | tic.settings.get_soft_error_response() 240 | 241 | # Serial -------------------------------------------------------- 242 | tic.settings.get_serial_7bit_responses() 243 | tic.settings.get_serial_14bit_device_number() 244 | tic.settings.get_serial_alt_device_number() 245 | tic.settings.get_serial_baud_rate() 246 | tic.settings.get_serial_command_timeout() 247 | tic.settings.get_serial_crc_for_commands() 248 | tic.settings.get_serial_crc_for_responses() 249 | tic.settings.get_serial_device_number() 250 | tic.settings.get_serial_enable_alt_device_number() 251 | tic.settings.get_serial_response_delay() 252 | 253 | # Encoder ------------------------------------------------------- 254 | tic.settings.get_encoder_postscaler() 255 | tic.settings.get_encoder_prescaler() 256 | tic.settings.get_encoder_unlimited() 257 | 258 | # Input conditioning -------------------------------------------- 259 | tic.settings.get_input_averaging_enabled() 260 | tic.settings.get_input_hysteresis() 261 | 262 | # RC and analog scaling ----------------------------------------- 263 | tic.settings.get_input_invert() 264 | tic.settings.get_input_max() 265 | tic.settings.get_input_min() 266 | tic.settings.get_input_neutral_max() 267 | tic.settings.get_input_neutral_min() 268 | tic.settings.get_input_scaling_degree() 269 | tic.settings.get_output_max() 270 | tic.settings.get_output_min() 271 | 272 | # Pin Configuration --------------------------------------------- 273 | # SCL 274 | tic.settings.get_scl_active_high() 275 | tic.settings.get_scl_config() 276 | tic.settings.get_scl_enable_analog() 277 | tic.settings.get_scl_enable_pull_up() 278 | tic.settings.get_scl_kill_switch() 279 | tic.settings.get_scl_limit_switch_forward() 280 | tic.settings.get_scl_limit_switch_reverse() 281 | tic.settings.get_scl_pin_function() 282 | # SDA 283 | tic.settings.get_sda_active_high() 284 | tic.settings.get_sda_config() 285 | tic.settings.get_sda_enable_analog() 286 | tic.settings.get_sda_enable_pull_up() 287 | tic.settings.get_sda_kill_switch() 288 | tic.settings.get_sda_limit_switch_forward() 289 | tic.settings.get_sda_limit_switch_reverse() 290 | tic.settings.get_sda_pin_function() 291 | # TX 292 | tic.settings.get_tx_active_high() 293 | tic.settings.get_tx_config() 294 | tic.settings.get_tx_enable_analog() 295 | tic.settings.get_tx_kill_switch() 296 | tic.settings.get_tx_limit_switch_forward() 297 | tic.settings.get_tx_limit_switch_reverse() 298 | tic.settings.get_tx_pin_function() 299 | # RX 300 | tic.settings.get_rx_active_high() 301 | tic.settings.get_rx_config() 302 | tic.settings.get_rx_enable_analog() 303 | tic.settings.get_rx_kill_switch() 304 | tic.settings.get_rx_limit_switch_forward() 305 | tic.settings.get_rx_limit_switch_reverse() 306 | tic.settings.get_rx_pin_function() 307 | # RC 308 | tic.settings.get_rc_active_high() 309 | tic.settings.get_rc_config() 310 | tic.settings.get_rc_kill_switch() 311 | tic.settings.get_rc_limit_switch_forward() 312 | tic.settings.get_rc_limit_switch_reverse() 313 | 314 | # Motor --------------------------------------------------------- 315 | tic.settings.get_current_limit() 316 | tic.settings.get_decay_mode() 317 | tic.settings.get_invert_motor_direction() 318 | tic.settings.get_max_acceleration() 319 | tic.settings.get_max_deceleration() 320 | tic.settings.get_max_speed() 321 | tic.settings.get_starting_speed() 322 | tic.settings.get_step_mode() 323 | 324 | # Homing -------------------------------------------------------- 325 | tic.settings.get_auto_homing() 326 | tic.settings.get_auto_homing_forward() 327 | tic.settings.get_homing_speed_away() 328 | tic.settings.get_homing_speed_towards() 329 | 330 | # T249-only ----------------------------------------------------- 331 | tic.settings.get_agc_bottom_current_limit() 332 | tic.settings.get_agc_current_boost_steps() 333 | tic.settings.get_agc_mode() 334 | tic.settings.get_agc_frequency_limit() 335 | 336 | # 36v4-only ----------------------------------------------------- 337 | tic.settings.get_hp_current_trip_blanking_time() 338 | tic.settings.get_hp_decay_mode() 339 | tic.settings.get_hp_enable_adaptive_blanking_time() 340 | tic.settings.get_hp_enable_unrestricted_current_limits() 341 | tic.settings.get_hp_fixed_off_time() 342 | tic.settings.get_hp_mixed_decay_transition_time() 343 | ``` 344 | 345 | Modifying settings is currently not supported. 346 | 347 | For more details, see the official [settings reference](https://www.pololu.com/docs/0J71/6). 348 | 349 | # Version history 350 | 351 | ## 0.3.0 (April 19, 2025) 352 | 353 | - Added a `.pyi` file for IDE/editor autocompletion and convenient documentation references. 354 | - Added support for Circuit Python 355 | - Fixed a bug with `set_current_limit()` 356 | - Handle gracefully a serial read returning `None` 357 | 358 | ## 0.2.2 (May 14, 2021) 359 | 360 | - Fixed some bugs for Micropython 361 | 362 | ## 0.2.1 (May 4, 2021) 363 | 364 | - Issue #1: Fixed bug that prevented multiple Tic controllers from working at the same time. 365 | 366 | ## 0.2.0 (April 20, 2021) 367 | 368 | - Added support for Micropython with serial and I2C. 369 | 370 | ## 0.1.0 (April 18, 2021) 371 | 372 | Initial release. 373 | 374 | # Running the tests 375 | 376 | For Python: 377 | 378 | ```shell 379 | docker run -it -v ${PWD}:/base -w /base python python /base/tests/tests.py 380 | ``` 381 | 382 | For Micropython: 383 | 384 | ```shell 385 | docker run -it -v ${PWD}:/base -w /base mitchins/micropython-linux micropython /base/tests/tests.py 386 | ``` 387 | 388 | For CircuitPython: 389 | 390 | ``` 391 | docker run -it -v ${PWD}:/base -w /base jphalip/circuitpython-linux micropython /base/tests/tests.py 392 | ``` 393 | 394 | # Notes for project maintainers 395 | 396 | To release a new version of this library: 397 | 398 | - Create a virtualenv and install the dependencies in `requirements.txt` 399 | - Update version number in `src/ticlib/__init__.py` 400 | - Add release notes in the "Version history" section above. 401 | - Create and push tag of the form: `v` (e.g. `v.0.2.1`) 402 | - Delete existing `dist/` directory, if any. 403 | - Run: `python3 -m build` 404 | - Run: `python3 -m twine upload dist/*` 405 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=42", 4 | "wheel" 5 | ] 6 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | build==1.2.2.post1 2 | twine==6.1.0 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = ticlib 3 | version = attr: ticlib.__version__ 4 | author = Julien Phalip 5 | author_email = jphalip@gmail.com 6 | description = Python library to drive Pololu Tic stepper motor controllers 7 | long_description = file: README.md 8 | license = BSD-2-Clause 9 | long_description_content_type = text/markdown 10 | url = https://github.com/jphalip/ticlib 11 | project_urls = 12 | Bug Tracker = https://github.com/jphalip/ticlib/issues 13 | classifiers = 14 | Programming Language :: Python :: 3 15 | License :: OSI Approved :: BSD License 16 | Operating System :: OS Independent 17 | 18 | [options] 19 | package_dir= 20 | =src 21 | packages = find: 22 | python_requires = >=3.6 23 | 24 | [options.packages.find] 25 | where = src -------------------------------------------------------------------------------- /src/ticlib/__init__.py: -------------------------------------------------------------------------------- 1 | # BSD 2-Clause License 2 | # 3 | # Copyright (c) 2021, Julien Phalip 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are met: 8 | # 9 | # 1. Redistributions of source code must retain the above copyright notice, this 10 | # list of conditions and the following disclaimer. 11 | # 12 | # 2. Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation 14 | # and/or other materials provided with the distribution. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | from .ticlib import * 28 | 29 | __version__ = '0.3.0' -------------------------------------------------------------------------------- /src/ticlib/ticlib.py: -------------------------------------------------------------------------------- 1 | # BSD 2-Clause License 2 | # 3 | # Copyright (c) 2021, Julien Phalip 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are met: 8 | # 9 | # 1. Redistributions of source code must retain the above copyright notice, this 10 | # list of conditions and the following disclaimer. 11 | # 12 | # 2. Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation 14 | # and/or other materials provided with the distribution. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | __all__ = [ 28 | 'TicSerial', 'TicI2C', 'TicUSB', 'SMBus2Backend', 'MachineI2CBackend', 29 | 'TIC_T825', 'TIC_T834', 'TIC_T500', 'TIC_N825', 'TIC_T249', 'TIC_36v4' 30 | ] 31 | 32 | import sys 33 | 34 | if sys.implementation.name in ['micropython', 'circuitpython']: 35 | def partial(function, *args): 36 | """ 37 | Substitute to Python's `functools.partial` 38 | """ 39 | def _partial(*extra_args): 40 | return function(*(args + extra_args)) 41 | return _partial 42 | else: 43 | from functools import partial 44 | 45 | try: 46 | from machine import I2C as machine_i2c 47 | except ImportError: 48 | machine_i2c = None 49 | 50 | try: 51 | from smbus2 import i2c_msg 52 | except ImportError: 53 | i2c_msg = None 54 | 55 | try: 56 | import serial 57 | except ImportError: 58 | serial = None 59 | 60 | try: 61 | import usb.core as usb_core 62 | except ImportError: 63 | usb_core = None 64 | 65 | 66 | class MachineI2CBackend(object): 67 | """ 68 | I2C backend that acts as a wrapper around the `machine.I2C` class. 69 | """ 70 | 71 | def __init__(self, i2c, address): 72 | if machine_i2c is None: 73 | raise Exception("Missing dependency: machine.I2C (Micropython)") 74 | self.i2c = i2c 75 | self.address = address 76 | 77 | def read(self, length): 78 | return self.i2c.readfrom(self.address, length) 79 | 80 | def write(self, serialized): 81 | self.i2c.writeto(self.address, serialized) 82 | 83 | 84 | class SMBus2Backend(object): 85 | """ 86 | I2C Backend that uses the smbus2 library. 87 | """ 88 | 89 | def __init__(self, bus, address): 90 | if i2c_msg is None: 91 | raise Exception("Missing dependency: smbus2") 92 | self.address = address 93 | self.bus = bus 94 | 95 | def read(self, length): 96 | read = i2c_msg.read(self.address, length) 97 | self.bus.i2c_rdwr(read) 98 | return read.__bytes__()[0: length] 99 | 100 | def write(self, serialized): 101 | write = i2c_msg.write(self.address, serialized) 102 | self.bus.i2c_rdwr(write) 103 | 104 | 105 | # Output formatting functions --------------------------------------------------- 106 | 107 | def boolean(bit_index, value): 108 | """ 109 | Returns `True` if the bit located at `bit_index` in the given `value` is set. Returns `False` otherwise. 110 | The least-significant bit is at index 0. 111 | """ 112 | int_value = int.from_bytes(bytearray(value), 'little') 113 | return (int_value & (1 << bit_index)) != 0 114 | 115 | 116 | def bit_range(start, end, value): 117 | """ 118 | Returns the bits between the `start` and `end` indices in the byte `value`. 119 | """ 120 | int_value = int.from_bytes(bytearray(value), 'little') 121 | binary = bin(int_value) 122 | # Remove first two characters 123 | binary = binary[2:] 124 | # Extract bits 125 | bits = ''.join(reversed(''.join(reversed(binary))[start:end + 1])) # Use "reversed()" instead of the less verbose [::-1] to be compatible with micropython 126 | # Convert to int 127 | return int(bits, 2) 128 | 129 | 130 | def signed_int(value): 131 | """ 132 | Converts the given byte value to a signed integer. 133 | """ 134 | result = int.from_bytes(bytearray(value), 'little') 135 | num_bytes = len(value) 136 | if result >= (1 << (num_bytes * 8 - 1)): 137 | result -= (1 << (num_bytes * 8)) 138 | return result 139 | 140 | 141 | def unsigned_int(value): 142 | """ 143 | Converts the given byte value to an unsigned integer. 144 | """ 145 | return int.from_bytes(bytearray(value), 'little') 146 | 147 | 148 | # USB IDs --------------------------------------------------- 149 | VENDOR = 0x1ffb # Pololu's vendor ID 150 | TIC_T825 = 0x00b3 151 | TIC_T834 = 0x00b5 152 | TIC_T500 = 0x00bd 153 | TIC_N825 = 0x00c3 154 | TIC_T249 = 0x00c9 155 | TIC_36v4 = 0x00cb 156 | 157 | 158 | # Commands --------------------------------------------------- 159 | 160 | # Command input formats: 161 | QUICK = 'QUICK' 162 | THIRTY_TWO_BITS = 'THIRTY_TWO_BITS' 163 | SEVEN_BITS = 'SEVEN_BITS' 164 | BLOCK_READ = 'BLOCK_READ' 165 | 166 | 167 | # See list of commands in the official documentation: https://www.pololu.com/docs/0J71/8 168 | COMMANDS = [ 169 | ('set_target_position', 0xE0, THIRTY_TWO_BITS), 170 | ('set_target_velocity', 0xE3, THIRTY_TWO_BITS), 171 | ('halt_and_set_position', 0xEC, THIRTY_TWO_BITS), 172 | ('halt_and_hold', 0x89, QUICK), 173 | ('go_home', 0x97, SEVEN_BITS), 174 | ('reset_command_timeout', 0x8C, QUICK), 175 | ('deenergize', 0x86, QUICK), 176 | ('energize', 0x85, QUICK), 177 | ('exit_safe_start', 0x83, QUICK), 178 | ('enter_safe_start', 0x8F, QUICK), 179 | ('reset', 0xB0, QUICK), 180 | ('clear_driver_error', 0x8A, QUICK), 181 | ('set_max_speed', 0xE6, THIRTY_TWO_BITS), 182 | ('set_starting_speed', 0xE5, THIRTY_TWO_BITS), 183 | ('set_max_acceleration', 0xEA, THIRTY_TWO_BITS), 184 | ('set_max_deceleration', 0xE9, THIRTY_TWO_BITS), 185 | ('set_step_mode', 0x94, SEVEN_BITS), 186 | ('set_current_limit', 0x91, SEVEN_BITS), 187 | ('set_decay_mode', 0x92, SEVEN_BITS), 188 | ('set_agc_option', 0x98, SEVEN_BITS), 189 | ] 190 | 191 | GET_VARIABLE_CMD = 0xA1 192 | GET_SETTING_CMD = 0xA8 193 | 194 | 195 | # Variables --------------------------------------------------- 196 | 197 | # See list of variables in the official documentation: https://www.pololu.com/docs/0J71/7 198 | VARIABLES = [ 199 | # General status ------------------------------------- 200 | ('operation_state', 0x00, 1, unsigned_int), 201 | ('misc_flags', 0x01, 1, None), 202 | ('error_status', 0x02, 2, None), 203 | ('error_occured', 0x04, 4, None), 204 | 205 | # Step planning --------------------------------------- 206 | ('planning_mode', 0x09, 1, unsigned_int), 207 | ('target_position', 0x0A, 4, signed_int), 208 | ('target_velocity', 0x0E, 4, signed_int), 209 | ('starting_speed', 0x12, 4, unsigned_int), 210 | ('max_speed', 0x16, 4, unsigned_int), 211 | ('max_deceleration', 0x1A, 4, unsigned_int), 212 | ('max_acceleration', 0x1E, 4, unsigned_int), 213 | ('current_position', 0x22, 4, signed_int), 214 | ('current_velocity', 0x26, 4, signed_int), 215 | ('acting_target_position', 0x2A, 4, signed_int), 216 | ('time_since_last_step', 0x2E, 4, unsigned_int), 217 | 218 | # Other ----------------------------------------------- 219 | ('device_reset', 0x32, 1, unsigned_int), 220 | ('vin_voltage', 0x33, 2, unsigned_int), 221 | ('uptime', 0x35, 4, unsigned_int), 222 | ('encoder_position', 0x39, 4, signed_int), 223 | ('rc_pulse', 0x3D, 2, unsigned_int), 224 | ('analog_reading_scl', 0x3F, 2, unsigned_int), 225 | ('analog_reading_sda', 0x41, 2, unsigned_int), 226 | ('analog_reading_tx', 0x43, 2, unsigned_int), 227 | ('analog_reading_rx', 0x45, 2, unsigned_int), 228 | ('digital_readings', 0x47, 1, None), 229 | ('pin_states', 0x48, 1, None), 230 | ('step_mode', 0x49, 1, unsigned_int), 231 | ('current_limit', 0x4A, 1, unsigned_int), 232 | ('decay_mode', 0x4B, 1, unsigned_int), # Not valid for 36v4 233 | ('input_state', 0x4C, 1, unsigned_int), 234 | ('input_after_averaging', 0x4D, 2, unsigned_int), 235 | ('input_after_hysteresis', 0x4F, 2, unsigned_int), 236 | ('input_after_scaling', 0x51, 4, signed_int), 237 | 238 | # T249-only ------------------------------------------- 239 | ('last_motor_driver_error', 0x55, 1, unsigned_int), 240 | ('agc_mode', 0x56, 1, unsigned_int), 241 | ('agc_bottom_current_limit', 0x57, 1, unsigned_int), 242 | ('agc_current_boost_steps', 0x58, 1, unsigned_int), 243 | ('agc_frequency_limit', 0x59, 1, unsigned_int), 244 | 245 | # 36v4-only ------------------------------------------- 246 | ('last_hp_driver_errors', 0xFF, 1, None), 247 | ] 248 | 249 | 250 | # Settings --------------------------------------------------- 251 | 252 | # See list of settings in the official documentation: https://www.pololu.com/docs/0J71/6 253 | SETTINGS = [ 254 | ('control_mode', 0x01, 1, unsigned_int), 255 | 256 | # Miscellaneous ------------------------------------------------- 257 | ('disable_safe_start', 0x03, 1, partial(boolean, 0)), 258 | ('ignore_err_line_high', 0x04, 1, partial(boolean, 0)), 259 | ('auto_clear_driver_error', 0x08, 1, partial(boolean, 0)), 260 | ('never_sleep', 0x02, 1, partial(boolean, 0)), 261 | ('vin_calibration', 0x14, 2, signed_int), 262 | 263 | # Soft error response ------------------------------------------- 264 | ('soft_error_response', 0x53, 1, unsigned_int), 265 | ('soft_error_position', 0x54, 4, signed_int), 266 | ('current_limit_during_error', 0x31, 1, unsigned_int), 267 | 268 | # Serial -------------------------------------------------------- 269 | ('serial_baud_rate', 0x06, 2, unsigned_int), # Returns odd result 270 | ('serial_enable_alt_device_number', 0x6A, 1, partial(boolean, 7)), 271 | ('serial_14bit_device_number', 0x0B, 1, partial(boolean, 3)), 272 | ('serial_response_delay', 0x5E, 1, unsigned_int), 273 | ('serial_command_timeout', 0x09, 2, unsigned_int), 274 | ('serial_crc_for_commands', 0x0B, 1, partial(boolean, 0)), 275 | ('serial_crc_for_responses', 0x0B, 1, partial(boolean, 1)), 276 | ('serial_7bit_responses', 0x0B, 1, partial(boolean, 2)), 277 | # Note: The device number and alternative device number settings are defined in the TicSerial class 278 | 279 | # Encoder ------------------------------------------------------- 280 | ('encoder_prescaler', 0x58, 4, unsigned_int), 281 | ('encoder_postscaler', 0x37, 4, unsigned_int), 282 | ('encoder_unlimited', 0x5C, 1, partial(boolean, 0)), 283 | 284 | # Input conditioning -------------------------------------------- 285 | ('input_averaging_enabled', 0x2E, 1, partial(boolean, 0)), 286 | ('input_hysteresis', 0x2F, 2, unsigned_int), 287 | 288 | # RC and analog scaling ----------------------------------------- 289 | ('input_invert', 0x21, 1, partial(boolean, 0)), 290 | ('input_max', 0x28, 2, unsigned_int), 291 | ('output_max', 0x32, 4, signed_int), 292 | ('input_neutral_max', 0x26, 2, unsigned_int), 293 | ('input_neutral_min', 0x24, 2, unsigned_int), 294 | ('input_min', 0x22, 2, unsigned_int), 295 | ('output_min', 0x2A, 4, signed_int), 296 | ('input_scaling_degree', 0x20, 1, unsigned_int), 297 | 298 | # Pin Configuration --------------------------------------------- 299 | # SCL 300 | ('scl_config', 0x3B, 1, None), 301 | ('scl_pin_function', 0x3B, 1, partial(bit_range, 0, 3)), 302 | ('scl_enable_analog', 0x3B, 1, partial(boolean, 6)), 303 | ('scl_enable_pull_up', 0x3B, 1, partial(boolean, 7)), 304 | ('scl_active_high', 0x36, 1, partial(boolean, 0)), 305 | ('scl_kill_switch', 0x5D, 1, partial(boolean, 0)), 306 | ('scl_limit_switch_forward', 0x5F, 1, partial(boolean, 0)), 307 | ('scl_limit_switch_reverse', 0x60, 1, partial(boolean, 0)), 308 | # SDA 309 | ('sda_config', 0x3C, 1, None), 310 | ('sda_pin_function', 0x3C, 1, partial(bit_range, 0, 3)), 311 | ('sda_enable_analog', 0x3C, 1, partial(boolean, 6)), 312 | ('sda_enable_pull_up', 0x3C, 1, partial(boolean, 7)), 313 | ('sda_active_high', 0x36, 1, partial(boolean, 1)), 314 | ('sda_kill_switch', 0x5D, 1, partial(boolean, 1)), 315 | ('sda_limit_switch_forward', 0x5F, 1, partial(boolean, 1)), 316 | ('sda_limit_switch_reverse', 0x60, 1, partial(boolean, 1)), 317 | # TX 318 | ('tx_config', 0x3D, 1, None), 319 | ('tx_pin_function', 0x3D, 1, partial(bit_range, 0, 3)), 320 | ('tx_enable_analog', 0x3D, 1, partial(boolean, 6)), 321 | ('tx_active_high', 0x36, 1, partial(boolean, 2)), 322 | ('tx_kill_switch', 0x5D, 1, partial(boolean, 2)), 323 | ('tx_limit_switch_forward', 0x5F, 1, partial(boolean, 2)), 324 | ('tx_limit_switch_reverse', 0x60, 1, partial(boolean, 2)), 325 | # RX 326 | ('rx_config', 0x3E, 1, None), 327 | ('rx_pin_function', 0x3E, 1, partial(bit_range, 0, 3)), 328 | ('rx_enable_analog', 0x3E, 1, partial(boolean, 6)), 329 | ('rx_active_high', 0x36, 1, partial(boolean, 3)), 330 | ('rx_kill_switch', 0x5D, 1, partial(boolean, 3)), 331 | ('rx_limit_switch_forward', 0x5F, 1, partial(boolean, 3)), 332 | ('rx_limit_switch_reverse', 0x60, 1, partial(boolean, 3)), 333 | # RC 334 | ('rc_config', 0x3F, 1, None), 335 | ('rc_active_high', 0x36, 1, partial(boolean, 4)), 336 | ('rc_kill_switch', 0x5D, 1, partial(boolean, 4)), 337 | ('rc_limit_switch_forward', 0x5F, 1, partial(boolean, 4)), 338 | ('rc_limit_switch_reverse', 0x60, 1, partial(boolean, 4)), 339 | 340 | # Motor --------------------------------------------------------- 341 | ('invert_motor_direction', 0x1B, 1, partial(boolean, 0)), 342 | ('max_speed', 0x47, 4, unsigned_int), 343 | ('starting_speed', 0x43, 4, unsigned_int), 344 | ('max_acceleration', 0x4F, 4, unsigned_int), 345 | ('max_deceleration', 0x4B, 4, unsigned_int), 346 | ('step_mode', 0x41, 1, unsigned_int), 347 | ('current_limit', 0x40, 1, unsigned_int), 348 | ('decay_mode', 0x42, 1, unsigned_int), 349 | 350 | # Homing -------------------------------------------------------- 351 | ('auto_homing', 0x02, 1, partial(boolean, 1)), 352 | ('auto_homing_forward', 0x03, 1, partial(boolean, 2)), 353 | ('homing_speed_towards', 0x61, 4, unsigned_int), 354 | ('homing_speed_away', 0x65, 4, unsigned_int), 355 | 356 | # T249-only ----------------------------------------------------- 357 | ('agc_mode', 0x6C, 1, unsigned_int), 358 | ('agc_bottom_current_limit', 0x6D, 1, unsigned_int), 359 | ('agc_current_boost_steps', 0x6E, 1, unsigned_int), 360 | ('agc_frequency_limit', 0x6F, 1, unsigned_int), 361 | 362 | # 36v4-only ----------------------------------------------------- 363 | ('hp_enable_unrestricted_current_limits', 0x6C, 1, partial(boolean, 0)), 364 | ('hp_fixed_off_time', 0xF6, 1, unsigned_int), 365 | ('hp_current_trip_blanking_time', 0xF8, 1, unsigned_int), 366 | ('hp_enable_adaptive_blanking_time', 0xF9, 1, partial(boolean, 0)), 367 | ('hp_mixed_decay_transition_time', 0xFA, 1, unsigned_int), 368 | ('hp_decay_mode', 0xFB, 1, unsigned_int), 369 | ] 370 | 371 | 372 | class Settings(object): 373 | """ 374 | Class used to manage the Tic's settings. 375 | """ 376 | 377 | def __init__(self, tic): 378 | self.tic = tic 379 | for setting in SETTINGS: 380 | name, offset, length, format_response = setting 381 | setattr( 382 | self, 383 | 'get_' + name, 384 | partial(self.tic._block_read, GET_SETTING_CMD, offset, length, format_response)) 385 | 386 | def get_serial_device_number(self): 387 | """ 388 | Gets the serial device number from two separate bytes in the Tic's settings. 389 | """ 390 | lower = self.tic._block_read(GET_SETTING_CMD, 0x07, 1) 391 | upper = self.tic._block_read(GET_SETTING_CMD, 0x69, 1) 392 | lower = bit_range(0, 6, lower) 393 | upper = bit_range(0, 6, upper) 394 | return (lower & 0x7F) | ((upper & 0x7F) << 7) 395 | 396 | def get_serial_alt_device_number(self): 397 | """ 398 | Gets the alternative serial device number from two separate bytes in the Tic's settings. 399 | """ 400 | lower = self.tic._block_read(GET_SETTING_CMD, 0x6A, 1) 401 | upper = self.tic._block_read(GET_SETTING_CMD, 0x6B, 1) 402 | lower = bit_range(0, 6, lower) 403 | upper = bit_range(0, 6, upper) 404 | return (lower & 0x7F) | ((upper & 0x7F) << 7) 405 | 406 | def get_all(self): 407 | """ 408 | Returns all of the Tic's settings and their values. 409 | """ 410 | result = {} 411 | for setting in SETTINGS: 412 | name, _, _, _ = setting 413 | result[name] = getattr(self, 'get_' + name)() 414 | result['serial_device_number'] = self.get_serial_device_number() 415 | result['serial_alt_device_number'] = self.get_serial_alt_device_number() 416 | return result 417 | 418 | 419 | # Main classes ----------------------------------------------- 420 | 421 | class TicBase(object): 422 | 423 | def __init__(self): 424 | self._define_commands() 425 | self._define_variables() 426 | self.settings = Settings(self) 427 | 428 | def _send_command(self, command_code, format, value=None): 429 | """ 430 | Sends command to the Tic. Must be defined by child classes. 431 | """ 432 | raise NotImplementedError 433 | 434 | def _block_read(self, command_code, offset, length, format_response=None): 435 | """ 436 | Returns the value of the specified variable or setting. Must be defined by child classes. 437 | """ 438 | raise NotImplementedError 439 | 440 | def _define_commands(self): 441 | """ 442 | Defines methods for all Tic commands. 443 | """ 444 | for command in COMMANDS: 445 | name, code, format = command 446 | setattr(self, name, partial(self._send_command, code, format)) 447 | 448 | def _define_variables(self): 449 | """ 450 | Defines methods for all Tic variables. 451 | """ 452 | self.variables = {} 453 | for variable in VARIABLES: 454 | name, offset, length, format_response = variable 455 | setattr( 456 | self, 457 | 'get_' + name, 458 | partial(self._block_read, GET_VARIABLE_CMD, offset, length, format_response)) 459 | 460 | def get_variables(self): 461 | """ 462 | Returns all Tic variables and their values. 463 | """ 464 | result = {} 465 | for variable in VARIABLES: 466 | name, _, _, _ = variable 467 | result[name] = getattr(self, 'get_' + name)() 468 | return result 469 | 470 | 471 | def _get_crc_7(message): 472 | """ 473 | Calculates and returns the integrity verification byte for the given message. Used only by the serial controller. 474 | For more details, see section "Cyclic Redundancy Check (CRC) error detection" at https://www.pololu.com/docs/0J71/9 475 | """ 476 | crc = 0 477 | for i in range(len(message)): 478 | crc ^= message[i] 479 | for j in range(8): 480 | if crc & 1: 481 | crc ^= 0x91 482 | crc >>= 1 483 | return crc.to_bytes(1, 'little') 484 | 485 | 486 | class TicSerial(TicBase): 487 | """ 488 | Serial driver for Tic stepper motor controllers. 489 | Reference: https://www.pololu.com/docs/0J71/9 490 | """ 491 | 492 | def __init__(self, port, device_number=None, crc_for_commands=False, crc_for_responses=False): 493 | self.port = port 494 | self.device_number = device_number 495 | self.crc_for_commands = crc_for_commands 496 | self.crc_for_responses = crc_for_responses 497 | super().__init__() 498 | 499 | def _send_command(self, command_code, format, value=None): 500 | """ 501 | Sends command to the Tic. 502 | """ 503 | if self.device_number is None: 504 | # Compact protocol 505 | serialized = bytes([command_code]) 506 | else: 507 | # Pololu protocol 508 | serialized = bytes([0xAA, self.device_number, command_code & 0x7F]) 509 | 510 | # Format the command parameter 511 | if format == SEVEN_BITS: 512 | serialized += bytes([value]) 513 | elif format == THIRTY_TWO_BITS: 514 | serialized += bytes([ 515 | ((value >> 7) & 1) | ((value >> 14) & 2) | ((value >> 21) & 4) | ((value >> 28) & 8), 516 | value >> 0 & 0x7F, 517 | value >> 8 & 0x7F, 518 | value >> 16 & 0x7F, 519 | value >> 24 & 0x7F 520 | ]) 521 | elif format == BLOCK_READ: 522 | serialized += value 523 | 524 | # Add CRC byte if required 525 | if self.crc_for_commands: 526 | serialized += _get_crc_7(serialized) 527 | 528 | # Write command to the bus 529 | self.port.write(serialized) 530 | 531 | def _block_read(self, command_code, offset, length, format_response=None): 532 | """ 533 | Returns the value of the specified variable or setting from the Tic. 534 | """ 535 | # Send command requesting the value 536 | if offset >= 128: 537 | # To access offsets between 128 and 255, we must set bit 6 of the length byte. 538 | # See https://www.pololu.com/docs/0J71/9 for more details 539 | self._send_command(command_code, BLOCK_READ, bytes([offset - 128, length + 64])) 540 | else: 541 | self._send_command(command_code, BLOCK_READ, bytes([offset, length])) 542 | # Read the returned value 543 | result = self._read_response(length) 544 | # Verify and format the returned value 545 | if result is None: 546 | raise RuntimeError("Read response returned 'None', read likely timed out") 547 | elif len(result) != length: 548 | raise RuntimeError("Expected to read {} bytes, got {}.".format(length, len(result))) 549 | if format_response is None: 550 | return result 551 | else: 552 | return format_response(result) 553 | 554 | def _read_response(self, length): 555 | """ 556 | Reads and returns the variable or setting returned by the Tic. 557 | """ 558 | if self.crc_for_responses: 559 | # Read extra byte at the end of the response 560 | response = self.port.read(length + 1) 561 | if len(response) != length + 1: 562 | raise RuntimeError("Response does not contain CRC byte") 563 | # Extract the main message and CRC from the response 564 | message = response[0:length] 565 | # Oddly, response[-1] returns an int instead of a byte, so we use this more convoluted notation: 566 | crc = int.to_bytes( 567 | response[-1], 1, 'little') 568 | # Verify that the CRC byte returned by the Tic is correct 569 | if crc == _get_crc_7(message): 570 | # Success: the CRC byte is correct 571 | return message 572 | else: 573 | raise RuntimeError("Response CRC check failed") 574 | else: 575 | return self.port.read(length) 576 | 577 | 578 | class TicI2C(TicBase): 579 | """ 580 | I2C driver for Tic stepper motor controllers. 581 | Reference: https://www.pololu.com/docs/0J71/10 582 | """ 583 | 584 | def __init__(self, backend): 585 | self.backend = backend 586 | super().__init__() 587 | 588 | def _send_command(self, command_code, format, value=None): 589 | """ 590 | Sends command to the Tic. 591 | """ 592 | serialized = bytes([command_code]) 593 | # Format the command parameter 594 | if format == SEVEN_BITS: 595 | serialized += bytes([value]) 596 | elif format == THIRTY_TWO_BITS: 597 | serialized += bytes([ 598 | value >> 0 & 0xFF, 599 | value >> 8 & 0xFF, 600 | value >> 16 & 0xFF, 601 | value >> 24 & 0xFF 602 | ]) 603 | elif format == BLOCK_READ: 604 | serialized += value 605 | # Write command to the bus 606 | self.backend.write(serialized) 607 | 608 | def _block_read(self, command_code, offset, length, format_response=None): 609 | """ 610 | Returns the value of the specified variable or setting from the Tic. 611 | """ 612 | self._send_command(command_code, BLOCK_READ, bytes([offset, length])) 613 | result = self.backend.read(length) 614 | if len(result) != length: 615 | raise RuntimeError("Expected to read {} bytes, got {}.".format(length, len(result))) 616 | if format_response is None: 617 | return result 618 | else: 619 | return format_response(result) 620 | 621 | 622 | class TicUSB(TicBase): 623 | """ 624 | USB driver for Tic stepper motor controllers. 625 | Reference: https://www.pololu.com/docs/0J71/11 626 | """ 627 | 628 | def __init__(self, product=None, serial_number=None): 629 | if usb_core is None: 630 | raise Exception("Missing dependency: pyusb") 631 | params = dict(idVendor=VENDOR) 632 | if product is not None: 633 | params['idProduct'] = product 634 | if serial_number is not None: 635 | params['serial_number'] = serial_number 636 | self.usb = usb_core.find(**params) 637 | if self.usb is None: 638 | raise Exception('USB device not found') 639 | self.usb.set_configuration() 640 | super().__init__() 641 | 642 | def _send_command(self, command_code, format, value=None): 643 | """ 644 | Sends command to the Tic. 645 | """ 646 | if format == QUICK: 647 | self.usb.ctrl_transfer(0x40, command_code, 0, 0, 0) 648 | elif format == SEVEN_BITS: 649 | self.usb.ctrl_transfer(0x40, command_code, value, 0, 0) 650 | elif format == THIRTY_TWO_BITS: 651 | self.usb.ctrl_transfer(0x40, command_code, value & 0xFFFF, value >> 16 & 0xFFFF, 0) 652 | 653 | def _block_read(self, command_code, offset, length, format_response=None): 654 | """ 655 | Returns the value of the specified variable or setting from the Tic. 656 | """ 657 | result = self.usb.ctrl_transfer(0xC0, command_code, 0, offset, length) 658 | if len(result) != length: 659 | raise RuntimeError("Expected to read {} bytes, got {}.".format(length, len(result))) 660 | if format_response is None: 661 | return result 662 | else: 663 | return format_response(result) -------------------------------------------------------------------------------- /src/ticlib/ticlib.pyi: -------------------------------------------------------------------------------- 1 | # BSD 2-Clause License 2 | # 3 | # Copyright (c) 2021, Julien Phalip 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are met: 8 | # 9 | # 1. Redistributions of source code must retain the above copyright notice, 10 | # this list of conditions and the following disclaimer. 11 | # 12 | # 2. Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation 14 | # and/or other materials provided with the distribution. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | from typing import Any, Optional, Dict 28 | 29 | __all__ = [ 30 | "TicSerial", "TicI2C", "TicUSB", 31 | "SMBus2Backend", "MachineI2CBackend", 32 | "TIC_T825", "TIC_T834", "TIC_T500", "TIC_N825", "TIC_T249", "TIC_36v4" 33 | ] 34 | 35 | VENDOR: int 36 | TIC_T825: int 37 | TIC_T834: int 38 | TIC_T500: int 39 | TIC_N825: int 40 | TIC_T249: int 41 | TIC_36v4: int 42 | 43 | QUICK: str 44 | THIRTY_TWO_BITS: str 45 | SEVEN_BITS: str 46 | BLOCK_READ: str 47 | 48 | COMMANDS: list[tuple[str, int, str]] 49 | VARIABLES: list[tuple[str, int, int, Any]] 50 | SETTINGS: list[tuple[str, int, int, Any]] 51 | 52 | GET_VARIABLE_CMD: int 53 | GET_SETTING_CMD: int 54 | 55 | 56 | def partial(function: Any, *args: Any) -> Any: 57 | """ 58 | **Description**: MicroPython fallback for functools.partial. 59 | """ 60 | ... 61 | 62 | 63 | def boolean(bit_index: int, value: bytes) -> bool: 64 | """ 65 | **Description**: Returns True if the bit located at `bit_index` in `value` is set. 66 | 67 | The least-significant bit is index 0. 68 | """ 69 | ... 70 | 71 | 72 | def bit_range(start: int, end: int, value: bytes) -> int: 73 | """ 74 | **Description**: Returns the bits between `start` and `end` in `value`. 75 | """ 76 | ... 77 | 78 | 79 | def signed_int(value: bytes) -> int: 80 | """ 81 | **Description**: Interprets `value` (in little-endian order) as a signed integer. 82 | """ 83 | ... 84 | 85 | 86 | def unsigned_int(value: bytes) -> int: 87 | """ 88 | **Description**: Interprets `value` (in little-endian order) as an unsigned integer. 89 | """ 90 | ... 91 | 92 | 93 | def _get_crc_7(message: bytes) -> bytes: 94 | """ 95 | **Description**: Calculates the 7-bit CRC for `message` per Pololu's documentation. 96 | """ 97 | ... 98 | 99 | 100 | class MachineI2CBackend: 101 | """ 102 | **Description**: I2C backend that wraps `machine.I2C`. 103 | """ 104 | 105 | def __init__(self, i2c: Any, address: int) -> None: ... 106 | def read(self, length: int) -> bytes: ... 107 | def write(self, serialized: bytes) -> None: ... 108 | 109 | 110 | class SMBus2Backend: 111 | """ 112 | **Description**: I2C backend that uses the smbus2 library. 113 | """ 114 | 115 | def __init__(self, bus: Any, address: int) -> None: ... 116 | def read(self, length: int) -> bytes: ... 117 | def write(self, serialized: bytes) -> None: ... 118 | 119 | 120 | # 121 | # Settings class – for reading non-volatile settings. 122 | # 123 | class Settings: 124 | """ 125 | **Description**: Class used to manage the Tic's settings. 126 | Dynamically adds `get_()` methods using partial(). 127 | """ 128 | 129 | def __init__(self, tic: Any) -> None: 130 | """ 131 | **Description**: For each setting in SETTINGS, sets an attribute 132 | `get_` that calls `tic._block_read(...)`. 133 | """ 134 | ... 135 | 136 | def get_serial_device_number(self) -> int: 137 | """ 138 | **Description**: Retrieves the serial device number from two separate bytes. 139 | """ 140 | ... 141 | 142 | def get_serial_alt_device_number(self) -> int: 143 | """ 144 | **Description**: Retrieves the alternative serial device number from two separate bytes. 145 | """ 146 | ... 147 | 148 | def get_all(self) -> dict[str, Any]: 149 | """ 150 | **Description**: Returns a dictionary of all settings (name -> value). 151 | """ 152 | ... 153 | 154 | def get_control_mode(self) -> int: 155 | """ 156 | **Description**: Determines the Tic's control mode. 157 | 158 | **Offset**: 0x01 159 | 160 | **Type**: unsigned 8-bit 161 | 162 | **Data**: 163 | 0: Serial / I2C / USB 164 | 1: STEP/DIR 165 | 2: RC position 166 | 3: RC speed 167 | 4: Analog position 168 | 5: Analog speed 169 | 6: Encoder position 170 | 7: Encoder speed 171 | 172 | **Default**: 0 (Serial / I2C / USB) 173 | """ 174 | ... 175 | 176 | def get_disable_safe_start(self) -> bool: 177 | """ 178 | **Description**: Disables the safe start feature if set. 179 | 180 | **Offset**: Bit 0 of byte 0x03 181 | 182 | **Type**: boolean 183 | 184 | **Default**: false 185 | """ 186 | ... 187 | 188 | def get_ignore_err_line_high(self) -> bool: 189 | """ 190 | **Description**: Disables the "ERR line high" error if set. 191 | 192 | **Offset**: Bit 0 of byte 0x04 193 | 194 | **Type**: boolean 195 | 196 | **Default**: false 197 | """ 198 | ... 199 | 200 | def get_auto_clear_driver_error(self) -> bool: 201 | """ 202 | **Description**: When enabled, the Tic periodically clears latched driver errors. 203 | 204 | **Offset**: Bit 0 of byte 0x08 205 | 206 | **Type**: boolean 207 | 208 | **Default**: true 209 | """ 210 | ... 211 | 212 | def get_never_sleep(self) -> bool: 213 | """ 214 | **Description**: Prevents the Tic from sleeping if USB power is present but VIN is not. 215 | 216 | **Offset**: Bit 0 of byte 0x02 217 | 218 | **Type**: boolean 219 | 220 | **Default**: false 221 | """ 222 | ... 223 | 224 | def get_vin_calibration(self) -> int: 225 | """ 226 | **Description**: Adjusts scaling of VIN readings (positive increases, negative decreases). 227 | 228 | **Offset**: 0x14 229 | 230 | **Type**: signed 16-bit 231 | 232 | **Default**: 0 233 | 234 | **Range**: -500 to +500 235 | """ 236 | ... 237 | 238 | def get_soft_error_response(self) -> int: 239 | """ 240 | **Description**: Sets the soft error response behavior. 241 | 242 | **Offset**: 0x53 243 | 244 | **Type**: unsigned 8-bit 245 | 246 | **Data**: 247 | 0: De-energize 248 | 1: Halt and hold 249 | 2: Decelerate to hold 250 | 3: Go to position 251 | 252 | **Default**: 2 (Decelerate to hold) 253 | """ 254 | ... 255 | 256 | def get_soft_error_position(self) -> int: 257 | """ 258 | **Description**: Position to move to if soft error response is "Go to position". 259 | 260 | **Offset**: 0x54 261 | 262 | **Type**: signed 32-bit 263 | 264 | **Default**: 0 265 | 266 | **Range**: -2,147,483,648 to 2,147,483,647 267 | """ 268 | ... 269 | 270 | def get_current_limit_during_error(self) -> int: 271 | """ 272 | **Description**: Uses a different current limit during errors, unless set to 0xFF. 273 | 274 | **Offset**: 0x31 275 | 276 | **Type**: unsigned 8-bit (or 0xFF to disable) 277 | 278 | **Default**: 0 279 | 280 | **Range**: 0 to the normal current limit (or disabled) 281 | """ 282 | ... 283 | 284 | def get_serial_baud_rate(self) -> int: 285 | """ 286 | **Description**: The serial baud rate (in bits per second), stored as a 16-bit generator value. 287 | 288 | **Offset**: 0x06 289 | 290 | **Type**: unsigned 16-bit 291 | 292 | **Default**: 9600 293 | 294 | **Range**: 200 to 115385 295 | """ 296 | ... 297 | 298 | def get_serial_enable_alt_device_number(self) -> bool: 299 | """ 300 | **Description**: If set, the Tic listens to an alternate device number. 301 | 302 | **Offset**: Bit 7 of byte 0x6A 303 | 304 | **Type**: boolean 305 | 306 | **Default**: false 307 | """ 308 | ... 309 | 310 | def get_serial_14bit_device_number(self) -> bool: 311 | """ 312 | **Description**: Allows device numbers up to 16383 when enabled. 313 | 314 | **Offset**: Bit 3 of byte 0x0B 315 | 316 | **Type**: boolean 317 | 318 | **Default**: false 319 | """ 320 | ... 321 | 322 | def get_serial_response_delay(self) -> int: 323 | """ 324 | **Description**: Minimum delay (in microseconds) before a serial response or I2C processing. 325 | 326 | **Offset**: 0x5E 327 | 328 | **Type**: unsigned 8-bit 329 | 330 | **Default**: 0 331 | 332 | **Range**: 0 to 255 333 | """ 334 | ... 335 | 336 | def get_serial_command_timeout(self) -> int: 337 | """ 338 | **Description**: Timeout (in ms) before a "command timeout" error is flagged. 339 | 340 | **Offset**: 0x09 341 | 342 | **Type**: unsigned 16-bit 343 | 344 | **Default**: 1000 345 | 346 | **Range**: 0 to 60000 347 | """ 348 | ... 349 | 350 | def get_serial_crc_for_commands(self) -> bool: 351 | """ 352 | **Description**: Requires a 7-bit CRC on incoming serial commands if true. 353 | 354 | **Offset**: Bit 0 of byte 0x0B 355 | 356 | **Type**: boolean 357 | 358 | **Default**: false 359 | """ 360 | ... 361 | 362 | def get_serial_crc_for_responses(self) -> bool: 363 | """ 364 | **Description**: Appends a 7-bit CRC to serial responses if true. 365 | 366 | **Offset**: Bit 1 of byte 0x0B 367 | 368 | **Type**: boolean 369 | 370 | **Default**: false 371 | """ 372 | ... 373 | 374 | def get_serial_7bit_responses(self) -> bool: 375 | """ 376 | **Description**: Encodes serial responses with 7-bit bytes only if true. 377 | 378 | **Offset**: Bit 2 of byte 0x0B 379 | 380 | **Type**: boolean 381 | 382 | **Default**: false 383 | """ 384 | ... 385 | 386 | def get_encoder_prescaler(self) -> int: 387 | """ 388 | **Description**: For encoder modes, number of encoder counts per stepper unit. 389 | 390 | **Offset**: 0x58 391 | 392 | **Type**: unsigned 32-bit 393 | 394 | **Default**: 1 395 | 396 | **Range**: 1 to 2,147,483,647 397 | """ 398 | ... 399 | 400 | def get_encoder_postscaler(self) -> int: 401 | """ 402 | **Description**: For encoder modes, size of each stepper unit. 403 | 404 | **Offset**: 0x37 405 | 406 | **Type**: unsigned 32-bit 407 | 408 | **Default**: 1 409 | 410 | **Range**: 1 to 2,147,483,647 411 | """ 412 | ... 413 | 414 | def get_encoder_unlimited(self) -> bool: 415 | """ 416 | **Description**: Enables unbounded position control if set. 417 | 418 | **Offset**: Bit 0 of 0x5C 419 | 420 | **Type**: boolean 421 | 422 | **Default**: false 423 | """ 424 | ... 425 | 426 | def get_input_averaging_enabled(self) -> bool: 427 | """ 428 | **Description**: Enables averaging for RC/analog inputs. 429 | 430 | **Offset**: Bit 0 of 0x2E 431 | 432 | **Type**: boolean 433 | 434 | **Default**: true 435 | """ 436 | ... 437 | 438 | def get_input_hysteresis(self) -> int: 439 | """ 440 | **Description**: Amount of hysteresis to apply to RC/analog inputs. 441 | 442 | **Offset**: 0x2F 443 | 444 | **Type**: unsigned 16-bit 445 | 446 | **Default**: 0 447 | 448 | **Range**: 0 to 65535 449 | """ 450 | ... 451 | 452 | def get_input_invert(self) -> bool: 453 | """ 454 | **Description**: Inverts input direction for RC/analog modes if true. 455 | 456 | **Offset**: 0x21 457 | 458 | **Type**: boolean 459 | 460 | **Default**: false 461 | """ 462 | ... 463 | 464 | def get_input_max(self) -> int: 465 | """ 466 | **Description**: Maximum value for RC/analog input scaling. 467 | 468 | **Offset**: 0x28 469 | 470 | **Type**: unsigned 16-bit 471 | 472 | **Default**: 4095 473 | 474 | **Range**: 0 to 4095 475 | """ 476 | ... 477 | 478 | def get_output_max(self) -> int: 479 | """ 480 | **Description**: Maximum target value for RC/analog scaling. 481 | 482 | **Offset**: 0x32 483 | 484 | **Type**: signed 32-bit 485 | 486 | **Default**: 200 487 | 488 | **Range**: 0 to 2,147,483,647 489 | """ 490 | ... 491 | 492 | def get_input_neutral_max(self) -> int: 493 | """ 494 | **Description**: Neutral zone upper bound for RC/analog inputs. 495 | 496 | **Offset**: 0x26 497 | 498 | **Type**: unsigned 16-bit 499 | 500 | **Default**: 2080 501 | 502 | **Range**: 0 to 4095 503 | """ 504 | ... 505 | 506 | def get_input_neutral_min(self) -> int: 507 | """ 508 | **Description**: Neutral zone lower bound for RC/analog inputs. 509 | 510 | **Offset**: 0x24 511 | 512 | **Type**: unsigned 16-bit 513 | 514 | **Default**: 2015 515 | 516 | **Range**: 0 to 4095 517 | """ 518 | ... 519 | 520 | def get_input_min(self) -> int: 521 | """ 522 | **Description**: Minimum value for RC/analog input scaling. 523 | 524 | **Offset**: 0x22 525 | 526 | **Type**: unsigned 16-bit 527 | 528 | **Default**: 0 529 | 530 | **Range**: 0 to 4095 531 | """ 532 | ... 533 | 534 | def get_output_min(self) -> int: 535 | """ 536 | **Description**: Minimum target value for RC/analog scaling. 537 | 538 | **Offset**: 0x2A 539 | 540 | **Type**: signed 32-bit 541 | 542 | **Default**: -200 543 | 544 | **Range**: -2,147,483,647 to 0 545 | """ 546 | ... 547 | 548 | def get_input_scaling_degree(self) -> int: 549 | """ 550 | **Description**: Determines the polynomial degree for scaling RC/analog inputs. 551 | 552 | **Offset**: 0x20 553 | 554 | **Type**: unsigned 8-bit 555 | 556 | **Data**: 557 | 0: linear 558 | 1: quadratic 559 | 2: cubic 560 | 561 | **Default**: 0 (linear) 562 | """ 563 | ... 564 | 565 | def get_scl_config(self) -> int: 566 | """ 567 | **Description**: Pin configuration for the SCL line. 568 | 569 | **Offset**: 0x3B 570 | 571 | **Type**: 8-bit 572 | 573 | **Default**: 0 574 | """ 575 | ... 576 | 577 | def get_scl_pin_function(self) -> int: 578 | """ 579 | **Description**: Extracted pin function from SCL configuration. 580 | 581 | **Offset**: 0x3B 582 | 583 | **Type**: bits [0..3] 584 | 585 | **Default**: 0 586 | 587 | **Data**: 588 | 0: Default 589 | 1: User I/O 590 | 2: User input 591 | 3: Potentiometer power 592 | 4: SCL 593 | 7: Kill switch 594 | 8: Limit switch forward 595 | 9: Limit switch reverse 596 | """ 597 | ... 598 | 599 | def get_scl_enable_analog(self) -> bool: 600 | """ 601 | **Description**: Whether analog readings are enabled on SCL. 602 | 603 | **Offset**: Bit 6 of 0x3B 604 | 605 | **Type**: boolean 606 | 607 | **Default**: false 608 | """ 609 | ... 610 | 611 | def get_scl_enable_pull_up(self) -> bool: 612 | """ 613 | **Description**: Whether internal pull-up is enabled on SCL. 614 | 615 | **Offset**: Bit 7 of 0x3B 616 | 617 | **Type**: boolean 618 | 619 | **Default**: false 620 | """ 621 | ... 622 | 623 | def get_scl_active_high(self) -> bool: 624 | """ 625 | **Description**: Determines if SCL switch is active high. 626 | 627 | **Offset**: Bit 0 of 0x36 628 | 629 | **Type**: boolean 630 | 631 | **Default**: false 632 | """ 633 | ... 634 | 635 | def get_scl_kill_switch(self) -> bool: 636 | """ 637 | **Description**: Indicates if SCL is mapped as a kill switch. 638 | 639 | **Offset**: Bit 0 of 0x5D 640 | 641 | **Type**: boolean 642 | 643 | **Default**: false 644 | """ 645 | ... 646 | 647 | def get_scl_limit_switch_forward(self) -> bool: 648 | """ 649 | **Description**: Indicates if SCL is used as a forward limit switch. 650 | 651 | **Offset**: Bit 0 of 0x5F 652 | 653 | **Type**: boolean 654 | 655 | **Default**: false 656 | """ 657 | ... 658 | 659 | def get_scl_limit_switch_reverse(self) -> bool: 660 | """ 661 | **Description**: Indicates if SCL is used as a reverse limit switch. 662 | 663 | **Offset**: Bit 0 of 0x60 664 | 665 | **Type**: boolean 666 | 667 | **Default**: false 668 | """ 669 | ... 670 | 671 | def get_sda_config(self) -> int: 672 | """ 673 | **Description**: Pin configuration for the SDA line. 674 | 675 | **Offset**: 0x3C 676 | 677 | **Type**: 8-bit 678 | 679 | **Default**: 0 680 | """ 681 | ... 682 | 683 | def get_sda_pin_function(self) -> int: 684 | """ 685 | **Description**: Extracted pin function for SDA. 686 | 687 | **Offset**: 0x3C 688 | 689 | **Type**: bits [0..3] 690 | 691 | **Default**: 0 692 | """ 693 | ... 694 | 695 | def get_sda_enable_analog(self) -> bool: 696 | """ 697 | **Description**: Whether analog readings are enabled on SDA. 698 | 699 | **Offset**: Bit 6 of 0x3C 700 | 701 | **Type**: boolean 702 | 703 | **Default**: false 704 | """ 705 | ... 706 | 707 | def get_sda_enable_pull_up(self) -> bool: 708 | """ 709 | **Description**: Whether internal pull-up is enabled on SDA. 710 | 711 | **Offset**: Bit 7 of 0x3C 712 | 713 | **Type**: boolean 714 | 715 | **Default**: false 716 | """ 717 | ... 718 | 719 | def get_sda_active_high(self) -> bool: 720 | """ 721 | **Description**: Determines if SDA switch is active high. 722 | 723 | **Offset**: Bit 1 of 0x36 724 | 725 | **Type**: boolean 726 | 727 | **Default**: false 728 | """ 729 | ... 730 | 731 | def get_sda_kill_switch(self) -> bool: 732 | """ 733 | **Description**: Indicates if SDA is mapped as a kill switch. 734 | 735 | **Offset**: Bit 1 of 0x5D 736 | 737 | **Type**: boolean 738 | 739 | **Default**: false 740 | """ 741 | ... 742 | 743 | def get_sda_limit_switch_forward(self) -> bool: 744 | """ 745 | **Description**: Indicates if SDA is used as a forward limit switch. 746 | 747 | **Offset**: Bit 1 of 0x5F 748 | 749 | **Type**: boolean 750 | 751 | **Default**: false 752 | """ 753 | ... 754 | 755 | def get_sda_limit_switch_reverse(self) -> bool: 756 | """ 757 | **Description**: Indicates if SDA is used as a reverse limit switch. 758 | 759 | **Offset**: Bit 1 of 0x60 760 | 761 | **Type**: boolean 762 | 763 | **Default**: false 764 | """ 765 | ... 766 | 767 | def get_tx_config(self) -> int: 768 | """ 769 | **Description**: Pin configuration for the TX line. 770 | 771 | **Offset**: 0x3D 772 | 773 | **Type**: 8-bit 774 | 775 | **Default**: 0 776 | """ 777 | ... 778 | 779 | def get_tx_pin_function(self) -> int: 780 | """ 781 | **Description**: Extracted pin function for TX. 782 | 783 | **Offset**: 0x3D 784 | 785 | **Type**: bits [0..3] 786 | 787 | **Default**: 0 788 | """ 789 | ... 790 | 791 | def get_tx_enable_analog(self) -> bool: 792 | """ 793 | **Description**: Whether analog readings are enabled on TX. 794 | 795 | **Offset**: Bit 6 of 0x3D 796 | 797 | **Type**: boolean 798 | 799 | **Default**: false 800 | """ 801 | ... 802 | 803 | def get_tx_active_high(self) -> bool: 804 | """ 805 | **Description**: Determines if TX switch is active high. 806 | 807 | **Offset**: Bit 2 of 0x36 808 | 809 | **Type**: boolean 810 | 811 | **Default**: false 812 | """ 813 | ... 814 | 815 | def get_tx_kill_switch(self) -> bool: 816 | """ 817 | **Description**: Indicates if TX is mapped as a kill switch. 818 | 819 | **Offset**: Bit 2 of 0x5D 820 | 821 | **Type**: boolean 822 | 823 | **Default**: false 824 | """ 825 | ... 826 | 827 | def get_tx_limit_switch_forward(self) -> bool: 828 | """ 829 | **Description**: Indicates if TX is used as a forward limit switch. 830 | 831 | **Offset**: Bit 2 of 0x5F 832 | 833 | **Type**: boolean 834 | 835 | **Default**: false 836 | """ 837 | ... 838 | 839 | def get_tx_limit_switch_reverse(self) -> bool: 840 | """ 841 | **Description**: Indicates if TX is used as a reverse limit switch. 842 | 843 | **Offset**: Bit 2 of 0x60 844 | 845 | **Type**: boolean 846 | 847 | **Default**: false 848 | """ 849 | ... 850 | 851 | def get_rx_config(self) -> int: 852 | """ 853 | **Description**: Pin configuration for the RX line. 854 | 855 | **Offset**: 0x3E 856 | 857 | **Type**: 8-bit 858 | 859 | **Default**: 0 860 | """ 861 | ... 862 | 863 | def get_rx_pin_function(self) -> int: 864 | """ 865 | **Description**: Extracted pin function for RX. 866 | 867 | **Offset**: 0x3E 868 | 869 | **Type**: bits [0..3] 870 | 871 | **Default**: 0 872 | """ 873 | ... 874 | 875 | def get_rx_enable_analog(self) -> bool: 876 | """ 877 | **Description**: Whether analog readings are enabled on RX. 878 | 879 | **Offset**: Bit 6 of 0x3E 880 | 881 | **Type**: boolean 882 | 883 | **Default**: false 884 | """ 885 | ... 886 | 887 | def get_rx_active_high(self) -> bool: 888 | """ 889 | **Description**: Determines if RX switch is active high. 890 | 891 | **Offset**: Bit 3 of 0x36 892 | 893 | **Type**: boolean 894 | 895 | **Default**: false 896 | """ 897 | ... 898 | 899 | def get_rx_kill_switch(self) -> bool: 900 | """ 901 | **Description**: Indicates if RX is mapped as a kill switch. 902 | 903 | **Offset**: Bit 3 of 0x5D 904 | 905 | **Type**: boolean 906 | 907 | **Default**: false 908 | """ 909 | ... 910 | 911 | def get_rx_limit_switch_forward(self) -> bool: 912 | """ 913 | **Description**: Indicates if RX is used as a forward limit switch. 914 | 915 | **Offset**: Bit 3 of 0x5F 916 | 917 | **Type**: boolean 918 | 919 | **Default**: false 920 | """ 921 | ... 922 | 923 | def get_rx_limit_switch_reverse(self) -> bool: 924 | """ 925 | **Description**: Indicates if RX is used as a reverse limit switch. 926 | 927 | **Offset**: Bit 3 of 0x60 928 | 929 | **Type**: boolean 930 | 931 | **Default**: false 932 | """ 933 | ... 934 | 935 | def get_rc_config(self) -> int: 936 | """ 937 | **Description**: Pin configuration for the RC line (pulse input). 938 | 939 | **Offset**: 0x3F 940 | 941 | **Type**: 8-bit 942 | 943 | **Default**: 0 944 | """ 945 | ... 946 | 947 | def get_rc_active_high(self) -> bool: 948 | """ 949 | **Description**: Determines if RC switch is active high. 950 | 951 | **Offset**: Bit 4 of 0x36 952 | 953 | **Type**: boolean 954 | 955 | **Default**: false 956 | """ 957 | ... 958 | 959 | def get_rc_kill_switch(self) -> bool: 960 | """ 961 | **Description**: Indicates if RC is mapped as a kill switch. 962 | 963 | **Offset**: Bit 4 of 0x5D 964 | 965 | **Type**: boolean 966 | 967 | **Default**: false 968 | """ 969 | ... 970 | 971 | def get_rc_limit_switch_forward(self) -> bool: 972 | """ 973 | **Description**: Indicates if RC is used as a forward limit switch. 974 | 975 | **Offset**: Bit 4 of 0x5F 976 | 977 | **Type**: boolean 978 | 979 | **Default**: false 980 | """ 981 | ... 982 | 983 | def get_rc_limit_switch_reverse(self) -> bool: 984 | """ 985 | **Description**: Indicates if RC is used as a reverse limit switch. 986 | 987 | **Offset**: Bit 4 of 0x60 988 | 989 | **Type**: boolean 990 | 991 | **Default**: false 992 | """ 993 | ... 994 | 995 | def get_invert_motor_direction(self) -> bool: 996 | """ 997 | **Description**: Reverses the motor direction if set. 998 | 999 | **Offset**: Bit 0 of 0x1B 1000 | 1001 | **Type**: boolean 1002 | 1003 | **Default**: false 1004 | """ 1005 | ... 1006 | 1007 | def get_max_speed(self) -> int: 1008 | """ 1009 | **Description**: The default maximum motor speed, which can be overridden. 1010 | 1011 | **Offset**: 0x47 1012 | 1013 | **Type**: unsigned 32-bit 1014 | 1015 | **Default**: 2000000 1016 | 1017 | **Range**: 0 to 500000000 1018 | 1019 | **Units**: microsteps per 10000 s 1020 | """ 1021 | ... 1022 | 1023 | def get_starting_speed(self) -> int: 1024 | """ 1025 | **Description**: The default starting speed; instant acceleration/deceleration is allowed below this value. 1026 | 1027 | **Offset**: 0x43 1028 | 1029 | **Type**: unsigned 32-bit 1030 | 1031 | **Default**: 0 1032 | 1033 | **Range**: 0 to 500000000 1034 | 1035 | **Units**: microsteps per 10000 s 1036 | """ 1037 | ... 1038 | 1039 | def get_max_acceleration(self) -> int: 1040 | """ 1041 | **Description**: The default maximum acceleration; can be overridden at runtime. 1042 | 1043 | **Offset**: 0x4F 1044 | 1045 | **Type**: unsigned 32-bit 1046 | 1047 | **Default**: 40000 1048 | 1049 | **Range**: 100 to 2147483647 1050 | 1051 | **Units**: microsteps per 100 s^2 1052 | """ 1053 | ... 1054 | 1055 | def get_max_deceleration(self) -> int: 1056 | """ 1057 | **Description**: The default maximum deceleration. A value of 0 means the max acceleration is used; 1058 | values below 100 are treated as 100. 1059 | 1060 | **Offset**: 0x4B 1061 | 1062 | **Type**: unsigned 32-bit 1063 | 1064 | **Default**: 0 (uses max accel) 1065 | 1066 | **Range**: 100 to 2147483647 1067 | 1068 | **Units**: microsteps per 100 s^2 1069 | """ 1070 | ... 1071 | 1072 | def get_step_mode(self) -> int: 1073 | """ 1074 | **Description**: The default microstepping mode for settings. This value may be overridden at runtime. 1075 | 1076 | **Offset**: 0x41 1077 | 1078 | **Type**: unsigned 8-bit 1079 | 1080 | **Data**: 1081 | 0: Full step 1082 | 1: 1/2 step 1083 | 2: 1/4 step 1084 | 3: 1/8 step 1085 | 4: 1/16 step 1086 | 5: 1/32 step 1087 | 6: 1/2 step 100% (T249 only) 1088 | 7: 1/64 step (36v4) 1089 | 8: 1/128 step (36v4) 1090 | 9: 1/256 step (36v4) 1091 | 1092 | **Default**: 0 (Full step) 1093 | """ 1094 | ... 1095 | 1096 | def get_current_limit(self) -> int: 1097 | """ 1098 | **Description**: The default coil current limit in driver-specific units. 1099 | 1100 | **Offset**: 0x40 1101 | 1102 | **Type**: unsigned 8-bit 1103 | """ 1104 | ... 1105 | 1106 | def get_decay_mode(self) -> int: 1107 | """ 1108 | **Description**: The default decay mode for non-HP Tics; not used for Tic 36v4. 1109 | 1110 | **Offset**: 0x42 1111 | 1112 | **Type**: unsigned 8-bit 1113 | 1114 | **Data**: 1115 | Tic T500: 0 = Automatic 1116 | Tic T825: 0 = Mixed, 1 = Slow, 2 = Fast 1117 | Tic T834: 0 = Mixed 50%, 1 = Slow, 2 = Fast, 3 = Mixed 25%, 4 = Mixed 75% 1118 | Tic T249: 0 = Mixed 1119 | 1120 | **Default**: 0 1121 | """ 1122 | ... 1123 | 1124 | def get_auto_homing(self) -> bool: 1125 | """ 1126 | **Description**: Enables automatic homing when the position is uncertain. 1127 | 1128 | **Offset**: Bit 1 of byte 0x02 1129 | 1130 | **Type**: boolean 1131 | 1132 | **Default**: false 1133 | """ 1134 | ... 1135 | 1136 | def get_auto_homing_forward(self) -> bool: 1137 | """ 1138 | **Description**: Determines if automatic homing is performed in the forward direction. 1139 | 1140 | **Offset**: Bit 2 of byte 0x02 1141 | 1142 | **Type**: boolean 1143 | 1144 | **Default**: false 1145 | """ 1146 | ... 1147 | 1148 | def get_homing_speed_towards(self) -> int: 1149 | """ 1150 | **Description**: The speed used when homing toward the limit switch. 1151 | 1152 | **Offset**: 0x61 1153 | 1154 | **Type**: unsigned 32-bit 1155 | 1156 | **Default**: 1000000 1157 | 1158 | **Range**: 0 to 500000000 1159 | 1160 | **Units**: microsteps per 10000 s 1161 | """ 1162 | ... 1163 | 1164 | def get_homing_speed_away(self) -> int: 1165 | """ 1166 | **Description**: The speed used briefly when homing away from the limit switch. 1167 | 1168 | **Offset**: 0x65 1169 | 1170 | **Type**: unsigned 32-bit 1171 | 1172 | **Default**: 1000000 1173 | 1174 | **Range**: 0 to 500000000 1175 | 1176 | **Units**: microsteps per 10000 s 1177 | """ 1178 | ... 1179 | 1180 | def get_agc_mode(self) -> int: 1181 | """ 1182 | **Description**: The default AGC mode for Tic T249. 1183 | 1184 | **Offset**: 0x6C 1185 | 1186 | **Type**: unsigned 8-bit 1187 | 1188 | **Data**: 1189 | 0: Off 1190 | 1: On 1191 | 2: Active off 1192 | 1193 | **Default**: 0 (Off) 1194 | """ 1195 | ... 1196 | 1197 | def get_agc_bottom_current_limit(self) -> int: 1198 | """ 1199 | **Description**: The default AGC bottom current limit for Tic T249. 1200 | 1201 | **Offset**: 0x6D 1202 | 1203 | **Type**: unsigned 8-bit 1204 | 1205 | **Data**: 1206 | 0: 45% 1207 | 1: 50% 1208 | 2: 55% 1209 | 3: 60% 1210 | 4: 65% 1211 | 5: 70% 1212 | 6: 75% 1213 | 7: 80% 1214 | 1215 | **Default**: 7 (80%) 1216 | """ 1217 | ... 1218 | 1219 | def get_agc_current_boost_steps(self) -> int: 1220 | """ 1221 | **Description**: The default AGC current boost steps for Tic T249. 1222 | 1223 | **Offset**: 0x6E 1224 | 1225 | **Type**: unsigned 8-bit 1226 | 1227 | **Data**: 1228 | 0: 5 steps 1229 | 1: 7 steps 1230 | 2: 9 steps 1231 | 3: 11 steps 1232 | 1233 | **Default**: 0 (5 steps) 1234 | """ 1235 | ... 1236 | 1237 | def get_agc_frequency_limit(self) -> int: 1238 | """ 1239 | **Description**: The default AGC frequency limit for Tic T249. 1240 | 1241 | **Offset**: 0x6F 1242 | 1243 | **Type**: unsigned 8-bit 1244 | 1245 | **Data**: 1246 | 0: Off 1247 | 1: 225 Hz 1248 | 2: 450 Hz 1249 | 3: 675 Hz 1250 | 1251 | **Default**: 0 (Off) 1252 | """ 1253 | ... 1254 | 1255 | def get_hp_enable_unrestricted_current_limits(self) -> bool: 1256 | """ 1257 | **Description**: Enables current limits above ~4000 mA on Tic 36v4 if set. 1258 | 1259 | **Offset**: Bit 0 of byte 0x6C 1260 | 1261 | **Type**: boolean 1262 | 1263 | **Default**: false 1264 | """ 1265 | ... 1266 | 1267 | def get_hp_fixed_off_time(self) -> int: 1268 | """ 1269 | **Description**: "Fixed off time" for the DRV8711-based driver on Tic 36v4. 1270 | 1271 | **Offset**: 0xF6 1272 | 1273 | **Type**: unsigned 8-bit 1274 | 1275 | **Default**: 25.5 us 1276 | 1277 | **Range**: 0.5 us to 128.0 us 1278 | """ 1279 | ... 1280 | 1281 | def get_hp_current_trip_blanking_time(self) -> int: 1282 | """ 1283 | **Description**: Minimum on-time for each PWM cycle on Tic 36v4. 1284 | 1285 | **Offset**: 0xF8 1286 | 1287 | **Type**: unsigned 8-bit 1288 | 1289 | **Default**: 1.00 us 1290 | 1291 | **Range**: 1.00 us to 5.10 us 1292 | """ 1293 | ... 1294 | 1295 | def get_hp_enable_adaptive_blanking_time(self) -> bool: 1296 | """ 1297 | **Description**: Enables adaptive blanking time on Tic 36v4 for low-current steps. 1298 | 1299 | **Offset**: Bit 0 of byte 0xF9 1300 | 1301 | **Type**: boolean 1302 | 1303 | **Default**: true 1304 | """ 1305 | ... 1306 | 1307 | def get_hp_mixed_decay_transition_time(self) -> int: 1308 | """ 1309 | **Description**: Time after which the driver on Tic 36v4 switches from fast to slow decay in mixed mode. 1310 | 1311 | **Offset**: 0xFA 1312 | 1313 | **Type**: unsigned 8-bit 1314 | 1315 | **Default**: 8.0 us 1316 | 1317 | **Range**: 0.0 us to 127.5 us 1318 | """ 1319 | ... 1320 | 1321 | def get_hp_decay_mode(self) -> int: 1322 | """ 1323 | **Description**: The decay mode for high-power Tic 36v4. 1324 | 1325 | **Offset**: 0xFB 1326 | 1327 | **Type**: unsigned 8-bit 1328 | """ 1329 | ... 1330 | 1331 | 1332 | # 1333 | # TicBase merges commands, variable getters, and settings. 1334 | # 1335 | class TicBase: 1336 | """ 1337 | **Description**: Base class for Pololu Tic controllers. 1338 | Dynamically defines: 1339 | - Commands (e.g. `set_target_position`) 1340 | - Variable getters (e.g. `get_operation_state`) 1341 | - Setting getters (e.g. `get_control_mode`) 1342 | """ 1343 | 1344 | settings: Settings 1345 | 1346 | def __init__(self) -> None: ... 1347 | 1348 | def _send_command(self, command_code: int, format: str, value: Optional[int] = None) -> None: ... 1349 | def _block_read(self, command_code: int, offset: int, length: int, 1350 | format_response: Any = None) -> Any: ... 1351 | def _define_commands(self) -> None: ... 1352 | def _define_variables(self) -> None: ... 1353 | 1354 | def get_variables(self) -> Dict[str, Any]: 1355 | """ 1356 | **Description**: Returns a dictionary mapping each variable name to its value. 1357 | """ 1358 | ... 1359 | 1360 | # 1361 | # --------------------------- 1362 | # Command stubs 1363 | # --------------------------- 1364 | # 1365 | def set_target_position(self, value: int) -> None: 1366 | """ 1367 | **Description**: Sets the Tic's target position in microsteps. 1368 | If control mode is Serial/I2C/USB, the motor moves to the specified position. 1369 | Otherwise, the command is silently ignored. 1370 | 1371 | **Command**: 0xE0 1372 | 1373 | **Format**: 32-bit write 1374 | 1375 | **Data**: target position, signed 32-bit 1376 | 1377 | **Range**: -2,147,483,648 to +2,147,483,647 1378 | 1379 | **Units**: microsteps 1380 | """ 1381 | ... 1382 | 1383 | def set_target_velocity(self, value: int) -> None: 1384 | """ 1385 | **Description**: Sets the Tic's target velocity. 1386 | If control mode is Serial/I2C/USB, the motor accelerates or decelerates to reach this velocity. 1387 | 1388 | **Command**: 0xE3 1389 | 1390 | **Format**: 32-bit write 1391 | 1392 | **Data**: target velocity, signed 32-bit 1393 | 1394 | **Range**: -500,000,000 to +500,000,000 1395 | 1396 | **Units**: microsteps per 10,000 s 1397 | """ 1398 | ... 1399 | 1400 | def halt_and_set_position(self, value: int) -> None: 1401 | """ 1402 | **Description**: Abruptly halts the motor (ignoring deceleration) and sets the 'Current position'. 1403 | Also clears the 'position uncertain' flag, sets input state to 'halt', and clears 'input after scaling'. 1404 | 1405 | **Command**: 0xEC 1406 | 1407 | **Format**: 32-bit write 1408 | 1409 | **Data**: current position, signed 32-bit 1410 | 1411 | **Range**: -2,147,483,648 to +2,147,483,647 1412 | 1413 | **Units**: microsteps 1414 | """ 1415 | ... 1416 | 1417 | def halt_and_hold(self) -> None: 1418 | """ 1419 | **Description**: Abruptly stops the motor, sets 'position uncertain' and input state to 'halt', 1420 | and clears the 'input after scaling' variable. 1421 | 1422 | **Command**: 0x89 1423 | 1424 | **Format**: Quick 1425 | """ 1426 | ... 1427 | 1428 | def go_home(self, value: int) -> None: 1429 | """ 1430 | **Description**: Initiates the homing procedure in the specified direction. 1431 | 1432 | **Command**: 0x97 1433 | 1434 | **Format**: 7-bit write 1435 | 1436 | **Data**: 1437 | 0: Home in reverse direction 1438 | 1: Home in forward direction 1439 | """ 1440 | ... 1441 | 1442 | def reset_command_timeout(self) -> None: 1443 | """ 1444 | **Description**: Resets the command timeout to prevent 'command timeout' errors. 1445 | 1446 | **Command**: 0x8C 1447 | 1448 | **Format**: Quick 1449 | """ 1450 | ... 1451 | 1452 | def deenergize(self) -> None: 1453 | """ 1454 | **Description**: Disables the driver, de-energizing the motor coils. 1455 | Sets the 'position uncertain' flag and the 'intentionally de-energized' error bit. 1456 | 1457 | **Command**: 0x86 1458 | 1459 | **Format**: Quick 1460 | """ 1461 | ... 1462 | 1463 | def energize(self) -> None: 1464 | """ 1465 | **Description**: Requests enabling the stepper driver and energizing the motor coils. 1466 | Clears the 'intentionally de-energized' error bit if no other errors exist. 1467 | 1468 | **Command**: 0x85 1469 | 1470 | **Format**: Quick 1471 | """ 1472 | ... 1473 | 1474 | def exit_safe_start(self) -> None: 1475 | """ 1476 | **Description**: Clears the 'safe start violation' error for approximately 200 ms, 1477 | allowing the motor to resume if control mode is Serial/I2C/USB. 1478 | 1479 | **Command**: 0x83 1480 | 1481 | **Format**: Quick 1482 | """ 1483 | ... 1484 | 1485 | def enter_safe_start(self) -> None: 1486 | """ 1487 | **Description**: Triggers safe start; if enabled, stops the motor using the soft error response. 1488 | 1489 | **Command**: 0x8F 1490 | 1491 | **Format**: Quick 1492 | """ 1493 | ... 1494 | 1495 | def reset(self) -> None: 1496 | """ 1497 | **Description**: Reloads settings from non-volatile memory, abruptly halts the motor, 1498 | resets the driver, clears errors, and enters safe start if configured. 1499 | (Note: This is not a full microcontroller reset; uptime is unaffected.) 1500 | 1501 | **Command**: 0xB0 1502 | 1503 | **Format**: Quick 1504 | """ 1505 | ... 1506 | 1507 | def clear_driver_error(self) -> None: 1508 | """ 1509 | **Description**: Clears a latched motor driver error if 'auto_clear_driver_error' is disabled. 1510 | Otherwise, it has no effect. 1511 | 1512 | **Command**: 0x8A 1513 | 1514 | **Format**: Quick 1515 | """ 1516 | ... 1517 | 1518 | def set_max_speed(self, value: int) -> None: 1519 | """ 1520 | **Description**: Temporarily sets the maximum allowed motor speed. 1521 | 1522 | **Command**: 0xE6 1523 | 1524 | **Format**: 32-bit write 1525 | 1526 | **Data**: max speed, unsigned 32-bit 1527 | 1528 | **Range**: 0 to 500,000,000 1529 | 1530 | **Units**: microsteps per 10,000 s 1531 | """ 1532 | ... 1533 | 1534 | def set_starting_speed(self, value: int) -> None: 1535 | """ 1536 | **Description**: Temporarily sets the starting speed (the speed below which instant 1537 | acceleration/deceleration is allowed). 1538 | 1539 | **Command**: 0xE5 1540 | 1541 | **Format**: 32-bit write 1542 | 1543 | **Data**: starting speed, unsigned 32-bit 1544 | 1545 | **Range**: 0 to 500,000,000 1546 | 1547 | **Units**: microsteps per 10,000 s 1548 | """ 1549 | ... 1550 | 1551 | def set_max_acceleration(self, value: int) -> None: 1552 | """ 1553 | **Description**: Temporarily sets the maximum allowed acceleration. 1554 | If the provided value is less than 100, it is treated as 100. 1555 | 1556 | **Command**: 0xEA 1557 | 1558 | **Format**: 32-bit write 1559 | 1560 | **Data**: max acceleration, unsigned 32-bit 1561 | 1562 | **Range**: 100 to 2,147,483,647 1563 | 1564 | **Units**: microsteps per 100 s^2 1565 | """ 1566 | ... 1567 | 1568 | def set_max_deceleration(self, value: int) -> None: 1569 | """ 1570 | **Description**: Temporarily sets the maximum allowed deceleration. 1571 | If 0, it is set equal to the current max acceleration; values below 100 are treated as 100. 1572 | 1573 | **Command**: 0xE9 1574 | 1575 | **Format**: 32-bit write 1576 | 1577 | **Data**: max deceleration, unsigned 32-bit 1578 | 1579 | **Range**: 100 to 2,147,483,647 1580 | 1581 | **Units**: microsteps per 100 s^2 1582 | """ 1583 | ... 1584 | 1585 | def set_step_mode(self, value: int) -> None: 1586 | """ 1587 | **Description**: Temporarily sets the microstepping mode. 1588 | 1589 | **Command**: 0x94 1590 | 1591 | **Format**: 7-bit write 1592 | 1593 | **Data**: step mode, unsigned 7-bit 1594 | 1595 | **Data Options**: 1596 | 0: Full step 1597 | 1: 1/2 step 1598 | 2: 1/4 step 1599 | 3: 1/8 step 1600 | 4: 1/16 step (Tic T834, Tic T825, and Tic 36v4 only) 1601 | 5: 1/32 step (Tic T834, Tic T825, and Tic 36v4 only) 1602 | 6: 1/2 step 100% (Tic T249 only) 1603 | 7: 1/64 step (Tic 36v4 only) 1604 | 8: 1/128 step (Tic 36v4 only) 1605 | 9: 1/256 step (Tic 36v4 only) 1606 | """ 1607 | ... 1608 | 1609 | def set_current_limit(self, value: int) -> None: 1610 | """ 1611 | **Description**: Temporarily sets the coil current limit. 1612 | The value is a 7-bit unsigned integer whose meaning depends on the Tic model. 1613 | 1614 | **Command**: 0x91 1615 | 1616 | **Format**: 7-bit write 1617 | 1618 | **Data**: current limit (in model-specific units, typically mA) 1619 | """ 1620 | ... 1621 | 1622 | def set_decay_mode(self, value: int) -> None: 1623 | """ 1624 | **Description**: Temporarily sets the driver decay mode. 1625 | (Note: This has no effect on Tic 36v4.) 1626 | 1627 | **Command**: 0x92 1628 | 1629 | **Format**: 7-bit write 1630 | 1631 | **Data**: decay mode, unsigned 7-bit 1632 | 1633 | **Data Options**: 1634 | Tic T500: 0 = Automatic 1635 | Tic T834: 0 = Mixed 50%, 1 = Slow, 2 = Fast, 3 = Mixed 25%, 4 = Mixed 75% 1636 | Tic T825: 0 = Mixed, 1 = Slow, 2 = Fast 1637 | Tic T249: 0 = Mixed 1638 | """ 1639 | ... 1640 | 1641 | def set_agc_option(self, value: int) -> None: 1642 | """ 1643 | **Description**: Temporarily changes an AGC option (only valid on Tic T249). 1644 | The upper 3 bits specify which AGC option; the lower 4 bits specify the new value. 1645 | 1646 | **Command**: 0x98 1647 | 1648 | **Format**: 7-bit write 1649 | 1650 | **Data**: upper 3 bits = AGC option, lower 4 bits = new value 1651 | """ 1652 | ... 1653 | 1654 | # 1655 | # --------------------------- 1656 | # Variable stubs (real-time variables) 1657 | # --------------------------- 1658 | # 1659 | def get_operation_state(self) -> int: 1660 | """ 1661 | **Description**: Returns the Tic's current operation state. 1662 | 1663 | **Offset**: 0x00 1664 | 1665 | **Type**: unsigned 8-bit 1666 | 1667 | Possible values: 1668 | 0: Reset 1669 | 2: De-energized 1670 | 4: Soft error 1671 | 6: Waiting for ERR line 1672 | 8: Starting up 1673 | 10: Normal 1674 | """ 1675 | ... 1676 | 1677 | def get_misc_flags(self) -> int: 1678 | """ 1679 | **Description**: Returns a bitmask of additional status flags. 1680 | 1681 | **Offset**: 0x01 1682 | 1683 | **Type**: unsigned 8-bit 1684 | 1685 | Bits: 1686 | Bit 0: Energized 1687 | Bit 1: Position uncertain 1688 | Bit 2: Forward limit active 1689 | Bit 3: Reverse limit active 1690 | Bit 4: Homing active 1691 | """ 1692 | ... 1693 | 1694 | def get_error_status(self) -> int: 1695 | """ 1696 | **Description**: Returns a bitmask of errors currently stopping the motor. 1697 | 1698 | **Offset**: 0x02 1699 | 1700 | **Type**: unsigned 16-bit 1701 | 1702 | Bits: 1703 | Bit 0: Intentionally de-energized 1704 | Bit 1: Motor driver error 1705 | Bit 2: Low VIN 1706 | Bit 3: Kill switch active 1707 | Bit 4: Required input invalid 1708 | Bit 5: Serial error 1709 | Bit 6: Command timeout 1710 | Bit 7: Safe start violation 1711 | Bit 8: ERR line high 1712 | """ 1713 | ... 1714 | 1715 | def get_error_occured(self) -> int: 1716 | """ 1717 | **Description**: Returns a bitmask of errors that have occurred since the last clear. 1718 | 1719 | **Offset**: 0x04 1720 | 1721 | **Type**: unsigned 32-bit 1722 | 1723 | Includes additional bits for serial framing, overrun, CRC, etc. 1724 | """ 1725 | ... 1726 | 1727 | def get_planning_mode(self) -> int: 1728 | """ 1729 | **Description**: Returns the current step planning mode. 1730 | 1731 | **Offset**: 0x09 1732 | 1733 | **Type**: unsigned 8-bit 1734 | 1735 | Data: 1736 | 0: Off (no target) 1737 | 1: Target position 1738 | 2: Target velocity 1739 | """ 1740 | ... 1741 | 1742 | def get_target_position(self) -> int: 1743 | """ 1744 | **Description**: Returns the motor's target position (only valid if planning mode is Target position). 1745 | 1746 | **Offset**: 0x0A 1747 | 1748 | **Type**: signed 32-bit 1749 | 1750 | **Range**: -2,147,483,648 to +2,147,483,647 1751 | 1752 | **Units**: microsteps 1753 | """ 1754 | ... 1755 | 1756 | def get_target_velocity(self) -> int: 1757 | """ 1758 | **Description**: Returns the motor's target velocity (only valid if planning mode is Target velocity). 1759 | 1760 | **Offset**: 0x0E 1761 | 1762 | **Type**: signed 32-bit 1763 | 1764 | **Range**: -500,000,000 to +500,000,000 1765 | 1766 | **Units**: microsteps per 10,000 s 1767 | """ 1768 | ... 1769 | 1770 | def get_starting_speed(self) -> int: 1771 | """ 1772 | **Description**: Returns the starting speed—the maximum speed at which instant acceleration is allowed. 1773 | 1774 | **Offset**: 0x12 1775 | 1776 | **Type**: unsigned 32-bit 1777 | 1778 | **Range**: 0 to 500,000,000 1779 | 1780 | **Units**: microsteps per 10,000 s 1781 | """ 1782 | ... 1783 | 1784 | def get_max_speed(self) -> int: 1785 | """ 1786 | **Description**: Returns the maximum allowed motor speed. 1787 | 1788 | **Offset**: 0x16 1789 | 1790 | **Type**: unsigned 32-bit 1791 | 1792 | **Range**: 0 to 500,000,000 1793 | 1794 | **Units**: microsteps per 10,000 s 1795 | """ 1796 | ... 1797 | 1798 | def get_max_deceleration(self) -> int: 1799 | """ 1800 | **Description**: Returns the maximum allowed deceleration. 1801 | 1802 | **Offset**: 0x1A 1803 | 1804 | **Type**: unsigned 32-bit 1805 | 1806 | **Range**: 100 to 2,147,483,647 1807 | 1808 | **Units**: microsteps per 100 s^2 1809 | """ 1810 | ... 1811 | 1812 | def get_max_acceleration(self) -> int: 1813 | """ 1814 | **Description**: Returns the maximum allowed acceleration. 1815 | 1816 | **Offset**: 0x1E 1817 | 1818 | **Type**: unsigned 32-bit 1819 | 1820 | **Range**: 100 to 2,147,483,647 1821 | 1822 | **Units**: microsteps per 100 s^2 1823 | """ 1824 | ... 1825 | 1826 | def get_current_position(self) -> int: 1827 | """ 1828 | **Description**: Returns the current position (accumulated commanded steps). 1829 | 1830 | **Offset**: 0x22 1831 | 1832 | **Type**: signed 32-bit 1833 | 1834 | **Range**: -2,147,483,648 to +2,147,483,647 1835 | 1836 | **Units**: microsteps 1837 | """ 1838 | ... 1839 | 1840 | def get_current_velocity(self) -> int: 1841 | """ 1842 | **Description**: Returns the current commanded velocity. 1843 | 1844 | **Offset**: 0x26 1845 | 1846 | **Type**: signed 32-bit 1847 | 1848 | **Range**: -500,000,000 to +500,000,000 1849 | 1850 | **Units**: microsteps per 10,000 s 1851 | """ 1852 | ... 1853 | 1854 | def get_acting_target_position(self) -> int: 1855 | """ 1856 | **Description**: Returns an internal variable used in target-position step planning. 1857 | 1858 | **Offset**: 0x2A 1859 | 1860 | **Type**: signed 32-bit 1861 | 1862 | **Units**: microsteps 1863 | """ 1864 | ... 1865 | 1866 | def get_time_since_last_step(self) -> int: 1867 | """ 1868 | **Description**: Returns the time since the last step (used for planning). 1869 | 1870 | **Offset**: 0x2E 1871 | 1872 | **Type**: unsigned 32-bit 1873 | 1874 | **Units**: 1/3 microseconds 1875 | """ 1876 | ... 1877 | 1878 | def get_device_reset(self) -> int: 1879 | """ 1880 | **Description**: Returns the cause of the last full microcontroller reset. 1881 | 1882 | **Offset**: 0x32 1883 | 1884 | **Type**: unsigned 8-bit 1885 | 1886 | Possible values: 1887 | 0: Power up 1888 | 1: Brown-out reset 1889 | 2: External reset 1890 | 4: Watchdog timer reset 1891 | 8: Software reset 1892 | 16: Stack overflow 1893 | 32: Stack underflow 1894 | """ 1895 | ... 1896 | 1897 | def get_vin_voltage(self) -> int: 1898 | """ 1899 | **Description**: Returns the measured VIN voltage. 1900 | 1901 | **Offset**: 0x33 1902 | 1903 | **Type**: unsigned 16-bit 1904 | 1905 | **Units**: millivolts 1906 | """ 1907 | ... 1908 | 1909 | def get_uptime(self) -> int: 1910 | """ 1911 | **Description**: Returns the time since the last full microcontroller reset. 1912 | (Note: This value is not affected by a 'reset' command.) 1913 | 1914 | **Offset**: 0x35 1915 | 1916 | **Type**: unsigned 32-bit 1917 | 1918 | **Units**: milliseconds 1919 | """ 1920 | ... 1921 | 1922 | def get_encoder_position(self) -> int: 1923 | """ 1924 | **Description**: Returns the raw quadrature encoder count from the TX/RX pins. 1925 | 1926 | **Offset**: 0x39 1927 | 1928 | **Type**: signed 32-bit 1929 | 1930 | **Units**: ticks 1931 | """ 1932 | ... 1933 | 1934 | def get_rc_pulse(self) -> int: 1935 | """ 1936 | **Description**: Returns the measured RC pulse width. 1937 | A value of 0xFFFF indicates an invalid reading. 1938 | 1939 | **Offset**: 0x3D 1940 | 1941 | **Type**: unsigned 16-bit 1942 | 1943 | **Units**: 1/12 microseconds 1944 | """ 1945 | ... 1946 | 1947 | def get_analog_reading_scl(self) -> int: 1948 | """ 1949 | **Description**: Returns the analog reading from SCL if enabled. 1950 | A value of 0xFFFF indicates unavailability. 1951 | 1952 | **Offset**: 0x3F 1953 | 1954 | **Type**: unsigned 16-bit 1955 | 1956 | **Range**: 0 to 0xFFFE 1957 | 1958 | **Units**: ~0 = 0 V, ~0xFFFE ≈ 5 V 1959 | """ 1960 | ... 1961 | 1962 | def get_analog_reading_sda(self) -> int: 1963 | """ 1964 | **Description**: Returns the analog reading from SDA if enabled. 1965 | A value of 0xFFFF indicates unavailability. 1966 | 1967 | **Offset**: 0x41 1968 | 1969 | **Type**: unsigned 16-bit 1970 | 1971 | **Range**: 0 to 0xFFFE 1972 | 1973 | **Units**: ~0 = 0 V, ~0xFFFE ≈ 5 V 1974 | """ 1975 | ... 1976 | 1977 | def get_analog_reading_tx(self) -> int: 1978 | """ 1979 | **Description**: Returns the analog reading from TX if enabled. 1980 | A value of 0xFFFF indicates unavailability. 1981 | 1982 | **Offset**: 0x43 1983 | 1984 | **Type**: unsigned 16-bit 1985 | 1986 | **Range**: 0 to 0xFFFE 1987 | 1988 | **Units**: ~0 = 0 V, ~0xFFFE ≈ 5 V 1989 | """ 1990 | ... 1991 | 1992 | def get_analog_reading_rx(self) -> int: 1993 | """ 1994 | **Description**: Returns the analog reading from RX if enabled. 1995 | A value of 0xFFFF indicates unavailability. 1996 | 1997 | **Offset**: 0x45 1998 | 1999 | **Type**: unsigned 16-bit 2000 | 2001 | **Range**: 0 to 0xFFFE 2002 | 2003 | **Units**: ~0 = 0 V, ~0xFFFE ≈ 5 V 2004 | """ 2005 | ... 2006 | 2007 | def get_digital_readings(self) -> int: 2008 | """ 2009 | **Description**: Returns a bitmask of digital readings from the control pins. 2010 | 2011 | **Offset**: 0x47 2012 | 2013 | **Type**: unsigned 8-bit 2014 | 2015 | Bits: 2016 | Bit 0: SCL 2017 | Bit 1: SDA 2018 | Bit 2: TX 2019 | Bit 3: RX 2020 | Bit 4: RC 2021 | """ 2022 | ... 2023 | 2024 | def get_pin_states(self) -> int: 2025 | """ 2026 | **Description**: Returns the state of the control pins. 2027 | Each pair of bits represents the state: 2028 | 0: High impedance 2029 | 1: Pulled up 2030 | 2: Output low 2031 | 3: Output high 2032 | 2033 | **Offset**: 0x48 2034 | 2035 | **Type**: unsigned 8-bit 2036 | """ 2037 | ... 2038 | 2039 | def get_step_mode(self) -> int: 2040 | """ 2041 | **Description**: Returns the current driver microstepping mode. 2042 | 2043 | **Offset**: 0x49 2044 | 2045 | **Type**: unsigned 8-bit 2046 | 2047 | Data Options: 2048 | 0: Full step 2049 | 1: 1/2 step 2050 | 2: 1/4 step 2051 | 3: 1/8 step 2052 | 4: 1/16 step 2053 | 5: 1/32 step 2054 | 6: 1/2 step 100% 2055 | 7: 1/64 step 2056 | 8: 1/128 step 2057 | 9: 1/256 step 2058 | """ 2059 | ... 2060 | 2061 | def get_current_limit(self) -> int: 2062 | """ 2063 | **Description**: Returns the coil current limit in driver-specific units. 2064 | 2065 | **Offset**: 0x4A 2066 | 2067 | **Type**: unsigned 8-bit 2068 | """ 2069 | ... 2070 | 2071 | def get_decay_mode(self) -> int: 2072 | """ 2073 | **Description**: Returns the driver decay mode. 2074 | (Note: This variable is not valid for Tic 36v4.) 2075 | 2076 | **Offset**: 0x4B 2077 | 2078 | **Type**: unsigned 8-bit 2079 | """ 2080 | ... 2081 | 2082 | def get_input_state(self) -> int: 2083 | """ 2084 | **Description**: Returns the current input state. 2085 | 2086 | **Offset**: 0x4C 2087 | 2088 | **Type**: unsigned 8-bit 2089 | 2090 | Possible states: 2091 | 0: Not ready 2092 | 1: Invalid 2093 | 2: Halt 2094 | 3: Target position 2095 | 4: Target velocity 2096 | """ 2097 | ... 2098 | 2099 | def get_input_after_averaging(self) -> int: 2100 | """ 2101 | **Description**: Returns the intermediate RC/analog reading after averaging. 2102 | A value of 0xFFFF indicates unavailability. 2103 | 2104 | **Offset**: 0x4D 2105 | 2106 | **Type**: unsigned 16-bit 2107 | """ 2108 | ... 2109 | 2110 | def get_input_after_hysteresis(self) -> int: 2111 | """ 2112 | **Description**: Returns the intermediate reading after hysteresis. 2113 | A value of 0xFFFF indicates unavailability. 2114 | 2115 | **Offset**: 0x4F 2116 | 2117 | **Type**: unsigned 16-bit 2118 | """ 2119 | ... 2120 | 2121 | def get_input_after_scaling(self) -> int: 2122 | """ 2123 | **Description**: Returns the final scaled input value (target position or velocity). 2124 | 2125 | **Offset**: 0x51 2126 | 2127 | **Type**: signed 32-bit 2128 | """ 2129 | ... 2130 | 2131 | def get_last_motor_driver_error(self) -> int: 2132 | """ 2133 | **Description**: Returns the cause of the last motor driver error (Tic T249 only). 2134 | 2135 | **Offset**: 0x55 2136 | 2137 | **Type**: unsigned 8-bit 2138 | 2139 | Data Options: 2140 | 0: None 2141 | 1: Over-current 2142 | 2: Over-temperature 2143 | """ 2144 | ... 2145 | 2146 | def get_agc_mode(self) -> int: 2147 | """ 2148 | **Description**: Returns the current AGC mode (Tic T249 only). 2149 | 2150 | **Offset**: 0x56 2151 | 2152 | **Type**: unsigned 8-bit 2153 | 2154 | Data Options: 2155 | 0: Off 2156 | 1: On 2157 | 2: Active off 2158 | """ 2159 | ... 2160 | 2161 | def get_agc_bottom_current_limit(self) -> int: 2162 | """ 2163 | **Description**: Returns the AGC bottom current limit (Tic T249 only). 2164 | 2165 | **Offset**: 0x57 2166 | 2167 | **Type**: unsigned 8-bit 2168 | 2169 | Data Options: 2170 | 0: 45% 2171 | 1: 50% 2172 | 2: 55% 2173 | 3: 60% 2174 | 4: 65% 2175 | 5: 70% 2176 | 6: 75% 2177 | 7: 80% 2178 | """ 2179 | ... 2180 | 2181 | def get_agc_current_boost_steps(self) -> int: 2182 | """ 2183 | **Description**: Returns the AGC current boost steps (Tic T249 only). 2184 | 2185 | **Offset**: 0x58 2186 | 2187 | **Type**: unsigned 8-bit 2188 | 2189 | Data Options: 2190 | 0: 5 steps 2191 | 1: 7 steps 2192 | 2: 9 steps 2193 | 3: 11 steps 2194 | """ 2195 | ... 2196 | 2197 | def get_agc_frequency_limit(self) -> int: 2198 | """ 2199 | **Description**: Returns the AGC frequency limit (Tic T249 only). 2200 | 2201 | **Offset**: 0x6F 2202 | 2203 | **Type**: unsigned 8-bit 2204 | 2205 | Data Options: 2206 | 0: Off 2207 | 1: 225 Hz 2208 | 2: 450 Hz 2209 | 3: 675 Hz 2210 | """ 2211 | ... 2212 | 2213 | def get_last_hp_driver_errors(self) -> int: 2214 | """ 2215 | **Description**: Returns a bitmask indicating the cause(s) of the last high-power driver error (Tic 36v4 only). 2216 | 2217 | **Offset**: 0xFF 2218 | 2219 | **Type**: unsigned 8-bit 2220 | 2221 | Bits: 2222 | Bit 0: Overtemperature 2223 | Bit 1: Overcurrent A 2224 | Bit 2: Overcurrent B 2225 | Bit 3: Predriver fault A 2226 | Bit 4: Predriver fault B 2227 | Bit 5: Undervoltage 2228 | Bit 7: Verification failure 2229 | """ 2230 | ... 2231 | 2232 | 2233 | class TicSerial(TicBase): 2234 | """ 2235 | **Description**: Serial driver for Tic stepper motor controllers. 2236 | Reference: https://www.pololu.com/docs/0J71/9 2237 | """ 2238 | 2239 | def __init__( 2240 | self, 2241 | port: Any, 2242 | device_number: Optional[int] = None, 2243 | crc_for_commands: bool = False, 2244 | crc_for_responses: bool = False 2245 | ) -> None: ... 2246 | def _send_command(self, command_code: int, format: str, value: Optional[int] = None) -> None: ... 2247 | def _block_read(self, command_code: int, offset: int, length: int, format_response: Any = None) -> Any: ... 2248 | def _read_response(self, length: int) -> bytes: ... 2249 | 2250 | 2251 | class TicI2C(TicBase): 2252 | """ 2253 | **Description**: I2C driver for Tic stepper motor controllers. 2254 | Reference: https://www.pololu.com/docs/0J71/10 2255 | """ 2256 | 2257 | def __init__(self, backend: Any) -> None: ... 2258 | def _send_command(self, command_code: int, format: str, value: Optional[int] = None) -> None: ... 2259 | def _block_read(self, command_code: int, offset: int, length: int, format_response: Any = None) -> Any: ... 2260 | 2261 | 2262 | class TicUSB(TicBase): 2263 | """ 2264 | **Description**: USB driver for Tic stepper motor controllers. 2265 | Reference: https://www.pololu.com/docs/0J71/11 2266 | """ 2267 | 2268 | def __init__( 2269 | self, 2270 | product: Optional[int] = None, 2271 | serial_number: Optional[str] = None 2272 | ) -> None: ... 2273 | def _send_command(self, command_code: int, format: str, value: Optional[int] = None) -> None: ... 2274 | def _block_read(self, command_code: int, offset: int, length: int, format_response: Any = None) -> Any: ... 2275 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jphalip/ticlib/a844724e1746f26853ca49254c6b18e3b8f7da23/tests/__init__.py -------------------------------------------------------------------------------- /tests/tests.py: -------------------------------------------------------------------------------- 1 | # BSD 2-Clause License 2 | # 3 | # Copyright (c) 2021, Julien Phalip 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are met: 8 | # 9 | # 1. Redistributions of source code must retain the above copyright notice, this 10 | # list of conditions and the following disclaimer. 11 | # 12 | # 2. Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation 14 | # and/or other materials provided with the distribution. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | import sys 28 | 29 | # Add the `ticlib` package to the system path 30 | sys.path.append('/'.join(__file__.split('/')[0:-2]) + '/src') 31 | 32 | from ticlib import * 33 | 34 | 35 | class MockSerialPort(object): 36 | 37 | def __init__(self): 38 | self.returned_values = None 39 | self.writes = [] 40 | 41 | def set_returned_values(self, values): 42 | self.returned_values = values 43 | 44 | def read(self, length): 45 | if self.returned_values: 46 | return self.returned_values.pop(0) 47 | 48 | def write(self, serialized): 49 | self.writes.append(serialized) 50 | 51 | 52 | class MockI2CBackend(object): 53 | 54 | def __init__(self): 55 | self.returned_values = None 56 | self.writes = [] 57 | 58 | def set_returned_values(self, values): 59 | self.returned_values = values 60 | 61 | def read(self, length): 62 | if self.returned_values: 63 | return self.returned_values.pop(0) 64 | 65 | def write(self, serialized): 66 | self.writes.append(serialized) 67 | 68 | 69 | def int_to_bytes(value, length): 70 | if value < 0: 71 | value = value + 2**32 72 | return value.to_bytes(length, 'little') 73 | 74 | 75 | class Tests(object): 76 | 77 | def test_serial_variable(self): 78 | tic = TicSerial(MockSerialPort()) 79 | tic.port.set_returned_values([int_to_bytes(99, 4)]) 80 | assert tic.get_current_position() == 99 81 | 82 | def test_serial_commands(self): 83 | tic = TicSerial(MockSerialPort()) 84 | # 32-bit parameter 85 | tic.set_target_position(-99) 86 | assert tic.port.writes[-1] == b'\xe0\x0f\x1d\x7f\x7f\x7f' 87 | # 7-bit parameter 88 | tic.go_home(1) 89 | assert tic.port.writes[-1] == b'\x97\x01' 90 | # Quick command (no parameters) 91 | tic.energize() 92 | assert tic.port.writes[-1] == b'\x85' 93 | 94 | def test_i2c_variable(self): 95 | tic = TicI2C(MockI2CBackend()) 96 | tic.backend.set_returned_values([int_to_bytes(99, 4)]) 97 | assert tic.get_current_position() == 99 98 | 99 | def test_i2c_commands(self): 100 | tic = TicI2C(MockI2CBackend()) 101 | # 32-bit parameter 102 | tic.set_target_position(-99) 103 | assert tic.backend.writes[-1] == b'\xe0\x9d\xff\xff\xff' 104 | # 7-bit parameter 105 | tic.go_home(1) 106 | assert tic.backend.writes[-1] == b'\x97\x01' 107 | # Quick command (no parameters) 108 | tic.energize() 109 | assert tic.backend.writes[-1] == b'\x85' 110 | 111 | def test_usb_variable(self): 112 | tic = TicUSB() 113 | tic.usb.set_returned_values([int_to_bytes(99, 4)]) 114 | assert tic.get_current_position() == 99 115 | 116 | def test_usb_commands(self): 117 | tic = TicUSB() 118 | # 32-bit parameter 119 | tic.set_target_position(-99) 120 | assert tic.usb.calls[-1] == (0x40, 0xE0, 65437, 65535, 0) 121 | # 7-bit parameter 122 | tic.go_home(1) 123 | assert tic.usb.calls[-1] == (0x40, 0x97, 1, 0, 0) 124 | # Quick command (no parameters) 125 | tic.energize() 126 | assert tic.usb.calls[-1] == (0x40, 0x85, 0, 0, 0) 127 | 128 | def test_multiple_tics(self): 129 | """ 130 | Ensure that multiple Tics can be used at the same time. 131 | See: https://github.com/jphalip/ticlib/issues/1 132 | """ 133 | tic1 = TicUSB() 134 | tic1.usb.set_returned_values([int_to_bytes(1, 4)]) 135 | tic2 = TicUSB() 136 | tic2.usb.set_returned_values([int_to_bytes(2, 4)]) 137 | assert tic1.get_current_position() == 1 138 | assert tic2.get_current_position() == 2 139 | 140 | def test_variable_incorrect_length(self): 141 | tic = TicUSB() 142 | tic.usb.set_returned_values([int_to_bytes(42, 2)]) 143 | try: 144 | tic.get_current_position() 145 | except RuntimeError as excinfo: 146 | assert str(excinfo) == "Expected to read 4 bytes, got 2." 147 | else: 148 | raise Exception() 149 | 150 | def test_boolean(self): 151 | """ 152 | Ensure that indexed booleans are correctly read from bytes. 153 | """ 154 | tic = TicUSB() 155 | tic.usb.set_returned_values([int_to_bytes(0b00000100, 1)]) 156 | assert tic.settings.get_serial_14bit_device_number() is False 157 | tic.usb.set_returned_values([int_to_bytes(0b00001000, 1)]) 158 | assert tic.settings.get_serial_14bit_device_number() is True 159 | 160 | def test_signed_int(self): 161 | tic = TicUSB() 162 | tic.usb.set_returned_values([int_to_bytes(-99, 4)]) 163 | assert tic.get_target_position() == -99 164 | 165 | def test_unsigned_int(self): 166 | tic = TicUSB() 167 | tic.usb.set_returned_values([int_to_bytes(-99, 4)]) 168 | assert tic.get_max_speed() == 4294967197 169 | 170 | def test_get_serial_device_number(self): 171 | tic = TicUSB() 172 | tic.usb.set_returned_values([int_to_bytes(0b00000010, 1), int_to_bytes(0b00000100, 1)]) 173 | assert tic.settings.get_serial_device_number() == 0b00001000000010 174 | 175 | def test_get_serial_alt_device_number(self): 176 | tic = TicUSB() 177 | tic.usb.set_returned_values([int_to_bytes(0b00000010, 1), int_to_bytes(0b00000100, 1)]) 178 | assert tic.settings.get_serial_alt_device_number() == 0b00001000000010 179 | 180 | 181 | if __name__ == '__main__': 182 | tests = Tests() 183 | for method in dir(tests): 184 | if method.startswith('test_'): 185 | getattr(tests, method)() 186 | print(method + " ... ok") 187 | -------------------------------------------------------------------------------- /tests/usb/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jphalip/ticlib/a844724e1746f26853ca49254c6b18e3b8f7da23/tests/usb/__init__.py -------------------------------------------------------------------------------- /tests/usb/core.py: -------------------------------------------------------------------------------- 1 | # BSD 2-Clause License 2 | # 3 | # Copyright (c) 2021, Julien Phalip 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are met: 8 | # 9 | # 1. Redistributions of source code must retain the above copyright notice, this 10 | # list of conditions and the following disclaimer. 11 | # 12 | # 2. Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation 14 | # and/or other materials provided with the distribution. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | class MockUSB(object): 28 | 29 | def __init__(self): 30 | self.calls = [] 31 | self.returned_values = None 32 | 33 | def set_returned_values(self, values): 34 | self.returned_values = values 35 | 36 | def ctrl_transfer(self, bmRequestType, bRequest, wValue, wIndex, data_or_wLength): 37 | self.calls.append((bmRequestType, bRequest, wValue, wIndex, data_or_wLength)) 38 | if self.returned_values: 39 | return self.returned_values.pop(0) 40 | 41 | def set_configuration(self): 42 | pass 43 | 44 | 45 | def find(idVendor): 46 | return MockUSB() 47 | --------------------------------------------------------------------------------