├── .gitignore ├── LICENSE ├── README.md ├── chapter10 ├── circle_pan_tilt_behavior.py ├── leds_led_shim.py ├── robot.py ├── servo_type_position.py ├── servos.py └── sonar_scan.py ├── chapter11 ├── drive_distance.py ├── drive_square.py ├── encoder_counter.py ├── leds_led_shim.py ├── pid_controller.py ├── robot.py ├── servos.py ├── straight_line_drive.py ├── test_distance_travelled.py └── test_encoders.py ├── chapter12 ├── accelerometer_vector.py ├── magnetometer_vector.py ├── plot_gyroscope.py ├── plot_temperature.py ├── robot_imu.py └── robot_pose.py ├── chapter13 ├── camera_stream.py ├── color_track_behavior.py ├── control_image_behavior.py ├── face_track_behavior.py ├── image_app_core.py ├── image_server.py ├── pid_controller.py └── templates │ ├── color_track_behavior.html │ ├── control_image_behavior.html │ └── image_server.html ├── chapter14 ├── camera_stream.py ├── image_app_core.py ├── line_follow_behavior.py ├── pid_controller.py ├── robot.py ├── servos.py ├── templates │ └── color_track_behavior.html └── test_line_find.py ├── chapter15 ├── control_server.py ├── my-robot-skill │ ├── __init__.py │ ├── dialog │ │ └── en-us │ │ │ ├── Robot.dialog │ │ │ ├── TestingRainbow.dialog │ │ │ ├── UnableToReach.dialog │ │ │ └── stopping.dialog │ ├── requirements.txt │ ├── settingsmeta.json │ └── vocab │ │ └── en-us │ │ ├── TestRainbow.voc │ │ ├── robot.voc │ │ └── stop.voc └── robot_modes.py ├── chapter16 ├── calibrate_gyro.py ├── delta_timer.py ├── drive_north_behavior.py ├── imu_settings.py ├── magnetometer_calibration.py ├── pid_controller.py ├── plot_acc_gyro_fusion.py ├── plot_accel_debug_pitch_and_roll.py ├── plot_accel_pitch_and_roll.py ├── plot_calibrated_gyroscope.py ├── plot_gyroscope.py ├── plot_integrated_gyro.py ├── plot_mag_heading.py ├── plot_pitch_and_roll.py ├── robot.py ├── robot_imu.py ├── robot_pose.py ├── servos.py ├── virtual_robot.py ├── visual_filtered_accelerometer.py ├── visual_fusion.py └── visual_gyroscope.py ├── chapter17 ├── 0_starting_point │ ├── accelerometer_vector.py │ ├── avoid_behavior.py │ ├── avoid_with_rainbows.py │ ├── behavior_line.py │ ├── behavior_path.py │ ├── calibrate_gyro.py │ ├── calibrate_magnetometer.py │ ├── camera_stream.py │ ├── circle_pan_tilt_behavior.py │ ├── color_track_behavior.py │ ├── control_image_behavior.py │ ├── control_server.py │ ├── delta_timer.py │ ├── drive_distance.py │ ├── drive_north_behavior.py │ ├── drive_square.py │ ├── encoder_counter.py │ ├── face_track_behavior.py │ ├── image_app_core.py │ ├── image_server.py │ ├── imu_settings.py │ ├── led_rainbow.py │ ├── leds_led_shim.py │ ├── leds_test.py │ ├── line_follow_behavior.py │ ├── magnetometer_vector.py │ ├── my-robot-skill │ │ ├── __init__.py │ │ ├── dialog │ │ │ └── en-us │ │ │ │ ├── Robot.dialog │ │ │ │ ├── TestingRainbow.dialog │ │ │ │ ├── UnableToReach.dialog │ │ │ │ └── stopping.dialog │ │ ├── requirements.txt │ │ ├── settingsmeta.json │ │ └── vocab │ │ │ └── en-us │ │ │ ├── TestRainbow.voc │ │ │ ├── robot.voc │ │ │ └── stop.voc │ ├── pid_controller.py │ ├── plot_acc_gyro_fusion.py │ ├── plot_accel_debug_pitch_and_roll.py │ ├── plot_accel_pitch_and_roll.py │ ├── plot_calibrated_gyroscope.py │ ├── plot_gyroscope.py │ ├── plot_imu_fusion.py │ ├── plot_integrated_gyro.py │ ├── plot_mag_heading.py │ ├── plot_temperature.py │ ├── robot.py │ ├── robot_imu.py │ ├── robot_modes.py │ ├── robot_pose.py │ ├── servo_type_position.py │ ├── servos.py │ ├── simple_avoid_behavior.py │ ├── sonar_scan.py │ ├── straight_line_drive.py │ ├── templates │ │ ├── color_track_behavior.html │ │ ├── control_image_behavior.html │ │ └── image_server.html │ ├── test_distance_sensors.py │ ├── test_distance_travelled.py │ ├── test_encoders.py │ ├── test_line_find.py │ ├── test_motors.py │ ├── test_rainbow.py │ ├── virtual_robot.py │ ├── visual_filtered_accelerometer.py │ ├── visual_fusion.py │ └── visual_gyroscope.py └── full_system │ ├── accelerometer_vector.py │ ├── avoid_behavior.py │ ├── avoid_with_rainbows.py │ ├── behavior_line.py │ ├── behavior_path.py │ ├── calibrate_gyro.py │ ├── calibrate_magnetometer.py │ ├── camera_stream.py │ ├── circle_pan_tilt_behavior.py │ ├── color_track_behavior.py │ ├── control_image_behavior.py │ ├── control_server.py │ ├── delta_timer.py │ ├── drive_distance.py │ ├── drive_north_behavior.py │ ├── drive_square.py │ ├── encoder_counter.py │ ├── face_track_behavior.py │ ├── image_app_core.py │ ├── image_server.py │ ├── imu_settings.py │ ├── led_rainbow.py │ ├── leds_led_shim.py │ ├── leds_test.py │ ├── line_follow_behavior.py │ ├── magnetometer_vector.py │ ├── manual_drive.py │ ├── menu_server.service │ ├── my-robot-skill │ ├── __init__.py │ ├── dialog │ │ └── en-us │ │ │ ├── Robot.dialog │ │ │ ├── TestingRainbow.dialog │ │ │ ├── UnableToReach.dialog │ │ │ └── stopping.dialog │ ├── requirements.txt │ ├── settingsmeta.json │ └── vocab │ │ └── en-us │ │ ├── TestRainbow.voc │ │ ├── robot.voc │ │ └── stop.voc │ ├── pid_controller.py │ ├── plot_acc_gyro_fusion.py │ ├── plot_accel_debug_pitch_and_roll.py │ ├── plot_accel_pitch_and_roll.py │ ├── plot_calibrated_gyroscope.py │ ├── plot_gyroscope.py │ ├── plot_imu_fusion.py │ ├── plot_integrated_gyro.py │ ├── plot_mag_heading.py │ ├── plot_temperature.py │ ├── robot.py │ ├── robot_imu.py │ ├── robot_modes.py │ ├── robot_pose.py │ ├── servo_type_position.py │ ├── servos.py │ ├── simple_avoid_behavior.py │ ├── sonar_scan.py │ ├── static │ ├── display.css │ └── touch-slider.js │ ├── straight_line_drive.py │ ├── templates │ ├── color_track_behavior.html │ ├── control_image_behavior.html │ ├── image_server.html │ ├── manual_drive.html │ └── menu.html │ ├── test_distance_sensors.py │ ├── test_distance_travelled.py │ ├── test_encoders.py │ ├── test_line_find.py │ ├── test_motors.py │ ├── test_rainbow.py │ ├── virtual_robot.py │ ├── visual_filtered_accelerometer.py │ ├── visual_fusion.py │ └── visual_gyroscope.py ├── chapter4 ├── readme.md └── wpa_supplicant_example.conf ├── chapter5 ├── hello.py └── readme.md ├── chapter7 ├── behavior_line.py ├── behavior_path.py ├── robot.py └── test_motors.py ├── chapter8 ├── avoid_behavior.py ├── robot.py ├── simple_avoid_behavior.py └── test_distance_sensors.py └── chapter9 ├── avoid_behavior.py ├── avoid_with_rainbows.py ├── diagrams └── generate_colour_wheel.py ├── led_rainbow.py ├── leds_led_shim.py ├── leds_test.py ├── robot.py └── test_rainbow.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.venv/ 3 | .idea/ 4 | /ext_lib/ 5 | *.pyc 6 | __pycache__ 7 | .vscode 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Packt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /chapter10/circle_pan_tilt_behavior.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | import math 3 | 4 | from robot import Robot 5 | class CirclePanTiltBehavior: 6 | def __init__(self, the_robot): 7 | self.robot = the_robot 8 | self.current_time = 0 9 | self.frames_per_circle = 50 10 | self.radians_per_frame = (2 * math.pi) / self.frames_per_circle 11 | self.radius = 30 12 | 13 | def run(self): 14 | while True: 15 | frame_number = self.current_time % self.frames_per_circle 16 | frame_in_radians = frame_number * self.radians_per_frame 17 | self.robot.set_pan(self.radius * math.cos(frame_in_radians)) 18 | self.robot.set_tilt(self.radius * math.sin(frame_in_radians)) 19 | sleep(0.05) 20 | self.current_time += 1 21 | 22 | bot = Robot() 23 | behavior = CirclePanTiltBehavior(bot) 24 | behavior.run() 25 | 26 | -------------------------------------------------------------------------------- /chapter10/leds_led_shim.py: -------------------------------------------------------------------------------- 1 | import ledshim 2 | 3 | 4 | class Leds: 5 | @property 6 | def count(self): 7 | return ledshim.width 8 | 9 | def set_one(self, led_number, color): 10 | ledshim.set_pixel(led_number, *color) 11 | 12 | def set_range(self, led_range, color): 13 | for pixel in led_range: 14 | ledshim.set_pixel(pixel, *color) 15 | 16 | def set_all(self, color): 17 | ledshim.set_all(*color) 18 | 19 | def clear(self): 20 | ledshim.clear() 21 | 22 | def show(self): 23 | ledshim.show() 24 | -------------------------------------------------------------------------------- /chapter10/robot.py: -------------------------------------------------------------------------------- 1 | from Raspi_MotorHAT import Raspi_MotorHAT 2 | from gpiozero import DistanceSensor 3 | 4 | import atexit 5 | import leds_led_shim 6 | from servos import Servos 7 | 8 | 9 | class Robot: 10 | def __init__(self, motorhat_addr=0x6f): 11 | # Setup the motorhat with the passed in address 12 | self._mh = Raspi_MotorHAT(addr=motorhat_addr) 13 | 14 | # get local variable for each motor 15 | self.left_motor = self._mh.getMotor(1) 16 | self.right_motor = self._mh.getMotor(2) 17 | 18 | # Setup The Distance Sensors 19 | self.left_distance_sensor = DistanceSensor(echo=17, trigger=27, queue_len=2) 20 | self.right_distance_sensor = DistanceSensor(echo=5, trigger=6, queue_len=2) 21 | 22 | # Setup the Leds 23 | self.leds = leds_led_shim.Leds() 24 | # Set up servo motors for pan and tilt. 25 | self.servos = Servos(addr=motorhat_addr) 26 | # ensure the motors get stopped when the code exits 27 | atexit.register(self.stop_all) 28 | 29 | def convert_speed(self, speed): 30 | # Choose the running mode 31 | mode = Raspi_MotorHAT.RELEASE 32 | if speed > 0: 33 | mode = Raspi_MotorHAT.FORWARD 34 | elif speed < 0: 35 | mode = Raspi_MotorHAT.BACKWARD 36 | 37 | # Scale the speed 38 | output_speed = (abs(speed) * 255) // 100 39 | return mode, int(output_speed) 40 | 41 | def set_left(self, speed): 42 | mode, output_speed = self.convert_speed(speed) 43 | self.left_motor.setSpeed(output_speed) 44 | self.left_motor.run(mode) 45 | 46 | def set_right(self, speed): 47 | mode, output_speed = self.convert_speed(speed) 48 | self.right_motor.setSpeed(output_speed) 49 | self.right_motor.run(mode) 50 | 51 | def stop_motors(self): 52 | self.left_motor.run(Raspi_MotorHAT.RELEASE) 53 | self.right_motor.run(Raspi_MotorHAT.RELEASE) 54 | 55 | def stop_all(self): 56 | self.stop_motors() 57 | 58 | # Clear the display 59 | self.leds.clear() 60 | self.leds.show() 61 | 62 | # Reset the servos 63 | self.servos.stop_all() 64 | 65 | def set_pan(self, angle): 66 | self.servos.set_servo_angle(1, angle) 67 | 68 | def set_tilt(self, angle): 69 | self.servos.set_servo_angle(0, angle) 70 | 71 | -------------------------------------------------------------------------------- /chapter10/servo_type_position.py: -------------------------------------------------------------------------------- 1 | from Raspi_MotorHAT.Raspi_PWM_Servo_Driver import PWM 2 | import atexit 3 | 4 | pwm = PWM(0x6f) 5 | # This sets the timebase for it all 6 | pwm_frequency = 100 7 | pwm.setPWMFreq(pwm_frequency) 8 | # Mid-point of the servo pulse length in milliseconds. 9 | servo_mid_point_ms = 1.5 10 | # What a deflection of 90 degrees is in pulse length in milliseconds 11 | deflect_90_in_ms = 0.5 12 | # Frequency is 1 divided by period, but working ms, we can use 1000 13 | period_in_ms = 1000 / pwm_frequency 14 | # The chip has 4096 steps in each period. 15 | pulse_steps = 4096 16 | # Steps for every millisecond. 17 | steps_per_ms = pulse_steps / period_in_ms 18 | # Steps for a degree. 19 | steps_per_degree = (deflect_90_in_ms * steps_per_ms) / 90 20 | # Mid-point of the servo in steps 21 | servo_mid_point_steps = servo_mid_point_ms * steps_per_ms 22 | 23 | def convert_degrees_to_steps(position): 24 | return int(servo_mid_point_steps + (position * steps_per_degree)) 25 | 26 | atexit.register(pwm.setPWM, 0, 0, 4096) 27 | 28 | while True: 29 | position = int(input("Type your position in degrees (90 to -90, 0 is middle): ")) 30 | end_step = convert_degrees_to_steps(position) 31 | pwm.setPWM(0, 0, end_step) 32 | -------------------------------------------------------------------------------- /chapter10/servos.py: -------------------------------------------------------------------------------- 1 | from Raspi_MotorHAT.Raspi_PWM_Servo_Driver import PWM 2 | 3 | class Servos: 4 | """PCA 9865 Servo motors""" 5 | 6 | def __init__(self, addr=0x6f, deflect_90_in_ms = 0.5): 7 | """addr: The i2c address of the PWM chip. 8 | deflect_90_in_ms: set this to calibrate the servo motors. 9 | it is what a deflection of 90 degrees is 10 | in terms of a pulse length in milliseconds.""" 11 | self._pwm = PWM(addr) 12 | # This sets the timebase for it all 13 | pwm_frequency = 100 14 | self._pwm.setPWMFreq(pwm_frequency) 15 | # Mid-point of the servo pulse length in milliseconds. 16 | servo_mid_point_ms = 1.5 17 | # Frequency is 1/period, but working ms, we can use 1000 18 | period_in_ms = 1000 / pwm_frequency 19 | # The chip has 4096 steps in each period. 20 | pulse_steps = 4096 21 | # Steps for every millisecond. 22 | steps_per_ms = pulse_steps / period_in_ms 23 | # Steps for a degree 24 | self.steps_per_degree = (deflect_90_in_ms * steps_per_ms) / 90 25 | # Mid-point of the servo in steps 26 | self.servo_mid_point_steps = servo_mid_point_ms * steps_per_ms 27 | 28 | # Map for channels 29 | self.channels = [0, 1, 14, 15] 30 | 31 | def stop_all(self): 32 | # 0 in start is nothing 33 | off_bit = 4096 # bit 12 is the OFF bit. 34 | self._pwm.setPWM(self.channels[0], 0, off_bit) 35 | self._pwm.setPWM(self.channels[1], 0, off_bit) 36 | self._pwm.setPWM(self.channels[2], 0, off_bit) 37 | self._pwm.setPWM(self.channels[3], 0, off_bit) 38 | 39 | def _convert_degrees_to_steps(self, position): 40 | return int(self.servo_mid_point_steps + (position * self.steps_per_degree)) 41 | 42 | def set_servo_angle(self, channel, angle): 43 | """position: The position in degrees from the center. -90 to 90""" 44 | # Validate 45 | if angle > 90 or angle < -90: 46 | raise ValueError("Angle outside of range") 47 | # Then set the position 48 | off_step = self._convert_degrees_to_steps(angle) 49 | self._pwm.setPWM(self.channels[channel], 0, off_step) 50 | 51 | -------------------------------------------------------------------------------- /chapter10/sonar_scan.py: -------------------------------------------------------------------------------- 1 | import time 2 | import math 3 | 4 | import matplotlib.pyplot as plt 5 | 6 | from robot import Robot 7 | 8 | start_scan =0 9 | lower_bound = -90 10 | upper_bound = 90 11 | scan_step = 5 12 | 13 | the_robot = Robot() 14 | the_robot.set_tilt(0) 15 | 16 | scan_data = {} 17 | # Make the sensor scan 18 | for facing in range(lower_bound, upper_bound, scan_step): 19 | the_robot.set_pan(-facing) 20 | time.sleep(0.1) 21 | scan_data[facing] = the_robot.left_distance_sensor.distance * 100 22 | # make plot 23 | axis = [math.radians(facing) for facing in scan_data.keys()] 24 | print(axis) 25 | print(scan_data.values()) 26 | plt.polar(axis, list(scan_data.values()), 'g-') 27 | # dump to png 28 | plt.savefig("scan.png") 29 | -------------------------------------------------------------------------------- /chapter11/drive_distance.py: -------------------------------------------------------------------------------- 1 | from robot import Robot, EncoderCounter 2 | from pid_controller import PIController 3 | import time 4 | import logging 5 | logger = logging.getLogger("drive_distance") 6 | 7 | def drive_distance(bot, distance, speed=80): 8 | # Use left as "primary" motor, the right is keeping up 9 | set_primary = bot.set_left 10 | primary_encoder = bot.left_encoder 11 | set_secondary = bot.set_right 12 | secondary_encoder = bot.right_encoder 13 | controller = PIController(proportional_constant=5, integral_constant=0.3) 14 | 15 | # start the motors, and start the loop 16 | set_primary(speed) 17 | set_secondary(speed) 18 | while primary_encoder.pulse_count < distance or secondary_encoder.pulse_count < distance: 19 | time.sleep(0.01) 20 | # How far off are we? 21 | error = primary_encoder.pulse_count - secondary_encoder.pulse_count 22 | adjustment = controller.get_value(error) 23 | # How fast should the motor move to get there? 24 | set_primary(int(speed - adjustment)) 25 | set_secondary(int(speed + adjustment)) 26 | # Some debug 27 | logger.debug(f"Encoders: primary: {primary_encoder.pulse_count}, secondary: {secondary_encoder.pulse_count}," 28 | f"e:{error} adjustment: {adjustment:.2f}") 29 | logger.info(f"Distances: primary: {primary_encoder.distance_in_mm()} mm, secondary: {secondary_encoder.distance_in_mm()} mm") 30 | 31 | logging.basicConfig(level=logging.DEBUG) 32 | bot = Robot() 33 | distance_to_drive = 1000 # in mm - this is a meter 34 | distance_in_ticks = EncoderCounter.mm_to_ticks(distance_to_drive) 35 | drive_distance(bot, distance_in_ticks) 36 | -------------------------------------------------------------------------------- /chapter11/encoder_counter.py: -------------------------------------------------------------------------------- 1 | from gpiozero import DigitalInputDevice 2 | import math 3 | class EncoderCounter: 4 | ticks_to_mm_const = None # you must set this up before using distance methods 5 | def __init__(self, pin_number): 6 | self.pulse_count = 0 7 | self.direction = 1 8 | self.device = DigitalInputDevice(pin=pin_number) 9 | self.device.pin.when_changed = self.when_changed 10 | 11 | def when_changed(self, time_ticks, state): 12 | self.pulse_count += self.direction 13 | 14 | def set_direction(self, direction): 15 | """This should be -1 or 1. """ 16 | assert abs(direction)==1, "Direction %s should be 1 or -1" % direction 17 | self.direction = direction 18 | 19 | def reset(self): 20 | self.pulse_count = 0 21 | 22 | def stop(self): 23 | self.device.close() 24 | 25 | def distance_in_mm(self): 26 | return int(self.pulse_count * EncoderCounter.ticks_to_mm_const) 27 | 28 | @staticmethod 29 | def mm_to_ticks(mm): 30 | return mm / EncoderCounter.ticks_to_mm_const 31 | 32 | @staticmethod 33 | def set_constants(wheel_diameter_mm, ticks_per_revolution): 34 | EncoderCounter.ticks_to_mm_const = (math.pi / ticks_per_revolution) * wheel_diameter_mm 35 | -------------------------------------------------------------------------------- /chapter11/leds_led_shim.py: -------------------------------------------------------------------------------- 1 | import ledshim 2 | 3 | 4 | class Leds: 5 | @property 6 | def count(self): 7 | return ledshim.width 8 | 9 | def set_one(self, led_number, color): 10 | ledshim.set_pixel(led_number, *color) 11 | 12 | def set_range(self, led_range, color): 13 | for pixel in led_range: 14 | ledshim.set_pixel(pixel, *color) 15 | 16 | def set_all(self, color): 17 | ledshim.set_all(*color) 18 | 19 | def clear(self): 20 | ledshim.clear() 21 | 22 | def show(self): 23 | ledshim.show() 24 | -------------------------------------------------------------------------------- /chapter11/pid_controller.py: -------------------------------------------------------------------------------- 1 | import logging 2 | logger = logging.getLogger("pid_controller") 3 | 4 | class PIController: 5 | def __init__(self, proportional_constant=0, integral_constant=0): 6 | self.proportional_constant = proportional_constant 7 | self.integral_constant = integral_constant 8 | 9 | # Running sums 10 | self.integral_sum = 0 11 | 12 | def handle_proportional(self, error): 13 | return self.proportional_constant * error 14 | 15 | def handle_integral(self, error): 16 | self.integral_sum += error 17 | return self.integral_constant * self.integral_sum 18 | 19 | def get_value(self, error): 20 | p = self.handle_proportional(error) 21 | i = self.handle_integral(error) 22 | logger.debug(f"P: {p}, I: {i:.2f}") 23 | return p + i 24 | -------------------------------------------------------------------------------- /chapter11/robot.py: -------------------------------------------------------------------------------- 1 | from Raspi_MotorHAT import Raspi_MotorHAT 2 | from gpiozero import DistanceSensor 3 | 4 | import atexit 5 | import leds_led_shim 6 | from servos import Servos 7 | from encoder_counter import EncoderCounter 8 | 9 | 10 | class Robot: 11 | wheel_diameter_mm = 70.0 12 | ticks_per_revolution = 40.0 13 | wheel_distance_mm = 132.0 14 | def __init__(self, motorhat_addr=0x6f): 15 | # Setup the motorhat with the passed in address 16 | self._mh = Raspi_MotorHAT(addr=motorhat_addr) 17 | 18 | self.left_motor = self._mh.getMotor(1) 19 | self.right_motor = self._mh.getMotor(2) 20 | 21 | # Setup the Leds 22 | self.leds = leds_led_shim.Leds() 23 | # Set up servo motors for pan and tilt. 24 | self.servos = Servos(addr=motorhat_addr) 25 | 26 | # Setup The Distance Sensors 27 | self.left_distance_sensor = DistanceSensor(echo=17, trigger=27, queue_len=2) 28 | self.right_distance_sensor = DistanceSensor(echo=5, trigger=6, queue_len=2) 29 | 30 | # Setup the Encoders 31 | EncoderCounter.set_constants(self.wheel_diameter_mm, self.ticks_per_revolution) 32 | self.left_encoder = EncoderCounter(4) 33 | self.right_encoder = EncoderCounter(26) 34 | 35 | # ensure the motors get stopped when the code exits 36 | atexit.register(self.stop_all) 37 | 38 | def convert_speed(self, speed): 39 | # Choose the running mode 40 | mode = Raspi_MotorHAT.RELEASE 41 | if speed > 0: 42 | mode = Raspi_MotorHAT.FORWARD 43 | elif speed < 0: 44 | mode = Raspi_MotorHAT.BACKWARD 45 | 46 | # Scale the speed 47 | output_speed = (abs(speed) * 255) // 100 48 | return mode, int(output_speed) 49 | 50 | def set_left(self, speed): 51 | mode, output_speed = self.convert_speed(speed) 52 | self.left_motor.setSpeed(output_speed) 53 | self.left_motor.run(mode) 54 | 55 | def set_right(self, speed): 56 | mode, output_speed = self.convert_speed(speed) 57 | self.right_motor.setSpeed(output_speed) 58 | self.right_motor.run(mode) 59 | 60 | def stop_motors(self): 61 | self.left_motor.run(Raspi_MotorHAT.RELEASE) 62 | self.right_motor.run(Raspi_MotorHAT.RELEASE) 63 | 64 | def stop_all(self): 65 | self.stop_motors() 66 | 67 | # Clear the display 68 | self.leds.clear() 69 | self.leds.show() 70 | 71 | # Clear any sensor handlers 72 | self.left_encoder.stop() 73 | self.right_encoder.stop() 74 | 75 | # Reset the servos 76 | self.servos.stop_all() 77 | 78 | def set_pan(self, angle): 79 | self.servos.set_servo_angle(1, angle) 80 | 81 | def set_tilt(self, angle): 82 | self.servos.set_servo_angle(0, angle) 83 | 84 | -------------------------------------------------------------------------------- /chapter11/servos.py: -------------------------------------------------------------------------------- 1 | from Raspi_MotorHAT.Raspi_PWM_Servo_Driver import PWM 2 | 3 | class Servos: 4 | """PCA 9865 Servo motors""" 5 | 6 | def __init__(self, addr=0x6f, deflect_90_in_ms = 0.5): 7 | """addr: The i2c address of the PWM chip. 8 | deflect_90_in_ms: set this to calibrate the servo motors. 9 | it is what a deflection of 90 degrees is 10 | in terms of a pulse length in milliseconds.""" 11 | self._pwm = PWM(addr) 12 | # This sets the timebase for it all 13 | pwm_frequency = 100 14 | self._pwm.setPWMFreq(pwm_frequency) 15 | # Mid-point of the servo pulse length in milliseconds. 16 | servo_mid_point_ms = 1.5 17 | # Frequency is 1/period, but working ms, we can use 1000 18 | period_in_ms = 1000 / pwm_frequency 19 | # The chip has 4096 steps in each period. 20 | pulse_steps = 4096 21 | # Steps for every millisecond. 22 | steps_per_ms = pulse_steps / period_in_ms 23 | # Steps for a degree 24 | self.steps_per_degree = (deflect_90_in_ms * steps_per_ms) / 90 25 | # Mid-point of the servo in steps 26 | self.servo_mid_point_steps = servo_mid_point_ms * steps_per_ms 27 | 28 | # Map for channels 29 | self.channels = [0, 1, 14, 15] 30 | 31 | def stop_all(self): 32 | # 0 in start is nothing 33 | off_bit = 4096 # bit 12 is the OFF bit. 34 | self._pwm.setPWM(self.channels[0], 0, off_bit) 35 | self._pwm.setPWM(self.channels[1], 0, off_bit) 36 | self._pwm.setPWM(self.channels[2], 0, off_bit) 37 | self._pwm.setPWM(self.channels[3], 0, off_bit) 38 | 39 | def _convert_degrees_to_steps(self, position): 40 | return int(self.servo_mid_point_steps + (position * self.steps_per_degree)) 41 | 42 | def set_servo_angle(self, channel, angle): 43 | """position: The position in degrees from the center. -90 to 90""" 44 | # Validate 45 | if angle > 90 or angle < -90: 46 | raise ValueError("Angle outside of range") 47 | # Then set the position 48 | off_step = self._convert_degrees_to_steps(angle) 49 | self._pwm.setPWM(self.channels[channel], 0, off_step) 50 | 51 | -------------------------------------------------------------------------------- /chapter11/straight_line_drive.py: -------------------------------------------------------------------------------- 1 | from robot import Robot 2 | from pid_controller import PIController 3 | import time 4 | import logging 5 | 6 | logger = logging.getLogger("straight_line") 7 | logging.basicConfig(level=logging.INFO) 8 | logging.getLogger("pid_controller").setLevel(logging.DEBUG) 9 | 10 | bot = Robot() 11 | stop_at_time = time.time() + 15 12 | 13 | speed = 80 14 | bot.set_left(speed) 15 | bot.set_right(speed) 16 | 17 | pid = PIController(proportional_constant=4, integral_constant=0.3) 18 | 19 | while time.time() < stop_at_time: 20 | time.sleep(0.01) 21 | # Calculate the error 22 | left = bot.left_encoder.pulse_count 23 | right = bot.right_encoder.pulse_count 24 | error = left - right 25 | 26 | # Get the speed 27 | adjustment = pid.get_value(error) 28 | right_speed = int(speed + adjustment) 29 | left_speed = int(speed - adjustment) 30 | 31 | logger.debug(f"error: {error} adjustment: {adjustment:.2f}") 32 | logger.info(f"left: {left} right: {right}, left_speed: {left_speed} right_speed: {right_speed}") 33 | bot.set_left(left_speed) 34 | bot.set_right(right_speed) 35 | -------------------------------------------------------------------------------- /chapter11/test_distance_travelled.py: -------------------------------------------------------------------------------- 1 | from robot import Robot 2 | import time 3 | import math 4 | import logging 5 | logger = logging.getLogger("test_distance_travelled") 6 | 7 | wheel_diameter_mm = 70.0 8 | ticks_per_revolution = 40.0 9 | ticks_to_mm_const = (math.pi / ticks_per_revolution) * wheel_diameter_mm 10 | 11 | def ticks_to_mm(ticks): 12 | return int(ticks_to_mm_const * ticks) 13 | 14 | bot = Robot() 15 | stop_at_time = time.time() + 1 16 | 17 | logging.basicConfig(level=logging.INFO) 18 | bot.set_left(90) 19 | bot.set_right(90) 20 | 21 | while time.time() < stop_at_time: 22 | logger.info("Left: {} Right: {}".format( 23 | ticks_to_mm(bot.left_encoder.pulse_count), 24 | ticks_to_mm(bot.right_encoder.pulse_count))) 25 | time.sleep(0.05) 26 | 27 | -------------------------------------------------------------------------------- /chapter11/test_encoders.py: -------------------------------------------------------------------------------- 1 | from robot import Robot 2 | import time 3 | import logging 4 | 5 | from gpiozero import DigitalInputDevice 6 | 7 | logger = logging.getLogger("test_encoders") 8 | 9 | class EncoderCounter(object): 10 | def __init__(self, pin_number): 11 | self.pulse_count = 0 12 | self.device = DigitalInputDevice(pin=pin_number) 13 | self.device.pin.when_changed = self.when_changed 14 | 15 | def when_changed(self, time_ticks, state): 16 | self.pulse_count += 1 17 | 18 | bot = Robot() 19 | left_encoder = EncoderCounter(4) 20 | right_encoder = EncoderCounter(26) 21 | 22 | stop_at_time = time.time() + 1 23 | 24 | logging.basicConfig(level=logging.INFO) 25 | bot.set_left(90) 26 | bot.set_right(90) 27 | while time.time() < stop_at_time: 28 | logger.info(f"Left: {left_encoder.pulse_count} Right: {right_encoder.pulse_count}") 29 | time.sleep(0.05) 30 | -------------------------------------------------------------------------------- /chapter12/accelerometer_vector.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | import logging 3 | from robot_imu import RobotImu 4 | from robot_pose import robot_view 5 | 6 | logging.basicConfig(level=logging.INFO) 7 | imu = RobotImu() 8 | robot_view() 9 | 10 | accel_arrow = vp.arrow(axis=vp.vector(0, 1, 0)) 11 | x_arrow = vp.arrow(axis=vp.vector(1, 0, 0), color=vp.color.red) 12 | y_arrow = vp.arrow(axis=vp.vector(0, 1, 0), color=vp.color.green) 13 | z_arrow = vp.arrow(axis=vp.vector(0, 0, 1), color=vp.color.blue) 14 | 15 | while True: 16 | vp.rate(100) 17 | accel = imu.read_accelerometer() 18 | print(f"Accelerometer: {accel}") 19 | accel_arrow.axis = accel.norm() 20 | -------------------------------------------------------------------------------- /chapter12/magnetometer_vector.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | import logging 3 | from robot_imu import RobotImu 4 | from robot_pose import robot_view 5 | 6 | logging.basicConfig(level=logging.INFO) 7 | imu = RobotImu() 8 | robot_view() 9 | 10 | mag_arrow = vp.arrow(pos=vp.vector(0, 0, 0)) 11 | x_arrow = vp.arrow(axis=vp.vector(1, 0, 0), color=vp.color.red) 12 | y_arrow = vp.arrow(axis=vp.vector(0, 1, 0), color=vp.color.green) 13 | z_arrow = vp.arrow(axis=vp.vector(0, 0, 1), color=vp.color.blue) 14 | 15 | while True: 16 | vp.rate(100) 17 | 18 | mag = imu.read_magnetometer() 19 | mag_arrow.axis = mag.norm() 20 | print(f"Magnetometer: {mag}") 21 | 22 | -------------------------------------------------------------------------------- /chapter12/plot_gyroscope.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | import time 3 | from robot_imu import RobotImu 4 | 5 | imu = RobotImu() 6 | 7 | vp.graph(xmin=0, xmax=60, scroll=True) 8 | graph_x = vp.gcurve(color=vp.color.red) 9 | graph_y = vp.gcurve(color=vp.color.green) 10 | graph_z = vp.gcurve(color=vp.color.blue) 11 | 12 | start = time.time() 13 | while True: 14 | vp.rate(100) 15 | elapsed = time.time() - start 16 | gyro = imu.read_gyroscope() 17 | graph_x.plot(elapsed, gyro.x) 18 | graph_y.plot(elapsed, gyro.y) 19 | graph_z.plot(elapsed, gyro.z) 20 | -------------------------------------------------------------------------------- /chapter12/plot_temperature.py: -------------------------------------------------------------------------------- 1 | """When running, 2 | use VPYTHON_PORT=9020 VPYTHON_NOBROWSER=true""" 3 | import vpython as vp 4 | import time 5 | import logging 6 | from robot_imu import RobotImu 7 | 8 | 9 | logging.basicConfig(level=logging.INFO) 10 | imu = RobotImu() 11 | vp.graph(xmin=0, xmax=60, scroll=True) 12 | temp_graph = vp.gcurve() 13 | start = time.time() 14 | while True: 15 | vp.rate(100) 16 | temperature = imu.read_temperature() 17 | logging.info("Temperature {}".format(temperature)) 18 | elapsed = time.time() - start 19 | temp_graph.plot(elapsed, temperature) 20 | 21 | -------------------------------------------------------------------------------- /chapter12/robot_imu.py: -------------------------------------------------------------------------------- 1 | from icm20948 import ICM20948 2 | from vpython import vector 3 | 4 | 5 | class RobotImu: 6 | """Define a common interface to an inertial measurement unit with temperature""" 7 | def __init__(self): 8 | self._imu = ICM20948() 9 | 10 | def read_temperature(self): 11 | """Read a temperature in degrees C.""" 12 | return self._imu.read_temperature() 13 | 14 | def read_gyroscope(self): 15 | """Return prescaled gyro data""" 16 | _, _, _, x, y, z = self._imu.read_accelerometer_gyro_data() 17 | return vector(x, y, z) 18 | 19 | def read_accelerometer(self): 20 | """Return accelerometer data""" 21 | accel_x, accel_y, accel_z, _, _, _ = self._imu.read_accelerometer_gyro_data() 22 | return vector(accel_x, accel_y, accel_z) 23 | 24 | def read_magnetometer(self): 25 | """Return magnetometer data""" 26 | mag_x, mag_y, mag_z = self._imu.read_magnetometer_data() 27 | return vector(mag_x, -mag_y, -mag_z) 28 | 29 | -------------------------------------------------------------------------------- /chapter12/robot_pose.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | 3 | 4 | def robot_view(): 5 | vp.scene.forward = vp.vector(-3, -1, -1) 6 | vp.scene.up = vp.vector(0, 0, 1) 7 | -------------------------------------------------------------------------------- /chapter13/camera_stream.py: -------------------------------------------------------------------------------- 1 | from picamera.array import PiRGBArray 2 | from picamera import PiCamera 3 | import numpy as np 4 | 5 | import cv2 6 | 7 | size = (320, 240) 8 | encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 90] 9 | 10 | 11 | def setup_camera(): 12 | camera = PiCamera() 13 | camera.resolution = size 14 | camera.framerate = 24 15 | camera.rotation = 180 16 | return camera 17 | 18 | 19 | def start_stream(camera): 20 | image_storage = PiRGBArray(camera, size=size) 21 | 22 | cam_stream = camera.capture_continuous(image_storage, format="bgr", use_video_port=True) 23 | for raw_frame in cam_stream: 24 | yield raw_frame.array 25 | image_storage.truncate(0) 26 | 27 | 28 | def get_encoded_bytes_for_frame(frame): 29 | result, encoded_image = cv2.imencode('.jpg', frame, encode_param) 30 | return encoded_image.tostring() 31 | -------------------------------------------------------------------------------- /chapter13/control_image_behavior.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from image_app_core import start_server_process, get_control_instruction, put_output_image 4 | import camera_stream 5 | 6 | def controlled_image_server_behavior(): 7 | camera = camera_stream.setup_camera() 8 | time.sleep(0.1) 9 | 10 | for frame in camera_stream.start_stream(camera): 11 | encoded_bytes = camera_stream.get_encoded_bytes_for_frame(frame) 12 | put_output_image(encoded_bytes) 13 | 14 | instruction = get_control_instruction() 15 | if instruction and instruction['command'] == "exit": 16 | print("Stopping") 17 | return 18 | 19 | process = start_server_process('control_image_behavior.html') 20 | 21 | try: 22 | controlled_image_server_behavior() 23 | finally: 24 | process.terminate() 25 | -------------------------------------------------------------------------------- /chapter13/image_app_core.py: -------------------------------------------------------------------------------- 1 | import time 2 | from multiprocessing import Process, Queue 3 | 4 | from flask import Flask, render_template, Response, request 5 | 6 | app = Flask(__name__) 7 | control_queue = Queue() 8 | display_queue = Queue(maxsize=2) 9 | display_template = 'image_server.html' 10 | 11 | @app.route('/') 12 | def index(): 13 | return render_template(display_template) 14 | 15 | def frame_generator(): 16 | while True: 17 | time.sleep(0.05) 18 | encoded_bytes = display_queue.get() 19 | yield (b'--frame\r\n' 20 | b'Content-Type: image/jpeg\r\n\r\n' + encoded_bytes + b'\r\n') 21 | 22 | @app.route('/display') 23 | def display(): 24 | return Response(frame_generator(), 25 | mimetype='multipart/x-mixed-replace; boundary=frame') 26 | 27 | @app.route('/control', methods=['POST']) 28 | def control(): 29 | control_queue.put(request.form) 30 | return Response('queued') 31 | 32 | def start_server_process(template_name): 33 | """Start the process, call .terminate to close it""" 34 | global display_template 35 | display_template = template_name 36 | server = Process(target=app.run, kwargs={"host": "0.0.0.0", "port": 5001}) 37 | server.start() 38 | return server 39 | 40 | def put_output_image(encoded_bytes): 41 | """Queue an output image""" 42 | if display_queue.empty(): 43 | display_queue.put(encoded_bytes) 44 | 45 | def get_control_instruction(): 46 | if control_queue.empty(): 47 | return None 48 | else: 49 | return control_queue.get() 50 | -------------------------------------------------------------------------------- /chapter13/image_server.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, Response 2 | import camera_stream 3 | import time 4 | 5 | app = Flask(__name__) 6 | 7 | @app.route('/') 8 | def index(): 9 | return render_template('image_server.html') 10 | 11 | def frame_generator(): 12 | """This is our main video feed""" 13 | camera = camera_stream.setup_camera() 14 | 15 | # allow the camera to warm up 16 | time.sleep(0.1) 17 | for frame in camera_stream.start_stream(camera): 18 | encoded_bytes = camera_stream.get_encoded_bytes_for_frame(frame) 19 | yield (b'--frame\r\n' 20 | b'Content-Type: image/jpeg\r\n\r\n' + encoded_bytes + b'\r\n') 21 | 22 | @app.route('/display') 23 | def display(): 24 | return Response(frame_generator(), 25 | mimetype='multipart/x-mixed-replace; boundary=frame') 26 | 27 | app.run(host="0.0.0.0", debug=True, port=5001) 28 | -------------------------------------------------------------------------------- /chapter13/pid_controller.py: -------------------------------------------------------------------------------- 1 | import logging 2 | logger = logging.getLogger("pid_controller") 3 | 4 | class PIController: 5 | def __init__(self, proportional_constant=0, integral_constant=0, windup_limit=None): 6 | self.proportional_constant = proportional_constant 7 | self.integral_constant = integral_constant 8 | self.windup_limit = windup_limit 9 | # Running sums 10 | self.integral_sum = 0 11 | 12 | def handle_proportional(self, error): 13 | return self.proportional_constant * error 14 | 15 | def handle_integral(self, error): 16 | if self.windup_limit is None or \ 17 | (abs(self.integral_sum) < self.windup_limit) or \ 18 | ((error > 0) != (self.integral_sum > 0)): 19 | self.integral_sum += error 20 | return self.integral_constant * self.integral_sum 21 | 22 | def get_value(self, error): 23 | p = self.handle_proportional(error) 24 | i = self.handle_integral(error) 25 | logger.debug(f"P: {p}, I: {i:.2f}") 26 | return p + i 27 | 28 | def reset(self): 29 | self.integral_sum = 0 30 | -------------------------------------------------------------------------------- /chapter13/templates/color_track_behavior.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Robot Image Server 5 | 6 | 7 |

