From 6b4660eb2399a8dffa20bd891a223327eccbde7f Mon Sep 17 00:00:00 2001 From: Danny Staple Date: Mon, 1 Aug 2022 09:53:21 +0100 Subject: [PATCH] Chapter 11 and 13 - wifi code (pre bluetooth) --- ch-11/1-encoder-speed/encoder_speed.py | 73 ++++++++++++ ch-11/1-encoder-speed/pio_encoder.py | 84 ++++++++++++++ ch-11/1-encoder-speed/robot.py | 52 +++++++++ ch-11/1-encoder-speed/robot_wifi.py | 25 ++++ ch-11/2-straight-line-speed/pid.py | 39 +++++++ ch-11/2-straight-line-speed/pio_encoder.py | 84 ++++++++++++++ ch-11/2-straight-line-speed/robot.py | 52 +++++++++ ch-11/2-straight-line-speed/robot_wifi.py | 25 ++++ ch-11/2-straight-line-speed/speed_control.py | 109 ++++++++++++++++++ .../all_sensors_test/all_sensors_test.py | 77 +++++++------ .../all_sensors_test/graphing.html | 2 + .../all_sensors_test/robot.py | 6 +- .../all_sensors_test/robot_wifi.py | 6 + ch-13/2-fusing-sensors/no_imu/graphing.html | 1 - ch-13/2-fusing-sensors/no_imu/no_sensors.py | 27 +++-- ch-13/2-fusing-sensors/no_imu/robot_wifi.py | 17 ++- ch-13/arena.FCStd | Bin 0 -> 20784 bytes 17 files changed, 623 insertions(+), 56 deletions(-) create mode 100644 ch-11/1-encoder-speed/encoder_speed.py create mode 100644 ch-11/1-encoder-speed/pio_encoder.py create mode 100755 ch-11/1-encoder-speed/robot.py create mode 100644 ch-11/1-encoder-speed/robot_wifi.py create mode 100644 ch-11/2-straight-line-speed/pid.py create mode 100644 ch-11/2-straight-line-speed/pio_encoder.py create mode 100755 ch-11/2-straight-line-speed/robot.py create mode 100644 ch-11/2-straight-line-speed/robot_wifi.py create mode 100644 ch-11/2-straight-line-speed/speed_control.py create mode 100644 ch-13/arena.FCStd diff --git a/ch-11/1-encoder-speed/encoder_speed.py b/ch-11/1-encoder-speed/encoder_speed.py new file mode 100644 index 0000000..d355258 --- /dev/null +++ b/ch-11/1-encoder-speed/encoder_speed.py @@ -0,0 +1,73 @@ +import time +import json + +from adafruit_esp32spi import adafruit_esp32spi_wsgiserver +from adafruit_wsgi.wsgi_app import WSGIApp + +import robot +import robot_wifi + + +class SpeedCountApp: + def __init__(self): + self.intended_speed = 0.9 + self.last_time = time.monotonic() + self.wifi = None + self.server = None + + def setup_wifi(self, app): + print("Setting up wifi.") + self.wifi, esp = robot_wifi.connect_to_wifi() + self.server = adafruit_esp32spi_wsgiserver.WSGIServer(80, application=app) + adafruit_esp32spi_wsgiserver.set_interface(esp) + print("Starting server") + + self.server.start() + ip_int = ".".join(str(int(n)) for n in esp.ip_address) + print(f"IP Address is {ip_int}") + + def index(self, request): + new_time = time.monotonic() + time_delta = new_time - self.last_time + self.last_time = new_time + + left_speed = robot.left_encoder.get_speed(time_delta) + right_speed = robot.right_encoder.get_speed(time_delta) + return ( + 200, + [("Content-Type", "application/json")], + [ + json.dumps( + { + "left_speed": left_speed, + "right_speed": right_speed, + "time": self.last_time, + } + ) + ], + ) + + def main_loop(self): + robot.set_left(self.intended_speed) + robot.set_right(self.intended_speed) + while True: + try: + self.server.update_poll() + except RuntimeError as e: + print(f"Server poll error: {type(e)}, {e}") + print(f"Resetting ESP...") + self.wifi.reset() + print("Reset complete.") + + def start(self): + app = WSGIApp() + app.route("/")(self.index) + print("Starting") + try: + self.setup_wifi(app) + self.main_loop() + finally: + robot.stop() + + +SpeedCountApp().start() diff --git a/ch-11/1-encoder-speed/pio_encoder.py b/ch-11/1-encoder-speed/pio_encoder.py new file mode 100644 index 0000000..b8ff3bc --- /dev/null +++ b/ch-11/1-encoder-speed/pio_encoder.py @@ -0,0 +1,84 @@ +import rp2pio +import adafruit_pioasm +import array + +program = """ +; use the osr for count +; input pins c1 c2 + + set y, 0 ; clear y + mov osr, y ; and clear osr +read: + ; x will be the old value + ; y the new values + mov x, y ; store old Y in x + in null, 32 ; Clear ISR - using y + in pins, 2 ; read two pins into y + mov y, isr + jmp x!=y, different ; Jump if its different + jmp read ; otherwise loop back to read + +different: + ; x has old value, y has new. + ; extract the upper bit of X. + in x, 31 ; get bit 31 - old p1 (remember which direction it came in) + in null, 31 ; keep only 1 bit + mov x, isr ; put this back in x + jmp !x, c1_old_zero + +c1_old_not_zero: + jmp pin, count_up + jmp count_down + +c1_old_zero: + jmp pin, count_down + ; fall through +count_up: + ; for a clockwise move - we'll add 1 by inverting + mov x, ~ osr ; store inverted OSR on x + jmp x--, fake ; use jump to take off 1 +fake: + mov x, ~ x ; invert back + jmp send +count_down: + ; for a clockwise move, just take one off + mov x, osr ; store osr in x + jmp x--, send ; dec and send +send: + ; send x. + mov isr, x ; send it + push noblock ; put ISR into input FIFO + mov osr, x ; put X back in OSR + jmp read ; loop back +""" + +assembled = adafruit_pioasm.assemble(program) + + +class QuadratureEncoder: + def __init__(self, first_pin, second_pin, reversed=False): + """Encoder with 2 pins. Must use sequential pins on the board""" + self.sm = rp2pio.StateMachine( + assembled, + frequency=0, + first_in_pin=first_pin, + jmp_pin=second_pin, + in_pin_count=2, + ) + self.reversed = reversed + self._buffer = array.array("i", [0]) + self.previous_reading = 0 + + def read(self): + while self.sm.in_waiting: + self.sm.readinto(self._buffer) + if self.reversed: + return -self._buffer[0] + else: + return self._buffer[0] + + def get_speed(self, delta_time): + new_read = self.read() + distance = new_read - self.previous_reading + self.previous_reading = new_read + return distance / delta_time diff --git a/ch-11/1-encoder-speed/robot.py b/ch-11/1-encoder-speed/robot.py new file mode 100755 index 0000000..2fc5e08 --- /dev/null +++ b/ch-11/1-encoder-speed/robot.py @@ -0,0 +1,52 @@ +import board +import pwmio +import pio_encoder +import busio +import adafruit_vl53l1x + + +motor_A1 = pwmio.PWMOut(board.GP17) +motor_A2 = pwmio.PWMOut(board.GP16) +motor_B1 = pwmio.PWMOut(board.GP18) +motor_B2 = pwmio.PWMOut(board.GP19) + +right_motor = motor_A1, motor_A2 +left_motor = motor_B1, motor_B2 + +right_encoder = pio_encoder.QuadratureEncoder(board.GP20, board.GP21, reversed=True) +left_encoder = pio_encoder.QuadratureEncoder(board.GP26, board.GP27) + +i2c0 = busio.I2C(sda=board.GP0, scl=board.GP1) +i2c1 = busio.I2C(sda=board.GP2, scl=board.GP3) + +left_distance = adafruit_vl53l1x.VL53L1X(i2c0) +right_distance = adafruit_vl53l1x.VL53L1X(i2c1) + + +def stop(): + motor_A1.duty_cycle = 0 + motor_A2.duty_cycle = 0 + motor_B1.duty_cycle = 0 + motor_B2.duty_cycle = 0 + + +def set_speed(motor, speed): + # Swap motor pins if we reverse the speed + if speed < 0: + direction = motor[1], motor[0] + speed = -speed + else: + direction = motor + speed = min(speed, 1) # limit to 1.0 + max_speed = 2 ** 16 - 1 + + direction[0].duty_cycle = int(max_speed * speed) + direction[1].duty_cycle = 0 + + +def set_left(speed): + set_speed(left_motor, speed) + + +def set_right(speed): + set_speed(right_motor, speed) diff --git a/ch-11/1-encoder-speed/robot_wifi.py b/ch-11/1-encoder-speed/robot_wifi.py new file mode 100644 index 0000000..7093914 --- /dev/null +++ b/ch-11/1-encoder-speed/robot_wifi.py @@ -0,0 +1,25 @@ +import board +import busio +from digitalio import DigitalInOut +from adafruit_esp32spi import adafruit_esp32spi +from adafruit_esp32spi import adafruit_esp32spi_wifimanager + +try: + from secrets import secrets +except ImportError: + print("WiFi secrets are kept in secrets.py, please add them there!") + raise + + +def connect_to_wifi(): + esp32_cs = DigitalInOut(board.GP10) + esp32_ready = DigitalInOut(board.GP9) + esp32_reset = DigitalInOut(board.GP8) + + spi = busio.SPI(board.GP14, MOSI=board.GP11, MISO=board.GP12) + esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) + esp.reset() + wifi = adafruit_esp32spi_wifimanager.ESPSPI_WiFiManager(esp, secrets) + wifi.connect() + + return wifi, esp diff --git a/ch-11/2-straight-line-speed/pid.py b/ch-11/2-straight-line-speed/pid.py new file mode 100644 index 0000000..e1e7264 --- /dev/null +++ b/ch-11/2-straight-line-speed/pid.py @@ -0,0 +1,39 @@ +class PID: + def __init__(self, proportional_k, integral_k, differential_k, set_point): + self.proportional_k = proportional_k + self.integral_k = integral_k + self.differential_k = differential_k + self.set_point = set_point + + self.error_sum = 0 + self.last_value = 0 + self.min_output = -1 + self.max_output = 1 + + self.dead_zone = 0.3 + + def update(self, measurement, time_delta): + error_value = measurement - self.set_point + proportional = error_value * self.proportional_k + + # calculate integral + self.error_sum += error_value * time_delta + # clamp it + self.error_sum = min(self.max_output, self.error_sum) + self.error_sum = max(self.min_output, self.error_sum) + + integral = self.error_sum * self.integral_k + + differentiated_error = (error_value - self.last_value) / time_delta + differential = differentiated_error * self.differential_k + self.last_value = error_value + + output = proportional + integral + differential + # clamp output + if abs(output) < self.dead_zone: + output = 0 + else: + output = min(self.max_output, output) + output = max(self.min_output, output) + + return output diff --git a/ch-11/2-straight-line-speed/pio_encoder.py b/ch-11/2-straight-line-speed/pio_encoder.py new file mode 100644 index 0000000..b8ff3bc --- /dev/null +++ b/ch-11/2-straight-line-speed/pio_encoder.py @@ -0,0 +1,84 @@ +import rp2pio +import adafruit_pioasm +import array + +program = """ +; use the osr for count +; input pins c1 c2 + + set y, 0 ; clear y + mov osr, y ; and clear osr +read: + ; x will be the old value + ; y the new values + mov x, y ; store old Y in x + in null, 32 ; Clear ISR - using y + in pins, 2 ; read two pins into y + mov y, isr + jmp x!=y, different ; Jump if its different + jmp read ; otherwise loop back to read + +different: + ; x has old value, y has new. + ; extract the upper bit of X. + in x, 31 ; get bit 31 - old p1 (remember which direction it came in) + in null, 31 ; keep only 1 bit + mov x, isr ; put this back in x + jmp !x, c1_old_zero + +c1_old_not_zero: + jmp pin, count_up + jmp count_down + +c1_old_zero: + jmp pin, count_down + ; fall through +count_up: + ; for a clockwise move - we'll add 1 by inverting + mov x, ~ osr ; store inverted OSR on x + jmp x--, fake ; use jump to take off 1 +fake: + mov x, ~ x ; invert back + jmp send +count_down: + ; for a clockwise move, just take one off + mov x, osr ; store osr in x + jmp x--, send ; dec and send +send: + ; send x. + mov isr, x ; send it + push noblock ; put ISR into input FIFO + mov osr, x ; put X back in OSR + jmp read ; loop back +""" + +assembled = adafruit_pioasm.assemble(program) + + +class QuadratureEncoder: + def __init__(self, first_pin, second_pin, reversed=False): + """Encoder with 2 pins. Must use sequential pins on the board""" + self.sm = rp2pio.StateMachine( + assembled, + frequency=0, + first_in_pin=first_pin, + jmp_pin=second_pin, + in_pin_count=2, + ) + self.reversed = reversed + self._buffer = array.array("i", [0]) + self.previous_reading = 0 + + def read(self): + while self.sm.in_waiting: + self.sm.readinto(self._buffer) + if self.reversed: + return -self._buffer[0] + else: + return self._buffer[0] + + def get_speed(self, delta_time): + new_read = self.read() + distance = new_read - self.previous_reading + self.previous_reading = new_read + return distance / delta_time diff --git a/ch-11/2-straight-line-speed/robot.py b/ch-11/2-straight-line-speed/robot.py new file mode 100755 index 0000000..2fc5e08 --- /dev/null +++ b/ch-11/2-straight-line-speed/robot.py @@ -0,0 +1,52 @@ +import board +import pwmio +import pio_encoder +import busio +import adafruit_vl53l1x + + +motor_A1 = pwmio.PWMOut(board.GP17) +motor_A2 = pwmio.PWMOut(board.GP16) +motor_B1 = pwmio.PWMOut(board.GP18) +motor_B2 = pwmio.PWMOut(board.GP19) + +right_motor = motor_A1, motor_A2 +left_motor = motor_B1, motor_B2 + +right_encoder = pio_encoder.QuadratureEncoder(board.GP20, board.GP21, reversed=True) +left_encoder = pio_encoder.QuadratureEncoder(board.GP26, board.GP27) + +i2c0 = busio.I2C(sda=board.GP0, scl=board.GP1) +i2c1 = busio.I2C(sda=board.GP2, scl=board.GP3) + +left_distance = adafruit_vl53l1x.VL53L1X(i2c0) +right_distance = adafruit_vl53l1x.VL53L1X(i2c1) + + +def stop(): + motor_A1.duty_cycle = 0 + motor_A2.duty_cycle = 0 + motor_B1.duty_cycle = 0 + motor_B2.duty_cycle = 0 + + +def set_speed(motor, speed): + # Swap motor pins if we reverse the speed + if speed < 0: + direction = motor[1], motor[0] + speed = -speed + else: + direction = motor + speed = min(speed, 1) # limit to 1.0 + max_speed = 2 ** 16 - 1 + + direction[0].duty_cycle = int(max_speed * speed) + direction[1].duty_cycle = 0 + + +def set_left(speed): + set_speed(left_motor, speed) + + +def set_right(speed): + set_speed(right_motor, speed) diff --git a/ch-11/2-straight-line-speed/robot_wifi.py b/ch-11/2-straight-line-speed/robot_wifi.py new file mode 100644 index 0000000..7093914 --- /dev/null +++ b/ch-11/2-straight-line-speed/robot_wifi.py @@ -0,0 +1,25 @@ +import board +import busio +from digitalio import DigitalInOut +from adafruit_esp32spi import adafruit_esp32spi +from adafruit_esp32spi import adafruit_esp32spi_wifimanager + +try: + from secrets import secrets +except ImportError: + print("WiFi secrets are kept in secrets.py, please add them there!") + raise + + +def connect_to_wifi(): + esp32_cs = DigitalInOut(board.GP10) + esp32_ready = DigitalInOut(board.GP9) + esp32_reset = DigitalInOut(board.GP8) + + spi = busio.SPI(board.GP14, MOSI=board.GP11, MISO=board.GP12) + esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) + esp.reset() + wifi = adafruit_esp32spi_wifimanager.ESPSPI_WiFiManager(esp, secrets) + wifi.connect() + + return wifi, esp diff --git a/ch-11/2-straight-line-speed/speed_control.py b/ch-11/2-straight-line-speed/speed_control.py new file mode 100644 index 0000000..edc4920 --- /dev/null +++ b/ch-11/2-straight-line-speed/speed_control.py @@ -0,0 +1,109 @@ +import time +import json + +from adafruit_esp32spi import adafruit_esp32spi_wsgiserver +from adafruit_wsgi.wsgi_app import WSGIApp + +import robot +import robot_wifi +import pid + + +class SpeedControlApp: + def __init__(self): + self.wifi = None + self.server = None + + self.intended_speed = 0.9 + self.last_time = time.monotonic() + self.speed_to_encoder_factor = 1/5100 + + self.left_speed_pid = pid.PID(0, -0.7, 0, self.intended_speed) + self.right_speed_pid = pid.PID(0, -0.7, 0, self.intended_speed) + self.left_pid_output = 0 + self.right_pid_output = 0 + self.left_speed = 0 + self.right_speed = 0 + + + def setup_wifi(self, app): + print("Setting up wifi.") + self.wifi, esp = robot_wifi.connect_to_wifi() + self.server = adafruit_esp32spi_wsgiserver.WSGIServer(80, application=app) + adafruit_esp32spi_wsgiserver.set_interface(esp) + print("Starting server") + + self.server.start() + ip_int = ".".join(str(int(n)) for n in esp.ip_address) + print(f"IP Address is {ip_int}") + + def update(self): + new_time = time.monotonic() + time_delta = new_time - self.last_time + self.last_time = new_time + + self.left_speed = robot.left_encoder.get_speed(time_delta) * self.speed_to_encoder_factor + self.right_speed = robot.right_encoder.get_speed(time_delta) * self.speed_to_encoder_factor + + self.left_pid_output = self.left_speed_pid.update(self.left_speed, time_delta) + self.right_pid_output = self.right_speed_pid.update(self.right_speed, time_delta) + + # print({ + # "left_speed": self.left_speed, + # "left_pid": self.left_pid_output, + # "right_speed": self.right_speed, + # "right_pid": self.right_pid_output, + # "time": self.last_time, + # "error_sum": self.left_speed_pid.error_sum + # }) + + # robot.set_left(self.left_pid_output) + # robot.set_right(self.right_pid_output) + + def movement_generator(self): + while True: + self.update() + data = json.dumps( + { + "left_speed": self.left_speed, + "left_pid": self.left_pid_output, + "right_speed": self.right_speed, + "right_pid": self.right_pid_output, + "time": self.last_time, + } + ) + "/n" + print(data) + yield data + + def index(self, request): + return ( + 200, + [("Content-Type", "application/json")], + self.movement_generator(), + ) + + def main_loop(self): + while True: + try: + self.update() + + # time.sleep(0.1) + self.server.update_poll() + except RuntimeError as e: + print(f"Server poll error: {type(e)}, {e}") + print(f"Resetting ESP...") + self.wifi.reset() + print("Reset complete.") + + def start(self): + app = WSGIApp() + app.route("/")(self.index) + print("Starting") + try: + self.setup_wifi(app) + self.main_loop() + finally: + robot.stop() + + +SpeedControlApp().start() diff --git a/ch-13/2-fusing-sensors/all_sensors_test/all_sensors_test.py b/ch-13/2-fusing-sensors/all_sensors_test/all_sensors_test.py index 71ef93c..c2b1288 100644 --- a/ch-13/2-fusing-sensors/all_sensors_test/all_sensors_test.py +++ b/ch-13/2-fusing-sensors/all_sensors_test/all_sensors_test.py @@ -9,22 +9,28 @@ import robot_wifi import pid -class FollowObject: +class AllSensorsApp: def __init__(self): - self.max_speed = 0.7 - self.follow_pid = pid.PID(0.1, 0.1, 0.015, 15) self.wifi = None self.server = None - self.last_time = time.monotonic() + self.intended_speed = 0.9 + self.speed_to_encoder_factor = 1/5000 + self.left_speed_pid = pid.PID(0, -0.7, 0, self.intended_speed) + self.right_speed_pid = pid.PID(0, -0.7, 0, self.intended_speed) + self.start_time = time.monotonic() + self.last_time = self.start_time + self.left_dist = 0 self.right_dist = 0 - self.pid_output = 0 + self.left_pid_output = 0 + self.right_pid_output = 0 self.left_speed = 0 self.right_speed = 0 - def setup_robot(self): - robot.left_distance.distance_mode = 1 + # def setup_robot(self): + # robot.left_distance.distance_mode = 1 + # robot.right_distance.distance_mode = 1 def setup_wifi(self, app): print("Setting up wifi.") @@ -46,14 +52,14 @@ class FollowObject: [ json.dumps( { - "last_value": self.follow_pid.last_value, - "pid_output": self.pid_output, + "left_speed_pid": self.left_pid_output, + "right_speed_pid": self.right_pid_output, "imu_z": imu_data[2], "left_speed": self.left_speed, "right_speed": self.right_speed, "left_distance": self.left_dist, "right_distance": self.right_dist, - "time": self.last_time, + "time": self.last_time - self.start_time, } ) ], @@ -65,33 +71,32 @@ class FollowObject: return 200, [("Content-Type", "text/html")], [fd.read()] def movement_update(self): - # do we have data - if robot.left_distance.data_ready: - self.left_dist = robot.left_distance.distance - self.right_dist = robot.right_distance.distance + # calculate time delta + new_time = time.monotonic() + time_delta = new_time - self.last_time + self.last_time = new_time - # calculate time delta - new_time = time.monotonic() - time_delta = new_time - self.last_time - self.last_time = new_time + self.left_speed = robot.left_encoder.get_speed(time_delta) * self.speed_to_encoder_factor + self.right_speed = robot.right_encoder.get_speed(time_delta) * self.speed_to_encoder_factor - # get speeds from pid - self.pid_output = self.follow_pid.update(self.left_dist, time_delta) - speed = self.pid_output * self.max_speed + self.left_pid_output = self.left_speed_pid.update(self.left_speed, time_delta) + self.right_pid_output = self.right_speed_pid.update(self.right_speed, time_delta) + + # robot.set_left(self.left_pid_output) + # robot.set_right(self.right_pid_output) - self.left_speed = robot.left_encoder.get_speed(time_delta) - self.right_speed = robot.right_encoder.get_speed(time_delta) + # # do we have data + # if robot.left_distance.data_ready: + # self.left_dist = robot.left_distance.distance + # self.right_dist = robot.right_distance.distance - # make movements - robot.set_left(speed) - robot.set_right(speed) - - # reset and loop - robot.left_distance.clear_interrupt() - robot.right_distance.clear_interrupt() + # # reset and loop + # robot.left_distance.clear_interrupt() + # robot.right_distance.clear_interrupt() def main_loop(self): - robot.left_distance.start_ranging() + # robot.left_distance.start_ranging() + # robot.right_distance.start_ranging() while True: try: self.movement_update() @@ -109,13 +114,15 @@ class FollowObject: app.route("/data")(self.data) print("Starting") try: - self.setup_robot() + # self.setup_robot() self.setup_wifi(app) self.main_loop() finally: robot.stop() - robot.left_distance.clear_interrupt() - robot.left_distance.stop_ranging() + # robot.left_distance.clear_interrupt() + # robot.left_distance.stop_ranging() + # robot.right_distance.clear_interrupt() + # robot.right_distance.stop_ranging() -FollowObject().start() +AllSensorsApp().start() diff --git a/ch-13/2-fusing-sensors/all_sensors_test/graphing.html b/ch-13/2-fusing-sensors/all_sensors_test/graphing.html index 5eef60c..35faa57 100644 --- a/ch-13/2-fusing-sensors/all_sensors_test/graphing.html +++ b/ch-13/2-fusing-sensors/all_sensors_test/graphing.html @@ -42,6 +42,8 @@ marks: [ Plot.line(current_dataset, {x: "time", y: "left_speed", stroke: "red"}), Plot.line(current_dataset, {x: "time", y: "right_speed", stroke: "blue"}), + Plot.line(current_dataset, {x: "time", y: "left_speed_pid", stroke: "orange"}), + Plot.line(current_dataset, {x: "time", y: "right_speed_pid", stroke: "green"}), ] }); var graphNode = document.getElementById("speed"); diff --git a/ch-13/2-fusing-sensors/all_sensors_test/robot.py b/ch-13/2-fusing-sensors/all_sensors_test/robot.py index 52ab332..95160c8 100755 --- a/ch-13/2-fusing-sensors/all_sensors_test/robot.py +++ b/ch-13/2-fusing-sensors/all_sensors_test/robot.py @@ -18,10 +18,10 @@ right_encoder = pio_encoder.QuadratureEncoder(board.GP20, board.GP21, reversed=T left_encoder = pio_encoder.QuadratureEncoder(board.GP26, board.GP27) i2c0 = busio.I2C(sda=board.GP0, scl=board.GP1) -i2c1 = busio.I2C(sda=board.GP2, scl=board.GP3) +# i2c1 = busio.I2C(sda=board.GP2, scl=board.GP3) -right_distance = adafruit_vl53l1x.VL53L1X(i2c0) -left_distance = adafruit_vl53l1x.VL53L1X(i2c1) +# right_distance = adafruit_vl53l1x.VL53L1X(i2c0) +# left_distance = adafruit_vl53l1x.VL53L1X(i2c1) imu = adafruit_bno055.BNO055_I2C(i2c0) diff --git a/ch-13/2-fusing-sensors/all_sensors_test/robot_wifi.py b/ch-13/2-fusing-sensors/all_sensors_test/robot_wifi.py index 7093914..a69d6ef 100644 --- a/ch-13/2-fusing-sensors/all_sensors_test/robot_wifi.py +++ b/ch-13/2-fusing-sensors/all_sensors_test/robot_wifi.py @@ -16,10 +16,16 @@ def connect_to_wifi(): esp32_ready = DigitalInOut(board.GP9) esp32_reset = DigitalInOut(board.GP8) + status_led = DigitalInOut(board.LED) + status_led.switch_to_output() + + spi = busio.SPI(board.GP14, MOSI=board.GP11, MISO=board.GP12) esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) esp.reset() wifi = adafruit_esp32spi_wifimanager.ESPSPI_WiFiManager(esp, secrets) wifi.connect() + status_led.value = 1 + return wifi, esp diff --git a/ch-13/2-fusing-sensors/no_imu/graphing.html b/ch-13/2-fusing-sensors/no_imu/graphing.html index 29d5e37..1cb2946 100644 --- a/ch-13/2-fusing-sensors/no_imu/graphing.html +++ b/ch-13/2-fusing-sensors/no_imu/graphing.html @@ -39,5 +39,4 @@ console.log(error.name === 'AbortError'); } } - // } diff --git a/ch-13/2-fusing-sensors/no_imu/no_sensors.py b/ch-13/2-fusing-sensors/no_imu/no_sensors.py index 63cca37..8fcd4b9 100644 --- a/ch-13/2-fusing-sensors/no_imu/no_sensors.py +++ b/ch-13/2-fusing-sensors/no_imu/no_sensors.py @@ -19,21 +19,20 @@ class RandomWalkSensors: self.last_time = self.start_time # self.app = None - def setup_wifi(self, app): - # self.app = app - print("Setting up wifi.") - self.wifi, esp = robot_wifi.connect_to_wifi() + def setup_server(self, esp, app): self.server = adafruit_esp32spi_wsgiserver.WSGIServer(80, application=app) adafruit_esp32spi_wsgiserver.set_interface(esp) print("Starting server") - self.server.start() - ip_int = ".".join(str(int(n)) for n in esp.ip_address) - print(f"IP Address is {ip_int}") def reconnect(self): + print(f"{time.monotonic()} Resetting esp... ") + self.wifi.reset() + print(f"{time.monotonic()} Reconnecting wifi... ") self.wifi.connect() + print(f"{time.monotonic()} starting server...") self.server.start() + print(f"{time.monotonic()} started server...") def data(self, request): @@ -74,17 +73,21 @@ class RandomWalkSensors: self.server.update_poll() except RuntimeError as e: traceback.print_exception(BaseException, e, e.__traceback__) - self.wifi.reset() - self.reconnect() - print("Reset complete.") + if not "Failed to send" in str(e): + self.reconnect() def start(self): app = WSGIApp() app.route("/")(self.index) app.route("/data")(self.data) print("Starting") - self.setup_wifi(app) - self.main_loop() + self.wifi, esp, spi = robot_wifi.connect_to_wifi() + try: + self.setup_server(esp, app) + self.main_loop() + finally: + spi.unlock() + spi.deinit() RandomWalkSensors().start() diff --git a/ch-13/2-fusing-sensors/no_imu/robot_wifi.py b/ch-13/2-fusing-sensors/no_imu/robot_wifi.py index a69d6ef..b32ecb3 100644 --- a/ch-13/2-fusing-sensors/no_imu/robot_wifi.py +++ b/ch-13/2-fusing-sensors/no_imu/robot_wifi.py @@ -18,14 +18,21 @@ def connect_to_wifi(): status_led = DigitalInOut(board.LED) status_led.switch_to_output() - - + print("Setting up wifi.") spi = busio.SPI(board.GP14, MOSI=board.GP11, MISO=board.GP12) - esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) - esp.reset() + print("SPI Configure") + esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset, + baudrate=300*1000 + ) + esp.ready_timeout = 1 + + print("Firmware vers.", esp.firmware_version) + wifi = adafruit_esp32spi_wifimanager.ESPSPI_WiFiManager(esp, secrets) wifi.connect() + ip_int = ".".join(str(int(n)) for n in esp.ip_address) + print(f"IP Address is {ip_int}") status_led.value = 1 - return wifi, esp + return wifi, esp, spi diff --git a/ch-13/arena.FCStd b/ch-13/arena.FCStd new file mode 100644 index 0000000000000000000000000000000000000000..7a7dc182dab86cd83c44af24cd2745399550eca9 GIT binary patch literal 20784 zcmZs?bC74f7Vq7fHl}Ucwrx(^*0gP#)5foD+qP}nJ#G8;bIyCK?sMwhRXf>9t^ASH zH*0;elPE}of}sEb0YL&)sNX1Sc3ipQ-~a)|WdH-ge%FfF8aZ2=*f`O-SzBG`>^cHQ z;=i)G1Li}UU7gJ^{{ZdQKP4d#ziB5`bk-(FKc0;RlUmis#X;wCY;}KnV1R?OfCd6- zASIM}PlK4IT6na1dbGW{zE5Y%|2hoCrs+uYy^UN_^HtRLT_K(t!Uw`V~kMsL_ zJHFdOK-ebU^*C>5W>K2e-`tKq^nWR#Rh9FE?0VTdxjnzXZ*+%}4^17ccAifMKpgs@ zQOMW#sPN!_dN~NWNp^4hI@$s0#Oo%W-{ij?c!dwoU_U07>p%YFkRK&&tS--X(yFI9 z-1fce`#PaK-dd2g`Q53vO>upNu{OC$aK90JYtwY!pO9sGp9tu=LC1???_vwXk4@hg zvere}c6X5XDB0iMtV}>SJO2&hcq%hOw%ct>Zs%2QG2F)vZCCuK(bJX~Kkvs{6JNJ;eyMnweE|Zs z?Rm1b-}tkI(w2O+Y*H zw4l_<&kwGk*{!|Vtt#%d*qU_EX@mNEakDIPbZw;}b(qo@&rrLB_&*>@clp>{GE|M9 zN9V(VvA{ z8a_{}+?SWTK}=T17}&3ld$J%P*QL!JA21>>FV|vR*o!zH1}w{0f18$~-0jo0OLftE zPO^It7;-jux~orz;TmwGb+Km8u{3U1?`LfzH)tAywY6UPeo&Ynhz|u%E^NS^5?IHp zD0orX?LmY4FZU}%Ka%+DJ~rWoV^@2NZS&o{O(5-;jenB){?xOT_w|Sj5cHz<+?lR} z=<;drOy}|mk#=PMcr`mE-o}W9gnQ%w9c7cD7Erimc@VpWq9J3M>_yJJ0zh%bh z%=KA5_RQOflJy_-yc=j#4ZL#-)CO}%TG&r$IdT=-Xrx@lCFxQMb+fAHLl35H5IAV# zQYrfw<$3AifRKUWVJu=qokfvbD8-;Kzr0$cQMG$mjT6!7g5T}erN*4-G2tJH6wz}Q zky26W%0(iyh)I-!VA}$}#2cVQ!FtI`50JB<5RohbO-b}#kTk`y24Eup^G?6kjb~S^GGD*gT zu5Nh(-&SUQD!(I39E|ZBBt^NOSo_VJf+k-x8}7V_0vzCPU52YNwrE;z^iOTu~!-%wVw)7n3vWcZ-F2Rq8UY+&~^Wy9&c#Ldnjf2 zYQh_62f3>-)r%^wEq2=Vg&81>Cs);D>xN9T`94J1Iqldd(jI(Jt0MZL#h^8n@MfH5 zP&K_dj>jd2i%&-DNFIM0?e-Y{m1a)@D>97_b-cMW5xB8#?#EtFYR=ds&T$N*rZSHM z>rC7Ymp5HD+IpvDwwu*2c*vxmJta~+SWXh&CiJ9UTnmMWVe`o2KfIv!Jda5m7oJ-jb;=CQTW)i+Cn5``WLKMCQZ7hvf4w zi3kg|(n819+w@0_9>-8;6UvWjJhh$TqbrqSRuDBx!3GF!l9to+eeEH}cvTh@Iy@l5 zlfPMB;)CsU*j0T^c72A9wN}|zooAeba?}_`9VdP~lbSuJWbEqoU1M%88(*A%d$18n zW2hov;S=<(NyN<);6psam_L=6{OjE?N6xKn3@7HGmIsl|*Owio=ymxvbrEwo4kn1+ zUN}UN1%1t$ytIQbKW%y3lYhoRSg#3nx7MpK*Iy7`el%GTi6!DlzgLNjLzT?4u=E)F zba(-6d8j7a)O$`3W?i&8&u1a_@O)O6O*d_Thov0kt?1%>0u;g84FJ_(A%B%DI90zLUbG*ZrX`BLNiG!4Rj9HEExp7Nin80Jat@X*twy zx!1jDP<~%g78>GASE{UI-JqZ8{*78%_c7<;6wkZUJ9s3n-!3zpt5dxyUR7quK^LcR z=$3cc-iCE-CUj!2M+jRiOKc6_tD;#5>fl8+q1#?7<>|1#7rRKGU{%!?_Z6YTA9@%$ z>~Ddm%T7ACD+8v(@2pI*%K+ zJoNa%-Un`C!#vU1&cwb2kVtFSVM^&XNn}ddTH5Q@YIbyFLNO-&b(D8blNMPOIKiv% zCd_?@P(T5>X`qK4O{2BAn9Irp(FEPq+lkgTY4>yqy-6}D9I<(U)ZU?8&}rX(nO@uO zAs6w3e*cv()3)hQpW>QEt(MwF{T6AuIk7{F%pE3%q^#y>3D4~!{h(1f42r2Tt|(f@ zD69@-DXzoPw-LX_jnxjJB-t9?X}&IWM?v5c%#OJRL@|O$8Y4oVdlR+;_NXjm@KS1v z#9mNp>mt6RV&bKaStU78PHHQ}@UvN*x)1z zUu_QdHf24cD3ExmagX8pd1D8hY8omt6Dq_m!3VidB<^E^i(Ew(XoEFEv8$t7ujMb5 zLuun^WWb>Fo3XKgs`+v>tQ$l)kHLd#oy20;n1R zQTOP<0V29nk) z-Gal=up8F=To0zWz^zfeOi>VfSOo;d4kQN#y{EEZ1z9l4^{F4>h-Owu<&_ZYPg$Vd z9>Yh^&akO?>id@yHN7Z#PK6LasNkBvDug1z7g_q9yCFZ~wEPJ^iPw-r(WyIA0LeRnp<$x*CJ!E{3~WiyE< z&_sSyK`0934W!5+B+u}zBqP`y#L=w%wS9sD>j|h}oJe#Z*T1K2ZTNxMc)-M1iaoLJ zr%s)833d=-oeeXH+8k-EP5oTqcX4h86G}L(+4D^4XCBnFW$FIx)(g;&EsKt&!+kdv z_a^`K!-qh*#TDh#x&-{)#k$hEVnIH`WNnCilmWpVW^kJeXi@WPoQ zG9PidVbP1F!h5s~Mzz9c4M!pt1=86tdMCy)CME%#>6M{5Ha|MbVuB`kx#m4}pPNiD z-cPLY=8YSjNsDS5x~x}(Wtp9$(L1~uX5P|E&z8({63mZe{x~GMFH3P{DU9$3*h)`g zcre}|0K3qO&O&K#w>|)&+w9#2ek}&-NAu#!(v8jjyH+DlYv^EQ=eZOjuLnY1UQ}gf>)0lt^xkwh_G% z9R}zDiDuz*AlwX?(LCP85d$TnMS*7dIDym=RcK$n6b3^Wp(~F=!Sx%aJXpH1Iu-U0 zCVH1L9d`qB57S~2DqDj_$r!OCqk99={7Y!M7+W9i$>)rxkQUPCOA!>w^19Q}0(@4K z(*;7O7oTPeWlh0EwM?iS`vUF0ZDqvK_Hj|5X7?oT@+17|6RgdoInZ;1Zx^q9I1dTg z{54KsXIi^%@psMQQENp1NPMO4ql6}Ey-FMnb)u@*7O*`jrgT}a3rA!*Ng)xe&UNp^ zd)8crcd!>3pt)MTipWsg{;HC`LS07+btMMc3pa#{aYbu~lCr3)ov=*B-6@FGpnScj z@}SJQ*&oNAgx#OKGa5^!x?fqet=c^%zRY$zSoy9=ISwFoJKuHptCrn>S6YgT&nT4Z zww)}ic=@`xJ7((l@D1e!1uXljY*xI>0zlJ;R z*{1qOR{YUR7vR{ToqQ3m@bGtNo0Ug9d7~~wgYKxk3`~T4oeIQCnZ#3L&#$@E_#qVB z3~0p%3Lxbn!o(nK*A>df7Y?w}XRe&*9b)uTK7sC2z}e3kU90zLfnb{PVAOUsoY<+$ zUt+QszZR4cYjO8g`P z8!m?nX*1PmBcQQ`4W1Y|ZS}K7Ff$48`=o6RN1*sGCL2}eSo=+1#cDMmY=M@hbQWt0Kri^+> zcUrmgvh$Cr1>Ez_A<;$Foje$g0as@VH*A|jmHfO*}Q;?Gl;H|Ik zrTYTcXwkIFC7agf=c2T9V^c)@=xcV*2E$Xh?4+!yKhUW3MV~mQYdb&m;)7W!**PyP zLc=w{%co0P-P9xRtlqX^>}su3-^#pq;|v@QjV5jIxk1~?WPcEHA)?bvwump`Y;fNW z(Cs#;sYvWkKY^RpDad|(zP`Prca`3(lUCwaCn>;6{c?9l~8bjksj`m9cAs7}@^>dLLae)Rjul-d8&`sRnQ0ADmZ*4ahMTtdDyb< zGGOyA2~IoN@}f8OPe#n(7kbxjTS?!eg_)t zs)3H&rcx)e5H%&7g3#82N?k}e&N-6%&`vVxqQ_$itGlQ#BC}Zg!x+G-MxSx6e_7L% z?pu;^j&ZM|?WQTDm^N zpRQHbyr^vhpWYv=PuP@IOhPw3xIYucrJBbT_L%9nRKTh6q%AlB?Sjt^xZqh|e{4Xg zvY=3^wx~iDmZZiX*eLXYC0uXGHb=8fe>IU?2KUOs#B@AwL-5OY5T;&92 z7PYBfxn@?wes|Y)*g+$FEll(QC9|h{+N>;ouTSH*Su`tSP*49SY@=GB8Vi{`3 z@@P>n|HeCLBynf-T|Ug+OzJqz68#V01-DC5x~t85B}}Rk&0@QZ9g|Q>$P^ujE^EZ2 z>p+Zp<~)Ylix|H95=aZ)!xPq68LyUA!B@s{)~MJKUN)W#!fxrzN^|=bTyN6tYVD;a z>^-3l*mjX{A$CWdzd4bu#qR=w0u~#_snffJD1HF!xn^RVD1Dn{KGE>iC0^d3Z_$6+ zu`D)-rL!?b=l(dNWb-W_)obitLs0L=;(%2iXM6pwU&a%x!lpz)S@rohY(9DDFCc7c-0hL97HC47Q2_fH-|>&&wZO=-ZA- z7XRwcAIC-L0(xc#l0sp|mnhXvum@>JNj5X0kVqZEE6XDEFten@T5Y}bix)`)62LEY z#o&=ikUB1ha}vOUPUy6{6m=x&ua@A!R6A;@Dqy*~f2fTygh4y{u9qQsngRV{v14U7 zR$3U66_L=R@o{u08g&zOzl>L5MxKQruUT$n(u9El33!d&uX?f{-C28|84#Bs2EtkQ zpNw!{*dL*;+Fx#dde?f{mh-+&MNiGsTO?jIGLrOO)tf`VV{>Cb(?;J$G!&{GkZ%XT zx|k>XTdE68b?C3g9KQC&N6-?JKm&V+v8gE>x-#VS;;+b*9fX#}(z!5h>RZoI(8G&T zr0+(5v1z%6RP`pA4n(o|^e}G*{7FyYmp_vcGwDj?*uX*naux<|PeG|jRLArpK~x^= z>vX~YQx2!*3rg^I6gh()AUiJ9@unN*dguI$)TeyFmQ}rwYMfeIYJ)MHd#D6vYVfME zUlE|>&8;Kd>$Dxep-s?s%k9c(k@%sxRM3qJW^vT$55pRHEaY+8qlylsU@ zjgaAYhpbpNcpoQVaw-_8&Zf$Iu76xk=3H_aXwnsiB_Oi*6bf_L6rbRwZqW#Hwn5nO zPA;$c6l&T98^}{%z9Lk80nvSy6@yMsIeB37dF)^DWl&zj%(9)A*;c-5gR)XKyR-{* zF_iD%d_LoFruozBy9ru~neExvO6Lq1pt6kOZ*bCea{<4V?e*7m&L?_&6-!#nArUT+s*2vaL<79k< zD0~zAAh>FnNNsM$3AM>GX#`xYgc|$J0HdNRfO}gB64}(mcS~O8g3sab-q*DG3?hl> zCGkrcEDL>oU3^IX)b48waXJ8K3P`*%foWGLJLShqzMB6#Wr@R<^5E@K2F({JU;a2L zh)P8(|F))@ba@WH3}30kbL&=7C)Ei8L`ciYNF1sGvAyqzX$^3qSbbcly!S>ncpei~ zdX|>*cCbK3gO!)mNp^!%RM2NH6uxYMWP>SM!MS1gWPo+xCt@?Vw~J*is`4NLF{y2p>O~vy zX&s1hJ}Ln>o%=j*WMap~O*Eg@8>n|CxWFgY4*&%@yE~F$H8Hs>IU4)K4kTbaQL1fV9 zsS14|{kI|4Yg)j~w;8IIfY6r+%Xk0652EA91}hiTg{>+4s}Uj@cwyS$3-;m@@-n^O zANngloy+6M&>qc=U*8O6RS7*3<|`6CNZQPUHxwi^F1|%QLuj%C{~9z)pJePOJuLb= z`Jjklp;l8^SV8%RzkzyxuLADrnLwmz2d433!3NZc z?nds3D>Nf9nK#`Va1#**-p1t6==Xx|5ECvGAWTJK6ObzBxY$v$jPS~xpVge3ka$=M z{}H4ydD_ahi|FmcrT9Uunn9fgmF^-!@fn5amMD_5#=?zjm#f8*-Hj6zxJr6NUh{~N zr825WA1-l1nUy{Gt&2XpICp8|$>G%j-fS)ezjh6hjG4}c*&{GzLh)M6oQ&d#oB$H( z3+^%vL>b*XoJLBYHOi8lkIBMh1wkaLFyQqxpa!zm$1&7nV|Ae)DiVF>ZKk!0BAZpT ze$WB^8MMLBNM#63xp~w;LjKSNN#K4i5VYnCU|-S=1WMz{ig|pY3$(KV`dX`$4LOpb-B{7)_A-SF!qeRo(EIj z7{SG?c>tzRPxAf1 zeZL9@4o<(!4eU(l3?1x1f&Ohg8J(Lo`fc5e1_T8E{~9y?V|}46YrD<>+j*|`+@k78 zFy_n`BRZ!Q3MVm>8Qm}r0%ji<3-a+qd`J>V>`_E=&;(`;)XA3vLM&$AW%SC8p^X6)E4S0^x9hscgDWuDEIgN%|M&Q#3A zc&4M+;Dbbkr{Ru;Aem~WD_tnb)Xi7f*F^y)l3bP{onxufaVG>zj@ANik2leIA*HO8 zBg8&(zD&Tv<`Jj@X%jb_U&B0s=t9sma4PJizByh`a>CKCfV7%Gy)){ZZGT{l9yb=Y z!&31T2NvVbV%&E8ha*Q7+eplatoeth3&ZBBrt~t&H-5gtJItou$Ns=8EkKYSx-&;HKwg>NF!!Q39(|eU>YO6mzx1J%QKZ{@ zjJaJ+E=OW1utyh3fe=q{dB0cOofLTjE{0l>Jy(B@n?`-)y)7)02IRV#g~rjf$$!Btez2b8Q!waleEW|DC%q-PqO zf78s$Xf-357tRS3%LHmcqjb24%*MoFnOii6-5{{$XTA;Nc(!0#XNhWDm2>bD106m+ zyfr67=G-4n?((Vimv;M3s2p7sR6k-?(kBdTo%cn(=n3g}((Ans+;ud^AErw<0%?sB zm2H+=RHBr5zY-9ZTAziqT~cld5q?7aTlB<^KU{G^fq;gw|3~yp{}Fwfj_n!)O32j* zwO4CWYea&;PF=vIQcBcwD;C_&!mBx z#CaT*Sr(zW9!1sMtqNlbRJ4KTa(DbW6&~*}yG9%_2-q4sV3*N@&J|>L4wwXQARM2k(;@D4ON-8y~ zC~GE>Yw9Xi%poK*;PiCzl7X4}@>qKpCAIl!5EN2M`9B&@rKB~tmJxfW@`NfDTHzm| z|H2x_Pt45iT1K{56$|LC*N3^*8{0qEt$Qn*8{{Y?X>gcm@33-w>VRe%rvHI5V8gaz zA0r~ryl=R}{`W+J(!Tl-1_lBO1^=Ik#Qe`hYE;(w9ms&<-Q7(T3!pnJsyu&HZ?MJ2 zrr30>$wVDM;fMZsYG19GA*-X92n!kTylj2SfZQd6XP-?bJu84t(piz?sSUl4>l`hL z=ZTNzId-m+a#Ja_GFM^-W-)d1WXxy)F1rqb8O5hiL7l(*_cBAlZ7 zsr4)_L{Lj*m$8(XAj2a;cb_jq3I zp?np`lVETp&iCU1mWYsCW6E65za#eO?=gh8KrD;!Bz^h9yG*CDLr($SSzkL{sRKIm zN8|XHz7pm94rK>onz)0FU3gwcYANvt)S9=TA2S`^n{Eg_%VMwK{~m7n^uDN|?_lkN z{6E9Z@*lsYCTqJU@UwHedc_F4ZT504W*0-y!*Wq7=fDb4>d(&@29dGutBEzQ0dBHD zowYJ562PLV``yuGC%F|=QE5k=gmnp!xNPLydQv)T^>xbU0 zMN_YAF1UWLE})s-L^D(1xz=R{R)PEEYYJ15=Bsanvyi3>_ft$t%j7dxolMYOaB~yv zgUv~k)A#D#0tqj3Ux7i?7`NR!PnSC$UB9qKN6qnY1i$(ovujPoL z&~Gy9%Y)Y^`&LO5OX*U+ovxYQIZ-+^l7n7O>^X~G@A!oZ%K)b2aa%RvfkJan=c?EEJ(1UF+mUf&`Z2$1QIQG0&(WIj8zLIHTDnQABo9VHPM?U)jALy?($IsL&do%tq%_lY zw&YeL;T4bj@n|=(+`t}lXDMCm4H~SboBH1X!9K(smx5LoaQAyVQoL;D`wY1G+Dn%m z!c}bpRI}6&MUG#0Ex|`@q?7ij=2;E%84F}ExktLzp8Lx|EW;0pM`6k#6US|&zSt}X z-EVqAc7}MGTspZ+}EK;j&oQVfoOhTw3>v zO7fFtJT4_lKDk9dn`5Aro6{d{QP5=cpC3^F)+@@blw;JlUUj~Yxcv(*S^o={+uwSH z^?B0U9j^Ra)-F9aGYq{}EDHcT%OW15Kq5l*m-@K+t=AWOM7KaqtG8e$JAwW3`56!GcYd{&);- zTZDT4TAFg|QQ)4>?zK>)<7XyUd>+xb!s4qxWZ!Mj8qtbA#J{@l5G z+rORDulLAW3pH$~twC_oeaZ#px^g>eq^lFJQb~71bh$@~DC+f0=}{ReTF`eqz@Hx8 zHqsrF2t4?7K5_V(6A*UbkiIr#?5p;=VH!`F{i7++_|=M2eYEP(q@ipS6*6o3Kn{?B zk(rvo8_*v_7v`aU*>$HXK=!aePGHL*z$_l5rH`fCX-`Z}jM6vdS}sI!S4iD^4>Lm1 zI+)I*kfl#pGm0F#E=BQI{*mbh6HnO)sk2J{Qk3nE|=g1Z<1z zcHYx1EQiv7e9{!F8~y&7B}v35JP4 z@KA3@bZieZB|DZePqo8iG0a&%iRU$iyM8oQ3FRnv&Nwxyl*Ux-+<(JncggRT6=)}R z>B*&=I~1Ow3cNd!MiI|Wc`5qlkR7Vkptpc#uOCu%R?R(j(KUB3-qj|}xdi|?or zU&W(Q-lJ2%)Vh`P_ZiOYM*9m<4L%LJdZ&NYwt zc)Fk11$A)F3DoH2<4-j&bXNHJ3H@)~&H@E@Qh)Pd_5boA+dsNJ{2xBd{D1k7DI!Q> zP`vQ-t#kN@nQdo{a$S8fWQ62+|G_Z^ImP+vb+dho7eA5yumPw02I3{y7t7~pJMysO zXQ!b?ha6gh!%$aJzhQX7j}*)VnJuzVex4f)MECiws8wNuCEO4%8TQJaz`M7%ct?=} z4mE9wEN$2H+^A1vN4LdM;F&1ttpUApzc#_9!cfLU9qgW}F7?ISz`w4VP7(oS1T7}y zt70PFv3P7~ZRIcL61=o*#pb6GyOEaoGVyD}6+`g&cC;0EUYy&E7BFgjUEsu|u@4su z8?}7mQv-#j8?I!00Zp#YF18`WwqOpCWm6U)C=bSaF!Gh`-W`y&m77xX1sDs6$ERqISagaKsC9 z6O4)Mu}RrQe$f)6;Cj$Fzt193UugqKa(xOacHflOr}I8rnpocQO>bP>U(NZ_?#LM~ z#`MInZk9d(hp2`_DYwGVMaGaeCb}87>7!l05>T0clDB%3s*A?DV)0-OoDVg^9y3`Q zi0sfHo^%@%P2Y1Pae0$mHLD+&Mx&pfZ7U(rDOmc^3EY-}$mh&m(xp(xN{+p`UPJHx z*6~3z;4?EOhiuLZR?6l=6A=BKUF znXX}SxZY#Jla*l(!YHJcOq1dghJ`Y}YT$I>G!8LGyx#AX!8;P%7m{3AsBmZeDETu9GEd36`20pV^D$Oq_PFV z;`Ep+lb|O2!2~9@f1uQ#k$CdYeVa~3dJkEMUlP%OxKkYJ49K8XBZjW5Y+@5boev{~ zYUn*WptPs~|0^k6qms^rlVfiN{Rm!jXl- z*CL%+e55h_N}_qpcBPJ000)OwISEHz`|;Rz!;g8AI15K=(jYt%{S|o)Mkm5W=4(p zQ^9az^L!Z@Eq=(Xuvo&(vRFc<-#VpUP?Px-qiXrF%p>=?M`Ke)0~uL$byQtIb}0TB zXgkA<=+sA%rHlC0 zLfGFI#dKgHs`DCHldf$ByeS8V3OEB+xR{RwIbxK^! z#&QoQAH9jCJqexbV?-(3Jv`dIoG&rcrhe~FQhGPleBI|5t(FEMibml(ezFvCxu z?(RLjW+t_!1QfT59M81|$(%>dyeJ|B%N?3^{Lf!Ltt%`m6}m-_ zjN5XKe}OC2eyhXinztS$u@C0wtK8a~HRo26@Z|!ZySaW?eUX-hmMI!F#ggLrcu>fW zLZav5SqVR<(dwG5GGQs5rrhC=-&khRnE^y!f|eQ9DtrnPEMi7~R0ZqT^kS1{syy$J zUKXgXke*ockW1OapI^_JgMM^UW(jMpVNY?Pw3?6Ur{+$huD&|IG%ZUOrY&PQZ@;_JLy3lpiQTZ0?it3zi?Vy8D>+3FG}|RSQ*!*eZVBHh8V{JqAaEnX zBT#Us=8T*qe~#y_os7mtPRC4>~v=_C(e=6dRWoC~Biefe! z1RA5cK{N-D}B zF!55F%n5boHIxrPAF6v!4Y70&L#ev7g00a&J5Ic@$#c= z1lm32^Yed}H*z$eiecYkmHA&>378) z;c=i!)|40Uiq986-(TwO{d#qnTClhFDLhAyR`22PC)nSaJ|p<&7nxN0osznyZZ(?x z@K-r>X350!d;MCMuJlTou__Jwf^F#v?KU@?s-?PgY z9!rBPJJ?ZLj^>$OJG-x9BBcW#Zk-LYZztJ?`DWZ31uXt0jo*!{B(Ayeu-3kmyjn>LJ(MfNt4(QI>)^`loKR{6iWYndLhN zpV>bZDZXV*P`KOs6KAtSqsqZV4cxNB2;qWTpcWH0{pIF#X+>2aIxQ# zrT9m(+0=FS=eO3?`(p_aLnW@4(?+A7)Jp>d!n6U)H~NV@#q$wY7B~Vzu zI>CV96#mPJKc3`kGV7X(EQlQYscr$+#=G*M3Rt9eGqpNVinD9|)R=Aj+QB^C8Dald zviOAL@wm#zLsaZ*tINh;_DzyM>W7XJO@@*dgYtx_^PjVnb`0YmCE1&&3QAD?ZF2vS zY}~aCa-VE0R57dHwYn2Q&~26j8G!dW@q2w8tz}e{u_TMe{JqmBtNW1V>l z`Brig3Bi_W4770htyu|}@Dz_Xy?@6vwCRZ3HUuD`0!3h;?+i%X+2Y@~cB~x%5=fs} zdcCJ*bb3on1oubvb%p9%e|1UC)c!hVcu5*qkW7k!K>xA6Sp0ax-ZCc8$1n{NQ+g8~ zV}xV{-Q?Z;;MqLx=A%cSBd)`>!U}Ab`sG7SXk;ZkB9wXV0_$@#TDI*NVD zj-4C1D}j}^zlel+Ngp5hhs9Or{O|q!>1in^DS6bxD6)rn`lC#GdQ$_7HHkssd)tD} zE$*c0-Q*}#tDzI(sVR@R^amYEZZ1o~w)64r_jyk}vfEHsTasJH%P(spNh3#-mOwG; zchC3KT>9CZo2`rr?Bd?yy4bNgC8vP3ur3NDf;WfdR}$~Wd4V~FzXxBJ3?6Mwlj1yr zj}orVr$jB-jriS}&l*a0zu5WzaA$eykrIC0nlqV?z@6T%ihfFQ1`btr3e}DWJ>lWT zc2RlrkIs|0TE|tlFIhkJLuY3Wiek1^@nzX|)gnebu2Is<8`OIl3kt=>Lrm+zbCePr z57HR7V{p#Onj7QVSYP21(i^sfIGk;LV8igY!JFV>)h`M)i<`Fz7+nQh1+nuqRMgj> z7&|P1u#yZfZlY=|SD@a}f3Ei}=$tC*RgA=!cA9F*eGcf=rM$&T6&oeT?-QfOmfmj;P~QOW*r3 zn=~+pxjlgBQW4zsX)G5u2EIg{JCOo@qw3Of8yx>>&|)6)S3(8JCodu^%If2ENXPRi zsM(6C99~bZMjGWX(&EdFK-V{maxeOKf}<4A{?35;E{JdN-;_vZzN2j!&!{RZR~zWU zMSLqBOeALSz^^f%0m-+GQ!0n*Wec+ws_p!NNfz|-{m~Tab#dbf?b{WYMk0gXD6MI? zjuW!v%Nw}1XSY#otLqj)U+-JLizER3^lV)$|Qhr%_j ztF)#-jHvsP~r|x+dc613UbnWS`Gj>+f3F zB<+-IK^j642Yi3kyX`_@lIxB6mp9&;eovM4sLF zLZtSc$F3$cOTHMoR21xiP1OBj1??Id$rdn4dR?N@;~o902)7=xNXkQt{n-N~DXWgJ z#o~$wf%eBdun?d~O>V3l)qVA12cDl!9YU#3)cZx3OE6RJR5S>a*W(-p14*T|%VEZ! z9W>$wuxV0TOyXzWMP*Bd=^orqh~rS z0JKb{(}QJI#W!vGf3GkHCu_p6%B|vWx8hXU7FDB@Cfyb}%WM{uRtu=-4kJc+b#%Xv zHrU_}Qp_EWy=unAaK31kN~KU!*$V6BN~r;??lB+g@)-G*%c4B0lsPe-MK7BqC4~3G z>we-dBApc`k+{3lQA=S4p;b|Pi}7lDtYJ9CBAp%5G$}wm)LBkeFgZaz^gYq5Q0^Bj zqu!ksqH;TXn@(26E6{7N^{ZY)yxi-QJ{lB;{;p?pI^7jYl28Y17KOr?6sm0V${?gn z4J1e0xFpz?$)Y|V1I*E*cvFypzJLlZb6khWq7l?zrvru7@XJ2RFk=ZQ@IM+fm1ys1PiZw zlqDauA?zg>oK3f3I3D~UWna)hyQBPK-s?m3r%IRclG?yPN!Yn7VF*ci# zBu0ckI0)haEwX!qQ5o~L01d#GBPp^$nzGF}C4~tQ6Dg49DX3}&HisrbX0@pCX+jG) zcUO+LwID(RE#~Wrp=h}QmEBppJ1Pq}<1MBH_Lpw^;`14aCqp$-Z3UIrXeBAtY9-ZB zoLV<+7;Ft)fSTdDoB$kt$f{ZbxbL%;&YRo`VuJPKyEfox!(7!w=H?`FH}peXW80TY z-x*C+iq|j7q}Z!Vf8}{96;Ec4AYdn`#jpPjE274pt`WR?;9d;V*7^aB6X%H9MjY@5 z<)H)3#WiT+FROplb93wFd0w8;wZGqz7r(iakBuBA&KsBceLLnO1IKH$SA2KvL2PY- z9C$5&qP7i@k&1BMpCG*b2UDG0>`eIqy`o|fh3_4=+8l_@)m&OQ& zA6&dx3+AM?R>i$&tt%+w>3h}r-a|`e&)>?*Es^QJwA5J4TGr(RByzAlmJ?fZ0>KA| zX{Z{ouycqEp5wVFL57dXD!-3TqGMLI)LGFYR<$&1e?gL%PNISNH^`TDssJJRE{0fj z+O58BGI_$#?j!D442&-e<9U_4Uso%9Jc`?g45 z*-|fFO*kO@R$Di+B#@3AH>zg!25m;L*~YdT&F~Ij&}?DA+Td&X&~Ld^-TXwe!}-EDU7ds5^5nTUiSxoHiQX6iUFX*irfpYpU7>E!4yAPZXexLU^myrd8f^UoO z7aUGEFfc#ZyD>!(yGopoAChO5aU>{m?@i!-b?=REPq(el7xl9@)H`3Uj$qz*dzxYg zyh8+`3m_M6XL{Lc&p0=8&XTKoHI|nI$#aC~K!05R!{3^OnYpKsC*x4kJRM0=R|QvJ@J(HMCg7U`j%9BT)4Z!Oyk5GNuACNq02QIZ zL*>CXZ`knk;$*p`E;#QPR>tq$9FA5yIbAj4`8ECwUwa|*t5p|BN9?xf#GrXcX}A+@ zw2($V)rp?viSDVZy&!YDbAm7$=&e>~HlsS|^l!S$(@Z z;TqR9-=>CmlDpFhOC`c=-Il5bol6$y>~v)hYE=U;m+v)QwfOpt`sE8bnCZkc-ZWBpDILr zC^Y23!<2tD{$*We2PyDSLJ9|~*NFD|n!qUtGNom(61>FrW4C6*!h0ON!O8o@FK#y^ ztXj8y`Kpk&qs&_laKD;b7;9xSsz>^qQiG}1JW6!VtA};r=IPkP4SMH#wX6w3 z?*+t%S?_7!yl+B|v_jun?-cm&%!D)>BS&; zdg_}{iO3ES<^On_>|?U=tsITDdAY>xJ`3{&EL{a3caGaB0?J7cva)Ud;mcg-{*gGk ziKJb4tTI(%_Il&~VjV|j$3c`}fW~)7+c7;Fd&Od}?>`F?TS0VF!%m>95y#|}qK{s5 za82-|suBNPjyPK{J|3Y;@0nQT@gQVid{9b$nz`{tKu$khF@n01AABwaaOQ@7H0R7F zLh)><&|IFtqxJdKq;Nj8m5 zju3L~$E%#Y!2^jtpw^cL(gImQ!m!BL{);Py-Dlp?9Yq;~M6Elz80iDa28xq(?z)CZ3 z+c{BV+9jLE0}t(n!=g>%K8^9k5ZYW#!rjM9CD9;5wY|6cz7s6lu__phYFH!HzSfra zLlLG%s=>?iU9AafyGq4l&=fI$0T|$fH{23blj7$pY@2{$IN(L*G=nlMURNNiftDe^ zg10HukNRi^Q_O#0wQZ@NYtkn^$iCaA!tygMU>d}0Jt{n^J>7srx6pvPveD0@-gOlb z;Xc{n)vv#@DIXJy*_Pukq-?)FA@Ypq)Gdw?N^A-k7!dq&!_Tz9BYEg<2DsryXRAa^ z#f((iu3?T1x*a>lxebb*U8Y&^--EkM)J!{eAJ3O8W9nwt_DT!A5x=yhUXO}0 zUJV)--hLj;%i2?dMdE?Mr)bmzhf2fo0pV0l<{}I;9uEP9!fD0+>{tfq?E;YC|7U@U82*~tY1g<~5!l5n!xg+F-CSM}S{GU$F zKBmSfj^mQvXSeD4BTC_B*<^Hcvt=sPObCXb z#LVkKev1n|yC>8q*_jl%ULL`-2j{rjM(Zw=uicJHK-H?M4Bej!oR?p)D>?Mm-Sn@6 z1J^z`yR?S1|HOBC2gGICDq~kVy#M&b-*KF!zva^BtPaKMtLRk8EM!)RhX`>^BK68C{Z}ywAI`jQ69(DT5&FK+|b=~phEd@c1 z2VPn<{Uvvr3Tld8K7U@(U4ZKe&fy0MG47& zjt@;qS-&lI!@6}p6{9!|UQLMBB_!*`NjMPchWq`(3AdRHxdyE=!YYR+qUA6(GAb$x z*pZN{+>Sx<^!yomWOi~rMExtgzvwY2o}QYaM+PC+!_?5zV^BQ30EQkvJwy#7JqE?o zo5j%Mr-!Mbr^ldpdYWPBVQT1l42q{0I4nI(4PB3*Y(^?=*$*T_7ri))2KyY-W?#s!0SZ zwzfeQc4kSew~AoJJ}t?L3nQ<63GRMnkDw zT1K<2UjS==I5C+tot9GswC2LJ^;`slsAV+UT90TlYc;fHXMJ8~&4p>}R=Kr|W?Q>y zY3l%J&CWU!q4XKTg=y;@2nJcpXtwngU_A?3v$O7$S#x39dVHkJT1K<2mm!)wLp1+y zy%(W$)?Apju8~{IXtwniU>*4X)~fl?nhVp`s}T&&5JcPA*Ad#3 NxVW&;aBu5Zjo({T&;S4c literal 0 HcmV?d00001