robot-hat 3.3.2__tar.gz → 3.3.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {robot_hat-3.3.2 → robot_hat-3.3.3}/CHANGELOG.md +23 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/PKG-INFO +42 -7
- {robot_hat-3.3.2 → robot_hat-3.3.3}/README.md +41 -6
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/i2c/i2c_bus.py +78 -32
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/i2c/smbus_manager.py +1 -1
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/motor/gpio_dc_motor.py +101 -22
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/motor/phase_motor.py +37 -8
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/pin.py +1 -4
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/services/base_motor_service.py +61 -13
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/services/motor_service.py +12 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/services/servo_service.py +56 -40
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/servos/gpio_angular_servo.py +65 -21
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/servos/servo.py +79 -43
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/version.py +3 -3
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat.egg-info/PKG-INFO +42 -7
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat.egg-info/scm_file_list.json +1 -0
- robot_hat-3.3.3/robot_hat.egg-info/scm_version.json +8 -0
- robot_hat-3.3.2/robot_hat.egg-info/scm_version.json +0 -8
- {robot_hat-3.3.2 → robot_hat-3.3.3}/.github/dependabot.yml +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/.github/workflows/ci.yml +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/.github/workflows/release.yml +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/.gitignore +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/.pre-commit-config.yaml +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/AGENTS.md +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/LICENSE +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/MANIFEST.in +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/Makefile +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/docs/as5048a.md +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/docs/hardware_validation.md +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/docs/lidar_2d.md +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/docs/localization_sensors.md +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/docs/migration_guide_v2.md +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/docs/migration_guide_v3.md +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/docs/pololu_miniimu9_v5.md +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/docs/sense_hat_sensors.md +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/hardware-validation/evidence/README.md +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/pyproject.toml +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/pyrightconfig.json +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/__init__.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/common/__init__.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/common/event_emitter.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/common/singleton_meta.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/data_types/__init__.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/data_types/angular_position.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/data_types/battery.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/data_types/bus.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/data_types/config/__init__.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/data_types/config/battery.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/data_types/config/hts221.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/data_types/config/ina219.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/data_types/config/ina226.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/data_types/config/ina260.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/data_types/config/lidar.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/data_types/config/lis3mdl.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/data_types/config/lps25h.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/data_types/config/lsm6ds33.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/data_types/config/lsm9ds1.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/data_types/config/lsm9ds1_magnetometer.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/data_types/config/motor.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/data_types/config/pwm.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/data_types/config/sh3001.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/data_types/encoder.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/data_types/environment.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/data_types/imu.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/data_types/lidar.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/data_types/magnetometer.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/data_types/motor.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/data_types/quadrature.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/data_types/uart.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/drivers/__init__.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/drivers/adc/INA219.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/drivers/adc/INA226.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/drivers/adc/INA260.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/drivers/adc/__init__.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/drivers/adc/sunfounder_adc.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/drivers/angle/__init__.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/drivers/angle/as5048a.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/drivers/angle/as5600l.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/drivers/gpio/__init__.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/drivers/gpio/gpiozero_digital_edge_input.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/drivers/pwm/__init__.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/drivers/pwm/pca9685.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/drivers/pwm/sunfounder_pwm.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/exceptions.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/factories/__init__.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/factories/battery_factory.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/factories/motor_factory.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/factories/pwm_factory.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/filedb.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/i2c/__init__.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/i2c/i2c_manager.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/i2c/retry_decorator.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/i2c/smbus_protocol.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/interfaces/__init__.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/interfaces/angular_position_abc.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/interfaces/battery_abc.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/interfaces/digital_edge_input_abc.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/interfaces/encoder_abc.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/interfaces/environmental_sensor_abc.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/interfaces/imu_abc.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/interfaces/lidar_2d_abc.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/interfaces/magnetometer_abc.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/interfaces/motor_abc.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/interfaces/pwm_driver_abc.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/interfaces/quadrature_counter_backend_abc.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/interfaces/servo_abc.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/interfaces/smbus_abc.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/interfaces/spi_abc.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/interfaces/uart_abc.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/mock/__init__.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/mock/angular_position.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/mock/encoder.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/mock/environmental_sensor.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/mock/imu.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/mock/lidar.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/mock/magnetometer.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/mock/quadrature_counter.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/mock/smbus2.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/mock/spi.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/mock/uart.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/mock/ultrasonic.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/motor/__init__.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/motor/i2c_dc_motor.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/motor/mixins/__init__.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/motor/mixins/motor_calibration.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/music.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/py.typed +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sensors/__init__.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sensors/angular_position/__init__.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sensors/angular_position/as5048a_angular_position.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sensors/angular_position/as5600l_angular_position.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sensors/encoder/__init__.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sensors/encoder/as5048a_encoder.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sensors/encoder/as5600l_encoder.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sensors/encoder/gpio_quadrature_counter.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sensors/encoder/quadrature_decoder.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sensors/encoder/quadrature_encoder.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sensors/environmental/__init__.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sensors/environmental/hts221.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sensors/environmental/lps25h.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sensors/imu/__init__.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sensors/imu/lsm6ds33.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sensors/imu/lsm9ds1.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sensors/imu/sh3001.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sensors/lidar/__init__.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sensors/lidar/rplidar_c1.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sensors/magnetometer/__init__.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sensors/magnetometer/lis3mdl.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sensors/magnetometer/lsm9ds1.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sensors/ultrasonic/HC_SR04.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sensors/ultrasonic/__init__.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/services/__init__.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/services/battery/__init__.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/services/battery/ina219_battery.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/services/battery/ina226_battery.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/services/battery/ina260_battery.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/services/battery/sunfounder_battery.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/services/single_motor_service.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/servos/__init__.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/spi/__init__.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/spi/spidev_device.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sunfounder/__init__.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sunfounder/accelerometer.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sunfounder/address_descriptions.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sunfounder/grayscale.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sunfounder/motor.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sunfounder/pin_descriptions.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sunfounder/pwm.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sunfounder/robot.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sunfounder/sunfounder_servo.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/sunfounder/utils.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/uart/__init__.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/uart/serial_uart.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/uart/usb_uart.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat/utils.py +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat.egg-info/SOURCES.txt +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat.egg-info/dependency_links.txt +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat.egg-info/requires.txt +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/robot_hat.egg-info/top_level.txt +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/setup.cfg +0 -0
- {robot_hat-3.3.2 → robot_hat-3.3.3}/tools/hil_motor_check.py +0 -0
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v3.3.3 (2026-10-16)
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Keep servo travel limits independent of pulse calibration. `Servo` maps
|
|
8
|
+
`real_min_angle`/`real_max_angle` to the configured pulse endpoints; narrower
|
|
9
|
+
`min_angle`/`max_angle` limits reduce travel without stretching the pulse range.
|
|
10
|
+
- Validate servo calibration ranges and reject non-finite commands. Serialize
|
|
11
|
+
servo commands with cleanup and reject writes after close.
|
|
12
|
+
- Initialize GPIO servos without pulses until commanded, and apply the service's
|
|
13
|
+
calibrated center consistently during initialization and calibration updates.
|
|
14
|
+
- Preserve servo calibration state when a hardware write fails and release
|
|
15
|
+
servo resources after failed service initialization.
|
|
16
|
+
- Hold GPIO motor enables low until direction outputs are ready, disable and
|
|
17
|
+
stop motors before releasing GPIO resources. Serialize movement and cleanup.
|
|
18
|
+
- Attempt stops for every managed motor after a command or stop failure, stop
|
|
19
|
+
all motors before service cleanup, and report cleanup failures to callers.
|
|
20
|
+
- Stop phase/enable motors before closing GPIO resources.
|
|
21
|
+
- Serialize shared I²C transactions and cleanup to protect SMBus address
|
|
22
|
+
selection, notify bus managers when context managers close.
|
|
23
|
+
- Keep shared GPIO factories open when deinitializing an individual pin, and
|
|
24
|
+
preserve active bus registrations when a different instance closes.
|
|
25
|
+
|
|
3
26
|
## v3.3.2 (2026-10-11)
|
|
4
27
|
|
|
5
28
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: robot_hat
|
|
3
|
-
Version: 3.3.
|
|
3
|
+
Version: 3.3.3
|
|
4
4
|
Summary: Extensible, typed robotics hardware library for motors, servos, sensors, 2D lidar, I2C, and UART.
|
|
5
5
|
Author-email: Karim Aziiev <karim.aziiev@gmail.com>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -362,6 +362,22 @@ for examples and custom `MotorABC` requirements.
|
|
|
362
362
|
|
|
363
363
|
GPIO motors are motors that are controlled entirely via direct GPIO calls; no I²C address or external PWM driver is needed. Examples include the Waveshare RPi Motor Driver Board with the MC33886 module.
|
|
364
364
|
|
|
365
|
+
`GPIODCMotor` holds its optional active-high enable pin low during initialization,
|
|
366
|
+
while stopped, and before releasing GPIO resources. It enables the bridge only
|
|
367
|
+
after setting the direction outputs. Each GPIO must belong to one device;
|
|
368
|
+
an indicator LED must not share a motor's enable or direction pin.
|
|
369
|
+
|
|
370
|
+
Motor services stop every motor before releasing resources and attempt every
|
|
371
|
+
stop even if one output fails. Stop and close failures are reported to the caller.
|
|
372
|
+
A failed movement command triggers a stop attempt for all managed motors, and
|
|
373
|
+
closed services reject movement commands.
|
|
374
|
+
|
|
375
|
+
GPIO cleanup releases pin ownership. Software cannot guarantee the electrical
|
|
376
|
+
state during boot, OS halt, power loss, or a crashed process. The motor driver
|
|
377
|
+
must have a hardware-defined disabled state when control inputs are undriven,
|
|
378
|
+
and an independent power cutoff or inhibit appropriate to the mechanism.
|
|
379
|
+
Validate startup, shutdown, and loss of control with wheels clear of the ground.
|
|
380
|
+
|
|
365
381
|
```python
|
|
366
382
|
from robot_hat import GPIODCMotorConfig, MotorFactory, MotorService, setup_env_vars
|
|
367
383
|
|
|
@@ -517,6 +533,29 @@ calibrated_angle = -1 × (input_angle + (-1 × calibration_offset))
|
|
|
517
533
|
|
|
518
534
|
The `ServoService` provides a high-level abstraction for managing servo operations. It allows easy configuration of the calibration mode, movement bounds, and custom calibration logic if needed.
|
|
519
535
|
|
|
536
|
+
`Servo` separates permitted travel (`min_angle`, `max_angle`) from pulse
|
|
537
|
+
calibration (`real_min_angle` at `min_pulse`, `real_max_angle` at `max_pulse`).
|
|
538
|
+
An allowed angle produces the same pulse when travel limits change. For
|
|
539
|
+
example, with measured calibration points of -45° at 1000 µs and +45° at
|
|
540
|
+
2000 µs, limits of -10° to +10° produce approximately 1389–1611 µs.
|
|
541
|
+
The default calibration assumes -90° to +90°; verify it for the particular
|
|
542
|
+
servo. These are two calibration points, not a request to find mechanical
|
|
543
|
+
end stops. Pulse rounding, PWM resolution, backlash, and servo behavior limit
|
|
544
|
+
physical accuracy. Wheel steering angle also depends on the linkage geometry.
|
|
545
|
+
|
|
546
|
+
When using `ServoService`, set its bounds to the desired movement relative
|
|
547
|
+
to the calibrated center. Give its underlying `Servo` the full calibrated
|
|
548
|
+
angle range so that the center offset can be applied without a second travel
|
|
549
|
+
clamp. `GPIOAngularServo` follows gpiozero's convention: its `min_angle` and
|
|
550
|
+
`max_angle` are the measured pulse endpoints; use `ServoService` for narrower
|
|
551
|
+
travel limits. GPIO servos start with pulses disabled until the first angle
|
|
552
|
+
command, including the calibrated center command issued by `ServoService`.
|
|
553
|
+
|
|
554
|
+
Direct `pulse_width_time()` calls are calibration operations and bypass travel
|
|
555
|
+
limits. Use angle commands for normal movement. Non-finite angle, offset, and
|
|
556
|
+
pulse commands are rejected before hardware writes; closed servos reject
|
|
557
|
+
further commands.
|
|
558
|
+
|
|
520
559
|
Here's how to use `ServoCalibrationMode` in your servo configuration:
|
|
521
560
|
|
|
522
561
|
**Example 1**: Steering servo using `ServoCalibrationMode.SUM`
|
|
@@ -548,6 +587,7 @@ driver = PWMFactory.create_pwm_driver(
|
|
|
548
587
|
bus=pwm_config.bus, # either a bus number or an smbus instance.
|
|
549
588
|
config=pwm_config,
|
|
550
589
|
)
|
|
590
|
+
driver.set_pwm_freq(pwm_config.freq)
|
|
551
591
|
|
|
552
592
|
steering_servo = ServoService(
|
|
553
593
|
servo=Servo(
|
|
@@ -560,8 +600,7 @@ steering_servo = ServoService(
|
|
|
560
600
|
# The minimum and maximum pulse widths (in microseconds) corresponding to the servo's physical movement.
|
|
561
601
|
min_pulse=500,
|
|
562
602
|
max_pulse=2500,
|
|
563
|
-
#
|
|
564
|
-
# These values are used to map the logical angle to the physical angle.
|
|
603
|
+
# Measured angles at min_pulse and max_pulse, respectively.
|
|
565
604
|
real_min_angle=-90.0,
|
|
566
605
|
real_max_angle=90.0,
|
|
567
606
|
),
|
|
@@ -571,8 +610,6 @@ steering_servo = ServoService(
|
|
|
571
610
|
calibration_mode=ServoCalibrationMode.SUM,
|
|
572
611
|
calibration_offset=-14.4,
|
|
573
612
|
)
|
|
574
|
-
driver.set_pwm_freq(pwm_config.freq)
|
|
575
|
-
|
|
576
613
|
steering_servo.set_angle(-30) # Turn left.
|
|
577
614
|
steering_servo.set_angle(15) # Turn slightly to the right.
|
|
578
615
|
steering_servo.reset() # Reset to the center position.
|
|
@@ -839,8 +876,6 @@ class MyRobotCar:
|
|
|
839
876
|
else:
|
|
840
877
|
servo = GPIOAngularServo(
|
|
841
878
|
pin=channel,
|
|
842
|
-
min_angle=min_angle,
|
|
843
|
-
max_angle=max_angle,
|
|
844
879
|
)
|
|
845
880
|
return ServoService(
|
|
846
881
|
servo=servo,
|
|
@@ -319,6 +319,22 @@ for examples and custom `MotorABC` requirements.
|
|
|
319
319
|
|
|
320
320
|
GPIO motors are motors that are controlled entirely via direct GPIO calls; no I²C address or external PWM driver is needed. Examples include the Waveshare RPi Motor Driver Board with the MC33886 module.
|
|
321
321
|
|
|
322
|
+
`GPIODCMotor` holds its optional active-high enable pin low during initialization,
|
|
323
|
+
while stopped, and before releasing GPIO resources. It enables the bridge only
|
|
324
|
+
after setting the direction outputs. Each GPIO must belong to one device;
|
|
325
|
+
an indicator LED must not share a motor's enable or direction pin.
|
|
326
|
+
|
|
327
|
+
Motor services stop every motor before releasing resources and attempt every
|
|
328
|
+
stop even if one output fails. Stop and close failures are reported to the caller.
|
|
329
|
+
A failed movement command triggers a stop attempt for all managed motors, and
|
|
330
|
+
closed services reject movement commands.
|
|
331
|
+
|
|
332
|
+
GPIO cleanup releases pin ownership. Software cannot guarantee the electrical
|
|
333
|
+
state during boot, OS halt, power loss, or a crashed process. The motor driver
|
|
334
|
+
must have a hardware-defined disabled state when control inputs are undriven,
|
|
335
|
+
and an independent power cutoff or inhibit appropriate to the mechanism.
|
|
336
|
+
Validate startup, shutdown, and loss of control with wheels clear of the ground.
|
|
337
|
+
|
|
322
338
|
```python
|
|
323
339
|
from robot_hat import GPIODCMotorConfig, MotorFactory, MotorService, setup_env_vars
|
|
324
340
|
|
|
@@ -474,6 +490,29 @@ calibrated_angle = -1 × (input_angle + (-1 × calibration_offset))
|
|
|
474
490
|
|
|
475
491
|
The `ServoService` provides a high-level abstraction for managing servo operations. It allows easy configuration of the calibration mode, movement bounds, and custom calibration logic if needed.
|
|
476
492
|
|
|
493
|
+
`Servo` separates permitted travel (`min_angle`, `max_angle`) from pulse
|
|
494
|
+
calibration (`real_min_angle` at `min_pulse`, `real_max_angle` at `max_pulse`).
|
|
495
|
+
An allowed angle produces the same pulse when travel limits change. For
|
|
496
|
+
example, with measured calibration points of -45° at 1000 µs and +45° at
|
|
497
|
+
2000 µs, limits of -10° to +10° produce approximately 1389–1611 µs.
|
|
498
|
+
The default calibration assumes -90° to +90°; verify it for the particular
|
|
499
|
+
servo. These are two calibration points, not a request to find mechanical
|
|
500
|
+
end stops. Pulse rounding, PWM resolution, backlash, and servo behavior limit
|
|
501
|
+
physical accuracy. Wheel steering angle also depends on the linkage geometry.
|
|
502
|
+
|
|
503
|
+
When using `ServoService`, set its bounds to the desired movement relative
|
|
504
|
+
to the calibrated center. Give its underlying `Servo` the full calibrated
|
|
505
|
+
angle range so that the center offset can be applied without a second travel
|
|
506
|
+
clamp. `GPIOAngularServo` follows gpiozero's convention: its `min_angle` and
|
|
507
|
+
`max_angle` are the measured pulse endpoints; use `ServoService` for narrower
|
|
508
|
+
travel limits. GPIO servos start with pulses disabled until the first angle
|
|
509
|
+
command, including the calibrated center command issued by `ServoService`.
|
|
510
|
+
|
|
511
|
+
Direct `pulse_width_time()` calls are calibration operations and bypass travel
|
|
512
|
+
limits. Use angle commands for normal movement. Non-finite angle, offset, and
|
|
513
|
+
pulse commands are rejected before hardware writes; closed servos reject
|
|
514
|
+
further commands.
|
|
515
|
+
|
|
477
516
|
Here's how to use `ServoCalibrationMode` in your servo configuration:
|
|
478
517
|
|
|
479
518
|
**Example 1**: Steering servo using `ServoCalibrationMode.SUM`
|
|
@@ -505,6 +544,7 @@ driver = PWMFactory.create_pwm_driver(
|
|
|
505
544
|
bus=pwm_config.bus, # either a bus number or an smbus instance.
|
|
506
545
|
config=pwm_config,
|
|
507
546
|
)
|
|
547
|
+
driver.set_pwm_freq(pwm_config.freq)
|
|
508
548
|
|
|
509
549
|
steering_servo = ServoService(
|
|
510
550
|
servo=Servo(
|
|
@@ -517,8 +557,7 @@ steering_servo = ServoService(
|
|
|
517
557
|
# The minimum and maximum pulse widths (in microseconds) corresponding to the servo's physical movement.
|
|
518
558
|
min_pulse=500,
|
|
519
559
|
max_pulse=2500,
|
|
520
|
-
#
|
|
521
|
-
# These values are used to map the logical angle to the physical angle.
|
|
560
|
+
# Measured angles at min_pulse and max_pulse, respectively.
|
|
522
561
|
real_min_angle=-90.0,
|
|
523
562
|
real_max_angle=90.0,
|
|
524
563
|
),
|
|
@@ -528,8 +567,6 @@ steering_servo = ServoService(
|
|
|
528
567
|
calibration_mode=ServoCalibrationMode.SUM,
|
|
529
568
|
calibration_offset=-14.4,
|
|
530
569
|
)
|
|
531
|
-
driver.set_pwm_freq(pwm_config.freq)
|
|
532
|
-
|
|
533
570
|
steering_servo.set_angle(-30) # Turn left.
|
|
534
571
|
steering_servo.set_angle(15) # Turn slightly to the right.
|
|
535
572
|
steering_servo.reset() # Reset to the center position.
|
|
@@ -796,8 +833,6 @@ class MyRobotCar:
|
|
|
796
833
|
else:
|
|
797
834
|
servo = GPIOAngularServo(
|
|
798
835
|
pin=channel,
|
|
799
|
-
min_angle=min_angle,
|
|
800
|
-
max_angle=max_angle,
|
|
801
836
|
)
|
|
802
837
|
return ServoService(
|
|
803
838
|
servo=servo,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
|
+
from threading import RLock
|
|
3
4
|
from types import TracebackType
|
|
4
5
|
from typing import TYPE_CHECKING, List, Optional, Sequence, Type, Union, cast
|
|
5
6
|
|
|
@@ -43,6 +44,8 @@ class I2CBus(SMBusABC):
|
|
|
43
44
|
SMBus = cast(Type[SMBusProtocol], RealSMBus)
|
|
44
45
|
|
|
45
46
|
self._bus = bus
|
|
47
|
+
self._lock = RLock()
|
|
48
|
+
self._closed = False
|
|
46
49
|
self._smbus = SMBus(bus, force)
|
|
47
50
|
self.emitter = EventEmitter()
|
|
48
51
|
_log.debug("SMBus initialized on bus %s with force=%s", bus, force)
|
|
@@ -55,10 +58,10 @@ class I2CBus(SMBusABC):
|
|
|
55
58
|
bus: Bus identifier to open.
|
|
56
59
|
"""
|
|
57
60
|
_log.debug("Opening SMBus on bus %s", bus)
|
|
58
|
-
|
|
61
|
+
with self._lock:
|
|
59
62
|
self._smbus.open(bus)
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
self._bus = bus
|
|
64
|
+
self._closed = False
|
|
62
65
|
|
|
63
66
|
def close(self) -> None:
|
|
64
67
|
"""
|
|
@@ -68,17 +71,28 @@ class I2CBus(SMBusABC):
|
|
|
68
71
|
is emitted and then removed from the emitter.
|
|
69
72
|
"""
|
|
70
73
|
_log.debug("Closing SMBus on bus %s", self._bus)
|
|
71
|
-
|
|
72
|
-
self.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
74
|
+
with self._lock:
|
|
75
|
+
if self._closed:
|
|
76
|
+
return
|
|
77
|
+
self._closed = True
|
|
78
|
+
try:
|
|
79
|
+
self._smbus.close()
|
|
80
|
+
except TimeoutError as err:
|
|
81
|
+
_log.error("Timeout closing SMBus '%s': %s", self._bus, err)
|
|
82
|
+
except OSError as err:
|
|
83
|
+
_log.error("OS Error closing SMBus '%s': %s", self._bus, err)
|
|
84
|
+
except Exception:
|
|
85
|
+
_log.error(
|
|
86
|
+
"Unexpected error closing SMBus '%s'", self._bus, exc_info=True
|
|
87
|
+
)
|
|
88
|
+
# Manager callbacks can acquire their own lock while another caller is
|
|
89
|
+
# opening a bus. Do not hold the transaction lock across callbacks.
|
|
90
|
+
self.emitter.emit("close", self)
|
|
91
|
+
self.emitter.off("close")
|
|
92
|
+
|
|
93
|
+
def _ensure_open(self) -> None:
|
|
94
|
+
if self._closed:
|
|
95
|
+
raise RuntimeError("I2C bus is closed")
|
|
82
96
|
|
|
83
97
|
@RETRY_DECORATOR
|
|
84
98
|
def enable_pec(self, enable: bool = False) -> None:
|
|
@@ -89,7 +103,9 @@ class I2CBus(SMBusABC):
|
|
|
89
103
|
enable: Whether to enable PEC.
|
|
90
104
|
"""
|
|
91
105
|
_log.debug("Setting PEC to %s", enable)
|
|
92
|
-
self.
|
|
106
|
+
with self._lock:
|
|
107
|
+
self._ensure_open()
|
|
108
|
+
self._smbus.enable_pec(enable)
|
|
93
109
|
|
|
94
110
|
@RETRY_DECORATOR
|
|
95
111
|
def write_quick(self, i2c_addr: int, force: Optional[bool] = None) -> None:
|
|
@@ -101,7 +117,9 @@ class I2CBus(SMBusABC):
|
|
|
101
117
|
force: Optional override for force behavior.
|
|
102
118
|
"""
|
|
103
119
|
_log.debug("write_quick: addr=%s, force=%s", i2c_addr, force)
|
|
104
|
-
self.
|
|
120
|
+
with self._lock:
|
|
121
|
+
self._ensure_open()
|
|
122
|
+
self._smbus.write_quick(i2c_addr, force)
|
|
105
123
|
|
|
106
124
|
@RETRY_DECORATOR
|
|
107
125
|
def read_byte(self, i2c_addr: int, force: Optional[bool] = None) -> int:
|
|
@@ -116,7 +134,9 @@ class I2CBus(SMBusABC):
|
|
|
116
134
|
The byte value read from the device.
|
|
117
135
|
"""
|
|
118
136
|
_log.debug("read_byte: addr=%s, force=%s", i2c_addr, force)
|
|
119
|
-
|
|
137
|
+
with self._lock:
|
|
138
|
+
self._ensure_open()
|
|
139
|
+
result = self._smbus.read_byte(i2c_addr, force)
|
|
120
140
|
_log.debug("read_byte result: %s", result)
|
|
121
141
|
return result
|
|
122
142
|
|
|
@@ -133,7 +153,9 @@ class I2CBus(SMBusABC):
|
|
|
133
153
|
force: Optional override for force behavior.
|
|
134
154
|
"""
|
|
135
155
|
_log.debug("write_byte: addr=%s, value=%s, force=%s", i2c_addr, value, force)
|
|
136
|
-
self.
|
|
156
|
+
with self._lock:
|
|
157
|
+
self._ensure_open()
|
|
158
|
+
self._smbus.write_byte(i2c_addr, value, force)
|
|
137
159
|
|
|
138
160
|
@RETRY_DECORATOR
|
|
139
161
|
def read_byte_data(
|
|
@@ -153,7 +175,9 @@ class I2CBus(SMBusABC):
|
|
|
153
175
|
_log.debug(
|
|
154
176
|
"Read_byte_data: addr=%s, register=%s, force=%s", i2c_addr, register, force
|
|
155
177
|
)
|
|
156
|
-
|
|
178
|
+
with self._lock:
|
|
179
|
+
self._ensure_open()
|
|
180
|
+
result = self._smbus.read_byte_data(i2c_addr, register, force)
|
|
157
181
|
_log.debug("read_byte_data result: %s", result)
|
|
158
182
|
return result
|
|
159
183
|
|
|
@@ -177,7 +201,9 @@ class I2CBus(SMBusABC):
|
|
|
177
201
|
value,
|
|
178
202
|
force,
|
|
179
203
|
)
|
|
180
|
-
self.
|
|
204
|
+
with self._lock:
|
|
205
|
+
self._ensure_open()
|
|
206
|
+
self._smbus.write_byte_data(i2c_addr, register, value, force)
|
|
181
207
|
|
|
182
208
|
@RETRY_DECORATOR
|
|
183
209
|
def read_word_data(
|
|
@@ -197,7 +223,9 @@ class I2CBus(SMBusABC):
|
|
|
197
223
|
_log.debug(
|
|
198
224
|
"read_word_data: addr=%s, register=%s, force=%s", i2c_addr, register, force
|
|
199
225
|
)
|
|
200
|
-
|
|
226
|
+
with self._lock:
|
|
227
|
+
self._ensure_open()
|
|
228
|
+
result = self._smbus.read_word_data(i2c_addr, register, force)
|
|
201
229
|
_log.debug("read_word_data result: %s", result)
|
|
202
230
|
return result
|
|
203
231
|
|
|
@@ -221,7 +249,9 @@ class I2CBus(SMBusABC):
|
|
|
221
249
|
value,
|
|
222
250
|
force,
|
|
223
251
|
)
|
|
224
|
-
self.
|
|
252
|
+
with self._lock:
|
|
253
|
+
self._ensure_open()
|
|
254
|
+
self._smbus.write_word_data(i2c_addr, register, value, force)
|
|
225
255
|
|
|
226
256
|
@RETRY_DECORATOR
|
|
227
257
|
def process_call(
|
|
@@ -246,7 +276,9 @@ class I2CBus(SMBusABC):
|
|
|
246
276
|
value,
|
|
247
277
|
force,
|
|
248
278
|
)
|
|
249
|
-
|
|
279
|
+
with self._lock:
|
|
280
|
+
self._ensure_open()
|
|
281
|
+
result = self._smbus.process_call(i2c_addr, register, value, force)
|
|
250
282
|
_log.debug("process_call result: %s", result)
|
|
251
283
|
return result
|
|
252
284
|
|
|
@@ -268,7 +300,9 @@ class I2CBus(SMBusABC):
|
|
|
268
300
|
_log.debug(
|
|
269
301
|
"read_block_data: addr=%s, register=%s, force=%s", i2c_addr, register, force
|
|
270
302
|
)
|
|
271
|
-
|
|
303
|
+
with self._lock:
|
|
304
|
+
self._ensure_open()
|
|
305
|
+
result = self._smbus.read_block_data(i2c_addr, register, force)
|
|
272
306
|
_log.debug("read_block_data result: %s", result)
|
|
273
307
|
return result
|
|
274
308
|
|
|
@@ -296,7 +330,9 @@ class I2CBus(SMBusABC):
|
|
|
296
330
|
data,
|
|
297
331
|
force,
|
|
298
332
|
)
|
|
299
|
-
self.
|
|
333
|
+
with self._lock:
|
|
334
|
+
self._ensure_open()
|
|
335
|
+
self._smbus.write_block_data(i2c_addr, register, data, force)
|
|
300
336
|
|
|
301
337
|
@RETRY_DECORATOR
|
|
302
338
|
def block_process_call(
|
|
@@ -325,7 +361,9 @@ class I2CBus(SMBusABC):
|
|
|
325
361
|
data,
|
|
326
362
|
force,
|
|
327
363
|
)
|
|
328
|
-
|
|
364
|
+
with self._lock:
|
|
365
|
+
self._ensure_open()
|
|
366
|
+
result = self._smbus.block_process_call(i2c_addr, register, data, force)
|
|
329
367
|
_log.debug("block_process_call result: %s", result)
|
|
330
368
|
return result
|
|
331
369
|
|
|
@@ -352,7 +390,9 @@ class I2CBus(SMBusABC):
|
|
|
352
390
|
length,
|
|
353
391
|
force,
|
|
354
392
|
)
|
|
355
|
-
|
|
393
|
+
with self._lock:
|
|
394
|
+
self._ensure_open()
|
|
395
|
+
result = self._smbus.read_i2c_block_data(i2c_addr, register, length, force)
|
|
356
396
|
_log.debug("read_i2c_block_data result: %s", result)
|
|
357
397
|
return result
|
|
358
398
|
|
|
@@ -380,7 +420,9 @@ class I2CBus(SMBusABC):
|
|
|
380
420
|
data,
|
|
381
421
|
force,
|
|
382
422
|
)
|
|
383
|
-
self.
|
|
423
|
+
with self._lock:
|
|
424
|
+
self._ensure_open()
|
|
425
|
+
self._smbus.write_i2c_block_data(i2c_addr, register, data, force)
|
|
384
426
|
|
|
385
427
|
@RETRY_DECORATOR
|
|
386
428
|
def i2c_rdwr(self, *i2c_msgs: "i2c_msg") -> None:
|
|
@@ -391,11 +433,15 @@ class I2CBus(SMBusABC):
|
|
|
391
433
|
*i2c_msgs: One or more message objects describing the transactions.
|
|
392
434
|
"""
|
|
393
435
|
_log.debug("i2c_rdwr: messages=%s", i2c_msgs)
|
|
394
|
-
|
|
436
|
+
with self._lock:
|
|
437
|
+
self._ensure_open()
|
|
438
|
+
return self._smbus.i2c_rdwr(*i2c_msgs)
|
|
395
439
|
|
|
396
440
|
def __enter__(self) -> "I2CBus":
|
|
397
441
|
_log.debug("Entering I2CBus context manager")
|
|
398
|
-
self.
|
|
442
|
+
with self._lock:
|
|
443
|
+
self._ensure_open()
|
|
444
|
+
self._smbus.__enter__()
|
|
399
445
|
return self
|
|
400
446
|
|
|
401
447
|
def __exit__(
|
|
@@ -405,7 +451,7 @@ class I2CBus(SMBusABC):
|
|
|
405
451
|
exc_tb: Optional[TracebackType],
|
|
406
452
|
) -> None:
|
|
407
453
|
"""
|
|
408
|
-
|
|
454
|
+
Close the bus and notify its manager when leaving the context.
|
|
409
455
|
"""
|
|
410
456
|
_log.debug("Exiting I2CBus context manager")
|
|
411
|
-
self.
|
|
457
|
+
self.close()
|
|
@@ -28,7 +28,7 @@ class SMBusManager:
|
|
|
28
28
|
def _on_bus_close(cls, closed_bus: "I2CBus") -> None:
|
|
29
29
|
normalized_bus = cls._normalize_bus(closed_bus._bus)
|
|
30
30
|
with cls._lock:
|
|
31
|
-
if normalized_bus
|
|
31
|
+
if cls._instances.get(normalized_bus) is closed_bus:
|
|
32
32
|
_log.debug("Removing closed bus %s from instances", normalized_bus)
|
|
33
33
|
cls._instances.pop(normalized_bus, None)
|
|
34
34
|
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
This module is intended for cases where a motor is controlled directly via
|
|
3
3
|
GPIO pins without I²C.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
gpiozero controls forward and reverse direction outputs, with optional PWM
|
|
6
|
+
on those outputs for speed control. A separate optional active-high enable
|
|
7
|
+
output gates the motor driver.
|
|
8
8
|
|
|
9
9
|
This class is suitable when the motor driver board (e.g., a
|
|
10
10
|
Waveshare/MC33886-based module) does not require or use an external PWM driver
|
|
@@ -12,7 +12,8 @@ and is controlled entirely through direct GPIO calls.
|
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
import logging
|
|
15
|
-
from
|
|
15
|
+
from threading import RLock
|
|
16
|
+
from typing import Callable, Optional, Union, cast
|
|
16
17
|
|
|
17
18
|
from robot_hat.data_types.config.motor import MotorDirection
|
|
18
19
|
from robot_hat.interfaces.motor_abc import MotorABC
|
|
@@ -27,8 +28,8 @@ class GPIODCMotor(MotorCalibration, MotorABC):
|
|
|
27
28
|
This implementation controls a motor using direct GPIO control. It drives the motor by toggling
|
|
28
29
|
GPIO output pins for forward and reverse directions.
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
gpiozero PWMOutputDevice instances provide speed control on the direction
|
|
32
|
+
pins when PWM is enabled.
|
|
32
33
|
|
|
33
34
|
Use Case:
|
|
34
35
|
- Ideal for hardware setups (like the Waveshare MC33886 module) that do not rely on a dedicated PWM
|
|
@@ -37,7 +38,8 @@ class GPIODCMotor(MotorCalibration, MotorABC):
|
|
|
37
38
|
|
|
38
39
|
Example wiring:
|
|
39
40
|
- Two GPIO pins are used for direction control (one for forward, one for reverse).
|
|
40
|
-
-
|
|
41
|
+
- An optional third GPIO pin is a digital active-high enable, held low
|
|
42
|
+
while the motor is stopped.
|
|
41
43
|
"""
|
|
42
44
|
|
|
43
45
|
def __init__(
|
|
@@ -57,14 +59,18 @@ class GPIODCMotor(MotorCalibration, MotorABC):
|
|
|
57
59
|
Args:
|
|
58
60
|
forward_pin: GPIO pin for forward direction.
|
|
59
61
|
backward_pin: GPIO pin for reverse direction.
|
|
60
|
-
pwm_pin:
|
|
61
|
-
pwm: Whether to
|
|
62
|
-
and speed control.
|
|
62
|
+
pwm_pin: Optional digital active-high enable pin for the motor driver.
|
|
63
|
+
pwm: Whether to use PWM on the direction outputs for variable speed.
|
|
63
64
|
calibration_direction: Initial calibration for the motor direction (+1 or -1).
|
|
64
65
|
calibration_speed_offset: Adjustment for the motor speed calibration.
|
|
65
66
|
name: Optional identifier for the motor for logging and debugging.
|
|
66
67
|
"""
|
|
67
|
-
from gpiozero import Motor
|
|
68
|
+
from gpiozero import DigitalOutputDevice, Motor
|
|
69
|
+
|
|
70
|
+
self._lock = RLock()
|
|
71
|
+
self._closed = False
|
|
72
|
+
self._motor: Optional[Motor] = None
|
|
73
|
+
self._enable: Optional[DigitalOutputDevice] = None
|
|
68
74
|
|
|
69
75
|
super().__init__(
|
|
70
76
|
calibration_direction=calibration_direction,
|
|
@@ -75,9 +81,14 @@ class GPIODCMotor(MotorCalibration, MotorABC):
|
|
|
75
81
|
self.name = name or f"F{forward_pin}-B{backward_pin}-P{pwm_pin}"
|
|
76
82
|
self._speed: float = 0
|
|
77
83
|
self._applied_speed: float = 0
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
84
|
+
try:
|
|
85
|
+
# Hold the bridge disabled while its direction outputs are acquired.
|
|
86
|
+
if pwm_pin is not None:
|
|
87
|
+
self._enable = DigitalOutputDevice(pwm_pin, initial_value=False)
|
|
88
|
+
self._motor = Motor(forward=forward_pin, backward=backward_pin, pwm=pwm)
|
|
89
|
+
except Exception:
|
|
90
|
+
self.close()
|
|
91
|
+
raise
|
|
81
92
|
_log.debug(
|
|
82
93
|
f"Initialized motor {self.name} with forward_pin={forward_pin}, backward_pin={backward_pin}, pwm_pin={pwm_pin}"
|
|
83
94
|
)
|
|
@@ -115,6 +126,20 @@ class GPIODCMotor(MotorCalibration, MotorABC):
|
|
|
115
126
|
Args:
|
|
116
127
|
speed: Target speed percentage within [-max_speed, max_speed].
|
|
117
128
|
"""
|
|
129
|
+
with self._lock:
|
|
130
|
+
if self._closed:
|
|
131
|
+
raise RuntimeError("Motor is closed")
|
|
132
|
+
try:
|
|
133
|
+
self._set_speed(speed)
|
|
134
|
+
except Exception:
|
|
135
|
+
try:
|
|
136
|
+
self.stop()
|
|
137
|
+
except Exception:
|
|
138
|
+
_log.exception("Failed to stop motor after a command failure")
|
|
139
|
+
raise
|
|
140
|
+
|
|
141
|
+
def _set_speed(self, speed: float) -> None:
|
|
142
|
+
assert self._motor is not None
|
|
118
143
|
logical_speed, applied_speed = self.apply_calibration(speed, self.max_speed)
|
|
119
144
|
if applied_speed == 0:
|
|
120
145
|
self.stop()
|
|
@@ -122,6 +147,9 @@ class GPIODCMotor(MotorCalibration, MotorABC):
|
|
|
122
147
|
|
|
123
148
|
applied_sign = 1 if applied_speed > 0 else -1
|
|
124
149
|
|
|
150
|
+
if self._enable is not None:
|
|
151
|
+
self._enable.off()
|
|
152
|
+
|
|
125
153
|
if applied_sign > 0:
|
|
126
154
|
command = self._motor.forward
|
|
127
155
|
log_direction = "forward"
|
|
@@ -144,6 +172,9 @@ class GPIODCMotor(MotorCalibration, MotorABC):
|
|
|
144
172
|
command(1)
|
|
145
173
|
applied_speed = applied_sign * self.max_speed
|
|
146
174
|
|
|
175
|
+
if self._enable is not None:
|
|
176
|
+
self._enable.on()
|
|
177
|
+
|
|
147
178
|
self._speed = logical_speed
|
|
148
179
|
self._applied_speed = applied_speed
|
|
149
180
|
|
|
@@ -151,24 +182,72 @@ class GPIODCMotor(MotorCalibration, MotorABC):
|
|
|
151
182
|
"""
|
|
152
183
|
Stop the motor.
|
|
153
184
|
"""
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
185
|
+
with self._lock:
|
|
186
|
+
if self._closed:
|
|
187
|
+
return
|
|
188
|
+
actions: list[Callable[[], None]] = []
|
|
189
|
+
if self._enable is not None:
|
|
190
|
+
actions.append(self._enable.off)
|
|
191
|
+
if self._motor is not None:
|
|
192
|
+
actions.append(self._motor.stop)
|
|
193
|
+
first_error: Exception | None = None
|
|
194
|
+
for action in actions:
|
|
195
|
+
try:
|
|
196
|
+
action()
|
|
197
|
+
except Exception as error:
|
|
198
|
+
if first_error is None:
|
|
199
|
+
first_error = error
|
|
200
|
+
if first_error is not None and self._motor is not None:
|
|
201
|
+
from gpiozero import OutputDevice
|
|
202
|
+
|
|
203
|
+
# A composite stop can fail before reaching its second output.
|
|
204
|
+
for output in (
|
|
205
|
+
self._motor.forward_device,
|
|
206
|
+
self._motor.backward_device,
|
|
207
|
+
):
|
|
208
|
+
try:
|
|
209
|
+
if isinstance(output, OutputDevice):
|
|
210
|
+
output.off()
|
|
211
|
+
except Exception:
|
|
212
|
+
_log.exception("Failed to clear motor direction output")
|
|
213
|
+
if first_error is not None:
|
|
214
|
+
raise first_error
|
|
215
|
+
self._speed = 0
|
|
216
|
+
self._applied_speed = 0
|
|
217
|
+
_log.debug("Motor stopped.")
|
|
158
218
|
|
|
159
219
|
def close(self) -> None:
|
|
160
220
|
"""
|
|
161
221
|
Close the underlying resources.
|
|
162
222
|
"""
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
223
|
+
with self._lock:
|
|
224
|
+
if self._closed:
|
|
225
|
+
return
|
|
226
|
+
first_error: Exception | None = None
|
|
227
|
+
actions: list[Callable[[], None]] = [self.stop]
|
|
228
|
+
if self._motor is not None:
|
|
229
|
+
actions.append(self._motor.close)
|
|
230
|
+
if self._enable is not None:
|
|
231
|
+
actions.append(self._enable.close)
|
|
232
|
+
for action in actions:
|
|
233
|
+
try:
|
|
234
|
+
action()
|
|
235
|
+
except Exception as error:
|
|
236
|
+
if first_error is None:
|
|
237
|
+
first_error = error
|
|
238
|
+
_log.exception("Failed to stop or close motor output")
|
|
239
|
+
self._closed = True
|
|
240
|
+
if first_error is not None:
|
|
241
|
+
raise first_error
|
|
166
242
|
|
|
167
243
|
def __del__(self) -> None:
|
|
168
244
|
"""
|
|
169
245
|
Destructor method.
|
|
170
246
|
"""
|
|
171
|
-
|
|
247
|
+
try:
|
|
248
|
+
self.close()
|
|
249
|
+
except Exception:
|
|
250
|
+
_log.exception("Failed to close motor during destruction")
|
|
172
251
|
|
|
173
252
|
|
|
174
253
|
def main() -> None:
|