Robot Image Server

8 |
9 | Start 10 | Stop
11 | Exit 12 | 13 | 14 | -------------------------------------------------------------------------------- /chapter13/templates/control_image_behavior.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Robot Image Server 5 | 6 | 7 |

Robot Image Server

8 |
9 | Exit 10 | 11 | 12 | -------------------------------------------------------------------------------- /chapter13/templates/image_server.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Robot Image Server 4 | 5 | 6 |

Robot Image Server

7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /chapter14/camera_stream.py: -------------------------------------------------------------------------------- 1 | from picamera.array import PiRGBArray 2 | from picamera import PiCamera 3 | import numpy as np 4 | 5 | import cv2 6 | 7 | size = (320, 240) 8 | encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 90] 9 | 10 | 11 | def setup_camera(): 12 | camera = PiCamera() 13 | camera.resolution = size 14 | camera.rotation = 180 15 | return camera 16 | 17 | 18 | def start_stream(camera): 19 | image_storage = PiRGBArray(camera, size=size) 20 | 21 | cam_stream = camera.capture_continuous(image_storage, format="bgr", use_video_port=True) 22 | for raw_frame in cam_stream: 23 | yield raw_frame.array 24 | image_storage.truncate(0) 25 | 26 | 27 | def get_encoded_bytes_for_frame(frame): 28 | result, encoded_image = cv2.imencode('.jpg', frame, encode_param) 29 | return encoded_image.tostring() 30 | -------------------------------------------------------------------------------- /chapter14/image_app_core.py: -------------------------------------------------------------------------------- 1 | import time 2 | from multiprocessing import Process, Queue 3 | 4 | from flask import Flask, render_template, Response, request 5 | 6 | app = Flask(__name__) 7 | control_queue = Queue() 8 | display_queue = Queue(maxsize=2) 9 | display_template = 'image_server.html' 10 | 11 | @app.route('/') 12 | def index(): 13 | return render_template(display_template) 14 | 15 | def frame_generator(): 16 | while True: 17 | time.sleep(0.05) 18 | encoded_bytes = display_queue.get() 19 | yield (b'--frame\r\n' 20 | b'Content-Type: image/jpeg\r\n\r\n' + encoded_bytes + b'\r\n') 21 | 22 | @app.route('/display') 23 | def display(): 24 | return Response(frame_generator(), 25 | mimetype='multipart/x-mixed-replace; boundary=frame') 26 | 27 | @app.route('/control', methods=['POST']) 28 | def control(): 29 | control_queue.put(request.form) 30 | return Response('queued') 31 | 32 | def start_server_process(template_name): 33 | """Start the process, call .terminate to close it""" 34 | global display_template 35 | display_template = template_name 36 | server = Process(target=app.run, kwargs={"host": "0.0.0.0", "port": 5001}) 37 | server.start() 38 | return server 39 | 40 | def put_output_image(encoded_bytes): 41 | """Queue an output image""" 42 | if display_queue.empty(): 43 | display_queue.put(encoded_bytes) 44 | 45 | def get_control_instruction(): 46 | if control_queue.empty(): 47 | return None 48 | else: 49 | return control_queue.get() 50 | -------------------------------------------------------------------------------- /chapter14/pid_controller.py: -------------------------------------------------------------------------------- 1 | import logging 2 | logger = logging.getLogger("pid_controller") 3 | 4 | class PIController: 5 | def __init__(self, proportional_constant=0, integral_constant=0, windup_limit=None): 6 | self.proportional_constant = proportional_constant 7 | self.integral_constant = integral_constant 8 | self.windup_limit = windup_limit 9 | # Running sums 10 | self.integral_sum = 0 11 | 12 | def handle_proportional(self, error): 13 | return self.proportional_constant * error 14 | 15 | def handle_integral(self, error, delta_time): 16 | if self.windup_limit is None or \ 17 | (abs(self.integral_sum) < self.windup_limit) or \ 18 | ((error > 0) != (self.integral_sum > 0)): 19 | self.integral_sum += error * delta_time 20 | return self.integral_constant * self.integral_sum 21 | 22 | def get_value(self, error, delta_time=1): 23 | p = self.handle_proportional(error) 24 | i = self.handle_integral(error, delta_time) 25 | logger.debug(f"P: {p}, I: {i:.2f}") 26 | return p + i 27 | 28 | def reset(self): 29 | self.integral_sum = 0 30 | -------------------------------------------------------------------------------- /chapter14/robot.py: -------------------------------------------------------------------------------- 1 | from Raspi_MotorHAT import Raspi_MotorHAT 2 | from gpiozero import DistanceSensor 3 | 4 | import atexit 5 | import leds_led_shim 6 | from servos import Servos 7 | from encoder_counter import EncoderCounter 8 | 9 | 10 | class Robot: 11 | wheel_diameter_mm = 70.0 12 | ticks_per_revolution = 40.0 13 | wheel_distance_mm = 132.0 14 | def __init__(self, motorhat_addr=0x6f): 15 | # Setup the motorhat with the passed in address 16 | self._mh = Raspi_MotorHAT(addr=motorhat_addr) 17 | 18 | self.left_motor = self._mh.getMotor(1) 19 | self.right_motor = self._mh.getMotor(2) 20 | 21 | # Setup the Leds 22 | self.leds = leds_led_shim.Leds() 23 | # Set up servo motors for pan and tilt. 24 | self.servos = Servos(addr=motorhat_addr, deflect_90_in_ms=1) 25 | 26 | # Setup The Distance Sensors 27 | self.left_distance_sensor = DistanceSensor(echo=17, trigger=27, queue_len=2) 28 | self.right_distance_sensor = DistanceSensor(echo=5, trigger=6, queue_len=2) 29 | 30 | # Setup the Encoders 31 | EncoderCounter.set_constants(self.wheel_diameter_mm, self.ticks_per_revolution) 32 | self.left_encoder = EncoderCounter(4) 33 | self.right_encoder = EncoderCounter(26) 34 | 35 | # ensure the motors get stopped when the code exits 36 | atexit.register(self.stop_all) 37 | 38 | def convert_speed(self, speed): 39 | # Choose the running mode 40 | mode = Raspi_MotorHAT.RELEASE 41 | if speed > 0: 42 | mode = Raspi_MotorHAT.FORWARD 43 | elif speed < 0: 44 | mode = Raspi_MotorHAT.BACKWARD 45 | 46 | # Scale the speed 47 | output_speed = (abs(speed) * 255) // 100 48 | return mode, int(output_speed) 49 | 50 | def set_left(self, speed): 51 | mode, output_speed = self.convert_speed(speed) 52 | self.left_motor.setSpeed(output_speed) 53 | self.left_motor.run(mode) 54 | 55 | def set_right(self, speed): 56 | mode, output_speed = self.convert_speed(speed) 57 | self.right_motor.setSpeed(output_speed) 58 | self.right_motor.run(mode) 59 | 60 | def stop_motors(self): 61 | self.left_motor.run(Raspi_MotorHAT.RELEASE) 62 | self.right_motor.run(Raspi_MotorHAT.RELEASE) 63 | 64 | def stop_all(self): 65 | self.stop_motors() 66 | 67 | # Clear the display 68 | self.leds.clear() 69 | self.leds.show() 70 | 71 | # Clear any sensor handlers 72 | self.left_encoder.stop() 73 | self.right_encoder.stop() 74 | 75 | # Reset the servos 76 | self.servos.stop_all() 77 | 78 | def set_pan(self, angle): 79 | self.servos.set_servo_angle(1, angle) 80 | 81 | def set_tilt(self, angle): 82 | self.servos.set_servo_angle(0, angle) 83 | 84 | -------------------------------------------------------------------------------- /chapter14/servos.py: -------------------------------------------------------------------------------- 1 | from Raspi_MotorHAT.Raspi_PWM_Servo_Driver import PWM 2 | 3 | class Servos: 4 | def __init__(self, addr=0x6f, deflect_90_in_ms = 0.5): 5 | """addr: The i2c address of the PWM chip. 6 | deflect_90_in_ms: set this to calibrate the servo motors. 7 | it is what a deflection of 90 degrees is 8 | in terms of a pulse length in milliseconds.""" 9 | self._pwm = PWM(addr) 10 | # This sets the timebase for it all 11 | pwm_frequency = 100 12 | self._pwm.setPWMFreq(pwm_frequency) 13 | # Mid-point of the servo pulse length in milliseconds. 14 | servo_mid_point_ms = 1.5 15 | # Frequency is 1/period, but working ms, we can use 1000 16 | period_in_ms = 1000 / pwm_frequency 17 | # The chip has 4096 steps in each period. 18 | pulse_steps = 4096 19 | # Steps for every millisecond. 20 | steps_per_ms = pulse_steps / period_in_ms 21 | # Steps for a degree 22 | self.steps_per_degree = (deflect_90_in_ms * steps_per_ms) / 90 23 | # Mid-point of the servo in steps 24 | self.servo_mid_point_steps = servo_mid_point_ms * steps_per_ms 25 | 26 | # Map for channels 27 | self.channels = [0, 1, 14, 15] 28 | 29 | def stop_all(self): 30 | # 0 in start is nothing, 4096 sets the OFF bit. 31 | self._pwm.setPWM(self.channels[0], 0, 4096) 32 | self._pwm.setPWM(self.channels[1], 0, 4096) 33 | self._pwm.setPWM(self.channels[2], 0, 4096) 34 | self._pwm.setPWM(self.channels[3], 0, 4096) 35 | 36 | def _convert_degrees_to_steps(self, position): 37 | return int(self.servo_mid_point_steps + (position * self.steps_per_degree)) 38 | 39 | def set_servo_angle(self, channel, angle): 40 | """position: The position in degrees from the center. -90 to 90""" 41 | # Validate 42 | if angle > 90 or angle < -90: 43 | raise ValueError("Angle outside of range") 44 | # Then set the position 45 | off_step = self._convert_degrees_to_steps(angle) 46 | self._pwm.setPWM(self.channels[channel], 0, off_step) 47 | 48 | -------------------------------------------------------------------------------- /chapter14/templates/color_track_behavior.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Robot Image Server 5 | 6 | 7 |

Robot Image Server

8 |
9 | Start 10 | Stop
11 | Exit 12 | 13 | 14 | -------------------------------------------------------------------------------- /chapter14/test_line_find.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | from matplotlib import pyplot as plt 4 | 5 | image = cv2.imread("carpet_line1 2.jpg") 6 | assert image is not None, "Unable to read file" 7 | 8 | resized = cv2.resize(image, (320, 240)) 9 | 10 | gray = cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY) 11 | blur = cv2.blur(gray, (5, 80)) 12 | row = blur[180].astype(np.int32) 13 | diff = np.diff(row) 14 | 15 | max_d = np.amax(diff, 0) 16 | min_d = np.amin(diff, 0) 17 | 18 | highest = np.where(diff == max_d)[0][0] 19 | lowest = np.where(diff == min_d)[0][0] 20 | middle = (highest + lowest) // 2 21 | 22 | x = np.arange(len(diff)) 23 | plt.plot(x, diff) 24 | plt.plot([lowest, lowest], [max_d, min_d], "g--") 25 | plt.plot([middle, middle], [max_d, min_d], "r-") 26 | plt.plot([highest, highest], [max_d, min_d], "g--") 27 | plt.savefig("carpet_line1_2_blur580.png") 28 | -------------------------------------------------------------------------------- /chapter15/control_server.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | from robot_modes import RobotModes 4 | # A Flask App contains all its routes. 5 | app = Flask(__name__) 6 | # Prepare our robot modes for use 7 | mode_manager = RobotModes() 8 | 9 | 10 | @app.route("/run/", methods=['POST']) 11 | def run(mode_name): 12 | # Use our robot app to run something with this mode_name 13 | mode_manager.run(mode_name) 14 | return "%s running" 15 | 16 | 17 | @app.route("/stop", methods=['POST']) 18 | def stop(): 19 | # Tell our system to stop the mode it's in. 20 | mode_manager.stop() 21 | return "Stopped" 22 | 23 | 24 | app.run(host="0.0.0.0", debug=True) 25 | -------------------------------------------------------------------------------- /chapter15/my-robot-skill/__init__.py: -------------------------------------------------------------------------------- 1 | from adapt.intent import IntentBuilder 2 | from mycroft import MycroftSkill, intent_handler 3 | from mycroft.util.log import LOG 4 | 5 | import requests 6 | 7 | 8 | class MyRobot(MycroftSkill): 9 | def __init__(self): 10 | super().__init__() 11 | self.base_url = self.settings.get("base_url") 12 | 13 | @intent_handler(IntentBuilder("") 14 | .require("Robot") 15 | .require("TestRainbow")) 16 | def handle_test_rainbow(self, message): 17 | self.handle_control('/run/test_rainbow', 'TestingRainbow') 18 | 19 | @intent_handler(IntentBuilder("") 20 | .require("Robot") 21 | .require("stop")) 22 | def handle_stop(self, message): 23 | self.handle_control('/stop', 'stopping') 24 | 25 | def handle_control(self, end_point, dialog_verb): 26 | try: 27 | requests.post(self.base_url + end_point) 28 | self.speak_dialog('Robot') 29 | self.speak_dialog(dialog_verb) 30 | except: 31 | self.speak_dialog("UnableToReach") 32 | LOG.exception("Unable to reach the robot") 33 | 34 | 35 | def create_skill(): 36 | return MyRobot() 37 | 38 | -------------------------------------------------------------------------------- /chapter15/my-robot-skill/dialog/en-us/Robot.dialog: -------------------------------------------------------------------------------- 1 | The Robot 2 | Robot 3 | -------------------------------------------------------------------------------- /chapter15/my-robot-skill/dialog/en-us/TestingRainbow.dialog: -------------------------------------------------------------------------------- 1 | is testing rainbows. 2 | is deploying rainbows. 3 | is starting rainbows. 4 | is lighting up. 5 | -------------------------------------------------------------------------------- /chapter15/my-robot-skill/dialog/en-us/UnableToReach.dialog: -------------------------------------------------------------------------------- 1 | Sorry I cannot reach the robot. 2 | The robot is unreachable. 3 | Have you turned the robot on? 4 | Is the control server running on the robot? 5 | -------------------------------------------------------------------------------- /chapter15/my-robot-skill/dialog/en-us/stopping.dialog: -------------------------------------------------------------------------------- 1 | is stopping. 2 | will stop. 3 | -------------------------------------------------------------------------------- /chapter15/my-robot-skill/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | -------------------------------------------------------------------------------- /chapter15/my-robot-skill/settingsmeta.json: -------------------------------------------------------------------------------- 1 | { 2 | "skillMetadata": { 3 | "sections": [ 4 | { 5 | "name": "Robot", 6 | "fields": [ 7 | { 8 | "name": "base_url", 9 | "type": "text", 10 | "label": "Base URL for the robot control server", 11 | "value": "http://myrobot.local:5000" 12 | } 13 | ] 14 | } 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /chapter15/my-robot-skill/vocab/en-us/TestRainbow.voc: -------------------------------------------------------------------------------- 1 | test rainbow 2 | test the leds 3 | deploy rainbows 4 | turn on the lights 5 | -------------------------------------------------------------------------------- /chapter15/my-robot-skill/vocab/en-us/robot.voc: -------------------------------------------------------------------------------- 1 | robot 2 | my robot 3 | ask robot to 4 | tell the robot to 5 | -------------------------------------------------------------------------------- /chapter15/my-robot-skill/vocab/en-us/stop.voc: -------------------------------------------------------------------------------- 1 | stop 2 | cease 3 | turn off 4 | stand down -------------------------------------------------------------------------------- /chapter15/robot_modes.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | class RobotModes(object): 4 | """Our robot behaviors and tests as running modes""" 5 | 6 | # Mode config goes from a "mode_name" to a script to run. Configured for look up. 7 | mode_config = { 8 | "avoid_behavior": "avoid_with_rainbows.py", 9 | "circle_head": "circle_pan_tilt_behavior.py", 10 | "test_leds": "leds_test.py", 11 | "test_rainbow": "test_rainbow.py", 12 | "straight_line": "straight_line_drive.py", 13 | "square": "drive_square.py", 14 | "line_following": "line_follow_behavior.py", 15 | "color_track": "color_track_behavior.py", 16 | "face_track": "face_track_behavior.py", 17 | } 18 | 19 | def __init__(self): 20 | self.current_process = None 21 | 22 | def is_running(self): 23 | """Check if there is a process running. Returncode is only set when a process finishes""" 24 | return self.current_process and self.current_process.returncode is None 25 | 26 | def run(self, mode_name): 27 | """Run the mode as a subprocess, but not if we still have one running""" 28 | if not self.is_running(): 29 | script = self.mode_config[mode_name] 30 | self.current_process = subprocess.Popen(["python3", script]) 31 | return True 32 | return False 33 | 34 | def stop(self): 35 | """Stop a process""" 36 | if self.is_running(): 37 | # Sending the signal sigint is (on Linux) similar to pressing ctrl-c. 38 | # That causes the behavior to clean up and exit. 39 | self.current_process.send_signal(subprocess.signal.SIGINT) 40 | self.current_process = None 41 | -------------------------------------------------------------------------------- /chapter16/calibrate_gyro.py: -------------------------------------------------------------------------------- 1 | from robot_imu import RobotImu 2 | import time 3 | import vpython as vp 4 | 5 | imu = RobotImu() 6 | 7 | gyro_min = vp.vector(0, 0, 0) 8 | gyro_max = vp.vector(0, 0, 0) 9 | 10 | for n in range(500): 11 | gyro = imu.read_gyroscope() 12 | gyro_min.x = min(gyro_min.x, gyro.x) 13 | gyro_min.y = min(gyro_min.y, gyro.y) 14 | gyro_min.z = min(gyro_min.z, gyro.z) 15 | 16 | gyro_max.x = max(gyro_max.x, gyro.x) 17 | gyro_max.y = max(gyro_max.y, gyro.y) 18 | gyro_max.z = max(gyro_max.z, gyro.z) 19 | 20 | offset = (gyro_min + gyro_max) / 2 21 | 22 | time.sleep(.01) 23 | 24 | print(f"Zero offset: {offset}.") 25 | -------------------------------------------------------------------------------- /chapter16/delta_timer.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | class DeltaTimer: 5 | def __init__(self): 6 | self.last = self.start = time.time() 7 | 8 | def update(self): 9 | current = time.time() 10 | dt = current - self.last 11 | elapsed = current - self.start 12 | self.last = current 13 | return dt, elapsed -------------------------------------------------------------------------------- /chapter16/drive_north_behavior.py: -------------------------------------------------------------------------------- 1 | """This behavior will turn to seek north, and then drive that way""" 2 | from robot_imu import RobotImu, ImuFusion 3 | from delta_timer import DeltaTimer 4 | from pid_controller import PIController 5 | from robot import Robot 6 | import imu_settings 7 | 8 | 9 | imu = RobotImu(mag_offsets=imu_settings.mag_offsets, 10 | gyro_offsets=imu_settings.gyro_offsets) 11 | fusion = ImuFusion(imu) 12 | timer = DeltaTimer() 13 | pid = PIController(0.7, 0.01) 14 | robot = Robot() 15 | base_speed = 70 16 | 17 | # Lets head for this heading 18 | heading_set_point = 0 19 | 20 | while True: 21 | dt, elapsed = timer.update() 22 | fusion.update(dt) 23 | heading_error = fusion.yaw - heading_set_point 24 | steer_value = pid.get_value(heading_error, delta_time=dt) 25 | print(f"Error: {heading_error}, Value:{steer_value:2f}, t: {elapsed}") 26 | robot.set_left(base_speed + steer_value) 27 | robot.set_right(base_speed - steer_value) 28 | -------------------------------------------------------------------------------- /chapter16/imu_settings.py: -------------------------------------------------------------------------------- 1 | from vpython import vector 2 | gyro_offsets = vector(-0.583969, 0.675573, -0.530534) 3 | mag_offsets = vector(7.725, 9.6, -25.275) 4 | -------------------------------------------------------------------------------- /chapter16/magnetometer_calibration.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | from robot_imu import RobotImu 3 | from imu_settings import mag_offsets 4 | 5 | imu = RobotImu(mag_offsets=mag_offsets) 6 | 7 | mag_min = vp.vector(0, 0, 0) 8 | mag_max = vp.vector(0, 0, 0) 9 | 10 | 11 | scatter_xy = vp.gdots(color=vp.color.red) 12 | scatter_yz = vp.gdots(color=vp.color.green) 13 | scatter_zx = vp.gdots(color=vp.color.blue) 14 | 15 | while True: 16 | vp.rate(100) 17 | mag = imu.read_magnetometer() 18 | 19 | mag_min.x = min(mag_min.x, mag.x) 20 | mag_min.y = min(mag_min.y, mag.y) 21 | mag_min.z = min(mag_min.z, mag.z) 22 | 23 | mag_max.x = max(mag_max.x, mag.x) 24 | mag_max.y = max(mag_max.y, mag.y) 25 | mag_max.z = max(mag_max.z, mag.z) 26 | offset = (mag_max + mag_min) / 2 27 | 28 | print(f"Magnetometer: {mag}. Offsets: {offset}.") 29 | scatter_xy.plot(mag.x, mag.y) 30 | scatter_yz.plot(mag.y, mag.z) 31 | scatter_zx.plot(mag.z, mag.x) 32 | -------------------------------------------------------------------------------- /chapter16/pid_controller.py: -------------------------------------------------------------------------------- 1 | import logging 2 | logger = logging.getLogger("pid_controller") 3 | 4 | class PIController: 5 | def __init__(self, proportional_constant=0, integral_constant=0, windup_limit=None): 6 | self.proportional_constant = proportional_constant 7 | self.integral_constant = integral_constant 8 | self.windup_limit = windup_limit 9 | # Running sums 10 | self.integral_sum = 0 11 | 12 | def handle_proportional(self, error): 13 | return self.proportional_constant * error 14 | 15 | def handle_integral(self, error, delta_time): 16 | if self.windup_limit is None or \ 17 | (abs(self.integral_sum) < self.windup_limit) or \ 18 | ((error > 0) != (self.integral_sum > 0)): 19 | self.integral_sum += error * delta_time 20 | return self.integral_constant * self.integral_sum 21 | 22 | def get_value(self, error, delta_time=1): 23 | p = self.handle_proportional(error) 24 | i = self.handle_integral(error, delta_time) 25 | logger.debug(f"P: {p}, I: {i:.2f}") 26 | return p + i 27 | 28 | def reset(self): 29 | self.integral_sum = 0 30 | -------------------------------------------------------------------------------- /chapter16/plot_acc_gyro_fusion.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | from robot_imu import RobotImu, ImuFusion 3 | from delta_timer import DeltaTimer 4 | import imu_settings 5 | 6 | imu = RobotImu(gyro_offsets=imu_settings.gyro_offsets) 7 | 8 | fusion = ImuFusion(imu) 9 | 10 | vp.graph(xmin=0, xmax=60, scroll=True) 11 | graph_pitch = vp.gcurve(color=vp.color.red) 12 | graph_roll = vp.gcurve(color=vp.color.green) 13 | 14 | 15 | timer = DeltaTimer() 16 | while True: 17 | vp.rate(100) 18 | dt, elapsed = timer.update() 19 | fusion.update(dt) 20 | graph_pitch.plot(elapsed, fusion.pitch) 21 | graph_roll.plot(elapsed, fusion.roll) 22 | -------------------------------------------------------------------------------- /chapter16/plot_accel_debug_pitch_and_roll.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | import logging 3 | import time 4 | from robot_imu import RobotImu 5 | 6 | logging.basicConfig(level=logging.INFO) 7 | imu = RobotImu() 8 | 9 | 10 | pr = vp.graph(xmin=0, xmax=60, scroll=True) 11 | graph_pitch = vp.gcurve(color=vp.color.red, graph=pr) 12 | graph_roll = vp.gcurve(color=vp.color.green, graph=pr) 13 | 14 | xyz = vp.graph(xmin=0, xmax=60, scroll=True) 15 | graph_x = vp.gcurve(color=vp.color.orange, graph=xyz) 16 | graph_y = vp.gcurve(color=vp.color.cyan, graph=xyz) 17 | graph_z = vp.gcurve(color=vp.color.purple, graph=xyz) 18 | 19 | start = time.time() 20 | while True: 21 | vp.rate(100) 22 | elapsed = time.time() - start 23 | pitch, roll = imu.read_accelerometer_pitch_and_roll() 24 | raw_accel = imu.read_accelerometer() 25 | graph_pitch.plot(elapsed, pitch) 26 | graph_roll.plot(elapsed, roll) 27 | print(f"Pitch: {pitch:.2f}, Roll: {roll:.2f}") 28 | graph_x.plot(elapsed, raw_accel.x) 29 | graph_y.plot(elapsed, raw_accel.y) 30 | graph_z.plot(elapsed, raw_accel.z) 31 | -------------------------------------------------------------------------------- /chapter16/plot_accel_pitch_and_roll.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | import time 3 | from robot_imu import RobotImu 4 | 5 | 6 | imu = RobotImu() 7 | 8 | vp.graph(xmin=0, xmax=60, scroll=True) 9 | graph_pitch = vp.gcurve(color=vp.color.red) 10 | graph_roll = vp.gcurve(color=vp.color.green) 11 | 12 | start = time.time() 13 | while True: 14 | vp.rate(100) 15 | elapsed = time.time() - start 16 | pitch, roll = imu.read_accelerometer_pitch_and_roll() 17 | graph_pitch.plot(elapsed, pitch) 18 | graph_roll.plot(elapsed, roll) 19 | -------------------------------------------------------------------------------- /chapter16/plot_calibrated_gyroscope.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | import time 3 | from robot_imu import RobotImu 4 | import imu_settings 5 | 6 | imu = RobotImu(gyro_offsets=imu_settings.gyro_offsets) 7 | 8 | vp.graph(xmin=0, xmax=60, ymax=360, ymin=-360, scroll=True) 9 | graph_x = vp.gcurve(color=vp.color.red) 10 | graph_y = vp.gcurve(color=vp.color.green) 11 | graph_z = vp.gcurve(color=vp.color.blue) 12 | 13 | start = time.time() 14 | while True: 15 | vp.rate(100) 16 | elapsed = time.time() - start 17 | gyro = imu.read_gyroscope() 18 | print(f"Gyro x: {gyro.x:.2f}, y: {gyro.y:.2f}, z: {gyro.z:.2f}") 19 | graph_x.plot(elapsed, gyro.x) 20 | graph_y.plot(elapsed, gyro.y) 21 | graph_z.plot(elapsed, gyro.z) 22 | -------------------------------------------------------------------------------- /chapter16/plot_gyroscope.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | import time 3 | from robot_imu import RobotImu 4 | 5 | imu = RobotImu() 6 | 7 | vp.graph(xmin=0, xmax=60, scroll=True) 8 | graph_x = vp.gcurve(color=vp.color.red) 9 | graph_y = vp.gcurve(color=vp.color.green) 10 | graph_z = vp.gcurve(color=vp.color.blue) 11 | 12 | start = time.time() 13 | while True: 14 | vp.rate(100) 15 | elapsed = time.time() - start 16 | gyro = imu.read_gyroscope() 17 | print(f"Gyro x: {gyro.x:.2f}, y: {gyro.y:.2f}, z: {gyro.z:.2f}") 18 | graph_x.plot(elapsed, gyro.x) 19 | graph_y.plot(elapsed, gyro.y) 20 | graph_z.plot(elapsed, gyro.z) 21 | -------------------------------------------------------------------------------- /chapter16/plot_integrated_gyro.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | import logging 3 | from robot_imu import RobotImu 4 | from delta_timer import DeltaTimer 5 | import imu_settings 6 | 7 | 8 | logging.basicConfig(level=logging.INFO) 9 | imu = RobotImu(gyro_offsets=imu_settings.gyro_offsets) 10 | 11 | vp.graph(xmin=0, xmax=60, scroll=True) 12 | graph_x = vp.gcurve(color=vp.color.red) 13 | graph_y = vp.gcurve(color=vp.color.green) 14 | graph_z = vp.gcurve(color=vp.color.blue) 15 | 16 | timer = DeltaTimer() 17 | pitch = 0 18 | roll = 0 19 | yaw = 0 20 | 21 | while True: 22 | vp.rate(100) 23 | dt, elapsed = timer.update() 24 | gyro = imu.read_gyroscope() 25 | roll += gyro.x * dt 26 | pitch += gyro.y * dt 27 | yaw += gyro.z * dt 28 | print(f"Elapsed: {elapsed:.2f}, Pitch: {pitch:.2f}, Roll: {roll:.2f}, Yaw: {yaw:.2f}") 29 | graph_x.plot(elapsed, pitch) 30 | graph_y.plot(elapsed, roll) 31 | graph_z.plot(elapsed, yaw) 32 | -------------------------------------------------------------------------------- /chapter16/plot_mag_heading.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | from robot_imu import RobotImu 3 | from delta_timer import DeltaTimer 4 | import imu_settings 5 | 6 | imu = RobotImu(mag_offsets=imu_settings.mag_offsets) 7 | 8 | vp.cylinder(radius=1, axis=vp.vector(0, 0, 1), 9 | pos=vp.vector(0, 0, -1)) 10 | needle = vp.arrow(axis=vp.vector(1, 0, 0), 11 | color=vp.color.red) 12 | 13 | vp.graph(xmin=0, xmax=60, scroll=True) 14 | graph_yaw = vp.gcurve(color=vp.color.blue) 15 | timer = DeltaTimer() 16 | 17 | 18 | while True: 19 | vp.rate(100) 20 | dt, elapsed = timer.update() 21 | mag = imu.read_magnetometer() 22 | yaw = -vp.atan2(mag.y, mag.x) 23 | graph_yaw.plot(elapsed, vp.degrees(yaw)) 24 | needle.axis = vp.vector(vp.sin(yaw), vp.cos(yaw), 0) 25 | -------------------------------------------------------------------------------- /chapter16/plot_pitch_and_roll.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | from robot_imu import RobotImu, ImuFusion 3 | from delta_timer import DeltaTimer 4 | import imu_settings 5 | 6 | imu = RobotImu(gyro_offsets=imu_settings.gyro_offsets) 7 | fusion = ImuFusion(imu) 8 | 9 | vp.graph(xmin=0, xmax=60, scroll=True) 10 | graph_pitch = vp.gcurve(color=vp.color.red) 11 | graph_roll = vp.gcurve(color=vp.color.green) 12 | 13 | timer = DeltaTimer() 14 | while True: 15 | vp.rate(100) 16 | dt, elapsed = timer.update() 17 | fusion.update(dt) 18 | graph_pitch.plot(elapsed, fusion.pitch) 19 | graph_roll.plot(elapsed, fusion.roll) 20 | -------------------------------------------------------------------------------- /chapter16/robot.py: -------------------------------------------------------------------------------- 1 | from Raspi_MotorHAT import Raspi_MotorHAT 2 | from gpiozero import DistanceSensor 3 | 4 | import atexit 5 | import leds_led_shim 6 | from servos import Servos 7 | from encoder_counter import EncoderCounter 8 | 9 | 10 | class Robot: 11 | wheel_diameter_mm = 70.0 12 | ticks_per_revolution = 40.0 13 | wheel_distance_mm = 132.0 14 | def __init__(self, motorhat_addr=0x6f): 15 | # Setup the motorhat with the passed in address 16 | self._mh = Raspi_MotorHAT(addr=motorhat_addr) 17 | 18 | self.left_motor = self._mh.getMotor(1) 19 | self.right_motor = self._mh.getMotor(2) 20 | 21 | # Setup the Leds 22 | self.leds = leds_led_shim.Leds() 23 | # Set up servo motors for pan and tilt. 24 | self.servos = Servos(addr=motorhat_addr, deflect_90_in_ms=1) 25 | 26 | # Setup The Distance Sensors 27 | self.left_distance_sensor = DistanceSensor(echo=17, trigger=27, queue_len=2) 28 | self.right_distance_sensor = DistanceSensor(echo=5, trigger=6, queue_len=2) 29 | 30 | # Setup the Encoders 31 | EncoderCounter.set_constants(self.wheel_diameter_mm, self.ticks_per_revolution) 32 | self.left_encoder = EncoderCounter(4) 33 | self.right_encoder = EncoderCounter(26) 34 | 35 | # ensure the motors get stopped when the code exits 36 | atexit.register(self.stop_all) 37 | 38 | def convert_speed(self, speed): 39 | # Choose the running mode 40 | mode = Raspi_MotorHAT.RELEASE 41 | if speed > 0: 42 | mode = Raspi_MotorHAT.FORWARD 43 | elif speed < 0: 44 | mode = Raspi_MotorHAT.BACKWARD 45 | 46 | # Scale the speed 47 | output_speed = (abs(speed) * 255) // 100 48 | return mode, int(output_speed) 49 | 50 | def set_left(self, speed): 51 | mode, output_speed = self.convert_speed(speed) 52 | self.left_motor.setSpeed(output_speed) 53 | self.left_motor.run(mode) 54 | 55 | def set_right(self, speed): 56 | mode, output_speed = self.convert_speed(speed) 57 | self.right_motor.setSpeed(output_speed) 58 | self.right_motor.run(mode) 59 | 60 | def stop_motors(self): 61 | self.left_motor.run(Raspi_MotorHAT.RELEASE) 62 | self.right_motor.run(Raspi_MotorHAT.RELEASE) 63 | 64 | def stop_all(self): 65 | self.stop_motors() 66 | 67 | # Clear the display 68 | self.leds.clear() 69 | self.leds.show() 70 | 71 | # Clear any sensor handlers 72 | self.left_encoder.stop() 73 | self.right_encoder.stop() 74 | 75 | # Reset the servos 76 | self.servos.stop_all() 77 | 78 | def set_pan(self, angle): 79 | self.servos.set_servo_angle(1, angle) 80 | 81 | def set_tilt(self, angle): 82 | self.servos.set_servo_angle(0, angle) 83 | 84 | -------------------------------------------------------------------------------- /chapter16/robot_pose.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | 3 | 4 | def robot_view(): 5 | vp.scene.forward = vp.vector(-3, -1, -1) 6 | vp.scene.up = vp.vector(0, 0, 1) 7 | -------------------------------------------------------------------------------- /chapter16/servos.py: -------------------------------------------------------------------------------- 1 | from Raspi_MotorHAT.Raspi_PWM_Servo_Driver import PWM 2 | 3 | class Servos: 4 | def __init__(self, addr=0x6f, deflect_90_in_ms = 0.5): 5 | """addr: The i2c address of the PWM chip. 6 | deflect_90_in_ms: set this to calibrate the servo motors. 7 | it is what a deflection of 90 degrees is 8 | in terms of a pulse length in milliseconds.""" 9 | self._pwm = PWM(addr) 10 | # This sets the timebase for it all 11 | pwm_frequency = 100 12 | self._pwm.setPWMFreq(pwm_frequency) 13 | # Mid-point of the servo pulse length in milliseconds. 14 | servo_mid_point_ms = 1.5 15 | # Frequency is 1/period, but working ms, we can use 1000 16 | period_in_ms = 1000 / pwm_frequency 17 | # The chip has 4096 steps in each period. 18 | pulse_steps = 4096 19 | # Steps for every millisecond. 20 | steps_per_ms = pulse_steps / period_in_ms 21 | # Steps for a degree 22 | self.steps_per_degree = (deflect_90_in_ms * steps_per_ms) / 90 23 | # Mid-point of the servo in steps 24 | self.servo_mid_point_steps = servo_mid_point_ms * steps_per_ms 25 | 26 | # Map for channels 27 | self.channels = [0, 1, 14, 15] 28 | 29 | def stop_all(self): 30 | # 0 in start is nothing, 4096 sets the OFF bit. 31 | self._pwm.setPWM(self.channels[0], 0, 4096) 32 | self._pwm.setPWM(self.channels[1], 0, 4096) 33 | self._pwm.setPWM(self.channels[2], 0, 4096) 34 | self._pwm.setPWM(self.channels[3], 0, 4096) 35 | 36 | def _convert_degrees_to_steps(self, position): 37 | return int(self.servo_mid_point_steps + (position * self.steps_per_degree)) 38 | 39 | def set_servo_angle(self, channel, angle): 40 | """position: The position in degrees from the center. -90 to 90""" 41 | # Validate 42 | if angle > 90 or angle < -90: 43 | raise ValueError("Angle outside of range") 44 | # Then set the position 45 | off_step = self._convert_degrees_to_steps(angle) 46 | self._pwm.setPWM(self.channels[channel], 0, off_step) 47 | 48 | -------------------------------------------------------------------------------- /chapter16/virtual_robot.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | from robot_pose import robot_view 3 | 4 | 5 | def make_robot(): 6 | chassis_width = 155 # left side to right 7 | chassis_thickness = 3 # plastic thickness 8 | chassis_length = 200 # front to back 9 | wheel_thickness = 26 10 | wheel_diameter = 70 11 | axle_x = 30 # wheel axle from 12 | axle_z = -20 13 | castor_position = vp.vector(-80, -6, -30) 14 | castor_radius = 14 15 | castor_thickness = 12 16 | 17 | base = vp.box(length=chassis_length, 18 | height=chassis_thickness, 19 | width=chassis_width) 20 | # rotate to match body - so Z is height and Y is width 21 | base.rotate(angle=vp.radians(90), 22 | axis=vp.vector(1, 0, 0)) 23 | wheel_dist = chassis_width/2 24 | wheel_l = vp.cylinder(radius=wheel_diameter/2, 25 | length=wheel_thickness, 26 | pos=vp.vector(axle_x, -wheel_dist, axle_z), 27 | axis=vp.vector(0, -1, 0)) 28 | wheel_r = vp.cylinder(radius=wheel_diameter/2, 29 | length=wheel_thickness, 30 | pos=vp.vector(axle_x, wheel_dist, axle_z), 31 | axis=vp.vector(0, 1, 0)) 32 | castor = vp.cylinder(radius=castor_radius, 33 | length=castor_thickness, 34 | pos=castor_position, 35 | axis=vp.vector(0, 1, 0)) 36 | return vp.compound([base, wheel_l, wheel_r, castor]) 37 | 38 | 39 | if __name__ == "__main__": 40 | robot_view() 41 | x_arrow = vp.arrow(axis=vp.vector(200, 0, 0), color=vp.color.red) 42 | y_arrow = vp.arrow(axis=vp.vector(0, 200, 0), color=vp.color.green) 43 | z_arrow = vp.arrow(axis=vp.vector(0, 0, 200), color=vp.color.blue) 44 | robot=make_robot() 45 | -------------------------------------------------------------------------------- /chapter16/visual_filtered_accelerometer.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | import logging 3 | from robot_imu import RobotImu, ComplementaryFilter 4 | import virtual_robot 5 | 6 | logging.basicConfig(level=logging.INFO) 7 | imu = RobotImu() 8 | model = virtual_robot.make_robot() 9 | virtual_robot.robot_view() 10 | 11 | filter = ComplementaryFilter(0.95).filter 12 | 13 | pitch = 0 14 | roll = 0 15 | 16 | while True: 17 | vp.rate(100) 18 | 19 | new_pitch, new_roll = imu.read_accelerometer_pitch_and_roll() 20 | pitch = filter(pitch, new_pitch) 21 | roll = filter(roll, new_roll) 22 | 23 | print(f"Pitch: {pitch:.2f}, Roll: {roll:.2f}") 24 | # reset the model 25 | model.up = vp.vector(0, 1, 0) 26 | model.axis = vp.vector(1, 0, 0) 27 | # Reposition it 28 | model.rotate(angle=vp.radians(roll), axis=vp.vector(1, 0, 0)) 29 | model.rotate(angle=vp.radians(pitch), axis=vp.vector(0, 1, 0)) 30 | -------------------------------------------------------------------------------- /chapter16/visual_fusion.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | from robot_imu import RobotImu, ImuFusion 3 | from delta_timer import DeltaTimer 4 | import imu_settings 5 | import virtual_robot 6 | 7 | imu = RobotImu(gyro_offsets=imu_settings.gyro_offsets, 8 | mag_offsets=imu_settings.mag_offsets) 9 | fusion = ImuFusion(imu) 10 | 11 | robot_view = vp.canvas(align="left") 12 | model = virtual_robot.make_robot() 13 | virtual_robot.robot_view() 14 | 15 | compass = vp.canvas(width=400, height=400) 16 | vp.cylinder(radius=1, axis=vp.vector(0, 0, 1), 17 | pos=vp.vector(0, 0, -1)) 18 | needle = vp.arrow(axis=vp.vector(1, 0, 0), 19 | color=vp.color.red) 20 | 21 | 22 | vp.graph(xmin=0, xmax=60, scroll=True) 23 | graph_roll = vp.gcurve(color=vp.color.red) 24 | graph_pitch = vp.gcurve(color=vp.color.green) 25 | graph_yaw = vp.gcurve(color=vp.color.blue) 26 | 27 | timer = DeltaTimer() 28 | 29 | while True: 30 | vp.rate(100) 31 | dt, elapsed = timer.update() 32 | fusion.update(dt) 33 | # reset the model 34 | model.up = vp.vector(0, 1, 0) 35 | model.axis = vp.vector(1, 0, 0) 36 | # Reposition it 37 | model.rotate(angle=vp.radians(fusion.roll), axis=vp.vector(1, 0, 0)) 38 | model.rotate(angle=vp.radians(fusion.pitch), axis=vp.vector(0, 1, 0)) 39 | model.rotate(angle=vp.radians(fusion.yaw), axis=vp.vector(0, 0, 1)) 40 | needle.axis = vp.vector( 41 | vp.sin(vp.radians(fusion.yaw)), 42 | vp.cos(vp.radians(fusion.yaw)), 43 | 0) 44 | graph_roll.plot(elapsed, fusion.roll) 45 | graph_pitch.plot(elapsed, fusion.pitch) 46 | graph_yaw.plot(elapsed, fusion.yaw) 47 | -------------------------------------------------------------------------------- /chapter16/visual_gyroscope.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | from robot_imu import RobotImu 3 | import time 4 | import imu_settings 5 | import virtual_robot 6 | 7 | 8 | imu = RobotImu(gyro_offsets=imu_settings.gyro_offsets) 9 | 10 | pitch = 0 11 | roll = 0 12 | yaw = 0 13 | 14 | model = virtual_robot.make_robot() 15 | virtual_robot.robot_view() 16 | 17 | latest = time.time() 18 | 19 | while True: 20 | vp.rate(1000) 21 | current = time.time() 22 | dt = current - latest 23 | latest = current 24 | gyro = imu.read_gyroscope() 25 | roll += gyro.x * dt 26 | pitch += gyro.y * dt 27 | yaw += gyro.z * dt 28 | 29 | # reset the model 30 | model.up = vp.vector(0, 1, 0) 31 | model.axis = vp.vector(1, 0, 0) 32 | # Reposition it 33 | model.rotate(angle=vp.radians(roll), axis=vp.vector(1, 0, 0)) 34 | model.rotate(angle=vp.radians(pitch), axis=vp.vector(0, 1, 0)) 35 | model.rotate(angle=vp.radians(yaw), axis=vp.vector(0, 0, 1)) 36 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/accelerometer_vector.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | import logging 3 | from robot_imu import RobotImu 4 | from robot_pose import robot_view 5 | 6 | logging.basicConfig(level=logging.INFO) 7 | imu = RobotImu() 8 | robot_view() 9 | 10 | accel_arrow = vp.arrow(axis=vp.vector(0, 1, 0)) 11 | x_arrow = vp.arrow(axis=vp.vector(1, 0, 0), color=vp.color.red) 12 | y_arrow = vp.arrow(axis=vp.vector(0, 1, 0), color=vp.color.green) 13 | z_arrow = vp.arrow(axis=vp.vector(0, 0, 1), color=vp.color.blue) 14 | 15 | while True: 16 | vp.rate(100) 17 | accel = imu.read_accelerometer() 18 | print(f"Accelerometer: {accel}") 19 | accel_arrow.axis = accel.norm() 20 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/avoid_with_rainbows.py: -------------------------------------------------------------------------------- 1 | from robot import Robot 2 | from time import sleep 3 | from led_rainbow import show_rainbow 4 | 5 | 6 | class ObstacleAvoidingBehavior: 7 | """Simple obstacle avoiding""" 8 | def __init__(self, the_robot): 9 | self.robot = the_robot 10 | self.speed = 60 11 | # Calculations for the LEDs 12 | self.led_half = int(self.robot.leds.count/2) 13 | self.sense_colour = 255, 0, 0 14 | 15 | def distance_to_led_bar(self, distance): 16 | # Invert so closer means more LED's. 17 | inverted = max(0, 1.0 - distance) 18 | led_bar = int(round(inverted * self.led_half)) + 1 19 | return led_bar 20 | 21 | def display_state(self, left_distance, right_distance): 22 | # Clear first 23 | self.robot.leds.clear() 24 | # Left side 25 | led_bar = self.distance_to_led_bar(left_distance) 26 | show_rainbow(self.robot.leds, range(led_bar)) 27 | # Right side 28 | led_bar = self.distance_to_led_bar(right_distance) 29 | # Bit trickier - must go from below the leds count, to the leds count. 30 | start = (self.robot.leds.count - 1) - (led_bar) 31 | right_range = range(self.robot.leds.count - 1, start, -1) 32 | show_rainbow(self.robot.leds, right_range) 33 | # Now show this display 34 | self.robot.leds.show() 35 | 36 | def get_speeds(self, nearest_distance): 37 | if nearest_distance >= 1.0: 38 | nearest_speed = self.speed 39 | furthest_speed = self.speed 40 | delay = 100 41 | elif nearest_distance > 0.5: 42 | nearest_speed = self.speed 43 | furthest_speed = self.speed * 0.8 44 | delay = 100 45 | elif nearest_distance > 0.2: 46 | nearest_speed = self.speed 47 | furthest_speed = self.speed * 0.6 48 | delay = 100 49 | elif nearest_distance > 0.1: 50 | nearest_speed = -self.speed * 0.4 51 | furthest_speed = -self.speed 52 | delay = 100 53 | else: # collison 54 | nearest_speed = -self.speed 55 | furthest_speed = -self.speed 56 | delay = 250 57 | return nearest_speed, furthest_speed, delay 58 | 59 | def run(self): 60 | while True: 61 | # Get the sensor readings in meters 62 | left_distance = self.robot.left_distance_sensor.distance 63 | right_distance = self.robot.right_distance_sensor.distance 64 | # Display this 65 | self.display_state(left_distance, right_distance) 66 | # Get speeds for motors from distances 67 | nearest_speed, furthest_speed, delay = self.get_speeds(min(left_distance, right_distance)) 68 | print(f"Distances: l {left_distance:.2f}, r {right_distance:.2f}. Speeds: n: {nearest_speed}, f: {furthest_speed}. Delay: {delay}") 69 | # and drive 70 | # Send this to the motors 71 | if left_distance < right_distance: 72 | self.robot.set_left(nearest_speed) 73 | self.robot.set_right(furthest_speed) 74 | else: 75 | self.robot.set_right(nearest_speed) 76 | self.robot.set_left(furthest_speed) 77 | # Wait our delay time 78 | sleep(delay * 0.001) 79 | 80 | 81 | bot = Robot() 82 | behavior = ObstacleAvoidingBehavior(bot) 83 | behavior.run() 84 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/behavior_line.py: -------------------------------------------------------------------------------- 1 | import robot 2 | from Raspi_MotorHAT import Raspi_MotorHAT 3 | from time import sleep 4 | 5 | r = robot.Robot() 6 | r.set_left(100) 7 | r.set_right(70) 8 | sleep(1) 9 | 10 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/behavior_path.py: -------------------------------------------------------------------------------- 1 | import robot 2 | from time import sleep 3 | 4 | def straight(bot, seconds): 5 | bot.set_left(100) 6 | bot.set_right(100) 7 | sleep(seconds) 8 | 9 | def turn_left(bot, seconds): 10 | bot.set_left(20) 11 | bot.set_right(80) 12 | sleep(seconds) 13 | 14 | def turn_right(bot, seconds): 15 | bot.set_left(80) 16 | bot.set_right(20) 17 | sleep(seconds) 18 | 19 | def spin_left(bot, seconds): 20 | bot.set_left(-100) 21 | bot.set_right(100) 22 | sleep(seconds) 23 | 24 | bot = robot.Robot() 25 | straight(bot, 1) 26 | turn_right(bot, 0.6) 27 | straight(bot, 0.6) 28 | turn_left(bot, 0.6) 29 | straight(bot, 0.6) 30 | turn_left(bot, 0.6) 31 | straight(bot, 0.3) 32 | spin_left(bot, 1) 33 | 34 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/calibrate_gyro.py: -------------------------------------------------------------------------------- 1 | from robot_imu import RobotImu 2 | import time 3 | import vpython as vp 4 | 5 | imu = RobotImu() 6 | 7 | gyro_min = vp.vector(0, 0, 0) 8 | gyro_max = vp.vector(0, 0, 0) 9 | 10 | for n in range(500): 11 | gyro = imu.read_gyroscope() 12 | gyro_min.x = min(gyro_min.x, gyro.x) 13 | gyro_min.y = min(gyro_min.y, gyro.y) 14 | gyro_min.z = min(gyro_min.z, gyro.z) 15 | 16 | gyro_max.x = max(gyro_max.x, gyro.x) 17 | gyro_max.y = max(gyro_max.y, gyro.y) 18 | gyro_max.z = max(gyro_max.z, gyro.z) 19 | 20 | offset = (gyro_min + gyro_max) / 2 21 | 22 | time.sleep(.01) 23 | 24 | print(f"Zero offset: {offset}.") 25 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/calibrate_magnetometer.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | from robot_imu import RobotImu 3 | from imu_settings import mag_offsets 4 | 5 | imu = RobotImu(mag_offsets=mag_offsets) 6 | 7 | mag_min = vp.vector(0, 0, 0) 8 | mag_max = vp.vector(0, 0, 0) 9 | 10 | 11 | scatter_xy = vp.gdots(color=vp.color.red) 12 | scatter_yz = vp.gdots(color=vp.color.green) 13 | scatter_zx = vp.gdots(color=vp.color.blue) 14 | 15 | while True: 16 | vp.rate(100) 17 | mag = imu.read_magnetometer() 18 | 19 | mag_min.x = min(mag_min.x, mag.x) 20 | mag_min.y = min(mag_min.y, mag.y) 21 | mag_min.z = min(mag_min.z, mag.z) 22 | 23 | mag_max.x = max(mag_max.x, mag.x) 24 | mag_max.y = max(mag_max.y, mag.y) 25 | mag_max.z = max(mag_max.z, mag.z) 26 | offset = (mag_max + mag_min) / 2 27 | 28 | print(f"Magnetometer: {mag}. Offsets: {offset}.") 29 | scatter_xy.plot(mag.x, mag.y) 30 | scatter_yz.plot(mag.y, mag.z) 31 | scatter_zx.plot(mag.z, mag.x) 32 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/camera_stream.py: -------------------------------------------------------------------------------- 1 | from picamera.array import PiRGBArray 2 | from picamera import PiCamera 3 | import numpy as np 4 | 5 | import cv2 6 | 7 | size = (320, 240) 8 | encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 90] 9 | 10 | 11 | def setup_camera(): 12 | camera = PiCamera() 13 | camera.resolution = size 14 | camera.rotation = 180 15 | return camera 16 | 17 | 18 | def start_stream(camera): 19 | image_storage = PiRGBArray(camera, size=size) 20 | 21 | cam_stream = camera.capture_continuous(image_storage, format="bgr", use_video_port=True) 22 | for raw_frame in cam_stream: 23 | yield raw_frame.array 24 | image_storage.truncate(0) 25 | 26 | 27 | def get_encoded_bytes_for_frame(frame): 28 | result, encoded_image = cv2.imencode('.jpg', frame, encode_param) 29 | return encoded_image.tostring() 30 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/circle_pan_tilt_behavior.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | import math 3 | 4 | from robot import Robot 5 | class CirclePanTiltBehavior: 6 | def __init__(self, the_robot): 7 | self.robot = the_robot 8 | self.current_time = 0 9 | self.frames_per_circle = 50 10 | self.radians_per_frame = (2 * math.pi) / self.frames_per_circle 11 | self.radius = 30 12 | 13 | def run(self): 14 | while True: 15 | frame_number = self.current_time % self.frames_per_circle 16 | frame_in_radians = frame_number * self.radians_per_frame 17 | self.robot.set_pan(self.radius * math.cos(frame_in_radians)) 18 | self.robot.set_tilt(self.radius * math.sin(frame_in_radians)) 19 | sleep(0.05) 20 | self.current_time += 1 21 | 22 | bot = Robot() 23 | behavior = CirclePanTiltBehavior(bot) 24 | behavior.run() 25 | 26 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/control_image_behavior.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from image_app_core import start_server_process, get_control_instruction, put_output_image 4 | import camera_stream 5 | 6 | def controlled_image_server_behavior(): 7 | camera = camera_stream.setup_camera() 8 | time.sleep(0.1) 9 | 10 | for frame in camera_stream.start_stream(camera): 11 | encoded_bytes = camera_stream.get_encoded_bytes_for_frame(frame) 12 | put_output_image(encoded_bytes) 13 | 14 | instruction = get_control_instruction() 15 | if instruction and instruction['command'] == "exit": 16 | print("Stopping") 17 | return 18 | 19 | process = start_server_process('control_image_behavior.html') 20 | 21 | try: 22 | controlled_image_server_behavior() 23 | finally: 24 | process.terminate() 25 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/control_server.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | from robot_modes import RobotModes 4 | # A Flask App contains all its routes. 5 | app = Flask(__name__) 6 | # Prepare our robot modes for use 7 | mode_manager = RobotModes() 8 | 9 | 10 | @app.route("/run/", methods=['POST']) 11 | def run(mode_name): 12 | # Use our robot app to run something with this mode_name 13 | mode_manager.run(mode_name) 14 | return "%s running" 15 | 16 | 17 | @app.route("/stop", methods=['POST']) 18 | def stop(): 19 | # Tell our system to stop the mode it's in. 20 | mode_manager.stop() 21 | return "Stopped" 22 | 23 | 24 | app.run(host="0.0.0.0", debug=True) 25 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/delta_timer.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | class DeltaTimer: 5 | def __init__(self): 6 | self.last = self.start = time.time() 7 | 8 | def update(self): 9 | current = time.time() 10 | dt = current - self.last 11 | elapsed = current - self.start 12 | self.last = current 13 | return dt, elapsed -------------------------------------------------------------------------------- /chapter17/0_starting_point/drive_distance.py: -------------------------------------------------------------------------------- 1 | from robot import Robot, EncoderCounter 2 | from pid_controller import PIController 3 | import time 4 | import logging 5 | logger = logging.getLogger("drive_distance") 6 | 7 | def drive_distance(bot, distance, speed=80): 8 | # Use left as "primary" motor, the right is keeping up 9 | set_primary = bot.set_left 10 | primary_encoder = bot.left_encoder 11 | set_secondary = bot.set_right 12 | secondary_encoder = bot.right_encoder 13 | controller = PIController(proportional_constant=5, integral_constant=0.3) 14 | 15 | # start the motors, and start the loop 16 | set_primary(speed) 17 | set_secondary(speed) 18 | while primary_encoder.pulse_count < distance or secondary_encoder.pulse_count < distance: 19 | time.sleep(0.01) 20 | # How far off are we? 21 | error = primary_encoder.pulse_count - secondary_encoder.pulse_count 22 | adjustment = controller.get_value(error) 23 | # How fast should the motor move to get there? 24 | set_primary(int(speed - adjustment)) 25 | set_secondary(int(speed + adjustment)) 26 | # Some debug 27 | logger.debug(f"Encoders: primary: {primary_encoder.pulse_count}, secondary: {secondary_encoder.pulse_count}," 28 | f"e:{error} adjustment: {adjustment:.2f}") 29 | logger.info(f"Distances: primary: {primary_encoder.distance_in_mm()} mm, secondary: {secondary_encoder.distance_in_mm()} mm") 30 | 31 | logging.basicConfig(level=logging.DEBUG) 32 | bot = Robot() 33 | distance_to_drive = 1000 # in mm - this is a meter 34 | distance_in_ticks = EncoderCounter.mm_to_ticks(distance_to_drive) 35 | drive_distance(bot, distance_in_ticks) 36 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/drive_north_behavior.py: -------------------------------------------------------------------------------- 1 | """This behavior will turn to seek north, and then drive that way""" 2 | from robot_imu import RobotImu, ImuFusion 3 | from delta_timer import DeltaTimer 4 | from pid_controller import PIController 5 | from robot import Robot 6 | import imu_settings 7 | 8 | 9 | imu = RobotImu(mag_offsets=imu_settings.mag_offsets, 10 | gyro_offsets=imu_settings.gyro_offsets) 11 | fusion = ImuFusion(imu) 12 | timer = DeltaTimer() 13 | pid = PIController(0.7, 0.01) 14 | robot = Robot() 15 | base_speed = 70 16 | 17 | # Lets head for this heading 18 | heading_set_point = 0 19 | 20 | while True: 21 | dt, elapsed = timer.update() 22 | fusion.update(dt) 23 | heading_error = fusion.yaw - heading_set_point 24 | steer_value = pid.get_value(heading_error, delta_time=dt) 25 | print(f"Error: {heading_error}, Value:{steer_value:2f}, t: {elapsed}") 26 | robot.set_left(base_speed + steer_value) 27 | robot.set_right(base_speed - steer_value) 28 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/encoder_counter.py: -------------------------------------------------------------------------------- 1 | from gpiozero import DigitalInputDevice 2 | import math 3 | class EncoderCounter: 4 | ticks_to_mm_const = None # you must set this up before using distance methods 5 | def __init__(self, pin_number): 6 | self.pulse_count = 0 7 | self.direction = 1 8 | self.device = DigitalInputDevice(pin=pin_number) 9 | self.device.pin.when_changed = self.when_changed 10 | 11 | def when_changed(self, time_ticks, state): 12 | self.pulse_count += self.direction 13 | 14 | def set_direction(self, direction): 15 | """This should be -1 or 1. """ 16 | assert abs(direction)==1, "Direction %s should be 1 or -1" % direction 17 | self.direction = direction 18 | 19 | def reset(self): 20 | self.pulse_count = 0 21 | 22 | def stop(self): 23 | self.device.close() 24 | 25 | def distance_in_mm(self): 26 | return int(self.pulse_count * EncoderCounter.ticks_to_mm_const) 27 | 28 | @staticmethod 29 | def mm_to_ticks(mm): 30 | return mm / EncoderCounter.ticks_to_mm_const 31 | 32 | @staticmethod 33 | def set_constants(wheel_diameter_mm, ticks_per_revolution): 34 | EncoderCounter.ticks_to_mm_const = (math.pi / ticks_per_revolution) * wheel_diameter_mm 35 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/image_app_core.py: -------------------------------------------------------------------------------- 1 | import time 2 | from multiprocessing import Process, Queue 3 | 4 | from flask import Flask, render_template, Response, request 5 | 6 | app = Flask(__name__) 7 | control_queue = Queue() 8 | display_queue = Queue(maxsize=2) 9 | display_template = 'image_server.html' 10 | 11 | @app.route('/') 12 | def index(): 13 | return render_template(display_template) 14 | 15 | def frame_generator(): 16 | while True: 17 | time.sleep(0.05) 18 | encoded_bytes = display_queue.get() 19 | yield (b'--frame\r\n' 20 | b'Content-Type: image/jpeg\r\n\r\n' + encoded_bytes + b'\r\n') 21 | 22 | @app.route('/display') 23 | def display(): 24 | return Response(frame_generator(), 25 | mimetype='multipart/x-mixed-replace; boundary=frame') 26 | 27 | @app.route('/control', methods=['POST']) 28 | def control(): 29 | control_queue.put(request.form) 30 | return Response('queued') 31 | 32 | def start_server_process(template_name): 33 | """Start the process, call .terminate to close it""" 34 | global display_template 35 | display_template = template_name 36 | server = Process(target=app.run, kwargs={"host": "0.0.0.0", "port": 5001}) 37 | server.start() 38 | return server 39 | 40 | def put_output_image(encoded_bytes): 41 | """Queue an output image""" 42 | if display_queue.empty(): 43 | display_queue.put(encoded_bytes) 44 | 45 | def get_control_instruction(): 46 | if control_queue.empty(): 47 | return None 48 | else: 49 | return control_queue.get() 50 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/image_server.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, Response 2 | import camera_stream 3 | import time 4 | 5 | app = Flask(__name__) 6 | 7 | @app.route('/') 8 | def index(): 9 | return render_template('image_server.html') 10 | 11 | def frame_generator(): 12 | """This is our main video feed""" 13 | camera = camera_stream.setup_camera() 14 | 15 | # allow the camera to warm up 16 | time.sleep(0.1) 17 | for frame in camera_stream.start_stream(camera): 18 | encoded_bytes = camera_stream.get_encoded_bytes_for_frame(frame) 19 | yield (b'--frame\r\n' 20 | b'Content-Type: image/jpeg\r\n\r\n' + encoded_bytes + b'\r\n') 21 | 22 | @app.route('/display') 23 | def display(): 24 | return Response(frame_generator(), 25 | mimetype='multipart/x-mixed-replace; boundary=frame') 26 | 27 | app.run(host="0.0.0.0", debug=True, port=5001) 28 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/imu_settings.py: -------------------------------------------------------------------------------- 1 | from vpython import vector 2 | gyro_offsets = vector(-0.583969, 0.675573, -0.530534) 3 | mag_offsets = vector(7.725, 9.6, -25.275) 4 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/led_rainbow.py: -------------------------------------------------------------------------------- 1 | 2 | import colorsys 3 | 4 | 5 | def show_rainbow(leds, led_range): 6 | led_range = list(led_range) 7 | hue_step = 1.0 / len(led_range) 8 | for index, led_address in enumerate(led_range): 9 | hue = hue_step * index 10 | rgb = colorsys.hsv_to_rgb(hue, 1.0, 0.6) 11 | rgb = [int(c*255) for c in rgb] 12 | leds.set_one(led_address, rgb) 13 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/leds_led_shim.py: -------------------------------------------------------------------------------- 1 | import ledshim 2 | 3 | 4 | class Leds: 5 | @property 6 | def count(self): 7 | return ledshim.width 8 | 9 | def set_one(self, led_number, color): 10 | ledshim.set_pixel(led_number, *color) 11 | 12 | def set_range(self, led_range, color): 13 | for pixel in led_range: 14 | ledshim.set_pixel(pixel, *color) 15 | 16 | def set_all(self, color): 17 | ledshim.set_all(*color) 18 | 19 | def clear(self): 20 | ledshim.clear() 21 | 22 | def show(self): 23 | ledshim.show() 24 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/leds_test.py: -------------------------------------------------------------------------------- 1 | from robot import Robot 2 | from time import sleep 3 | 4 | bot = Robot() 5 | red = (255, 0, 0) 6 | blue = (0, 0, 255) 7 | 8 | while True: 9 | print("red") 10 | bot.leds.set_all(red) 11 | bot.leds.show() 12 | sleep(0.5) 13 | print("blue") 14 | bot.leds.set_all(blue) 15 | bot.leds.show() 16 | sleep(0.5) 17 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/magnetometer_vector.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | import logging 3 | from robot_imu import RobotImu 4 | from robot_pose import robot_view 5 | 6 | logging.basicConfig(level=logging.INFO) 7 | imu = RobotImu() 8 | robot_view() 9 | 10 | mag_arrow = vp.arrow(pos=vp.vector(0, 0, 0)) 11 | x_arrow = vp.arrow(axis=vp.vector(1, 0, 0), color=vp.color.red) 12 | y_arrow = vp.arrow(axis=vp.vector(0, 1, 0), color=vp.color.green) 13 | z_arrow = vp.arrow(axis=vp.vector(0, 0, 1), color=vp.color.blue) 14 | 15 | while True: 16 | vp.rate(100) 17 | 18 | mag = imu.read_magnetometer() 19 | mag_arrow.axis = mag.norm() 20 | print(f"Magnetometer: {mag}") 21 | 22 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/my-robot-skill/__init__.py: -------------------------------------------------------------------------------- 1 | from adapt.intent import IntentBuilder 2 | from mycroft import MycroftSkill, intent_handler 3 | from mycroft.util.log import LOG 4 | 5 | import requests 6 | 7 | 8 | class MyRobot(MycroftSkill): 9 | def __init__(self): 10 | super().__init__() 11 | self.base_url = self.settings.get("base_url") 12 | 13 | @intent_handler(IntentBuilder("") 14 | .require("Robot") 15 | .require("TestRainbow")) 16 | def handle_test_rainbow(self, message): 17 | self.handle_control('/run/test_rainbow', 'TestingRainbow') 18 | 19 | @intent_handler(IntentBuilder("") 20 | .require("Robot") 21 | .require("stop")) 22 | def handle_stop(self, message): 23 | self.handle_control('/stop', 'stopping') 24 | 25 | def handle_control(self, end_point, dialog_verb): 26 | try: 27 | requests.post(self.base_url + end_point) 28 | self.speak_dialog('Robot') 29 | self.speak_dialog(dialog_verb) 30 | except: 31 | self.speak_dialog("UnableToReach") 32 | LOG.exception("Unable to reach the robot") 33 | 34 | 35 | def create_skill(): 36 | return MyRobot() 37 | 38 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/my-robot-skill/dialog/en-us/Robot.dialog: -------------------------------------------------------------------------------- 1 | The Robot 2 | Robot 3 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/my-robot-skill/dialog/en-us/TestingRainbow.dialog: -------------------------------------------------------------------------------- 1 | is testing rainbows. 2 | is deploying rainbows. 3 | is starting rainbows. 4 | is lighting up. 5 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/my-robot-skill/dialog/en-us/UnableToReach.dialog: -------------------------------------------------------------------------------- 1 | Sorry I cannot reach the robot. 2 | The robot is unreachable. 3 | Have you turned the robot on? 4 | Is the control server running on the robot? 5 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/my-robot-skill/dialog/en-us/stopping.dialog: -------------------------------------------------------------------------------- 1 | is stopping. 2 | will stop. 3 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/my-robot-skill/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/my-robot-skill/settingsmeta.json: -------------------------------------------------------------------------------- 1 | { 2 | "base_url": "http://myrobot.local:5000", 3 | "skillMetadata": { 4 | "sections": [ 5 | { 6 | "name": "Robot", 7 | "fields": [ 8 | { 9 | "name": "base_url", 10 | "type": "text", 11 | "label": "Base URL for the robot control server", 12 | "value": "https://myrobots.local:5000" 13 | } 14 | ] 15 | } 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/my-robot-skill/vocab/en-us/TestRainbow.voc: -------------------------------------------------------------------------------- 1 | test rainbow 2 | test the leds 3 | deploy rainbows 4 | turn on the lights 5 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/my-robot-skill/vocab/en-us/robot.voc: -------------------------------------------------------------------------------- 1 | robot 2 | my robot 3 | ask robot to 4 | tell the robot to 5 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/my-robot-skill/vocab/en-us/stop.voc: -------------------------------------------------------------------------------- 1 | stop 2 | cease 3 | turn off 4 | stand down -------------------------------------------------------------------------------- /chapter17/0_starting_point/pid_controller.py: -------------------------------------------------------------------------------- 1 | import logging 2 | logger = logging.getLogger("pid_controller") 3 | 4 | class PIController: 5 | def __init__(self, proportional_constant=0, integral_constant=0, windup_limit=None): 6 | self.proportional_constant = proportional_constant 7 | self.integral_constant = integral_constant 8 | self.windup_limit = windup_limit 9 | # Running sums 10 | self.integral_sum = 0 11 | 12 | def handle_proportional(self, error): 13 | return self.proportional_constant * error 14 | 15 | def handle_integral(self, error, delta_time): 16 | if self.windup_limit is None or \ 17 | (abs(self.integral_sum) < self.windup_limit) or \ 18 | ((error > 0) != (self.integral_sum > 0)): 19 | self.integral_sum += error * delta_time 20 | return self.integral_constant * self.integral_sum 21 | 22 | def get_value(self, error, delta_time=1): 23 | p = self.handle_proportional(error) 24 | i = self.handle_integral(error, delta_time) 25 | logger.debug(f"P: {p}, I: {i:.2f}") 26 | return p + i 27 | 28 | def reset(self): 29 | self.integral_sum = 0 30 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/plot_acc_gyro_fusion.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | from robot_imu import RobotImu, ImuFusion 3 | from delta_timer import DeltaTimer 4 | import imu_settings 5 | 6 | imu = RobotImu(gyro_offsets=imu_settings.gyro_offsets) 7 | 8 | fusion = ImuFusion(imu) 9 | 10 | vp.graph(xmin=0, xmax=60, scroll=True) 11 | graph_pitch = vp.gcurve(color=vp.color.red) 12 | graph_roll = vp.gcurve(color=vp.color.green) 13 | 14 | 15 | timer = DeltaTimer() 16 | while True: 17 | vp.rate(100) 18 | dt, elapsed = timer.update() 19 | fusion.update(dt) 20 | graph_pitch.plot(elapsed, fusion.pitch) 21 | graph_roll.plot(elapsed, fusion.roll) 22 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/plot_accel_debug_pitch_and_roll.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | import logging 3 | import time 4 | from robot_imu import RobotImu 5 | 6 | logging.basicConfig(level=logging.INFO) 7 | imu = RobotImu() 8 | 9 | 10 | pr = vp.graph(xmin=0, xmax=60, scroll=True) 11 | graph_pitch = vp.gcurve(color=vp.color.red, graph=pr) 12 | graph_roll = vp.gcurve(color=vp.color.green, graph=pr) 13 | 14 | xyz = vp.graph(xmin=0, xmax=60, scroll=True) 15 | graph_x = vp.gcurve(color=vp.color.orange, graph=xyz) 16 | graph_y = vp.gcurve(color=vp.color.cyan, graph=xyz) 17 | graph_z = vp.gcurve(color=vp.color.purple, graph=xyz) 18 | 19 | start = time.time() 20 | while True: 21 | vp.rate(100) 22 | elapsed = time.time() - start 23 | pitch, roll = imu.read_accelerometer_pitch_and_roll() 24 | raw_accel = imu.read_accelerometer() 25 | graph_pitch.plot(elapsed, pitch) 26 | graph_roll.plot(elapsed, roll) 27 | print(f"Pitch: {pitch:.2f}, Roll: {roll:.2f}") 28 | graph_x.plot(elapsed, raw_accel.x) 29 | graph_y.plot(elapsed, raw_accel.y) 30 | graph_z.plot(elapsed, raw_accel.z) 31 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/plot_accel_pitch_and_roll.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | import time 3 | from robot_imu import RobotImu 4 | 5 | 6 | imu = RobotImu() 7 | 8 | vp.graph(xmin=0, xmax=60, scroll=True) 9 | graph_pitch = vp.gcurve(color=vp.color.red) 10 | graph_roll = vp.gcurve(color=vp.color.green) 11 | 12 | start = time.time() 13 | while True: 14 | vp.rate(100) 15 | elapsed = time.time() - start 16 | pitch, roll = imu.read_accelerometer_pitch_and_roll() 17 | graph_pitch.plot(elapsed, pitch) 18 | graph_roll.plot(elapsed, roll) 19 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/plot_calibrated_gyroscope.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | import time 3 | from robot_imu import RobotImu 4 | import imu_settings 5 | 6 | imu = RobotImu(gyro_offsets=imu_settings.gyro_offsets) 7 | 8 | vp.graph(xmin=0, xmax=60, ymax=360, ymin=-360, scroll=True) 9 | graph_x = vp.gcurve(color=vp.color.red) 10 | graph_y = vp.gcurve(color=vp.color.green) 11 | graph_z = vp.gcurve(color=vp.color.blue) 12 | 13 | start = time.time() 14 | while True: 15 | vp.rate(100) 16 | elapsed = time.time() - start 17 | gyro = imu.read_gyroscope() 18 | print(f"Gyro x: {gyro.x:.2f}, y: {gyro.y:.2f}, z: {gyro.z:.2f}") 19 | graph_x.plot(elapsed, gyro.x) 20 | graph_y.plot(elapsed, gyro.y) 21 | graph_z.plot(elapsed, gyro.z) 22 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/plot_gyroscope.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | import time 3 | from robot_imu import RobotImu 4 | 5 | imu = RobotImu() 6 | 7 | vp.graph(xmin=0, xmax=60, scroll=True) 8 | graph_x = vp.gcurve(color=vp.color.red) 9 | graph_y = vp.gcurve(color=vp.color.green) 10 | graph_z = vp.gcurve(color=vp.color.blue) 11 | 12 | start = time.time() 13 | while True: 14 | vp.rate(100) 15 | elapsed = time.time() - start 16 | gyro = imu.read_gyroscope() 17 | print(f"Gyro x: {gyro.x:.2f}, y: {gyro.y:.2f}, z: {gyro.z:.2f}") 18 | graph_x.plot(elapsed, gyro.x) 19 | graph_y.plot(elapsed, gyro.y) 20 | graph_z.plot(elapsed, gyro.z) 21 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/plot_imu_fusion.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | from robot_imu import RobotImu, ImuFusion 3 | from delta_timer import DeltaTimer 4 | import imu_settings 5 | 6 | imu = RobotImu(gyro_offsets=imu_settings.gyro_offsets, 7 | mag_offsets=imu_settings.mag_offsets) 8 | fusion = ImuFusion(imu) 9 | 10 | vp.graph(xmin=0, xmax=60, scroll=True) 11 | graph_pitch = vp.gcurve(color=vp.color.red) 12 | graph_roll = vp.gcurve(color=vp.color.green) 13 | graph_yaw = vp.gcurve(color=vp.color.blue) 14 | 15 | timer = DeltaTimer() 16 | while True: 17 | vp.rate(100) 18 | dt, elapsed = timer.update() 19 | fusion.update(dt) 20 | graph_pitch.plot(elapsed, fusion.pitch) 21 | graph_roll.plot(elapsed, fusion.roll) 22 | graph_yaw.plot(elapsed, fusion.yaw) 23 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/plot_integrated_gyro.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | import logging 3 | from robot_imu import RobotImu 4 | from delta_timer import DeltaTimer 5 | import imu_settings 6 | 7 | 8 | logging.basicConfig(level=logging.INFO) 9 | imu = RobotImu(gyro_offsets=imu_settings.gyro_offsets) 10 | 11 | vp.graph(xmin=0, xmax=60, scroll=True) 12 | graph_x = vp.gcurve(color=vp.color.red) 13 | graph_y = vp.gcurve(color=vp.color.green) 14 | graph_z = vp.gcurve(color=vp.color.blue) 15 | 16 | timer = DeltaTimer() 17 | pitch = 0 18 | roll = 0 19 | yaw = 0 20 | 21 | while True: 22 | vp.rate(100) 23 | dt, elapsed = timer.update() 24 | gyro = imu.read_gyroscope() 25 | roll += gyro.x * dt 26 | pitch += gyro.y * dt 27 | yaw += gyro.z * dt 28 | print(f"Elapsed: {elapsed:.2f}, Pitch: {pitch:.2f}, Roll: {roll:.2f}, Yaw: {yaw:.2f}") 29 | graph_x.plot(elapsed, pitch) 30 | graph_y.plot(elapsed, roll) 31 | graph_z.plot(elapsed, yaw) 32 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/plot_mag_heading.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | from robot_imu import RobotImu 3 | from delta_timer import DeltaTimer 4 | import imu_settings 5 | 6 | imu = RobotImu(mag_offsets=imu_settings.mag_offsets) 7 | 8 | vp.graph(xmin=0, xmax=60, scroll=True) 9 | graph_yaw = vp.gcurve(color=vp.color.blue) 10 | timer = DeltaTimer() 11 | 12 | while True: 13 | vp.rate(100) 14 | dt, elapsed = timer.update() 15 | mag = imu.read_magnetometer() 16 | yaw = -vp.degrees(vp.atan2(mag.y, mag.x)) 17 | graph_yaw.plot(elapsed, yaw) 18 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/plot_temperature.py: -------------------------------------------------------------------------------- 1 | """When running, 2 | use VPYTHON_PORT=9020 VPYTHON_NOBROWSER=true""" 3 | import vpython as vp 4 | import time 5 | import logging 6 | from robot_imu import RobotImu 7 | 8 | 9 | logging.basicConfig(level=logging.INFO) 10 | imu = RobotImu() 11 | vp.graph(xmin=0, xmax=60, scroll=True) 12 | temp_graph = vp.gcurve() 13 | start = time.time() 14 | while True: 15 | vp.rate(100) 16 | temperature = imu.read_temperature() 17 | logging.info("Temperature {}".format(temperature)) 18 | elapsed = time.time() - start 19 | temp_graph.plot(elapsed, temperature) 20 | 21 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/robot.py: -------------------------------------------------------------------------------- 1 | from Raspi_MotorHAT import Raspi_MotorHAT 2 | from gpiozero import DistanceSensor 3 | 4 | import atexit 5 | import leds_led_shim 6 | from servos import Servos 7 | from encoder_counter import EncoderCounter 8 | 9 | 10 | class Robot: 11 | wheel_diameter_mm = 70.0 12 | ticks_per_revolution = 40.0 13 | wheel_distance_mm = 132.0 14 | def __init__(self, motorhat_addr=0x6f): 15 | # Setup the motorhat with the passed in address 16 | self._mh = Raspi_MotorHAT(addr=motorhat_addr) 17 | 18 | self.left_motor = self._mh.getMotor(1) 19 | self.right_motor = self._mh.getMotor(2) 20 | 21 | # Setup the Leds 22 | self.leds = leds_led_shim.Leds() 23 | # Set up servo motors for pan and tilt. 24 | self.servos = Servos(addr=motorhat_addr, deflect_90_in_ms=1) 25 | 26 | # Setup The Distance Sensors 27 | self.left_distance_sensor = DistanceSensor(echo=17, trigger=27, queue_len=2) 28 | self.right_distance_sensor = DistanceSensor(echo=5, trigger=6, queue_len=2) 29 | 30 | # Setup the Encoders 31 | EncoderCounter.set_constants(self.wheel_diameter_mm, self.ticks_per_revolution) 32 | self.left_encoder = EncoderCounter(4) 33 | self.right_encoder = EncoderCounter(26) 34 | 35 | # ensure the motors get stopped when the code exits 36 | atexit.register(self.stop_all) 37 | 38 | def convert_speed(self, speed): 39 | # Choose the running mode 40 | mode = Raspi_MotorHAT.RELEASE 41 | if speed > 0: 42 | mode = Raspi_MotorHAT.FORWARD 43 | elif speed < 0: 44 | mode = Raspi_MotorHAT.BACKWARD 45 | 46 | # Scale the speed 47 | output_speed = (abs(speed) * 255) // 100 48 | return mode, int(output_speed) 49 | 50 | def set_left(self, speed): 51 | mode, output_speed = self.convert_speed(speed) 52 | self.left_motor.setSpeed(output_speed) 53 | self.left_motor.run(mode) 54 | 55 | def set_right(self, speed): 56 | mode, output_speed = self.convert_speed(speed) 57 | self.right_motor.setSpeed(output_speed) 58 | self.right_motor.run(mode) 59 | 60 | def stop_motors(self): 61 | self.left_motor.run(Raspi_MotorHAT.RELEASE) 62 | self.right_motor.run(Raspi_MotorHAT.RELEASE) 63 | 64 | def stop_all(self): 65 | self.stop_motors() 66 | 67 | # Clear the display 68 | self.leds.clear() 69 | self.leds.show() 70 | 71 | # Clear any sensor handlers 72 | self.left_encoder.stop() 73 | self.right_encoder.stop() 74 | 75 | # Reset the servos 76 | self.servos.stop_all() 77 | 78 | def set_pan(self, angle): 79 | self.servos.set_servo_angle(1, angle) 80 | 81 | def set_tilt(self, angle): 82 | self.servos.set_servo_angle(0, angle) 83 | 84 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/robot_modes.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | class RobotModes(object): 4 | """Our robot behaviors and tests as running modes""" 5 | 6 | # Mode config goes from a "mode_name" to a script to run. Configured for look up. 7 | mode_config = { 8 | "avoid_behavior": "avoid_with_rainbows.py", 9 | "circle_head": "circle_pan_tilt_behavior.py", 10 | "test_leds": "leds_test.py", 11 | "test_rainbow": "test_rainbow.py", 12 | "straight_line": "straight_line_drive.py", 13 | "square": "drive_square.py", 14 | "line_following": "line_follow_behavior.py", 15 | "color_track": "color_track_behavior.py", 16 | "face_track": "face_track_behavior.py", 17 | } 18 | 19 | def __init__(self): 20 | self.current_process = None 21 | 22 | def is_running(self): 23 | """Check if there is a process running. Returncode is only set when a process finishes""" 24 | return self.current_process and self.current_process.returncode is None 25 | 26 | def run(self, mode_name): 27 | """Run the mode as a subprocess, but not if we still have one running""" 28 | if not self.is_running(): 29 | script = self.mode_config[mode_name] 30 | self.current_process = subprocess.Popen(["python3", script]) 31 | return True 32 | return False 33 | 34 | def stop(self): 35 | """Stop a process""" 36 | if self.is_running(): 37 | # Sending the signal sigint is (on Linux) similar to pressing ctrl-c. 38 | # That causes the behavior to clean up and exit. 39 | self.current_process.send_signal(subprocess.signal.SIGINT) 40 | self.current_process = None 41 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/robot_pose.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | 3 | 4 | def robot_view(): 5 | vp.scene.forward = vp.vector(-3, -1, -1) 6 | vp.scene.up = vp.vector(0, 0, 1) 7 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/servo_type_position.py: -------------------------------------------------------------------------------- 1 | from Raspi_MotorHAT.Raspi_PWM_Servo_Driver import PWM 2 | import atexit 3 | 4 | pwm = PWM(0x6f) 5 | # This sets the timebase for it all 6 | pwm_frequency = 100 7 | pwm.setPWMFreq(pwm_frequency) 8 | # Mid-point of the servo pulse length in milliseconds. 9 | servo_mid_point_ms = 1.5 10 | # What a deflection of 90 degrees is in pulse length in milliseconds 11 | deflect_90_in_ms = 0.5 12 | # Frequency is 1 divided by period, but working ms, we can use 1000 13 | period_in_ms = 1000 / pwm_frequency 14 | # The chip has 4096 steps in each period. 15 | pulse_steps = 4096 16 | # Steps for every millisecond. 17 | steps_per_ms = pulse_steps / period_in_ms 18 | # Steps for a degree. 19 | steps_per_degree = (deflect_90_in_ms * steps_per_ms) / 90 20 | # Mid-point of the servo in steps 21 | servo_mid_point_steps = servo_mid_point_ms * steps_per_ms 22 | 23 | def convert_degrees_to_steps(position): 24 | return int(servo_mid_point_steps + (position * steps_per_degree)) 25 | 26 | atexit.register(pwm.setPWM, 0, 0, 4096) 27 | 28 | while True: 29 | position = int(input("Type your position in degrees (90 to -90, 0 is middle): ")) 30 | end_step = convert_degrees_to_steps(position) 31 | pwm.setPWM(0, 0, end_step) 32 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/servos.py: -------------------------------------------------------------------------------- 1 | from Raspi_MotorHAT.Raspi_PWM_Servo_Driver import PWM 2 | 3 | class Servos: 4 | def __init__(self, addr=0x6f, deflect_90_in_ms = 0.5): 5 | """addr: The i2c address of the PWM chip. 6 | deflect_90_in_ms: set this to calibrate the servo motors. 7 | it is what a deflection of 90 degrees is 8 | in terms of a pulse length in milliseconds.""" 9 | self._pwm = PWM(addr) 10 | # This sets the timebase for it all 11 | pwm_frequency = 100 12 | self._pwm.setPWMFreq(pwm_frequency) 13 | # Mid-point of the servo pulse length in milliseconds. 14 | servo_mid_point_ms = 1.5 15 | # Frequency is 1/period, but working ms, we can use 1000 16 | period_in_ms = 1000 / pwm_frequency 17 | # The chip has 4096 steps in each period. 18 | pulse_steps = 4096 19 | # Steps for every millisecond. 20 | steps_per_ms = pulse_steps / period_in_ms 21 | # Steps for a degree 22 | self.steps_per_degree = (deflect_90_in_ms * steps_per_ms) / 90 23 | # Mid-point of the servo in steps 24 | self.servo_mid_point_steps = servo_mid_point_ms * steps_per_ms 25 | 26 | # Map for channels 27 | self.channels = [0, 1, 14, 15] 28 | 29 | def stop_all(self): 30 | # 0 in start is nothing, 4096 sets the OFF bit. 31 | self._pwm.setPWM(self.channels[0], 0, 4096) 32 | self._pwm.setPWM(self.channels[1], 0, 4096) 33 | self._pwm.setPWM(self.channels[2], 0, 4096) 34 | self._pwm.setPWM(self.channels[3], 0, 4096) 35 | 36 | def _convert_degrees_to_steps(self, position): 37 | return int(self.servo_mid_point_steps + (position * self.steps_per_degree)) 38 | 39 | def set_servo_angle(self, channel, angle): 40 | """position: The position in degrees from the center. -90 to 90""" 41 | # Validate 42 | if angle > 90 or angle < -90: 43 | raise ValueError("Angle outside of range") 44 | # Then set the position 45 | off_step = self._convert_degrees_to_steps(angle) 46 | self._pwm.setPWM(self.channels[channel], 0, off_step) 47 | 48 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/simple_avoid_behavior.py: -------------------------------------------------------------------------------- 1 | from robot import Robot 2 | from time import sleep 3 | 4 | class ObstacleAvoidingBehavior: 5 | """Simple obstacle avoiding""" 6 | def __init__(self, the_robot): 7 | self.robot = the_robot 8 | self.speed = 60 9 | 10 | def get_motor_speed(self, distance): 11 | """This method chooses a speed for a motor based on the distance from a sensor""" 12 | if distance < 0.2: 13 | return -self.speed 14 | else: 15 | return self.speed 16 | 17 | def run(self): 18 | while True: 19 | # Get the sensor readings in meters 20 | left_distance = self.robot.left_distance_sensor.distance 21 | right_distance = self.robot.right_distance_sensor.distance 22 | print("Left: {l:.2f}, Right: {r:.2f}".format(l=left_distance, r=right_distance)) 23 | # and drive 24 | left_speed = self.get_motor_speed(left_distance) 25 | self.robot.set_right(left_speed) 26 | right_speed = self.get_motor_speed(right_distance) 27 | self.robot.set_left(right_speed) 28 | # Wait a little 29 | sleep(0.05) 30 | 31 | bot = Robot() 32 | behavior = ObstacleAvoidingBehavior(bot) 33 | behavior.run() 34 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/sonar_scan.py: -------------------------------------------------------------------------------- 1 | import time 2 | import math 3 | 4 | import matplotlib.pyplot as plt 5 | 6 | from robot import Robot 7 | 8 | start_scan =0 9 | lower_bound = -90 10 | upper_bound = 90 11 | scan_step = 5 12 | 13 | the_robot = Robot() 14 | the_robot.set_tilt(0) 15 | 16 | scan_data = {} 17 | # Make the sensor scan 18 | for facing in range(lower_bound, upper_bound, scan_step): 19 | the_robot.set_pan(-facing) 20 | time.sleep(0.1) 21 | scan_data[facing] = the_robot.left_distance_sensor.distance * 100 22 | # make plot 23 | axis = [math.radians(facing) for facing in scan_data.keys()] 24 | print(axis) 25 | print(scan_data.values()) 26 | plt.polar(axis, list(scan_data.values()), 'g-') 27 | # dump to png 28 | plt.savefig("scan.png") 29 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/straight_line_drive.py: -------------------------------------------------------------------------------- 1 | from robot import Robot 2 | from pid_controller import PIController 3 | import time 4 | import logging 5 | 6 | logger = logging.getLogger("straight_line") 7 | logging.basicConfig(level=logging.INFO) 8 | logging.getLogger("pid_controller").setLevel(logging.DEBUG) 9 | 10 | bot = Robot() 11 | stop_at_time = time.time() + 15 12 | 13 | speed = 80 14 | bot.set_left(speed) 15 | bot.set_right(speed) 16 | 17 | pid = PIController(proportional_constant=4, integral_constant=0.3) 18 | 19 | while time.time() < stop_at_time: 20 | time.sleep(0.01) 21 | # Calculate the error 22 | left = bot.left_encoder.pulse_count 23 | right = bot.right_encoder.pulse_count 24 | error = left - right 25 | 26 | # Get the speed 27 | adjustment = pid.get_value(error) 28 | right_speed = int(speed + adjustment) 29 | left_speed = int(speed - adjustment) 30 | 31 | logger.debug(f"error: {error} adjustment: {adjustment:.2f}") 32 | logger.info(f"left: {left} right: {right}, left_speed: {left_speed} right_speed: {right_speed}") 33 | bot.set_left(left_speed) 34 | bot.set_right(right_speed) 35 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/templates/color_track_behavior.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Robot Image Server 5 | 6 | 7 |

Robot Image Server

8 |
9 | Start 10 | Stop
11 | Exit 12 | 13 | 14 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/templates/control_image_behavior.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Robot Image Server 5 | 6 | 7 |

Robot Image Server

8 |
9 | Exit 10 | 11 | 12 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/templates/image_server.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Robot Image Server 4 | 5 | 6 |

Robot Image Server

7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/test_distance_sensors.py: -------------------------------------------------------------------------------- 1 | import time 2 | from gpiozero import DistanceSensor 3 | 4 | print("Prepare GPIO Pins") 5 | sensor_l = DistanceSensor(echo=17, trigger=27, queue_len=2) 6 | sensor_r = DistanceSensor(echo=5, trigger=6, queue_len=2) 7 | 8 | while True: 9 | print("Left: {l:.2f}, Right: {r:.2f}".format( 10 | l=sensor_l.distance * 100, 11 | r=sensor_r.distance * 100)) 12 | time.sleep(0.1) 13 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/test_distance_travelled.py: -------------------------------------------------------------------------------- 1 | from robot import Robot 2 | import time 3 | import math 4 | import logging 5 | logger = logging.getLogger("test_distance_travelled") 6 | 7 | wheel_diameter_mm = 70.0 8 | ticks_per_revolution = 40.0 9 | ticks_to_mm_const = (math.pi / ticks_per_revolution) * wheel_diameter_mm 10 | 11 | def ticks_to_mm(ticks): 12 | return int(ticks_to_mm_const * ticks) 13 | 14 | bot = Robot() 15 | stop_at_time = time.time() + 1 16 | 17 | logging.basicConfig(level=logging.INFO) 18 | bot.set_left(90) 19 | bot.set_right(90) 20 | 21 | while time.time() < stop_at_time: 22 | logger.info("Left: {} Right: {}".format( 23 | ticks_to_mm(bot.left_encoder.pulse_count), 24 | ticks_to_mm(bot.right_encoder.pulse_count))) 25 | time.sleep(0.05) 26 | 27 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/test_encoders.py: -------------------------------------------------------------------------------- 1 | from robot import Robot 2 | import time 3 | import logging 4 | 5 | from gpiozero import DigitalInputDevice 6 | 7 | logger = logging.getLogger("test_encoders") 8 | 9 | class EncoderCounter(object): 10 | def __init__(self, pin_number): 11 | self.pulse_count = 0 12 | self.device = DigitalInputDevice(pin=pin_number) 13 | self.device.pin.when_changed = self.when_changed 14 | 15 | def when_changed(self, time_ticks, state): 16 | self.pulse_count += 1 17 | 18 | bot = Robot() 19 | left_encoder = EncoderCounter(4) 20 | right_encoder = EncoderCounter(26) 21 | 22 | stop_at_time = time.time() + 1 23 | 24 | logging.basicConfig(level=logging.INFO) 25 | bot.set_left(90) 26 | bot.set_right(90) 27 | while time.time() < stop_at_time: 28 | logger.info(f"Left: {left_encoder.pulse_count} Right: {right_encoder.pulse_count}") 29 | time.sleep(0.05) 30 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/test_line_find.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | from matplotlib import pyplot as plt 4 | 5 | image = cv2.imread("carpet_line1 2.jpg") 6 | assert image is not None, "Unable to read file" 7 | 8 | resized = cv2.resize(image, (320, 240)) 9 | 10 | gray = cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY) 11 | blur = cv2.blur(gray, (5, 80)) 12 | row = blur[180].astype(np.int32) 13 | diff = np.diff(row) 14 | 15 | max_d = np.amax(diff, 0) 16 | min_d = np.amin(diff, 0) 17 | 18 | highest = np.where(diff == max_d)[0][0] 19 | lowest = np.where(diff == min_d)[0][0] 20 | middle = (highest + lowest) // 2 21 | 22 | x = np.arange(len(diff)) 23 | plt.plot(x, diff) 24 | plt.plot([lowest, lowest], [max_d, min_d], "g--") 25 | plt.plot([middle, middle], [max_d, min_d], "r-") 26 | plt.plot([highest, highest], [max_d, min_d], "g--") 27 | plt.savefig("carpet_line1_2_blur580.png") 28 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/test_motors.py: -------------------------------------------------------------------------------- 1 | from Raspi_MotorHAT import Raspi_MotorHAT 2 | 3 | import time 4 | import atexit 5 | 6 | mh = Raspi_MotorHAT(addr=0x6f) 7 | lm = mh.getMotor(1) 8 | rm = mh.getMotor(2) 9 | 10 | def turn_off_motors(): 11 | lm.run(Raspi_MotorHAT.RELEASE) 12 | rm.run(Raspi_MotorHAT.RELEASE) 13 | 14 | atexit.register(turn_off_motors) 15 | 16 | lm.setSpeed(255) 17 | rm.setSpeed(255) 18 | 19 | lm.run(Raspi_MotorHAT.FORWARD) 20 | rm.run(Raspi_MotorHAT.FORWARD) 21 | time.sleep(1) 22 | 23 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/test_rainbow.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | from robot import Robot 3 | from led_rainbow import show_rainbow 4 | 5 | bot = Robot() 6 | 7 | while True: 8 | print("on") 9 | show_rainbow(bot.leds, range(bot.leds.count)) 10 | bot.leds.show() 11 | sleep(0.5) 12 | print("off") 13 | bot.leds.clear() 14 | bot.leds.show() 15 | sleep(0.5) 16 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/virtual_robot.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | from robot_pose import robot_view 3 | 4 | 5 | def make_robot(): 6 | chassis_width = 155 # left side to right 7 | chassis_thickness = 3 # plastic thickness 8 | chassis_length = 200 # front to back 9 | wheel_thickness = 26 10 | wheel_diameter = 70 11 | axle_x = 30 # wheel axle from 12 | axle_z = -20 13 | rear_castor_position = vp.vector(-80, -6, -30) 14 | rear_castor_radius = 14 15 | rear_caster_thickness = 12 16 | 17 | base = vp.box(length=chassis_length, 18 | height=chassis_thickness, 19 | width=chassis_width) 20 | # rotate to match body - so Z is height and Y is width 21 | base.rotate(angle=vp.radians(90), 22 | axis=vp.vector(1, 0, 0)) 23 | wheel_dist = chassis_width/2 24 | wheel_l = vp.cylinder(radius=wheel_diameter/2, 25 | length=wheel_thickness, 26 | pos=vp.vector(axle_x, -wheel_dist, axle_z), 27 | axis=vp.vector(0, -1, 0)) 28 | wheel_r = vp.cylinder(radius=wheel_diameter/2, 29 | length=wheel_thickness, 30 | pos=vp.vector(axle_x, wheel_dist, axle_z), 31 | axis=vp.vector(0, 1, 0)) 32 | rear_castor = vp.cylinder(radius=rear_castor_radius, 33 | length=rear_caster_thickness, 34 | pos=rear_castor_position, 35 | axis=vp.vector(0, 1, 0)) 36 | return vp.compound([base, wheel_l, wheel_r, rear_castor]) 37 | 38 | 39 | if __name__ == "__main__": 40 | robot_view() 41 | x_arrow = vp.arrow(axis=vp.vector(200, 0, 0), color=vp.color.red) 42 | y_arrow = vp.arrow(axis=vp.vector(0, 200, 0), color=vp.color.green) 43 | z_arrow = vp.arrow(axis=vp.vector(0, 0, 200), color=vp.color.blue) 44 | robot=make_robot() 45 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/visual_filtered_accelerometer.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | import logging 3 | from robot_imu import RobotImu, ComplementaryFilter 4 | import virtual_robot 5 | 6 | logging.basicConfig(level=logging.INFO) 7 | imu = RobotImu() 8 | model = virtual_robot.make_robot() 9 | virtual_robot.robot_view() 10 | 11 | filter = ComplementaryFilter(0.95).filter 12 | 13 | pitch = 0 14 | roll = 0 15 | 16 | while True: 17 | vp.rate(100) 18 | 19 | new_pitch, new_roll = imu.read_accelerometer_pitch_and_roll() 20 | pitch = filter(pitch, new_pitch) 21 | roll = filter(roll, new_roll) 22 | 23 | print(f"Pitch: {pitch:.2f}, Roll: {roll:.2f}") 24 | # reset the model 25 | model.up = vp.vector(0, 1, 0) 26 | model.axis = vp.vector(1, 0, 0) 27 | # Reposition it 28 | model.rotate(angle=vp.radians(roll), axis=vp.vector(1, 0, 0)) 29 | model.rotate(angle=vp.radians(pitch), axis=vp.vector(0, 1, 0)) 30 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/visual_fusion.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | from robot_imu import RobotImu, ImuFusion 3 | from delta_timer import DeltaTimer 4 | import imu_settings 5 | import virtual_robot 6 | 7 | imu = RobotImu(gyro_offsets=imu_settings.gyro_offsets, 8 | mag_offsets=imu_settings.mag_offsets) 9 | fusion = ImuFusion(imu) 10 | 11 | model = virtual_robot.make_robot() 12 | virtual_robot.robot_view() 13 | 14 | timer = DeltaTimer() 15 | 16 | while True: 17 | vp.rate(100) 18 | dt, elapsed = timer.update() 19 | fusion.update(dt) 20 | # reset the model 21 | model.up = vp.vector(0, 1, 0) 22 | model.axis = vp.vector(1, 0, 0) 23 | # Reposition it 24 | model.rotate(angle=vp.radians(fusion.roll), axis=vp.vector(1, 0, 0)) 25 | model.rotate(angle=vp.radians(fusion.pitch), axis=vp.vector(0, 1, 0)) 26 | model.rotate(angle=vp.radians(fusion.yaw), axis=vp.vector(0, 0, 1)) 27 | -------------------------------------------------------------------------------- /chapter17/0_starting_point/visual_gyroscope.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | from robot_imu import RobotImu 3 | import time 4 | import imu_settings 5 | import virtual_robot 6 | 7 | 8 | imu = RobotImu(gyro_offsets=imu_settings.gyro_offsets) 9 | 10 | pitch = 0 11 | roll = 0 12 | yaw = 0 13 | 14 | model = virtual_robot.make_robot() 15 | virtual_robot.robot_view() 16 | 17 | latest = time.time() 18 | 19 | while True: 20 | vp.rate(1000) 21 | current = time.time() 22 | dt = latest - current 23 | latest = current 24 | gyro = imu.read_gyroscope() 25 | pitch += gyro.x * dt 26 | roll += gyro.y * dt 27 | yaw += gyro.z * dt 28 | 29 | # reset the model 30 | model.up = vp.vector(0, 1, 0) 31 | model.axis = vp.vector(1, 0, 0) 32 | # Reposition it 33 | model.rotate(angle=vp.radians(pitch), axis=vp.vector(1, 0, 0)) 34 | model.rotate(angle=vp.radians(roll), axis=vp.vector(0, 1, 0)) 35 | model.rotate(angle=vp.radians(yaw), axis=vp.vector(0, 0, 1)) 36 | -------------------------------------------------------------------------------- /chapter17/full_system/accelerometer_vector.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | import logging 3 | from robot_imu import RobotImu 4 | from robot_pose import robot_view 5 | 6 | logging.basicConfig(level=logging.INFO) 7 | imu = RobotImu() 8 | robot_view() 9 | 10 | accel_arrow = vp.arrow(axis=vp.vector(0, 1, 0)) 11 | x_arrow = vp.arrow(axis=vp.vector(1, 0, 0), color=vp.color.red) 12 | y_arrow = vp.arrow(axis=vp.vector(0, 1, 0), color=vp.color.green) 13 | z_arrow = vp.arrow(axis=vp.vector(0, 0, 1), color=vp.color.blue) 14 | 15 | while True: 16 | vp.rate(100) 17 | accel = imu.read_accelerometer() 18 | print(f"Accelerometer: {accel}") 19 | accel_arrow.axis = accel.norm() 20 | -------------------------------------------------------------------------------- /chapter17/full_system/avoid_with_rainbows.py: -------------------------------------------------------------------------------- 1 | from robot import Robot 2 | from time import sleep 3 | from led_rainbow import show_rainbow 4 | 5 | 6 | class ObstacleAvoidingBehavior: 7 | """Simple obstacle avoiding""" 8 | def __init__(self, the_robot): 9 | self.robot = the_robot 10 | self.speed = 60 11 | # Calculations for the LEDs 12 | self.led_half = int(self.robot.leds.count/2) 13 | self.sense_colour = 255, 0, 0 14 | 15 | def distance_to_led_bar(self, distance): 16 | # Invert so closer means more LED's. 17 | inverted = max(0, 1.0 - distance) 18 | led_bar = int(round(inverted * self.led_half)) + 1 19 | return led_bar 20 | 21 | def display_state(self, left_distance, right_distance): 22 | # Clear first 23 | self.robot.leds.clear() 24 | # Left side 25 | led_bar = self.distance_to_led_bar(left_distance) 26 | show_rainbow(self.robot.leds, range(led_bar)) 27 | # Right side 28 | led_bar = self.distance_to_led_bar(right_distance) 29 | # Bit trickier - must go from below the leds count, to the leds count. 30 | start = (self.robot.leds.count - 1) - (led_bar) 31 | right_range = range(self.robot.leds.count - 1, start, -1) 32 | show_rainbow(self.robot.leds, right_range) 33 | # Now show this display 34 | self.robot.leds.show() 35 | 36 | def get_speeds(self, nearest_distance): 37 | if nearest_distance >= 1.0: 38 | nearest_speed = self.speed 39 | furthest_speed = self.speed 40 | delay = 100 41 | elif nearest_distance > 0.5: 42 | nearest_speed = self.speed 43 | furthest_speed = self.speed * 0.8 44 | delay = 100 45 | elif nearest_distance > 0.2: 46 | nearest_speed = self.speed 47 | furthest_speed = self.speed * 0.6 48 | delay = 100 49 | elif nearest_distance > 0.1: 50 | nearest_speed = -self.speed * 0.4 51 | furthest_speed = -self.speed 52 | delay = 100 53 | else: # collison 54 | nearest_speed = -self.speed 55 | furthest_speed = -self.speed 56 | delay = 250 57 | return nearest_speed, furthest_speed, delay 58 | 59 | def run(self): 60 | while True: 61 | # Get the sensor readings in meters 62 | left_distance = self.robot.left_distance_sensor.distance 63 | right_distance = self.robot.right_distance_sensor.distance 64 | # Display this 65 | self.display_state(left_distance, right_distance) 66 | # Get speeds for motors from distances 67 | nearest_speed, furthest_speed, delay = self.get_speeds(min(left_distance, right_distance)) 68 | print(f"Distances: l {left_distance:.2f}, r {right_distance:.2f}. Speeds: n: {nearest_speed}, f: {furthest_speed}. Delay: {delay}") 69 | # and drive 70 | # Send this to the motors 71 | if left_distance < right_distance: 72 | self.robot.set_left(nearest_speed) 73 | self.robot.set_right(furthest_speed) 74 | else: 75 | self.robot.set_right(nearest_speed) 76 | self.robot.set_left(furthest_speed) 77 | # Wait our delay time 78 | sleep(delay * 0.001) 79 | 80 | 81 | bot = Robot() 82 | behavior = ObstacleAvoidingBehavior(bot) 83 | behavior.run() 84 | -------------------------------------------------------------------------------- /chapter17/full_system/behavior_line.py: -------------------------------------------------------------------------------- 1 | import robot 2 | from Raspi_MotorHAT import Raspi_MotorHAT 3 | from time import sleep 4 | 5 | r = robot.Robot() 6 | r.set_left(100) 7 | r.set_right(70) 8 | sleep(1) 9 | 10 | -------------------------------------------------------------------------------- /chapter17/full_system/behavior_path.py: -------------------------------------------------------------------------------- 1 | import robot 2 | from time import sleep 3 | 4 | def straight(bot, seconds): 5 | bot.set_left(100) 6 | bot.set_right(100) 7 | sleep(seconds) 8 | 9 | def turn_left(bot, seconds): 10 | bot.set_left(20) 11 | bot.set_right(80) 12 | sleep(seconds) 13 | 14 | def turn_right(bot, seconds): 15 | bot.set_left(80) 16 | bot.set_right(20) 17 | sleep(seconds) 18 | 19 | def spin_left(bot, seconds): 20 | bot.set_left(-100) 21 | bot.set_right(100) 22 | sleep(seconds) 23 | 24 | bot = robot.Robot() 25 | straight(bot, 1) 26 | turn_right(bot, 0.6) 27 | straight(bot, 0.6) 28 | turn_left(bot, 0.6) 29 | straight(bot, 0.6) 30 | turn_left(bot, 0.6) 31 | straight(bot, 0.3) 32 | spin_left(bot, 1) 33 | 34 | -------------------------------------------------------------------------------- /chapter17/full_system/calibrate_gyro.py: -------------------------------------------------------------------------------- 1 | from robot_imu import RobotImu 2 | import time 3 | import vpython as vp 4 | 5 | imu = RobotImu() 6 | 7 | gyro_min = vp.vector(0, 0, 0) 8 | gyro_max = vp.vector(0, 0, 0) 9 | 10 | for n in range(500): 11 | gyro = imu.read_gyroscope() 12 | gyro_min.x = min(gyro_min.x, gyro.x) 13 | gyro_min.y = min(gyro_min.y, gyro.y) 14 | gyro_min.z = min(gyro_min.z, gyro.z) 15 | 16 | gyro_max.x = max(gyro_max.x, gyro.x) 17 | gyro_max.y = max(gyro_max.y, gyro.y) 18 | gyro_max.z = max(gyro_max.z, gyro.z) 19 | 20 | offset = (gyro_min + gyro_max) / 2 21 | 22 | time.sleep(.01) 23 | 24 | print(f"Zero offset: {offset}.") 25 | -------------------------------------------------------------------------------- /chapter17/full_system/calibrate_magnetometer.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | from robot_imu import RobotImu 3 | from imu_settings import mag_offsets 4 | 5 | imu = RobotImu(mag_offsets=mag_offsets) 6 | 7 | mag_min = vp.vector(0, 0, 0) 8 | mag_max = vp.vector(0, 0, 0) 9 | 10 | 11 | scatter_xy = vp.gdots(color=vp.color.red) 12 | scatter_yz = vp.gdots(color=vp.color.green) 13 | scatter_zx = vp.gdots(color=vp.color.blue) 14 | 15 | while True: 16 | vp.rate(100) 17 | mag = imu.read_magnetometer() 18 | 19 | mag_min.x = min(mag_min.x, mag.x) 20 | mag_min.y = min(mag_min.y, mag.y) 21 | mag_min.z = min(mag_min.z, mag.z) 22 | 23 | mag_max.x = max(mag_max.x, mag.x) 24 | mag_max.y = max(mag_max.y, mag.y) 25 | mag_max.z = max(mag_max.z, mag.z) 26 | offset = (mag_max + mag_min) / 2 27 | 28 | print(f"Magnetometer: {mag}. Offsets: {offset}.") 29 | scatter_xy.plot(mag.x, mag.y) 30 | scatter_yz.plot(mag.y, mag.z) 31 | scatter_zx.plot(mag.z, mag.x) 32 | -------------------------------------------------------------------------------- /chapter17/full_system/camera_stream.py: -------------------------------------------------------------------------------- 1 | from picamera.array import PiRGBArray 2 | from picamera import PiCamera 3 | import numpy as np 4 | 5 | import cv2 6 | 7 | size = (320, 240) 8 | encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 90] 9 | 10 | 11 | def setup_camera(): 12 | camera = PiCamera() 13 | camera.resolution = size 14 | camera.rotation = 180 15 | return camera 16 | 17 | 18 | def start_stream(camera): 19 | image_storage = PiRGBArray(camera, size=size) 20 | 21 | cam_stream = camera.capture_continuous(image_storage, format="bgr", use_video_port=True) 22 | for raw_frame in cam_stream: 23 | yield raw_frame.array 24 | image_storage.truncate(0) 25 | 26 | 27 | def get_encoded_bytes_for_frame(frame): 28 | result, encoded_image = cv2.imencode('.jpg', frame, encode_param) 29 | return encoded_image.tostring() 30 | -------------------------------------------------------------------------------- /chapter17/full_system/circle_pan_tilt_behavior.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | import math 3 | 4 | from robot import Robot 5 | class CirclePanTiltBehavior: 6 | def __init__(self, the_robot): 7 | self.robot = the_robot 8 | self.current_time = 0 9 | self.frames_per_circle = 50 10 | self.radians_per_frame = (2 * math.pi) / self.frames_per_circle 11 | self.radius = 30 12 | 13 | def run(self): 14 | while True: 15 | frame_number = self.current_time % self.frames_per_circle 16 | frame_in_radians = frame_number * self.radians_per_frame 17 | self.robot.set_pan(self.radius * math.cos(frame_in_radians)) 18 | self.robot.set_tilt(self.radius * math.sin(frame_in_radians)) 19 | sleep(0.05) 20 | self.current_time += 1 21 | 22 | bot = Robot() 23 | behavior = CirclePanTiltBehavior(bot) 24 | behavior.run() 25 | 26 | -------------------------------------------------------------------------------- /chapter17/full_system/control_image_behavior.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from image_app_core import start_server_process, get_control_instruction, put_output_image 4 | import camera_stream 5 | 6 | def controlled_image_server_behavior(): 7 | camera = camera_stream.setup_camera() 8 | time.sleep(0.1) 9 | 10 | for frame in camera_stream.start_stream(camera): 11 | encoded_bytes = camera_stream.get_encoded_bytes_for_frame(frame) 12 | put_output_image(encoded_bytes) 13 | 14 | instruction = get_control_instruction() 15 | if instruction and instruction['command'] == "exit": 16 | print("Stopping") 17 | return 18 | 19 | process = start_server_process('control_image_behavior.html') 20 | 21 | try: 22 | controlled_image_server_behavior() 23 | finally: 24 | process.terminate() 25 | -------------------------------------------------------------------------------- /chapter17/full_system/control_server.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, jsonify 2 | from robot_modes import RobotModes 3 | from leds_led_shim import Leds 4 | 5 | # A Flask App contains all its routes. 6 | app = Flask(__name__) 7 | # Prepare our robot modes for use 8 | mode_manager = RobotModes() 9 | leds = Leds() 10 | leds.set_one(1, [0, 255, 0]) 11 | leds.show() 12 | 13 | 14 | @app.after_request 15 | def add_header(response): 16 | response.headers['Cache-Control'] = "no-cache, no-store, must-revalidate" 17 | return response 18 | 19 | 20 | @app.route("/") 21 | def index(): 22 | return render_template('menu.html', menu=mode_manager.menu_config) 23 | 24 | 25 | @app.route("/run/", methods=['POST']) 26 | def run(mode_name): 27 | global leds 28 | if leds: 29 | leds.clear() 30 | leds.show() 31 | leds = None 32 | 33 | # Use our robot app to run something with this mode_name 34 | mode_manager.run(mode_name) 35 | response = {'message': f'{mode_name} running'} 36 | if mode_manager.should_redirect(mode_name): 37 | response['redirect'] = True 38 | return jsonify(response) 39 | 40 | 41 | @app.route("/stop", methods=['POST']) 42 | def stop(): 43 | # Tell our system to stop the mode it's in. 44 | mode_manager.stop() 45 | return jsonify({'message': "Stopped"}) 46 | 47 | 48 | app.run(host="0.0.0.0") 49 | -------------------------------------------------------------------------------- /chapter17/full_system/delta_timer.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | class DeltaTimer: 5 | def __init__(self): 6 | self.last = self.start = time.time() 7 | 8 | def update(self): 9 | current = time.time() 10 | dt = current - self.last 11 | elapsed = current - self.start 12 | self.last = current 13 | return dt, elapsed -------------------------------------------------------------------------------- /chapter17/full_system/drive_distance.py: -------------------------------------------------------------------------------- 1 | from robot import Robot, EncoderCounter 2 | from pid_controller import PIController 3 | import time 4 | import logging 5 | logger = logging.getLogger("drive_distance") 6 | 7 | def drive_distance(bot, distance, speed=80): 8 | # Use left as "primary" motor, the right is keeping up 9 | set_primary = bot.set_left 10 | primary_encoder = bot.left_encoder 11 | set_secondary = bot.set_right 12 | secondary_encoder = bot.right_encoder 13 | controller = PIController(proportional_constant=5, integral_constant=0.3) 14 | 15 | # start the motors, and start the loop 16 | set_primary(speed) 17 | set_secondary(speed) 18 | while primary_encoder.pulse_count < distance or secondary_encoder.pulse_count < distance: 19 | time.sleep(0.01) 20 | # How far off are we? 21 | error = primary_encoder.pulse_count - secondary_encoder.pulse_count 22 | adjustment = controller.get_value(error) 23 | # How fast should the motor move to get there? 24 | set_primary(int(speed - adjustment)) 25 | set_secondary(int(speed + adjustment)) 26 | # Some debug 27 | logger.debug(f"Encoders: primary: {primary_encoder.pulse_count}, secondary: {secondary_encoder.pulse_count}," 28 | f"e:{error} adjustment: {adjustment:.2f}") 29 | logger.info(f"Distances: primary: {primary_encoder.distance_in_mm()} mm, secondary: {secondary_encoder.distance_in_mm()} mm") 30 | 31 | logging.basicConfig(level=logging.DEBUG) 32 | bot = Robot() 33 | distance_to_drive = 1000 # in mm - this is a meter 34 | distance_in_ticks = EncoderCounter.mm_to_ticks(distance_to_drive) 35 | drive_distance(bot, distance_in_ticks) 36 | -------------------------------------------------------------------------------- /chapter17/full_system/drive_north_behavior.py: -------------------------------------------------------------------------------- 1 | """This behavior will turn to seek north, and then drive that way""" 2 | from robot_imu import RobotImu, ImuFusion 3 | from delta_timer import DeltaTimer 4 | from pid_controller import PIController 5 | from robot import Robot 6 | import imu_settings 7 | 8 | 9 | imu = RobotImu(mag_offsets=imu_settings.mag_offsets, 10 | gyro_offsets=imu_settings.gyro_offsets) 11 | fusion = ImuFusion(imu) 12 | timer = DeltaTimer() 13 | pid = PIController(0.7, 0.01) 14 | robot = Robot() 15 | base_speed = 70 16 | 17 | # Lets head for this heading 18 | heading_set_point = 0 19 | 20 | while True: 21 | dt, elapsed = timer.update() 22 | fusion.update(dt) 23 | heading_error = fusion.yaw - heading_set_point 24 | steer_value = pid.get_value(heading_error, delta_time=dt) 25 | print(f"Error: {heading_error}, Value:{steer_value:2f}, t: {elapsed}") 26 | robot.set_left(base_speed + steer_value) 27 | robot.set_right(base_speed - steer_value) 28 | -------------------------------------------------------------------------------- /chapter17/full_system/encoder_counter.py: -------------------------------------------------------------------------------- 1 | from gpiozero import DigitalInputDevice 2 | import math 3 | class EncoderCounter: 4 | ticks_to_mm_const = None # you must set this up before using distance methods 5 | def __init__(self, pin_number): 6 | self.pulse_count = 0 7 | self.direction = 1 8 | self.device = DigitalInputDevice(pin=pin_number) 9 | self.device.pin.when_changed = self.when_changed 10 | 11 | def when_changed(self, time_ticks, state): 12 | self.pulse_count += self.direction 13 | 14 | def set_direction(self, direction): 15 | """This should be -1 or 1. """ 16 | assert abs(direction)==1, "Direction %s should be 1 or -1" % direction 17 | self.direction = direction 18 | 19 | def reset(self): 20 | self.pulse_count = 0 21 | 22 | def stop(self): 23 | self.device.close() 24 | 25 | def distance_in_mm(self): 26 | return int(self.pulse_count * EncoderCounter.ticks_to_mm_const) 27 | 28 | @staticmethod 29 | def mm_to_ticks(mm): 30 | return mm / EncoderCounter.ticks_to_mm_const 31 | 32 | @staticmethod 33 | def set_constants(wheel_diameter_mm, ticks_per_revolution): 34 | EncoderCounter.ticks_to_mm_const = (math.pi / ticks_per_revolution) * wheel_diameter_mm 35 | -------------------------------------------------------------------------------- /chapter17/full_system/image_app_core.py: -------------------------------------------------------------------------------- 1 | import time 2 | from multiprocessing import Process, Queue 3 | 4 | from flask import Flask, render_template, Response, request 5 | 6 | app = Flask(__name__) 7 | control_queue = Queue() 8 | display_queue = Queue(maxsize=2) 9 | display_template = 'image_server.html' 10 | 11 | 12 | @app.after_request 13 | def add_header(response): 14 | response.headers['Cache-Control'] = "no-cache, no-store, must-revalidate" 15 | return response 16 | 17 | 18 | @app.route('/') 19 | def index(): 20 | return render_template(display_template) 21 | 22 | 23 | def frame_generator(): 24 | while True: 25 | time.sleep(0.05) 26 | encoded_bytes = display_queue.get() 27 | yield (b'--frame\r\n' 28 | b'Content-Type: image/jpeg\r\n\r\n' + encoded_bytes + b'\r\n') 29 | 30 | 31 | @app.route('/display') 32 | def display(): 33 | return Response(frame_generator(), 34 | mimetype='multipart/x-mixed-replace; boundary=frame') 35 | 36 | 37 | @app.route('/control', methods=['POST']) 38 | def control(): 39 | control_queue.put(request.form) 40 | return Response('queued') 41 | 42 | 43 | def start_server_process(template_name): 44 | """Start the process, call .terminate to close it""" 45 | global display_template 46 | display_template = template_name 47 | server = Process(target=app.run, kwargs={"host": "0.0.0.0", "port": 5001}) 48 | server.start() 49 | return server 50 | 51 | 52 | def put_output_image(encoded_bytes): 53 | """Queue an output image""" 54 | if display_queue.empty(): 55 | display_queue.put(encoded_bytes) 56 | 57 | 58 | def get_control_instruction(): 59 | if control_queue.empty(): 60 | return None 61 | else: 62 | return control_queue.get() 63 | -------------------------------------------------------------------------------- /chapter17/full_system/image_server.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, Response 2 | import camera_stream 3 | import time 4 | 5 | app = Flask(__name__) 6 | 7 | @app.route('/') 8 | def index(): 9 | return render_template('image_server.html') 10 | 11 | def frame_generator(): 12 | """This is our main video feed""" 13 | camera = camera_stream.setup_camera() 14 | 15 | # allow the camera to warm up 16 | time.sleep(0.1) 17 | for frame in camera_stream.start_stream(camera): 18 | encoded_bytes = camera_stream.get_encoded_bytes_for_frame(frame) 19 | yield (b'--frame\r\n' 20 | b'Content-Type: image/jpeg\r\n\r\n' + encoded_bytes + b'\r\n') 21 | 22 | @app.route('/display') 23 | def display(): 24 | return Response(frame_generator(), 25 | mimetype='multipart/x-mixed-replace; boundary=frame') 26 | 27 | app.run(host="0.0.0.0", debug=True, port=5001) 28 | -------------------------------------------------------------------------------- /chapter17/full_system/imu_settings.py: -------------------------------------------------------------------------------- 1 | from vpython import vector 2 | gyro_offsets = vector(-0.583969, 0.675573, -0.530534) 3 | mag_offsets = vector(7.725, 9.6, -25.275) 4 | -------------------------------------------------------------------------------- /chapter17/full_system/led_rainbow.py: -------------------------------------------------------------------------------- 1 | 2 | import colorsys 3 | 4 | 5 | def show_rainbow(leds, led_range): 6 | led_range = list(led_range) 7 | hue_step = 1.0 / len(led_range) 8 | for index, led_address in enumerate(led_range): 9 | hue = hue_step * index 10 | rgb = colorsys.hsv_to_rgb(hue, 1.0, 0.6) 11 | rgb = [int(c*255) for c in rgb] 12 | leds.set_one(led_address, rgb) 13 | -------------------------------------------------------------------------------- /chapter17/full_system/leds_led_shim.py: -------------------------------------------------------------------------------- 1 | import ledshim 2 | 3 | 4 | class Leds: 5 | @property 6 | def count(self): 7 | return ledshim.width 8 | 9 | def set_one(self, led_number, color): 10 | ledshim.set_pixel(led_number, *color) 11 | 12 | def set_range(self, led_range, color): 13 | for pixel in led_range: 14 | ledshim.set_pixel(pixel, *color) 15 | 16 | def set_all(self, color): 17 | ledshim.set_all(*color) 18 | 19 | def clear(self): 20 | ledshim.clear() 21 | 22 | def show(self): 23 | ledshim.show() 24 | -------------------------------------------------------------------------------- /chapter17/full_system/leds_test.py: -------------------------------------------------------------------------------- 1 | from robot import Robot 2 | from time import sleep 3 | 4 | bot = Robot() 5 | red = (255, 0, 0) 6 | blue = (0, 0, 255) 7 | 8 | while True: 9 | print("red") 10 | bot.leds.set_all(red) 11 | bot.leds.show() 12 | sleep(0.5) 13 | print("blue") 14 | bot.leds.set_all(blue) 15 | bot.leds.show() 16 | sleep(0.5) 17 | -------------------------------------------------------------------------------- /chapter17/full_system/magnetometer_vector.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | import logging 3 | from robot_imu import RobotImu 4 | from robot_pose import robot_view 5 | 6 | logging.basicConfig(level=logging.INFO) 7 | imu = RobotImu() 8 | robot_view() 9 | 10 | mag_arrow = vp.arrow(pos=vp.vector(0, 0, 0)) 11 | x_arrow = vp.arrow(axis=vp.vector(1, 0, 0), color=vp.color.red) 12 | y_arrow = vp.arrow(axis=vp.vector(0, 1, 0), color=vp.color.green) 13 | z_arrow = vp.arrow(axis=vp.vector(0, 0, 1), color=vp.color.blue) 14 | 15 | while True: 16 | vp.rate(100) 17 | 18 | mag = imu.read_magnetometer() 19 | mag_arrow.axis = mag.norm() 20 | print(f"Magnetometer: {mag}") 21 | 22 | -------------------------------------------------------------------------------- /chapter17/full_system/manual_drive.py: -------------------------------------------------------------------------------- 1 | import time 2 | from robot import Robot 3 | from image_app_core import start_server_process, get_control_instruction, put_output_image 4 | import camera_stream 5 | 6 | 7 | TIMEOUT_IN = 1 8 | 9 | class ManualDriveBehavior(object): 10 | def __init__(self, robot): 11 | self.robot = robot 12 | self.last_time = time.time() 13 | 14 | def process_control(self): 15 | instruction = get_control_instruction() 16 | while instruction: 17 | self.last_time = time.time() 18 | self.handle_instruction(instruction) 19 | instruction = get_control_instruction() 20 | 21 | def handle_instruction(self, instruction): 22 | command = instruction['command'] 23 | if command == "set_left": 24 | self.robot.set_left(int(instruction['speed'])) 25 | elif command == "set_right": 26 | self.robot.set_right(int(instruction['speed'])) 27 | elif command == "exit": 28 | print("stopping") 29 | exit() 30 | else: 31 | raise ValueError(f"Unknown instruction: {instruction}") 32 | 33 | def make_display(self, frame): 34 | """Create display output, and put it on the queue""" 35 | encoded_bytes = camera_stream.get_encoded_bytes_for_frame(frame) 36 | put_output_image(encoded_bytes) 37 | 38 | def run(self): 39 | # Set pan and tilt to middle, then clear it. 40 | self.robot.set_pan(0) 41 | self.robot.set_tilt(0) 42 | # start camera 43 | camera = camera_stream.setup_camera() 44 | # warm up and servo move time 45 | time.sleep(0.1) 46 | # Servo's will be in place - stop them for now. 47 | self.robot.servos.stop_all() 48 | print("Setup Complete") 49 | 50 | # Main loop 51 | for frame in camera_stream.start_stream(camera): 52 | self.make_display(frame) 53 | self.process_control() 54 | # Auto stop 55 | if time.time() > self.last_time + TIMEOUT_IN: 56 | self.robot.stop_motors() 57 | 58 | print("Setting up") 59 | behavior = ManualDriveBehavior(Robot()) 60 | process = start_server_process('manual_drive.html') 61 | try: 62 | behavior.run() 63 | except: 64 | process.terminate() 65 | -------------------------------------------------------------------------------- /chapter17/full_system/menu_server.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Robot Menu Web Service 3 | After=network.target 4 | 5 | [Install] 6 | WantedBy=multi-user.target 7 | 8 | [Service] 9 | WorkingDirectory=/home/pi 10 | User=pi 11 | Environment=LD_PRELOAD=/usr/lib/arm-linux-gnueabihf/libatomic.so.1 12 | ExecStart=/usr/bin/env python3 control_server.py 13 | -------------------------------------------------------------------------------- /chapter17/full_system/my-robot-skill/__init__.py: -------------------------------------------------------------------------------- 1 | from adapt.intent import IntentBuilder 2 | from mycroft import MycroftSkill, intent_handler 3 | from mycroft.util.log import LOG 4 | 5 | import requests 6 | 7 | 8 | class MyRobot(MycroftSkill): 9 | def __init__(self): 10 | super().__init__() 11 | self.base_url = self.settings.get("base_url") 12 | 13 | @intent_handler(IntentBuilder("") 14 | .require("Robot") 15 | .require("TestRainbow")) 16 | def handle_test_rainbow(self, message): 17 | self.handle_control('/run/test_rainbow', 'TestingRainbow') 18 | 19 | @intent_handler(IntentBuilder("") 20 | .require("Robot") 21 | .require("stop")) 22 | def handle_stop(self, message): 23 | self.handle_control('/stop', 'stopping') 24 | 25 | def handle_control(self, end_point, dialog_verb): 26 | try: 27 | requests.post(self.base_url + end_point) 28 | self.speak_dialog('Robot') 29 | self.speak_dialog(dialog_verb) 30 | except: 31 | self.speak_dialog("UnableToReach") 32 | LOG.exception("Unable to reach the robot") 33 | 34 | 35 | def create_skill(): 36 | return MyRobot() 37 | 38 | -------------------------------------------------------------------------------- /chapter17/full_system/my-robot-skill/dialog/en-us/Robot.dialog: -------------------------------------------------------------------------------- 1 | The Robot 2 | Robot 3 | -------------------------------------------------------------------------------- /chapter17/full_system/my-robot-skill/dialog/en-us/TestingRainbow.dialog: -------------------------------------------------------------------------------- 1 | is testing rainbows. 2 | is deploying rainbows. 3 | is starting rainbows. 4 | is lighting up. 5 | -------------------------------------------------------------------------------- /chapter17/full_system/my-robot-skill/dialog/en-us/UnableToReach.dialog: -------------------------------------------------------------------------------- 1 | Sorry I cannot reach the robot. 2 | The robot is unreachable. 3 | Have you turned the robot on? 4 | Is the control server running on the robot? 5 | -------------------------------------------------------------------------------- /chapter17/full_system/my-robot-skill/dialog/en-us/stopping.dialog: -------------------------------------------------------------------------------- 1 | is stopping. 2 | will stop. 3 | -------------------------------------------------------------------------------- /chapter17/full_system/my-robot-skill/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | -------------------------------------------------------------------------------- /chapter17/full_system/my-robot-skill/settingsmeta.json: -------------------------------------------------------------------------------- 1 | { 2 | "base_url": "http://myrobot.local:5000", 3 | "skillMetadata": { 4 | "sections": [ 5 | { 6 | "name": "Robot", 7 | "fields": [ 8 | { 9 | "name": "base_url", 10 | "type": "text", 11 | "label": "Base URL for the robot control server", 12 | "value": "https://myrobots.local:5000" 13 | } 14 | ] 15 | } 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /chapter17/full_system/my-robot-skill/vocab/en-us/TestRainbow.voc: -------------------------------------------------------------------------------- 1 | test rainbow 2 | test the leds 3 | deploy rainbows 4 | turn on the lights 5 | -------------------------------------------------------------------------------- /chapter17/full_system/my-robot-skill/vocab/en-us/robot.voc: -------------------------------------------------------------------------------- 1 | robot 2 | my robot 3 | ask robot to 4 | tell the robot to 5 | -------------------------------------------------------------------------------- /chapter17/full_system/my-robot-skill/vocab/en-us/stop.voc: -------------------------------------------------------------------------------- 1 | stop 2 | cease 3 | turn off 4 | stand down -------------------------------------------------------------------------------- /chapter17/full_system/pid_controller.py: -------------------------------------------------------------------------------- 1 | import logging 2 | logger = logging.getLogger("pid_controller") 3 | 4 | class PIController: 5 | def __init__(self, proportional_constant=0, integral_constant=0, windup_limit=None): 6 | self.proportional_constant = proportional_constant 7 | self.integral_constant = integral_constant 8 | self.windup_limit = windup_limit 9 | # Running sums 10 | self.integral_sum = 0 11 | 12 | def handle_proportional(self, error): 13 | return self.proportional_constant * error 14 | 15 | def handle_integral(self, error, delta_time): 16 | if self.windup_limit is None or \ 17 | (abs(self.integral_sum) < self.windup_limit) or \ 18 | ((error > 0) != (self.integral_sum > 0)): 19 | self.integral_sum += error * delta_time 20 | return self.integral_constant * self.integral_sum 21 | 22 | def get_value(self, error, delta_time=1): 23 | p = self.handle_proportional(error) 24 | i = self.handle_integral(error, delta_time) 25 | logger.debug(f"P: {p}, I: {i:.2f}") 26 | return p + i 27 | 28 | def reset(self): 29 | self.integral_sum = 0 30 | -------------------------------------------------------------------------------- /chapter17/full_system/plot_acc_gyro_fusion.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | from robot_imu import RobotImu, ImuFusion 3 | from delta_timer import DeltaTimer 4 | import imu_settings 5 | 6 | imu = RobotImu(gyro_offsets=imu_settings.gyro_offsets) 7 | 8 | fusion = ImuFusion(imu) 9 | 10 | vp.graph(xmin=0, xmax=60, scroll=True) 11 | graph_pitch = vp.gcurve(color=vp.color.red) 12 | graph_roll = vp.gcurve(color=vp.color.green) 13 | 14 | 15 | timer = DeltaTimer() 16 | while True: 17 | vp.rate(100) 18 | dt, elapsed = timer.update() 19 | fusion.update(dt) 20 | graph_pitch.plot(elapsed, fusion.pitch) 21 | graph_roll.plot(elapsed, fusion.roll) 22 | -------------------------------------------------------------------------------- /chapter17/full_system/plot_accel_debug_pitch_and_roll.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | import logging 3 | import time 4 | from robot_imu import RobotImu 5 | 6 | logging.basicConfig(level=logging.INFO) 7 | imu = RobotImu() 8 | 9 | 10 | pr = vp.graph(xmin=0, xmax=60, scroll=True) 11 | graph_pitch = vp.gcurve(color=vp.color.red, graph=pr) 12 | graph_roll = vp.gcurve(color=vp.color.green, graph=pr) 13 | 14 | xyz = vp.graph(xmin=0, xmax=60, scroll=True) 15 | graph_x = vp.gcurve(color=vp.color.orange, graph=xyz) 16 | graph_y = vp.gcurve(color=vp.color.cyan, graph=xyz) 17 | graph_z = vp.gcurve(color=vp.color.purple, graph=xyz) 18 | 19 | start = time.time() 20 | while True: 21 | vp.rate(100) 22 | elapsed = time.time() - start 23 | pitch, roll = imu.read_accelerometer_pitch_and_roll() 24 | raw_accel = imu.read_accelerometer() 25 | graph_pitch.plot(elapsed, pitch) 26 | graph_roll.plot(elapsed, roll) 27 | print(f"Pitch: {pitch:.2f}, Roll: {roll:.2f}") 28 | graph_x.plot(elapsed, raw_accel.x) 29 | graph_y.plot(elapsed, raw_accel.y) 30 | graph_z.plot(elapsed, raw_accel.z) 31 | -------------------------------------------------------------------------------- /chapter17/full_system/plot_accel_pitch_and_roll.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | import time 3 | from robot_imu import RobotImu 4 | 5 | 6 | imu = RobotImu() 7 | 8 | vp.graph(xmin=0, xmax=60, scroll=True) 9 | graph_pitch = vp.gcurve(color=vp.color.red) 10 | graph_roll = vp.gcurve(color=vp.color.green) 11 | 12 | start = time.time() 13 | while True: 14 | vp.rate(100) 15 | elapsed = time.time() - start 16 | pitch, roll = imu.read_accelerometer_pitch_and_roll() 17 | graph_pitch.plot(elapsed, pitch) 18 | graph_roll.plot(elapsed, roll) 19 | -------------------------------------------------------------------------------- /chapter17/full_system/plot_calibrated_gyroscope.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | import time 3 | from robot_imu import RobotImu 4 | import imu_settings 5 | 6 | imu = RobotImu(gyro_offsets=imu_settings.gyro_offsets) 7 | 8 | vp.graph(xmin=0, xmax=60, ymax=360, ymin=-360, scroll=True) 9 | graph_x = vp.gcurve(color=vp.color.red) 10 | graph_y = vp.gcurve(color=vp.color.green) 11 | graph_z = vp.gcurve(color=vp.color.blue) 12 | 13 | start = time.time() 14 | while True: 15 | vp.rate(100) 16 | elapsed = time.time() - start 17 | gyro = imu.read_gyroscope() 18 | print(f"Gyro x: {gyro.x:.2f}, y: {gyro.y:.2f}, z: {gyro.z:.2f}") 19 | graph_x.plot(elapsed, gyro.x) 20 | graph_y.plot(elapsed, gyro.y) 21 | graph_z.plot(elapsed, gyro.z) 22 | -------------------------------------------------------------------------------- /chapter17/full_system/plot_gyroscope.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | import time 3 | from robot_imu import RobotImu 4 | 5 | imu = RobotImu() 6 | 7 | vp.graph(xmin=0, xmax=60, scroll=True) 8 | graph_x = vp.gcurve(color=vp.color.red) 9 | graph_y = vp.gcurve(color=vp.color.green) 10 | graph_z = vp.gcurve(color=vp.color.blue) 11 | 12 | start = time.time() 13 | while True: 14 | vp.rate(100) 15 | elapsed = time.time() - start 16 | gyro = imu.read_gyroscope() 17 | print(f"Gyro x: {gyro.x:.2f}, y: {gyro.y:.2f}, z: {gyro.z:.2f}") 18 | graph_x.plot(elapsed, gyro.x) 19 | graph_y.plot(elapsed, gyro.y) 20 | graph_z.plot(elapsed, gyro.z) 21 | -------------------------------------------------------------------------------- /chapter17/full_system/plot_imu_fusion.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | from robot_imu import RobotImu, ImuFusion 3 | from delta_timer import DeltaTimer 4 | import imu_settings 5 | 6 | imu = RobotImu(gyro_offsets=imu_settings.gyro_offsets, 7 | mag_offsets=imu_settings.mag_offsets) 8 | fusion = ImuFusion(imu) 9 | 10 | vp.graph(xmin=0, xmax=60, scroll=True) 11 | graph_pitch = vp.gcurve(color=vp.color.red) 12 | graph_roll = vp.gcurve(color=vp.color.green) 13 | graph_yaw = vp.gcurve(color=vp.color.blue) 14 | 15 | timer = DeltaTimer() 16 | while True: 17 | vp.rate(100) 18 | dt, elapsed = timer.update() 19 | fusion.update(dt) 20 | graph_pitch.plot(elapsed, fusion.pitch) 21 | graph_roll.plot(elapsed, fusion.roll) 22 | graph_yaw.plot(elapsed, fusion.yaw) 23 | -------------------------------------------------------------------------------- /chapter17/full_system/plot_integrated_gyro.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | import logging 3 | from robot_imu import RobotImu 4 | from delta_timer import DeltaTimer 5 | import imu_settings 6 | 7 | 8 | logging.basicConfig(level=logging.INFO) 9 | imu = RobotImu(gyro_offsets=imu_settings.gyro_offsets) 10 | 11 | vp.graph(xmin=0, xmax=60, scroll=True) 12 | graph_x = vp.gcurve(color=vp.color.red) 13 | graph_y = vp.gcurve(color=vp.color.green) 14 | graph_z = vp.gcurve(color=vp.color.blue) 15 | 16 | timer = DeltaTimer() 17 | pitch = 0 18 | roll = 0 19 | yaw = 0 20 | 21 | while True: 22 | vp.rate(100) 23 | dt, elapsed = timer.update() 24 | gyro = imu.read_gyroscope() 25 | roll += gyro.x * dt 26 | pitch += gyro.y * dt 27 | yaw += gyro.z * dt 28 | print(f"Elapsed: {elapsed:.2f}, Pitch: {pitch:.2f}, Roll: {roll:.2f}, Yaw: {yaw:.2f}") 29 | graph_x.plot(elapsed, pitch) 30 | graph_y.plot(elapsed, roll) 31 | graph_z.plot(elapsed, yaw) 32 | -------------------------------------------------------------------------------- /chapter17/full_system/plot_mag_heading.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | from robot_imu import RobotImu 3 | from delta_timer import DeltaTimer 4 | import imu_settings 5 | 6 | imu = RobotImu(mag_offsets=imu_settings.mag_offsets) 7 | 8 | vp.graph(xmin=0, xmax=60, scroll=True) 9 | graph_yaw = vp.gcurve(color=vp.color.blue) 10 | timer = DeltaTimer() 11 | 12 | while True: 13 | vp.rate(100) 14 | dt, elapsed = timer.update() 15 | mag = imu.read_magnetometer() 16 | yaw = -vp.degrees(vp.atan2(mag.y, mag.x)) 17 | graph_yaw.plot(elapsed, yaw) 18 | -------------------------------------------------------------------------------- /chapter17/full_system/plot_temperature.py: -------------------------------------------------------------------------------- 1 | """When running, 2 | use VPYTHON_PORT=9020 VPYTHON_NOBROWSER=true""" 3 | import vpython as vp 4 | import time 5 | import logging 6 | from robot_imu import RobotImu 7 | 8 | 9 | logging.basicConfig(level=logging.INFO) 10 | imu = RobotImu() 11 | vp.graph(xmin=0, xmax=60, scroll=True) 12 | temp_graph = vp.gcurve() 13 | start = time.time() 14 | while True: 15 | vp.rate(100) 16 | temperature = imu.read_temperature() 17 | logging.info("Temperature {}".format(temperature)) 18 | elapsed = time.time() - start 19 | temp_graph.plot(elapsed, temperature) 20 | 21 | -------------------------------------------------------------------------------- /chapter17/full_system/robot.py: -------------------------------------------------------------------------------- 1 | from Raspi_MotorHAT import Raspi_MotorHAT 2 | from gpiozero import DistanceSensor 3 | 4 | import atexit 5 | import leds_led_shim 6 | from servos import Servos 7 | from encoder_counter import EncoderCounter 8 | 9 | 10 | class Robot: 11 | wheel_diameter_mm = 70.0 12 | ticks_per_revolution = 40.0 13 | wheel_distance_mm = 132.0 14 | def __init__(self, motorhat_addr=0x6f): 15 | # Setup the motorhat with the passed in address 16 | self._mh = Raspi_MotorHAT(addr=motorhat_addr) 17 | 18 | self.left_motor = self._mh.getMotor(1) 19 | self.right_motor = self._mh.getMotor(2) 20 | 21 | # Setup the Leds 22 | self.leds = leds_led_shim.Leds() 23 | # Set up servo motors for pan and tilt. 24 | self.servos = Servos(addr=motorhat_addr, deflect_90_in_ms=1) 25 | 26 | # Setup The Distance Sensors 27 | self.left_distance_sensor = DistanceSensor(echo=17, trigger=27, queue_len=2) 28 | self.right_distance_sensor = DistanceSensor(echo=5, trigger=6, queue_len=2) 29 | 30 | # Setup the Encoders 31 | EncoderCounter.set_constants(self.wheel_diameter_mm, self.ticks_per_revolution) 32 | self.left_encoder = EncoderCounter(4) 33 | self.right_encoder = EncoderCounter(26) 34 | 35 | # ensure the motors get stopped when the code exits 36 | atexit.register(self.stop_all) 37 | 38 | def convert_speed(self, speed): 39 | # Choose the running mode 40 | mode = Raspi_MotorHAT.RELEASE 41 | if speed > 0: 42 | mode = Raspi_MotorHAT.FORWARD 43 | elif speed < 0: 44 | mode = Raspi_MotorHAT.BACKWARD 45 | 46 | # Scale the speed 47 | output_speed = (abs(speed) * 255) // 100 48 | return mode, int(output_speed) 49 | 50 | def set_left(self, speed): 51 | mode, output_speed = self.convert_speed(speed) 52 | self.left_motor.setSpeed(output_speed) 53 | self.left_motor.run(mode) 54 | 55 | def set_right(self, speed): 56 | mode, output_speed = self.convert_speed(speed) 57 | self.right_motor.setSpeed(output_speed) 58 | self.right_motor.run(mode) 59 | 60 | def stop_motors(self): 61 | self.left_motor.run(Raspi_MotorHAT.RELEASE) 62 | self.right_motor.run(Raspi_MotorHAT.RELEASE) 63 | 64 | def stop_all(self): 65 | self.stop_motors() 66 | 67 | # Clear the display 68 | self.leds.clear() 69 | self.leds.show() 70 | 71 | # Clear any sensor handlers 72 | self.left_encoder.stop() 73 | self.right_encoder.stop() 74 | 75 | # Reset the servos 76 | self.servos.stop_all() 77 | 78 | def set_pan(self, angle): 79 | self.servos.set_servo_angle(1, angle) 80 | 81 | def set_tilt(self, angle): 82 | self.servos.set_servo_angle(0, angle) 83 | 84 | -------------------------------------------------------------------------------- /chapter17/full_system/robot_modes.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | class RobotModes(object): 4 | """Our robot behaviors and tests as running modes""" 5 | 6 | # Mode config goes from a "mode_name" to a script to run. Configured for look up. 7 | mode_config = { 8 | "avoid_behavior": {"script": "avoid_behavior.py"}, 9 | "circle_head": {"script": "circle_pan_tilt_behavior.py"}, 10 | "test_rainbow": {"script": "test_rainbow.py"}, 11 | "test_leds": {"script": "leds_test.py"}, 12 | "line_following": {"script": "line_follow_behavior.py", "server": True}, 13 | "color_track": {"script": "color_track_behavior.py", "server": True}, 14 | "face_track": {"script": "face_track_behavior.py", "server": True}, 15 | "manual_drive": {"script": "manual_drive.py", "server": True}, 16 | "behavior_line": {"script": "straight_line_drive.py"}, 17 | "drive_north": {"script": "drive_north.py"} 18 | } 19 | 20 | menu_config = [ 21 | {"mode_name": "avoid_behavior", "text": "Avoid Behavior"}, 22 | {"mode_name": "circle_head", "text": "Circle Head"}, 23 | {"mode_name": "test_rainbow", "text": "LED Rainbow"}, 24 | {"mode_name": "test_leds", "text": "Test LEDs"}, 25 | {"mode_name": "line_following", "text": "Line Following"}, 26 | {"mode_name": "color_track", "text": "Color Tracking"}, 27 | {"mode_name": "face_track", "text": "Face Tracking"}, 28 | {"mode_name": "manual_drive", "text": "Drive Manually"}, 29 | {"mode_name": "behavior_line", "text": "Drive In A Line"}, 30 | {"mode_name": "drive_north", "text": "Drive North"} 31 | ] 32 | 33 | def __init__(self): 34 | self.current_process = None 35 | 36 | def is_running(self): 37 | """Check if there is a process running. Returncode is only set when a process finishes""" 38 | return self.current_process and self.current_process.returncode is None 39 | 40 | def run(self, mode_name): 41 | """Run the mode as a subprocess, but not if we still have one running""" 42 | while self.is_running(): 43 | self.stop() 44 | 45 | script = self.mode_config[mode_name]['script'] 46 | self.current_process = subprocess.Popen(["python3", script]) 47 | 48 | def stop(self): 49 | """Stop a process""" 50 | if self.is_running(): 51 | # Sending the signal sigint is (on Linux) similar to pressing ctrl-c. 52 | # That causes the behavior to clean up and exit. 53 | self.current_process.send_signal(subprocess.signal.SIGINT) 54 | self.current_process = None 55 | 56 | def should_redirect(self, mode_name): 57 | return self.mode_config[mode_name].get('server') is True and self.is_running() 58 | -------------------------------------------------------------------------------- /chapter17/full_system/robot_pose.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | 3 | 4 | def robot_view(): 5 | vp.scene.forward = vp.vector(-3, -1, -1) 6 | vp.scene.up = vp.vector(0, 0, 1) 7 | -------------------------------------------------------------------------------- /chapter17/full_system/servo_type_position.py: -------------------------------------------------------------------------------- 1 | from Raspi_MotorHAT.Raspi_PWM_Servo_Driver import PWM 2 | import atexit 3 | 4 | pwm = PWM(0x6f) 5 | # This sets the timebase for it all 6 | pwm_frequency = 100 7 | pwm.setPWMFreq(pwm_frequency) 8 | # Mid-point of the servo pulse length in milliseconds. 9 | servo_mid_point_ms = 1.5 10 | # What a deflection of 90 degrees is in pulse length in milliseconds 11 | deflect_90_in_ms = 0.5 12 | # Frequency is 1 divided by period, but working ms, we can use 1000 13 | period_in_ms = 1000 / pwm_frequency 14 | # The chip has 4096 steps in each period. 15 | pulse_steps = 4096 16 | # Steps for every millisecond. 17 | steps_per_ms = pulse_steps / period_in_ms 18 | # Steps for a degree. 19 | steps_per_degree = (deflect_90_in_ms * steps_per_ms) / 90 20 | # Mid-point of the servo in steps 21 | servo_mid_point_steps = servo_mid_point_ms * steps_per_ms 22 | 23 | def convert_degrees_to_steps(position): 24 | return int(servo_mid_point_steps + (position * steps_per_degree)) 25 | 26 | atexit.register(pwm.setPWM, 0, 0, 4096) 27 | 28 | while True: 29 | position = int(input("Type your position in degrees (90 to -90, 0 is middle): ")) 30 | end_step = convert_degrees_to_steps(position) 31 | pwm.setPWM(0, 0, end_step) 32 | -------------------------------------------------------------------------------- /chapter17/full_system/servos.py: -------------------------------------------------------------------------------- 1 | from Raspi_MotorHAT.Raspi_PWM_Servo_Driver import PWM 2 | 3 | class Servos: 4 | def __init__(self, addr=0x6f, deflect_90_in_ms = 0.5): 5 | """addr: The i2c address of the PWM chip. 6 | deflect_90_in_ms: set this to calibrate the servo motors. 7 | it is what a deflection of 90 degrees is 8 | in terms of a pulse length in milliseconds.""" 9 | self._pwm = PWM(addr) 10 | # This sets the timebase for it all 11 | pwm_frequency = 100 12 | self._pwm.setPWMFreq(pwm_frequency) 13 | # Mid-point of the servo pulse length in milliseconds. 14 | servo_mid_point_ms = 1.5 15 | # Frequency is 1/period, but working ms, we can use 1000 16 | period_in_ms = 1000 / pwm_frequency 17 | # The chip has 4096 steps in each period. 18 | pulse_steps = 4096 19 | # Steps for every millisecond. 20 | steps_per_ms = pulse_steps / period_in_ms 21 | # Steps for a degree 22 | self.steps_per_degree = (deflect_90_in_ms * steps_per_ms) / 90 23 | # Mid-point of the servo in steps 24 | self.servo_mid_point_steps = servo_mid_point_ms * steps_per_ms 25 | 26 | # Map for channels 27 | self.channels = [0, 1, 14, 15] 28 | 29 | def stop_all(self): 30 | # 0 in start is nothing, 4096 sets the OFF bit. 31 | self._pwm.setPWM(self.channels[0], 0, 4096) 32 | self._pwm.setPWM(self.channels[1], 0, 4096) 33 | self._pwm.setPWM(self.channels[2], 0, 4096) 34 | self._pwm.setPWM(self.channels[3], 0, 4096) 35 | 36 | def _convert_degrees_to_steps(self, position): 37 | return int(self.servo_mid_point_steps + (position * self.steps_per_degree)) 38 | 39 | def set_servo_angle(self, channel, angle): 40 | """position: The position in degrees from the center. -90 to 90""" 41 | # Validate 42 | if angle > 90 or angle < -90: 43 | raise ValueError("Angle outside of range") 44 | # Then set the position 45 | off_step = self._convert_degrees_to_steps(angle) 46 | self._pwm.setPWM(self.channels[channel], 0, off_step) 47 | 48 | -------------------------------------------------------------------------------- /chapter17/full_system/simple_avoid_behavior.py: -------------------------------------------------------------------------------- 1 | from robot import Robot 2 | from time import sleep 3 | 4 | class ObstacleAvoidingBehavior: 5 | """Simple obstacle avoiding""" 6 | def __init__(self, the_robot): 7 | self.robot = the_robot 8 | self.speed = 60 9 | 10 | def get_motor_speed(self, distance): 11 | """This method chooses a speed for a motor based on the distance from a sensor""" 12 | if distance < 0.2: 13 | return -self.speed 14 | else: 15 | return self.speed 16 | 17 | def run(self): 18 | while True: 19 | # Get the sensor readings in meters 20 | left_distance = self.robot.left_distance_sensor.distance 21 | right_distance = self.robot.right_distance_sensor.distance 22 | print("Left: {l:.2f}, Right: {r:.2f}".format(l=left_distance, r=right_distance)) 23 | # and drive 24 | left_speed = self.get_motor_speed(left_distance) 25 | self.robot.set_right(left_speed) 26 | right_speed = self.get_motor_speed(right_distance) 27 | self.robot.set_left(right_speed) 28 | # Wait a little 29 | sleep(0.05) 30 | 31 | bot = Robot() 32 | behavior = ObstacleAvoidingBehavior(bot) 33 | behavior.run() 34 | -------------------------------------------------------------------------------- /chapter17/full_system/sonar_scan.py: -------------------------------------------------------------------------------- 1 | import time 2 | import math 3 | 4 | import matplotlib.pyplot as plt 5 | 6 | from robot import Robot 7 | 8 | start_scan =0 9 | lower_bound = -90 10 | upper_bound = 90 11 | scan_step = 5 12 | 13 | the_robot = Robot() 14 | the_robot.set_tilt(0) 15 | 16 | scan_data = {} 17 | # Make the sensor scan 18 | for facing in range(lower_bound, upper_bound, scan_step): 19 | the_robot.set_pan(-facing) 20 | time.sleep(0.1) 21 | scan_data[facing] = the_robot.left_distance_sensor.distance * 100 22 | # make plot 23 | axis = [math.radians(facing) for facing in scan_data.keys()] 24 | print(axis) 25 | print(scan_data.values()) 26 | plt.polar(axis, list(scan_data.values()), 'g-') 27 | # dump to png 28 | plt.savefig("scan.png") 29 | -------------------------------------------------------------------------------- /chapter17/full_system/static/display.css: -------------------------------------------------------------------------------- 1 | .slider_track { 2 | width: 10vw; 3 | height: 90vh; 4 | border: 1px solid blue; 5 | background-color: lightblue; 6 | } 7 | .slider_tick { 8 | fill: mistyrose; 9 | } 10 | #left_slider { 11 | position: absolute; 12 | left: 5vw; 13 | top: 5vh; 14 | } 15 | #right_slider { 16 | position: absolute; 17 | right: 5vw; 18 | top: 5vh; 19 | } 20 | .button { 21 | display: block; 22 | height: 10vh; 23 | text-align: center; 24 | font-size: 2em; 25 | line-height: 10vh; 26 | text-decoration: none; 27 | background-color: blue; 28 | color: white; 29 | } 30 | 31 | #exitbutton { 32 | width: 40vh; 33 | margin-top: 5vh; 34 | margin-left: auto; 35 | margin-right: auto; 36 | } 37 | 38 | #video { 39 | text-align: center; 40 | } 41 | 42 | #video img { 43 | margin-top: 20vh; 44 | width: 80vmin; 45 | height: auto; 46 | } 47 | 48 | .menu { 49 | width: 100%; 50 | margin-top: 0; 51 | margin-bottom: 0; 52 | padding: 0; 53 | } 54 | 55 | .menu li { 56 | list-style-type: none; 57 | list-style-position: initial; 58 | } 59 | 60 | .menu .button { 61 | margin-left: auto; 62 | margin-right: auto; 63 | width: 60vw; 64 | border: 1px solid lightblue; 65 | } 66 | -------------------------------------------------------------------------------- /chapter17/full_system/static/touch-slider.js: -------------------------------------------------------------------------------- 1 | function makeSlider(id, when_changed) { 2 | // internal data 3 | let touched = false; 4 | let changed = false; 5 | let position = 0; 6 | const slider = $('#' + id); 7 | // internal functions 8 | const set_position = function(new_position) { 9 | position = Math.round(new_position); 10 | slider.find('.slider_tick')[0].setAttribute('cy', position); 11 | changed = true; 12 | }; 13 | slider.on('touchmove', event => { 14 | let touch = event.targetTouches[0]; 15 | // Get the touch relative to the top of the slider 16 | let from_top = touch.pageY - slider.offset().top; 17 | // Convert to twice a percentage of the track. (0 is the middle) 18 | let relative_touch = (from_top / slider.height()) * 200; 19 | set_position(relative_touch - 100); 20 | touched = true; 21 | event.preventDefault(); 22 | }); 23 | slider.on('touchend', () => touched = false); 24 | 25 | const update = function() { 26 | if(!touched && Math.abs(position) > 0) { 27 | // drift back to the middle - add 0.5 to the movement, in the right direction to round up to at least 1% 28 | let error = 0 - position; 29 | let change = (0.3 * error) + (Math.sign(error) * 0.5); 30 | set_position(position + change); 31 | // console.log(id + ": " + position); 32 | } 33 | }; 34 | setInterval(update, 50); 35 | 36 | const update_if_changed = function() { 37 | if(changed) { 38 | changed = false; 39 | // Call the callback - invert the track so 'up' is positive 40 | when_changed(-position); 41 | } 42 | }; 43 | setInterval(update_if_changed, 200); 44 | } 45 | -------------------------------------------------------------------------------- /chapter17/full_system/straight_line_drive.py: -------------------------------------------------------------------------------- 1 | from robot import Robot 2 | from pid_controller import PIController 3 | import time 4 | import logging 5 | 6 | logger = logging.getLogger("straight_line") 7 | logging.basicConfig(level=logging.INFO) 8 | logging.getLogger("pid_controller").setLevel(logging.DEBUG) 9 | 10 | bot = Robot() 11 | stop_at_time = time.time() + 15 12 | 13 | speed = 80 14 | bot.set_left(speed) 15 | bot.set_right(speed) 16 | 17 | pid = PIController(proportional_constant=4, integral_constant=0.3) 18 | 19 | while time.time() < stop_at_time: 20 | time.sleep(0.01) 21 | # Calculate the error 22 | left = bot.left_encoder.pulse_count 23 | right = bot.right_encoder.pulse_count 24 | error = left - right 25 | 26 | # Get the speed 27 | adjustment = pid.get_value(error) 28 | right_speed = int(speed + adjustment) 29 | left_speed = int(speed - adjustment) 30 | 31 | logger.debug(f"error: {error} adjustment: {adjustment:.2f}") 32 | logger.info(f"left: {left} right: {right}, left_speed: {left_speed} right_speed: {right_speed}") 33 | bot.set_left(left_speed) 34 | bot.set_right(right_speed) 35 | -------------------------------------------------------------------------------- /chapter17/full_system/templates/color_track_behavior.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Robot Image Server 5 | 6 | 7 |

Robot Image Server

8 |
9 | Start 10 | Stop
11 | Exit 12 | 13 | 14 | -------------------------------------------------------------------------------- /chapter17/full_system/templates/control_image_behavior.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Robot Image Server 5 | 6 | 7 |

Robot Image Server

8 |
9 | Exit 10 | 11 | 12 | -------------------------------------------------------------------------------- /chapter17/full_system/templates/image_server.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Robot Image Server 4 | 5 | 6 |

Robot Image Server

7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /chapter17/full_system/templates/manual_drive.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Manually Drive The Robot 9 | 10 | 11 | 12 | 13 | 14 | Exit 15 |
16 | 17 | 18 | 19 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /chapter17/full_system/templates/menu.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | My Robot Menu 5 | 6 | 7 | 8 | 9 |

My Robot Menu

10 |

11 | 21 | 22 | 32 | 33 | -------------------------------------------------------------------------------- /chapter17/full_system/test_distance_sensors.py: -------------------------------------------------------------------------------- 1 | import time 2 | from gpiozero import DistanceSensor 3 | 4 | print("Prepare GPIO Pins") 5 | sensor_l = DistanceSensor(echo=17, trigger=27, queue_len=2) 6 | sensor_r = DistanceSensor(echo=5, trigger=6, queue_len=2) 7 | 8 | while True: 9 | print("Left: {l:.2f}, Right: {r:.2f}".format( 10 | l=sensor_l.distance * 100, 11 | r=sensor_r.distance * 100)) 12 | time.sleep(0.1) 13 | -------------------------------------------------------------------------------- /chapter17/full_system/test_distance_travelled.py: -------------------------------------------------------------------------------- 1 | from robot import Robot 2 | import time 3 | import math 4 | import logging 5 | logger = logging.getLogger("test_distance_travelled") 6 | 7 | wheel_diameter_mm = 70.0 8 | ticks_per_revolution = 40.0 9 | ticks_to_mm_const = (math.pi / ticks_per_revolution) * wheel_diameter_mm 10 | 11 | def ticks_to_mm(ticks): 12 | return int(ticks_to_mm_const * ticks) 13 | 14 | bot = Robot() 15 | stop_at_time = time.time() + 1 16 | 17 | logging.basicConfig(level=logging.INFO) 18 | bot.set_left(90) 19 | bot.set_right(90) 20 | 21 | while time.time() < stop_at_time: 22 | logger.info("Left: {} Right: {}".format( 23 | ticks_to_mm(bot.left_encoder.pulse_count), 24 | ticks_to_mm(bot.right_encoder.pulse_count))) 25 | time.sleep(0.05) 26 | 27 | -------------------------------------------------------------------------------- /chapter17/full_system/test_encoders.py: -------------------------------------------------------------------------------- 1 | from robot import Robot 2 | import time 3 | import logging 4 | 5 | from gpiozero import DigitalInputDevice 6 | 7 | logger = logging.getLogger("test_encoders") 8 | 9 | class EncoderCounter(object): 10 | def __init__(self, pin_number): 11 | self.pulse_count = 0 12 | self.device = DigitalInputDevice(pin=pin_number) 13 | self.device.pin.when_changed = self.when_changed 14 | 15 | def when_changed(self, time_ticks, state): 16 | self.pulse_count += 1 17 | 18 | bot = Robot() 19 | left_encoder = EncoderCounter(4) 20 | right_encoder = EncoderCounter(26) 21 | 22 | stop_at_time = time.time() + 1 23 | 24 | logging.basicConfig(level=logging.INFO) 25 | bot.set_left(90) 26 | bot.set_right(90) 27 | while time.time() < stop_at_time: 28 | logger.info(f"Left: {left_encoder.pulse_count} Right: {right_encoder.pulse_count}") 29 | time.sleep(0.05) 30 | -------------------------------------------------------------------------------- /chapter17/full_system/test_line_find.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | from matplotlib import pyplot as plt 4 | 5 | image = cv2.imread("carpet_line1 2.jpg") 6 | assert image is not None, "Unable to read file" 7 | 8 | resized = cv2.resize(image, (320, 240)) 9 | 10 | gray = cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY) 11 | blur = cv2.blur(gray, (5, 80)) 12 | row = blur[180].astype(np.int32) 13 | diff = np.diff(row) 14 | 15 | max_d = np.amax(diff, 0) 16 | min_d = np.amin(diff, 0) 17 | 18 | highest = np.where(diff == max_d)[0][0] 19 | lowest = np.where(diff == min_d)[0][0] 20 | middle = (highest + lowest) // 2 21 | 22 | x = np.arange(len(diff)) 23 | plt.plot(x, diff) 24 | plt.plot([lowest, lowest], [max_d, min_d], "g--") 25 | plt.plot([middle, middle], [max_d, min_d], "r-") 26 | plt.plot([highest, highest], [max_d, min_d], "g--") 27 | plt.savefig("carpet_line1_2_blur580.png") 28 | -------------------------------------------------------------------------------- /chapter17/full_system/test_motors.py: -------------------------------------------------------------------------------- 1 | from Raspi_MotorHAT import Raspi_MotorHAT 2 | 3 | import time 4 | import atexit 5 | 6 | mh = Raspi_MotorHAT(addr=0x6f) 7 | lm = mh.getMotor(1) 8 | rm = mh.getMotor(2) 9 | 10 | def turn_off_motors(): 11 | lm.run(Raspi_MotorHAT.RELEASE) 12 | rm.run(Raspi_MotorHAT.RELEASE) 13 | 14 | atexit.register(turn_off_motors) 15 | 16 | lm.setSpeed(255) 17 | rm.setSpeed(255) 18 | 19 | lm.run(Raspi_MotorHAT.FORWARD) 20 | rm.run(Raspi_MotorHAT.FORWARD) 21 | time.sleep(1) 22 | 23 | -------------------------------------------------------------------------------- /chapter17/full_system/test_rainbow.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | from robot import Robot 3 | from led_rainbow import show_rainbow 4 | 5 | bot = Robot() 6 | 7 | while True: 8 | print("on") 9 | show_rainbow(bot.leds, range(bot.leds.count)) 10 | bot.leds.show() 11 | sleep(0.5) 12 | print("off") 13 | bot.leds.clear() 14 | bot.leds.show() 15 | sleep(0.5) 16 | -------------------------------------------------------------------------------- /chapter17/full_system/virtual_robot.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | from robot_pose import robot_view 3 | 4 | 5 | def make_robot(): 6 | chassis_width = 155 # left side to right 7 | chassis_thickness = 3 # plastic thickness 8 | chassis_length = 200 # front to back 9 | wheel_thickness = 26 10 | wheel_diameter = 70 11 | axle_x = 30 # wheel axle from 12 | axle_z = -20 13 | rear_castor_position = vp.vector(-80, -6, -30) 14 | rear_castor_radius = 14 15 | rear_caster_thickness = 12 16 | 17 | base = vp.box(length=chassis_length, 18 | height=chassis_thickness, 19 | width=chassis_width) 20 | # rotate to match body - so Z is height and Y is width 21 | base.rotate(angle=vp.radians(90), 22 | axis=vp.vector(1, 0, 0)) 23 | wheel_dist = chassis_width/2 24 | wheel_l = vp.cylinder(radius=wheel_diameter/2, 25 | length=wheel_thickness, 26 | pos=vp.vector(axle_x, -wheel_dist, axle_z), 27 | axis=vp.vector(0, -1, 0)) 28 | wheel_r = vp.cylinder(radius=wheel_diameter/2, 29 | length=wheel_thickness, 30 | pos=vp.vector(axle_x, wheel_dist, axle_z), 31 | axis=vp.vector(0, 1, 0)) 32 | rear_castor = vp.cylinder(radius=rear_castor_radius, 33 | length=rear_caster_thickness, 34 | pos=rear_castor_position, 35 | axis=vp.vector(0, 1, 0)) 36 | return vp.compound([base, wheel_l, wheel_r, rear_castor]) 37 | 38 | 39 | if __name__ == "__main__": 40 | robot_view() 41 | x_arrow = vp.arrow(axis=vp.vector(200, 0, 0), color=vp.color.red) 42 | y_arrow = vp.arrow(axis=vp.vector(0, 200, 0), color=vp.color.green) 43 | z_arrow = vp.arrow(axis=vp.vector(0, 0, 200), color=vp.color.blue) 44 | robot=make_robot() 45 | -------------------------------------------------------------------------------- /chapter17/full_system/visual_filtered_accelerometer.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | import logging 3 | from robot_imu import RobotImu, ComplementaryFilter 4 | import virtual_robot 5 | 6 | logging.basicConfig(level=logging.INFO) 7 | imu = RobotImu() 8 | model = virtual_robot.make_robot() 9 | virtual_robot.robot_view() 10 | 11 | filter = ComplementaryFilter(0.95).filter 12 | 13 | pitch = 0 14 | roll = 0 15 | 16 | while True: 17 | vp.rate(100) 18 | 19 | new_pitch, new_roll = imu.read_accelerometer_pitch_and_roll() 20 | pitch = filter(pitch, new_pitch) 21 | roll = filter(roll, new_roll) 22 | 23 | print(f"Pitch: {pitch:.2f}, Roll: {roll:.2f}") 24 | # reset the model 25 | model.up = vp.vector(0, 1, 0) 26 | model.axis = vp.vector(1, 0, 0) 27 | # Reposition it 28 | model.rotate(angle=vp.radians(roll), axis=vp.vector(1, 0, 0)) 29 | model.rotate(angle=vp.radians(pitch), axis=vp.vector(0, 1, 0)) 30 | -------------------------------------------------------------------------------- /chapter17/full_system/visual_fusion.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | from robot_imu import RobotImu, ImuFusion 3 | from delta_timer import DeltaTimer 4 | import imu_settings 5 | import virtual_robot 6 | 7 | imu = RobotImu(gyro_offsets=imu_settings.gyro_offsets, 8 | mag_offsets=imu_settings.mag_offsets) 9 | fusion = ImuFusion(imu) 10 | 11 | model = virtual_robot.make_robot() 12 | virtual_robot.robot_view() 13 | 14 | timer = DeltaTimer() 15 | 16 | while True: 17 | vp.rate(100) 18 | dt, elapsed = timer.update() 19 | fusion.update(dt) 20 | # reset the model 21 | model.up = vp.vector(0, 1, 0) 22 | model.axis = vp.vector(1, 0, 0) 23 | # Reposition it 24 | model.rotate(angle=vp.radians(fusion.roll), axis=vp.vector(1, 0, 0)) 25 | model.rotate(angle=vp.radians(fusion.pitch), axis=vp.vector(0, 1, 0)) 26 | model.rotate(angle=vp.radians(fusion.yaw), axis=vp.vector(0, 0, 1)) 27 | -------------------------------------------------------------------------------- /chapter17/full_system/visual_gyroscope.py: -------------------------------------------------------------------------------- 1 | import vpython as vp 2 | from robot_imu import RobotImu 3 | import time 4 | import imu_settings 5 | import virtual_robot 6 | 7 | 8 | imu = RobotImu(gyro_offsets=imu_settings.gyro_offsets) 9 | 10 | pitch = 0 11 | roll = 0 12 | yaw = 0 13 | 14 | model = virtual_robot.make_robot() 15 | virtual_robot.robot_view() 16 | 17 | latest = time.time() 18 | 19 | while True: 20 | vp.rate(1000) 21 | current = time.time() 22 | dt = latest - current 23 | latest = current 24 | gyro = imu.read_gyroscope() 25 | pitch += gyro.x * dt 26 | roll += gyro.y * dt 27 | yaw += gyro.z * dt 28 | 29 | # reset the model 30 | model.up = vp.vector(0, 1, 0) 31 | model.axis = vp.vector(1, 0, 0) 32 | # Reposition it 33 | model.rotate(angle=vp.radians(pitch), axis=vp.vector(1, 0, 0)) 34 | model.rotate(angle=vp.radians(roll), axis=vp.vector(0, 1, 0)) 35 | model.rotate(angle=vp.radians(yaw), axis=vp.vector(0, 0, 1)) 36 | -------------------------------------------------------------------------------- /chapter4/readme.md: -------------------------------------------------------------------------------- 1 | Creating A Headless Pi 2 | ====================== 3 | 4 | This chapter is about making a Raspberry Pi headless, and reaching it from the network. 5 | 6 | Using WiFi and SSH 7 | ================== 8 | 9 | This example wpa_supplicant should be copied onto the BOOT partition of a Raspberry Pi SD card, as wpa_supplicant.conf. 10 | The SSID and PSK will need to be substituted for your own details. 11 | 12 | Also an "ssh" file should be present on the SD Card BOOT partition to enable this too. 13 | 14 | These will make your Raspberry Pi contactable on your WiFi network. 15 | 16 | Contacting The Pi 17 | ================= 18 | 19 | When it is turned on, the Raspberry Pi will respond on a network to the name "raspberrypi.local" until you change it to a more unique hostname. 20 | 21 | You will need to ensure your computer supports MDNS. Most current Linux and Apple Mac desktops already have this support. 22 | 23 | ## Windows 24 | 25 | You will need to install the Apple Bonjour softwarem from https://support.apple.com/downloads/bonjour-for-windows, unless you already have iTunes or Skype installed. -------------------------------------------------------------------------------- /chapter4/wpa_supplicant_example.conf: -------------------------------------------------------------------------------- 1 | country= 2 | update_config=1 3 | ctrl_interface=/var/run/wpa_supplicant 4 | 5 | network={ 6 | ssid="" 7 | psk="" 8 | } -------------------------------------------------------------------------------- /chapter5/hello.py: -------------------------------------------------------------------------------- 1 | import socket 2 | print('%s is alive!' % socket.gethostname()) 3 | -------------------------------------------------------------------------------- /chapter5/readme.md: -------------------------------------------------------------------------------- 1 | # Using sftp to Upload Files From Your PC 2 | 3 | This hello.py is just a sample file, to upload to the Pi using SFTP and then run using python. 4 | 5 | Use Filezilla, or a preferred SFTP client to link to the Pi and send this file to it to get comfortable with SFTP file transfers. -------------------------------------------------------------------------------- /chapter7/behavior_line.py: -------------------------------------------------------------------------------- 1 | import robot 2 | from Raspi_MotorHAT import Raspi_MotorHAT 3 | from time import sleep 4 | 5 | r = robot.Robot() 6 | r.set_left(100) 7 | r.set_right(70) 8 | sleep(1) 9 | 10 | -------------------------------------------------------------------------------- /chapter7/behavior_path.py: -------------------------------------------------------------------------------- 1 | import robot 2 | from time import sleep 3 | 4 | def straight(bot, seconds): 5 | bot.set_left(100) 6 | bot.set_right(100) 7 | sleep(seconds) 8 | 9 | def turn_left(bot, seconds): 10 | bot.set_left(20) 11 | bot.set_right(80) 12 | sleep(seconds) 13 | 14 | def turn_right(bot, seconds): 15 | bot.set_left(80) 16 | bot.set_right(20) 17 | sleep(seconds) 18 | 19 | def spin_left(bot, seconds): 20 | bot.set_left(-100) 21 | bot.set_right(100) 22 | sleep(seconds) 23 | 24 | bot = robot.Robot() 25 | straight(bot, 1) 26 | turn_right(bot, 0.6) 27 | straight(bot, 0.6) 28 | turn_left(bot, 0.6) 29 | straight(bot, 0.6) 30 | turn_left(bot, 0.6) 31 | straight(bot, 0.3) 32 | spin_left(bot, 1) 33 | 34 | -------------------------------------------------------------------------------- /chapter7/robot.py: -------------------------------------------------------------------------------- 1 | from Raspi_MotorHAT import Raspi_MotorHAT 2 | 3 | import atexit 4 | 5 | 6 | class Robot: 7 | def __init__(self, motorhat_addr=0x6f): 8 | # Setup the motorhat with the passed in address 9 | self._mh = Raspi_MotorHAT(addr=motorhat_addr) 10 | 11 | # get local variable for each motor 12 | self.left_motor = self._mh.getMotor(1) 13 | self.right_motor = self._mh.getMotor(2) 14 | 15 | # ensure the motors get stopped when the code exits 16 | atexit.register(self.stop_motors) 17 | 18 | def convert_speed(self, speed): 19 | # Choose the running mode 20 | mode = Raspi_MotorHAT.RELEASE 21 | if speed > 0: 22 | mode = Raspi_MotorHAT.FORWARD 23 | elif speed < 0: 24 | mode = Raspi_MotorHAT.BACKWARD 25 | 26 | # Scale the speed 27 | output_speed = (abs(speed) * 255) // 100 28 | return mode, int(output_speed) 29 | 30 | def set_left(self, speed): 31 | mode, output_speed = self.convert_speed(speed) 32 | self.left_motor.setSpeed(output_speed) 33 | self.left_motor.run(mode) 34 | 35 | def set_right(self, speed): 36 | mode, output_speed = self.convert_speed(speed) 37 | self.right_motor.setSpeed(output_speed) 38 | self.right_motor.run(mode) 39 | 40 | def stop_motors(self): 41 | self.left_motor.run(Raspi_MotorHAT.RELEASE) 42 | self.right_motor.run(Raspi_MotorHAT.RELEASE) 43 | 44 | -------------------------------------------------------------------------------- /chapter7/test_motors.py: -------------------------------------------------------------------------------- 1 | from Raspi_MotorHAT import Raspi_MotorHAT 2 | 3 | import time 4 | import atexit 5 | 6 | mh = Raspi_MotorHAT(addr=0x6f) 7 | lm = mh.getMotor(1) 8 | rm = mh.getMotor(2) 9 | 10 | def turn_off_motors(): 11 | lm.run(Raspi_MotorHAT.RELEASE) 12 | rm.run(Raspi_MotorHAT.RELEASE) 13 | 14 | atexit.register(turn_off_motors) 15 | 16 | lm.setSpeed(255) 17 | rm.setSpeed(255) 18 | 19 | lm.run(Raspi_MotorHAT.FORWARD) 20 | rm.run(Raspi_MotorHAT.FORWARD) 21 | time.sleep(1) 22 | 23 | -------------------------------------------------------------------------------- /chapter8/avoid_behavior.py: -------------------------------------------------------------------------------- 1 | from robot import Robot 2 | from time import sleep 3 | 4 | class ObstacleAvoidingBehavior: 5 | """Simple obstacle avoiding""" 6 | def __init__(self, the_robot): 7 | self.robot = the_robot 8 | self.speed = 60 9 | 10 | def get_speeds(self, nearest_distance): 11 | if nearest_distance >= 1.0: 12 | nearest_speed = self.speed 13 | furthest_speed = self.speed 14 | delay = 100 15 | elif nearest_distance > 0.5: 16 | nearest_speed = self.speed 17 | furthest_speed = self.speed * 0.8 18 | delay = 100 19 | elif nearest_distance > 0.2: 20 | nearest_speed = self.speed 21 | furthest_speed = self.speed * 0.6 22 | delay = 100 23 | elif nearest_distance > 0.1: 24 | nearest_speed = -self.speed * 0.4 25 | furthest_speed = -self.speed 26 | delay = 100 27 | else: # collison 28 | nearest_speed = -self.speed 29 | furthest_speed = -self.speed 30 | delay = 250 31 | return nearest_speed, furthest_speed, delay 32 | 33 | def run(self): 34 | while True: 35 | # Get the sensor readings in meters 36 | left_distance = self.robot.left_distance_sensor.distance 37 | right_distance = self.robot.right_distance_sensor.distance 38 | # Get speeds for motors from distances 39 | nearest_speed, furthest_speed, delay = self.get_speeds(min(left_distance, right_distance)) 40 | print(f"Distances: l {left_distance:.2f}, r {right_distance:.2f}. Speeds: n: {nearest_speed}, f: {furthest_speed}. Delay: {delay}") 41 | # and drive 42 | # Send this to the motors 43 | if left_distance < right_distance: 44 | self.robot.set_left(nearest_speed) 45 | self.robot.set_right(furthest_speed) 46 | else: 47 | self.robot.set_right(nearest_speed) 48 | self.robot.set_left(furthest_speed) 49 | # Wait our delay time 50 | sleep(delay * 0.001) 51 | 52 | 53 | bot = Robot() 54 | behavior = ObstacleAvoidingBehavior(bot) 55 | behavior.run() 56 | 57 | -------------------------------------------------------------------------------- /chapter8/robot.py: -------------------------------------------------------------------------------- 1 | from Raspi_MotorHAT import Raspi_MotorHAT 2 | from gpiozero import DistanceSensor 3 | 4 | import atexit 5 | 6 | 7 | class Robot: 8 | def __init__(self, motorhat_addr=0x6f): 9 | # Setup the motorhat with the passed in address 10 | self._mh = Raspi_MotorHAT(addr=motorhat_addr) 11 | 12 | self.left_motor = self._mh.getMotor(1) 13 | self.right_motor = self._mh.getMotor(2) 14 | 15 | # Setup The Distance Sensors 16 | self.left_distance_sensor = DistanceSensor(echo=17, trigger=27, queue_len=2) 17 | self.right_distance_sensor = DistanceSensor(echo=5, trigger=6, queue_len=2) 18 | 19 | # ensure the motors get stopped when the code exits 20 | atexit.register(self.stop_all) 21 | 22 | def convert_speed(self, speed): 23 | # Choose the running mode 24 | mode = Raspi_MotorHAT.RELEASE 25 | if speed > 0: 26 | mode = Raspi_MotorHAT.FORWARD 27 | elif speed < 0: 28 | mode = Raspi_MotorHAT.BACKWARD 29 | 30 | # Scale the speed 31 | output_speed = (abs(speed) * 255) // 100 32 | return mode, int(output_speed) 33 | 34 | def set_left(self, speed): 35 | mode, output_speed = self.convert_speed(speed) 36 | self.left_motor.setSpeed(output_speed) 37 | self.left_motor.run(mode) 38 | 39 | def set_right(self, speed): 40 | mode, output_speed = self.convert_speed(speed) 41 | self.right_motor.setSpeed(output_speed) 42 | self.right_motor.run(mode) 43 | 44 | def stop_motors(self): 45 | self.left_motor.run(Raspi_MotorHAT.RELEASE) 46 | self.right_motor.run(Raspi_MotorHAT.RELEASE) 47 | 48 | def stop_all(self): 49 | self.stop_motors() 50 | -------------------------------------------------------------------------------- /chapter8/simple_avoid_behavior.py: -------------------------------------------------------------------------------- 1 | from robot import Robot 2 | from time import sleep 3 | 4 | class ObstacleAvoidingBehavior: 5 | """Simple obstacle avoiding""" 6 | def __init__(self, the_robot): 7 | self.robot = the_robot 8 | self.speed = 60 9 | 10 | def get_motor_speed(self, distance): 11 | """This method chooses a speed for a motor based on the distance from a sensor""" 12 | if distance < 0.2: 13 | return -self.speed 14 | else: 15 | return self.speed 16 | 17 | def run(self): 18 | while True: 19 | # Get the sensor readings in meters 20 | left_distance = self.robot.left_distance_sensor.distance 21 | right_distance = self.robot.right_distance_sensor.distance 22 | print("Left: {l:.2f}, Right: {r:.2f}".format(l=left_distance, r=right_distance)) 23 | # and drive 24 | left_speed = self.get_motor_speed(left_distance) 25 | self.robot.set_right(left_speed) 26 | right_speed = self.get_motor_speed(right_distance) 27 | self.robot.set_left(right_speed) 28 | # Wait a little 29 | sleep(0.05) 30 | 31 | bot = Robot() 32 | behavior = ObstacleAvoidingBehavior(bot) 33 | behavior.run() 34 | -------------------------------------------------------------------------------- /chapter8/test_distance_sensors.py: -------------------------------------------------------------------------------- 1 | import time 2 | from gpiozero import DistanceSensor 3 | 4 | print("Prepare GPIO Pins") 5 | sensor_l = DistanceSensor(echo=17, trigger=27, queue_len=2) 6 | sensor_r = DistanceSensor(echo=5, trigger=6, queue_len=2) 7 | 8 | while True: 9 | print("Left: {l:.2f}, Right: {r:.2f}".format( 10 | l=sensor_l.distance * 100, 11 | r=sensor_r.distance * 100)) 12 | time.sleep(0.1) 13 | -------------------------------------------------------------------------------- /chapter9/avoid_behavior.py: -------------------------------------------------------------------------------- 1 | from robot import Robot 2 | from time import sleep 3 | 4 | class ObstacleAvoidingBehavior: 5 | """Simple obstacle avoiding""" 6 | def __init__(self, the_robot): 7 | self.robot = the_robot 8 | self.speed = 60 9 | # Calculations for the LEDs 10 | self.led_half = int(self.robot.leds.count/2) 11 | print(self.led_half) 12 | self.sense_colour = 255, 0, 0 13 | 14 | def distance_to_led_bar(self, distance): 15 | # Invert so closer means more LED's. 16 | inverted = max(0, 1.0 - distance) 17 | led_bar = int(round(inverted * self.led_half)) + 1 18 | return led_bar 19 | 20 | def display_state(self, left_distance, right_distance): 21 | # Clear first 22 | self.robot.leds.clear() 23 | # Left side 24 | led_bar = self.distance_to_led_bar(left_distance) 25 | # print(led_bar) 26 | self.robot.leds.set_range(range(led_bar), self.sense_colour) 27 | # Right side 28 | led_bar = self.distance_to_led_bar(right_distance) 29 | # Bit trickier - must go from below the leds count, to the leds count. 30 | start = (self.robot.leds.count - 1) - led_bar 31 | self.robot.leds.set_range(range(start, self.robot.leds.count - 1), self.sense_colour) 32 | # Now show this display 33 | self.robot.leds.show() 34 | 35 | def get_speeds(self, nearest_distance): 36 | if nearest_distance >= 1.0: 37 | nearest_speed = self.speed 38 | furthest_speed = self.speed 39 | delay = 100 40 | elif nearest_distance > 0.5: 41 | nearest_speed = self.speed 42 | furthest_speed = self.speed * 0.8 43 | delay = 100 44 | elif nearest_distance > 0.2: 45 | nearest_speed = self.speed 46 | furthest_speed = self.speed * 0.6 47 | delay = 100 48 | elif nearest_distance > 0.1: 49 | nearest_speed = -self.speed * 0.4 50 | furthest_speed = -self.speed 51 | delay = 100 52 | else: # collison 53 | nearest_speed = -self.speed 54 | furthest_speed = -self.speed 55 | delay = 250 56 | return nearest_speed, furthest_speed, delay 57 | 58 | def run(self): 59 | while True: 60 | # Get the sensor readings in meters 61 | left_distance = self.robot.left_distance_sensor.distance 62 | right_distance = self.robot.right_distance_sensor.distance 63 | # Display this 64 | self.display_state(left_distance, right_distance) 65 | # Get speeds for motors from distances 66 | nearest_speed, furthest_speed, delay = self.get_speeds(min(left_distance, right_distance)) 67 | print(f"Distances: l {left_distance:.2f}, r {right_distance:.2f}. Speeds: n: {nearest_speed}, f: {furthest_speed}. Delay: {delay}") 68 | # and drive 69 | # Send this to the motors 70 | if left_distance < right_distance: 71 | self.robot.set_left(nearest_speed) 72 | self.robot.set_right(furthest_speed) 73 | else: 74 | self.robot.set_right(nearest_speed) 75 | self.robot.set_left(furthest_speed) 76 | # Wait our delay time 77 | sleep(delay * 0.001) 78 | 79 | 80 | bot = Robot() 81 | behavior = ObstacleAvoidingBehavior(bot) 82 | behavior.run() 83 | 84 | -------------------------------------------------------------------------------- /chapter9/avoid_with_rainbows.py: -------------------------------------------------------------------------------- 1 | from robot import Robot 2 | from time import sleep 3 | from led_rainbow import show_rainbow 4 | 5 | 6 | class ObstacleAvoidingBehavior: 7 | """Simple obstacle avoiding""" 8 | def __init__(self, the_robot): 9 | self.robot = the_robot 10 | self.speed = 60 11 | # Calculations for the LEDs 12 | self.led_half = int(self.robot.leds.count/2) 13 | self.sense_colour = 255, 0, 0 14 | 15 | def distance_to_led_bar(self, distance): 16 | # Invert so closer means more LED's. 17 | inverted = max(0, 1.0 - distance) 18 | led_bar = int(round(inverted * self.led_half)) + 1 19 | return led_bar 20 | 21 | def display_state(self, left_distance, right_distance): 22 | # Clear first 23 | self.robot.leds.clear() 24 | # Left side 25 | led_bar = self.distance_to_led_bar(left_distance) 26 | show_rainbow(self.robot.leds, range(led_bar)) 27 | # Right side 28 | led_bar = self.distance_to_led_bar(right_distance) 29 | # Bit trickier - must go from below the leds count, to the leds count. 30 | start = (self.robot.leds.count - 1) - (led_bar) 31 | right_range = range(self.robot.leds.count - 1, start, -1) 32 | show_rainbow(self.robot.leds, right_range) 33 | # Now show this display 34 | self.robot.leds.show() 35 | 36 | def get_speeds(self, nearest_distance): 37 | if nearest_distance >= 1.0: 38 | nearest_speed = self.speed 39 | furthest_speed = self.speed 40 | delay = 100 41 | elif nearest_distance > 0.5: 42 | nearest_speed = self.speed 43 | furthest_speed = self.speed * 0.8 44 | delay = 100 45 | elif nearest_distance > 0.2: 46 | nearest_speed = self.speed 47 | furthest_speed = self.speed * 0.6 48 | delay = 100 49 | elif nearest_distance > 0.1: 50 | nearest_speed = -self.speed * 0.4 51 | furthest_speed = -self.speed 52 | delay = 100 53 | else: # collison 54 | nearest_speed = -self.speed 55 | furthest_speed = -self.speed 56 | delay = 250 57 | return nearest_speed, furthest_speed, delay 58 | 59 | def run(self): 60 | while True: 61 | # Get the sensor readings in meters 62 | left_distance = self.robot.left_distance_sensor.distance 63 | right_distance = self.robot.right_distance_sensor.distance 64 | # Display this 65 | self.display_state(left_distance, right_distance) 66 | # Get speeds for motors from distances 67 | nearest_speed, furthest_speed, delay = self.get_speeds(min(left_distance, right_distance)) 68 | print(f"Distances: l {left_distance:.2f}, r {right_distance:.2f}. Speeds: n: {nearest_speed}, f: {furthest_speed}. Delay: {delay}") 69 | # and drive 70 | # Send this to the motors 71 | if left_distance < right_distance: 72 | self.robot.set_left(nearest_speed) 73 | self.robot.set_right(furthest_speed) 74 | else: 75 | self.robot.set_right(nearest_speed) 76 | self.robot.set_left(furthest_speed) 77 | # Wait our delay time 78 | sleep(delay * 0.001) 79 | 80 | 81 | bot = Robot() 82 | behavior = ObstacleAvoidingBehavior(bot) 83 | behavior.run() 84 | -------------------------------------------------------------------------------- /chapter9/diagrams/generate_colour_wheel.py: -------------------------------------------------------------------------------- 1 | from matplotlib import pyplot as plt, colors, cm 2 | import numpy as np 3 | 4 | hue = np.arange(0, 2* np.pi, 1/360) 5 | hue_array = np.ones_like(hue) 6 | 7 | hsv_map = plt.get_cmap('hsv') 8 | norm = colors.Normalize(0, 2*np.pi) 9 | 10 | tick_units = np.pi/3 11 | 12 | fig = plt.figure(figsize=(5, 5)) 13 | # rgb_color = colors.hsv_to_rgb(hue) 14 | ax = plt.subplot(polar=True) 15 | 16 | theta_ticks = np.arange(0, 6, 1) * tick_units 17 | ax.xaxis.set_ticks(theta_ticks) 18 | ax.xaxis.set_ticklabels( 19 | ['0\N{DEGREE SIGN}\nred', 20 | '60\N{DEGREE SIGN}\nyellow', 21 | '120\N{DEGREE SIGN}\ngreen', 22 | '180\N{DEGREE SIGN}\ncyan', 23 | '240\N{DEGREE SIGN}\nblue', 24 | '300\N{DEGREE SIGN}\npurple'], fontsize=16 25 | ) 26 | ax.tick_params(pad=-70, rotation='auto', grid_alpha=0.3) 27 | 28 | for n in theta_ticks: 29 | ax.plot([n, n], [1.0, 1.1], linewidth=4, color=[0, 0, 0]) 30 | 31 | ax.plot(hue, np.full_like(hue, 0.7), c=[0,0,0]) 32 | ax.scatter(hue, hue_array, c=hue, s=80, cmap=hsv_map) 33 | ax.set_rlim([0.5, 1.1]) 34 | ax.set_yticks([]) 35 | ax.set_theta_zero_location('N') 36 | ax.set_theta_direction(-1) 37 | plt.show() 38 | 39 | -------------------------------------------------------------------------------- /chapter9/led_rainbow.py: -------------------------------------------------------------------------------- 1 | 2 | import colorsys 3 | 4 | 5 | def show_rainbow(leds, led_range): 6 | led_range = list(led_range) 7 | hue_step = 1.0 / len(led_range) 8 | for index, led_address in enumerate(led_range): 9 | hue = hue_step * index 10 | rgb = colorsys.hsv_to_rgb(hue, 1.0, 0.6) 11 | rgb = [int(c*255) for c in rgb] 12 | leds.set_one(led_address, rgb) 13 | -------------------------------------------------------------------------------- /chapter9/leds_led_shim.py: -------------------------------------------------------------------------------- 1 | import ledshim 2 | 3 | 4 | class Leds: 5 | @property 6 | def count(self): 7 | return ledshim.width 8 | 9 | def set_one(self, led_number, color): 10 | ledshim.set_pixel(led_number, *color) 11 | 12 | def set_range(self, led_range, color): 13 | for pixel in led_range: 14 | ledshim.set_pixel(pixel, *color) 15 | 16 | def set_all(self, color): 17 | ledshim.set_all(*color) 18 | 19 | def clear(self): 20 | ledshim.clear() 21 | 22 | def show(self): 23 | ledshim.show() 24 | -------------------------------------------------------------------------------- /chapter9/leds_test.py: -------------------------------------------------------------------------------- 1 | from robot import Robot 2 | from time import sleep 3 | 4 | bot = Robot() 5 | red = (255, 0, 0) 6 | blue = (0, 0, 255) 7 | 8 | while True: 9 | print("red") 10 | bot.leds.set_all(red) 11 | bot.leds.show() 12 | sleep(0.5) 13 | print("blue") 14 | bot.leds.set_all(blue) 15 | bot.leds.show() 16 | sleep(0.5) 17 | -------------------------------------------------------------------------------- /chapter9/robot.py: -------------------------------------------------------------------------------- 1 | from Raspi_MotorHAT import Raspi_MotorHAT 2 | from gpiozero import DistanceSensor 3 | 4 | import atexit 5 | import leds_led_shim 6 | 7 | 8 | class Robot: 9 | def __init__(self, motorhat_addr=0x6f): 10 | # Setup the motorhat with the passed in address 11 | self._mh = Raspi_MotorHAT(addr=motorhat_addr) 12 | 13 | # get local variable for each motor 14 | self.left_motor = self._mh.getMotor(1) 15 | self.right_motor = self._mh.getMotor(2) 16 | 17 | # Setup The Distance Sensors 18 | self.left_distance_sensor = DistanceSensor(echo=17, trigger=27, queue_len=2) 19 | self.right_distance_sensor = DistanceSensor(echo=5, trigger=6, queue_len=2) 20 | 21 | # Setup the Leds 22 | self.leds = leds_led_shim.Leds() 23 | # ensure the motors get stopped when the code exits 24 | atexit.register(self.stop_all) 25 | 26 | def convert_speed(self, speed): 27 | # Choose the running mode 28 | mode = Raspi_MotorHAT.RELEASE 29 | if speed > 0: 30 | mode = Raspi_MotorHAT.FORWARD 31 | elif speed < 0: 32 | mode = Raspi_MotorHAT.BACKWARD 33 | 34 | # Scale the speed 35 | output_speed = (abs(speed) * 255) // 100 36 | return mode, int(output_speed) 37 | 38 | def set_left(self, speed): 39 | mode, output_speed = self.convert_speed(speed) 40 | self.left_motor.setSpeed(output_speed) 41 | self.left_motor.run(mode) 42 | 43 | def set_right(self, speed): 44 | mode, output_speed = self.convert_speed(speed) 45 | self.right_motor.setSpeed(output_speed) 46 | self.right_motor.run(mode) 47 | 48 | def stop_motors(self): 49 | self.left_motor.run(Raspi_MotorHAT.RELEASE) 50 | self.right_motor.run(Raspi_MotorHAT.RELEASE) 51 | 52 | def stop_all(self): 53 | self.stop_motors() 54 | 55 | # Clear the display 56 | self.leds.clear() 57 | self.leds.show() 58 | -------------------------------------------------------------------------------- /chapter9/test_rainbow.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | from robot import Robot 3 | from led_rainbow import show_rainbow 4 | 5 | bot = Robot() 6 | 7 | while True: 8 | print("on") 9 | show_rainbow(bot.leds, range(bot.leds.count)) 10 | bot.leds.show() 11 | sleep(0.5) 12 | print("off") 13 | bot.leds.clear() 14 | bot.leds.show() 15 | sleep(0.5) 16 | --------------------------------------------------------------------------------