From c06bea7dee3867d7a35b37744fcb7f8c755591e9 Mon Sep 17 00:00:00 2001 From: chrisruk Date: Thu, 2 Feb 2023 16:26:36 +0000 Subject: [PATCH 01/32] Test ordering of motor run_to_position commands (#176) * Test ordering of motor run_to_position commands * Restructure to use Future * Use Future to obtain voltage of build HAT * Support multiple non-blocking commands simultaneously * Enable 'del' to function correctly * Bump threshold temporarily * Refactor variable names --- buildhat/devices.py | 8 +++--- buildhat/hat.py | 9 ++++--- buildhat/motors.py | 26 +++++++++--------- buildhat/serinterface.py | 58 ++++++++++++++++++++++++++++------------ test/motors.py | 54 +++++++++++++++++++++++++++++++++++-- 5 files changed, 115 insertions(+), 40 deletions(-) diff --git a/buildhat/devices.py b/buildhat/devices.py index 2f14453..df94983 100644 --- a/buildhat/devices.py +++ b/buildhat/devices.py @@ -3,6 +3,7 @@ import os import sys import weakref +from concurrent.futures import Future from .exc import DeviceError from .serinterface import BuildHAT @@ -202,11 +203,10 @@ def get(self): idx = self._combimode else: raise DeviceError("Not in simple or combimode") + ftr = Future() + self._hat.portftr[self.port].append(ftr) self._write(f"port {self.port} ; selonce {idx}\r") - # wait for data - with Device._instance.portcond[self.port]: - Device._instance.portcond[self.port].wait() - return self._conn.data + return ftr.result() def mode(self, modev): """Set combimode or simple mode diff --git a/buildhat/hat.py b/buildhat/hat.py index 782a87f..395a3d6 100644 --- a/buildhat/hat.py +++ b/buildhat/hat.py @@ -1,5 +1,7 @@ """HAT handling functionality""" +from concurrent.futures import Future + from .devices import Device @@ -45,11 +47,10 @@ def get_vin(self): :return: Voltage on the input power jack :rtype: float """ + ftr = Future() + Device._instance.vinftr.append(ftr) Device._instance.write(b"vin\r") - with Device._instance.vincond: - Device._instance.vincond.wait() - - return Device._instance.vin + return ftr.result() def _set_led(self, intmode): if isinstance(intmode, int) and intmode >= -1 and intmode <= 3: diff --git a/buildhat/motors.py b/buildhat/motors.py index 2a457e4..fb18355 100644 --- a/buildhat/motors.py +++ b/buildhat/motors.py @@ -3,6 +3,7 @@ import threading import time from collections import deque +from concurrent.futures import Future from enum import Enum from threading import Condition @@ -205,9 +206,10 @@ def _run_positional_ramp(self, pos, newpos, speed): cmd = (f"port {self.port}; combi 0 {self._combi} ; select 0 ; selrate {self._interval}; " f"pid {self.port} 0 1 s4 0.0027777778 0 5 0 .1 3; " f"set ramp {pos} {newpos} {dur} 0\r") + ftr = Future() + self._hat.rampftr[self.port].append(ftr) self._write(cmd) - with self._hat.rampcond[self.port]: - self._hat.rampcond[self.port].wait() + ftr.result() if self._release: time.sleep(0.2) self.coast() @@ -228,9 +230,7 @@ def run_for_degrees(self, degrees, speed=None, blocking=True): if not (speed >= -100 and speed <= 100): raise MotorError("Invalid Speed") if not blocking: - th = threading.Thread(target=self._run_for_degrees, args=(degrees, speed)) - th.daemon = True - th.start() + self._queue((self._run_for_degrees, (degrees, speed))) else: self._run_for_degrees(degrees, speed) @@ -251,9 +251,7 @@ def run_to_position(self, degrees, speed=None, blocking=True, direction="shortes if degrees < -180 or degrees > 180: raise MotorError("Invalid angle") if not blocking: - th = threading.Thread(target=self._run_to_position, args=(degrees, speed, direction)) - th.daemon = True - th.start() + self._queue((self._run_to_position, (degrees, speed, direction))) else: self._run_to_position(degrees, speed, direction) @@ -262,9 +260,10 @@ def _run_for_seconds(self, seconds, speed): cmd = (f"port {self.port} ; combi 0 {self._combi} ; select 0 ; selrate {self._interval}; " f"pid {self.port} 0 0 s1 1 0 0.003 0.01 0 100; " f"set pulse {speed} 0.0 {seconds} 0\r") + ftr = Future() + self._hat.pulseftr[self.port].append(ftr) self._write(cmd) - with self._hat.pulsecond[self.port]: - self._hat.pulsecond[self.port].wait() + ftr.result() if self._release: self.coast() self._runmode = MotorRunmode.NONE @@ -283,9 +282,7 @@ def run_for_seconds(self, seconds, speed=None, blocking=True): if not (speed >= -100 and speed <= 100): raise MotorError("Invalid Speed") if not blocking: - th = threading.Thread(target=self._run_for_seconds, args=(seconds, speed)) - th.daemon = True - th.start() + self._queue((self._run_for_seconds, (seconds, speed))) else: self._run_for_seconds(seconds, speed) @@ -444,6 +441,9 @@ def release(self, value): raise MotorError("Must pass boolean") self._release = value + def _queue(self, cmd): + Device._instance.motorqueue[self.port].put(cmd) + class MotorPair: """Pair of motors diff --git a/buildhat/serinterface.py b/buildhat/serinterface.py index b46b3ce..d134b2b 100644 --- a/buildhat/serinterface.py +++ b/buildhat/serinterface.py @@ -84,13 +84,13 @@ def __init__(self, firmware, signature, version, device="/dev/serial0", debug=Fa self.cond = Condition() self.state = HatState.OTHER self.connections = [] - self.portcond = [] - self.pulsecond = [] - self.rampcond = [] + self.portftr = [] + self.pulseftr = [] + self.rampftr = [] + self.vinftr = [] + self.motorqueue = [] self.fin = False self.running = True - self.vincond = Condition() - self.vin = None if debug: tmp = tempfile.NamedTemporaryFile(suffix=".log", prefix="buildhat-", delete=False) logging.basicConfig(filename=tmp.name, format='%(asctime)s %(message)s', @@ -98,9 +98,10 @@ def __init__(self, firmware, signature, version, device="/dev/serial0", debug=Fa for _ in range(4): self.connections.append(Connection()) - self.portcond.append(Condition()) - self.pulsecond.append(Condition()) - self.rampcond.append(Condition()) + self.portftr.append([]) + self.pulseftr.append([]) + self.rampftr.append([]) + self.motorqueue.append(queue.Queue()) self.ser = serial.Serial(device, 115200, timeout=5) # Check if we're in the bootloader or the firmware @@ -150,6 +151,11 @@ def __init__(self, firmware, signature, version, device="/dev/serial0", debug=Fa self.cb.daemon = True self.cb.start() + for q in self.motorqueue: + ml = threading.Thread(target=self.motorloop, args=(q,)) + ml.daemon = True + ml.start() + # Drop timeout value to 1s listevt = threading.Event() self.ser.timeout = 1 @@ -266,6 +272,8 @@ def shutdown(self): self.running = False self.th.join() self.cbqueue.put(()) + for q in self.motorqueue: + q.put((None, None)) self.cb.join() turnoff = "" for p in range(4): @@ -278,6 +286,20 @@ def shutdown(self): self.write(f"{turnoff}\r".encode()) self.write(b"port 0 ; select ; port 1 ; select ; port 2 ; select ; port 3 ; select ; echo 0\r") + def motorloop(self, q): + """Event handling for non-blocking motor commands + + :param q: Queue of motor functions + """ + while self.running: + func, data = q.get() + if func is None: + break + else: + func(*data) + func = None # Necessary for 'del' to function correctly on motor object + data = None + def callbackloop(self, q): """Event handling for callbacks @@ -330,11 +352,11 @@ def loop(self, cond, uselist, q, listevt): if uselist and listevt.is_set(): count += 1 elif cmp(msg, BuildHAT.RAMPDONE): - with self.rampcond[portid]: - self.rampcond[portid].notify() + ftr = self.rampftr[portid].pop() + ftr.set_result(True) elif cmp(msg, BuildHAT.PULSEDONE): - with self.pulsecond[portid]: - self.pulsecond[portid].notify() + ftr = self.pulseftr[portid].pop() + ftr.set_result(True) if uselist and count == 4: with cond: @@ -362,11 +384,13 @@ def runit(): if callit is not None: q.put((callit, newdata)) self.connections[portid].data = newdata - with self.portcond[portid]: - self.portcond[portid].notify() + try: + ftr = self.portftr[portid].pop() + ftr.set_result(newdata) + except IndexError: + pass if len(line) >= 5 and line[1] == "." and line.endswith(" V"): vin = float(line.split(" ")[0]) - self.vin = vin - with self.vincond: - self.vincond.notify() + ftr = self.vinftr.pop() + ftr.set_result(vin) diff --git a/test/motors.py b/test/motors.py index 70f388a..b6fa87e 100644 --- a/test/motors.py +++ b/test/motors.py @@ -10,6 +10,8 @@ class TestMotor(unittest.TestCase): """Test motors""" + THRESHOLD_DISTANCE = 15 + def test_rotations(self): """Test motor rotating""" m = Motor('A') @@ -19,18 +21,66 @@ def test_rotations(self): rotated = (pos2 - pos1) / 360 self.assertLess(abs(rotated - 2), 0.5) + def test_nonblocking(self): + """Test motor nonblocking mode""" + m = Motor('A') + last = 0 + for delay in [1, 0]: + for _ in range(3): + m.run_to_position(90, blocking=False) + time.sleep(delay) + m.run_to_position(90, blocking=False) + time.sleep(delay) + m.run_to_position(90, blocking=False) + time.sleep(delay) + m.run_to_position(last, blocking=False) + time.sleep(delay) + # Wait for a bit, before reading last position + time.sleep(7) + pos1 = m.get_aposition() + diff = abs((last - pos1 + 180) % 360 - 180) + self.assertLess(diff, self.THRESHOLD_DISTANCE) + + def test_nonblocking_multiple(self): + """Test motor nonblocking mode""" + m1 = Motor('A') + m2 = Motor('B') + last = 0 + for delay in [1, 0]: + for _ in range(3): + m1.run_to_position(90, blocking=False) + m2.run_to_position(90, blocking=False) + time.sleep(delay) + m1.run_to_position(90, blocking=False) + m2.run_to_position(90, blocking=False) + time.sleep(delay) + m1.run_to_position(90, blocking=False) + m2.run_to_position(90, blocking=False) + time.sleep(delay) + m1.run_to_position(last, blocking=False) + m2.run_to_position(last, blocking=False) + time.sleep(delay) + # Wait for a bit, before reading last position + time.sleep(7) + pos1 = m1.get_aposition() + diff = abs((last - pos1 + 180) % 360 - 180) + self.assertLess(diff, self.THRESHOLD_DISTANCE) + pos2 = m2.get_aposition() + diff = abs((last - pos2 + 180) % 360 - 180) + self.assertLess(diff, self.THRESHOLD_DISTANCE) + def test_position(self): """Test motor goes to desired position""" m = Motor('A') m.run_to_position(0) pos1 = m.get_aposition() diff = abs((0 - pos1 + 180) % 360 - 180) - self.assertLess(diff, 10) + self.assertLess(diff, self.THRESHOLD_DISTANCE) m.run_to_position(180) pos1 = m.get_aposition() diff = abs((180 - pos1 + 180) % 360 - 180) - self.assertLess(diff, 10) + self.assertLess(diff, self.THRESHOLD_DISTANCE) def test_time(self): """Test motor runs for correct duration""" From 68e52938c8db54a127149f32bb360afbbe4a0c09 Mon Sep 17 00:00:00 2001 From: chrisruk Date: Thu, 2 Feb 2023 16:31:05 +0000 Subject: [PATCH 02/32] Use set rather than plimit for train light control (#188) * Use set rather than plimit for train light control * Use coast to switch off LEDs --- buildhat/light.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/buildhat/light.py b/buildhat/light.py index 7472f5c..1748e55 100644 --- a/buildhat/light.py +++ b/buildhat/light.py @@ -30,4 +30,12 @@ def brightness(self, brightness): """ if not (brightness >= 0 and brightness <= 100): raise LightError("Need brightness arg, of 0 to 100") - self._write(f"port {self.port} ; on ; plimit {brightness / 100.0}\r") + if brightness > 0: + self._write(f"port {self.port} ; on ; set {brightness / 100.0}\r") + else: + self.off() + + def off(self): + """Turn off lights""" + # Using coast to turn off DIY lights completely + self._write(f"port {self.port} ; coast\r") From a6ac1fbffcc94ec938c7e0383a4d5f5e8f7925d2 Mon Sep 17 00:00:00 2001 From: chrisruk Date: Fri, 3 Feb 2023 12:27:45 +0000 Subject: [PATCH 03/32] Ensure mixed blocking/nonblocking functions work together (#189) --- buildhat/motors.py | 9 +++++++++ buildhat/serinterface.py | 1 + test/motors.py | 14 ++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/buildhat/motors.py b/buildhat/motors.py index fb18355..9af22e7 100644 --- a/buildhat/motors.py +++ b/buildhat/motors.py @@ -232,6 +232,7 @@ def run_for_degrees(self, degrees, speed=None, blocking=True): if not blocking: self._queue((self._run_for_degrees, (degrees, speed))) else: + self._wait_for_nonblocking() self._run_for_degrees(degrees, speed) def run_to_position(self, degrees, speed=None, blocking=True, direction="shortest"): @@ -253,6 +254,7 @@ def run_to_position(self, degrees, speed=None, blocking=True, direction="shortes if not blocking: self._queue((self._run_to_position, (degrees, speed, direction))) else: + self._wait_for_nonblocking() self._run_to_position(degrees, speed, direction) def _run_for_seconds(self, seconds, speed): @@ -284,6 +286,7 @@ def run_for_seconds(self, seconds, speed=None, blocking=True): if not blocking: self._queue((self._run_for_seconds, (seconds, speed))) else: + self._wait_for_nonblocking() self._run_for_seconds(seconds, speed) def start(self, speed=None): @@ -292,6 +295,7 @@ def start(self, speed=None): :param speed: Speed ranging from -100 to 100 :raises MotorError: Occurs when invalid speed specified """ + self._wait_for_nonblocking() if self._runmode == MotorRunmode.FREE: if self._currentspeed == speed: # Already running at this speed, do nothing @@ -316,6 +320,7 @@ def start(self, speed=None): def stop(self): """Stop motor""" + self._wait_for_nonblocking() self._runmode = MotorRunmode.NONE self._currentspeed = 0 self.coast() @@ -444,6 +449,10 @@ def release(self, value): def _queue(self, cmd): Device._instance.motorqueue[self.port].put(cmd) + def _wait_for_nonblocking(self): + """Wait for nonblocking commands to finish""" + Device._instance.motorqueue[self.port].join() + class MotorPair: """Pair of motors diff --git a/buildhat/serinterface.py b/buildhat/serinterface.py index d134b2b..77d1a71 100644 --- a/buildhat/serinterface.py +++ b/buildhat/serinterface.py @@ -299,6 +299,7 @@ def motorloop(self, q): func(*data) func = None # Necessary for 'del' to function correctly on motor object data = None + q.task_done() def callbackloop(self, q): """Event handling for callbacks diff --git a/test/motors.py b/test/motors.py index b6fa87e..865b9ce 100644 --- a/test/motors.py +++ b/test/motors.py @@ -69,6 +69,20 @@ def test_nonblocking_multiple(self): diff = abs((last - pos2 + 180) % 360 - 180) self.assertLess(diff, self.THRESHOLD_DISTANCE) + def test_nonblocking_mixed(self): + """Test motor nonblocking mode mixed with blocking mode""" + m = Motor('A') + m.run_for_seconds(5, blocking=False) + m.run_for_degrees(360) + m.run_for_seconds(5, blocking=False) + m.run_to_position(180) + m.run_for_seconds(5, blocking=False) + m.run_for_seconds(5) + m.run_for_seconds(5, blocking=False) + m.start() + m.run_for_seconds(5, blocking=False) + m.stop() + def test_position(self): """Test motor goes to desired position""" m = Motor('A') From 9c1dbb97e34ff45f6e7e718ee37fee16f382378a Mon Sep 17 00:00:00 2001 From: chrisruk Date: Fri, 24 Feb 2023 12:34:24 +0000 Subject: [PATCH 04/32] Improve sensor interval (#192) * Initial rate improvements * Add test for color sensor * Add test for motors at low intervals * Reduce default interval to 10ms to give 100Hz of data readings * Add toggle to position/speed This is so that we don't end up with a busy loop, which interfers with processing serial data from another thread. I think if there wasn't a GIL this problem might not exist. * Dont need to resend combi setting --- buildhat/devices.py | 29 ++++++++++++++++--------- buildhat/motors.py | 6 ++--- buildhat/serinterface.py | 7 ++++++ test/color.py | 47 ++++++++++++++++++++++++++++++++++++++++ test/motors.py | 42 ++++++++++++++++++++++++++++++++--- 5 files changed, 115 insertions(+), 16 deletions(-) create mode 100644 test/color.py diff --git a/buildhat/devices.py b/buildhat/devices.py index df94983..e8bf086 100644 --- a/buildhat/devices.py +++ b/buildhat/devices.py @@ -58,8 +58,9 @@ def __init__(self, port): Device._setup() self._simplemode = -1 self._combimode = -1 + self._modestr = "" self._typeid = self._conn.typeid - self._interval = 100 + self._interval = 10 if ( self._typeid in Device._device_names and Device._device_names[self._typeid][0] != type(self).__name__ # noqa: W503 @@ -196,16 +197,10 @@ def get(self): :raises DeviceError: Occurs if device not in valid mode """ self.isconnected() - idx = -1 - if self._simplemode != -1: - idx = self._simplemode - elif self._combimode != -1: - idx = self._combimode - else: + if self._simplemode == -1 and self._combimode == -1: raise DeviceError("Not in simple or combimode") ftr = Future() self._hat.portftr[self.port].append(ftr) - self._write(f"port {self.port} ; selonce {idx}\r") return ftr.result() def mode(self, modev): @@ -215,18 +210,32 @@ def mode(self, modev): """ self.isconnected() if isinstance(modev, list): - self._combimode = 0 modestr = "" for t in modev: modestr += f"{t[0]} {t[1]} " - self._write(f"port {self.port} ; combi {self._combimode} {modestr}\r") + if self._simplemode == -1 and self._combimode == 0 and self._modestr == modestr: + return + self._write(f"port {self.port}; select\r") + self._combimode = 0 + self._write((f"port {self.port} ; combi {self._combimode} {modestr} ; " + f"select {self._combimode} ; " + f"selrate {self._interval}\r")) self._simplemode = -1 + self._modestr = modestr + self._conn.combimode = 0 + self._conn.simplemode = -1 else: + if self._combimode == -1 and self._simplemode == int(modev): + return # Remove combi mode if self._combimode != -1: self._write(f"port {self.port} ; combi {self._combimode}\r") + self._write(f"port {self.port}; select\r") self._combimode = -1 self._simplemode = int(modev) + self._write(f"port {self.port} ; select {int(modev)} ; selrate {self._interval}\r") + self._conn.combimode = -1 + self._conn.simplemode = int(modev) def select(self): """Request data from mode diff --git a/buildhat/motors.py b/buildhat/motors.py index 9af22e7..0027fff 100644 --- a/buildhat/motors.py +++ b/buildhat/motors.py @@ -203,7 +203,7 @@ def _run_positional_ramp(self, pos, newpos, speed): # Collapse speed range to -5 to 5 speed *= 0.05 dur = abs((newpos - pos) / speed) - cmd = (f"port {self.port}; combi 0 {self._combi} ; select 0 ; selrate {self._interval}; " + cmd = (f"port {self.port}; select 0 ; selrate {self._interval}; " f"pid {self.port} 0 1 s4 0.0027777778 0 5 0 .1 3; " f"set ramp {pos} {newpos} {dur} 0\r") ftr = Future() @@ -259,7 +259,7 @@ def run_to_position(self, degrees, speed=None, blocking=True, direction="shortes def _run_for_seconds(self, seconds, speed): self._runmode = MotorRunmode.SECONDS - cmd = (f"port {self.port} ; combi 0 {self._combi} ; select 0 ; selrate {self._interval}; " + cmd = (f"port {self.port} ; select 0 ; selrate {self._interval}; " f"pid {self.port} 0 0 s1 1 0 0.003 0.01 0 100; " f"set pulse {speed} 0.0 {seconds} 0\r") ftr = Future() @@ -311,7 +311,7 @@ def start(self, speed=None): raise MotorError("Invalid Speed") cmd = f"port {self.port} ; set {speed}\r" if self._runmode == MotorRunmode.NONE: - cmd = (f"port {self.port} ; combi 0 {self._combi} ; select 0 ; selrate {self._interval}; " + cmd = (f"port {self.port} ; select 0 ; selrate {self._interval}; " f"pid {self.port} 0 0 s1 1 0 0.003 0.01 0 100; " f"set {speed}\r") self._runmode = MotorRunmode.FREE diff --git a/buildhat/serinterface.py b/buildhat/serinterface.py index 77d1a71..af1ab1b 100644 --- a/buildhat/serinterface.py +++ b/buildhat/serinterface.py @@ -31,6 +31,8 @@ def __init__(self): self.typeid = -1 self.connected = False self.callit = None + self.simplemode = -1 + self.combimode = -1 def update(self, typeid, connected, callit=None): """Update connection information for port @@ -381,6 +383,11 @@ def runit(): else: if d != "": newdata.append(int(d)) + # Check data was for our current mode + if line[2] == "M" and self.connections[portid].simplemode != int(line[3]): + continue + elif line[2] == "C" and self.connections[portid].combimode != int(line[3]): + continue callit = self.connections[portid].callit if callit is not None: q.put((callit, newdata)) diff --git a/test/color.py b/test/color.py new file mode 100644 index 0000000..688e23c --- /dev/null +++ b/test/color.py @@ -0,0 +1,47 @@ +"""Test Color Sensor functionality""" +import time +import unittest + +from buildhat import ColorSensor + + +class TestColor(unittest.TestCase): + """Test color sensor functions""" + + def test_color_interval(self): + """Test color sensor interval""" + color = ColorSensor('A') + color.avg_reads = 1 + color.interval = 10 + count = 1000 + expected_dur = count * color.interval * 1e-3 + + start = time.time() + for _ in range(count): + color.get_ambient_light() + end = time.time() + diff = abs((end - start) - expected_dur) + self.assertLess(diff, 0.25) + + start = time.time() + for _ in range(count): + color.get_color_rgbi() + end = time.time() + diff = abs((end - start) - expected_dur) + self.assertLess(diff, 0.25) + + def test_caching(self): + """Test to make sure we're not reading cached data""" + color = ColorSensor('A') + color.avg_reads = 1 + color.interval = 1 + + for _ in range(100): + color.mode(2) + self.assertEqual(len(color.get()), 1) + color.mode(5) + self.assertEqual(len(color.get()), 4) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/motors.py b/test/motors.py index 865b9ce..fad9c65 100644 --- a/test/motors.py +++ b/test/motors.py @@ -185,22 +185,28 @@ def test_continuous_start(self): """Test starting motor for 5mins""" t = time.time() + (60 * 5) m = Motor('A') + toggle = 0 while time.time() < t: - m.start(0) + m.start(toggle) + toggle ^= 1 def test_continuous_degrees(self): """Test setting degrees for 5mins""" t = time.time() + (60 * 5) m = Motor('A') + toggle = 0 while time.time() < t: - m.run_for_degrees(0) + m.run_for_degrees(toggle) + toggle ^= 1 def test_continuous_position(self): """Test setting position of motor for 5mins""" t = time.time() + (60 * 5) m = Motor('A') + toggle = 0 while time.time() < t: - m.run_to_position(0) + m.run_to_position(toggle) + toggle ^= 1 def test_continuous_feedback(self): """Test feedback of motor for 30mins""" @@ -211,6 +217,36 @@ def test_continuous_feedback(self): while time.time() < t: _ = (m.get_speed(), m.get_position(), m.get_aposition()) + def test_interval(self): + """Test motor interval""" + m = Motor('A') + m.interval = 10 + count = 1000 + expected_dur = count * m.interval * 1e-3 + start = time.time() + for _ in range(count): + m.get_position() + end = time.time() + diff = abs((end - start) - expected_dur) + self.assertLess(diff, expected_dur * 0.1) + + def test_dual_interval(self): + """Test dual motor interval""" + m1 = Motor('A') + m2 = Motor('B') + for interval in [10, 5]: + m1.interval = interval + m2.interval = interval + count = 1000 + expected_dur = count * m1.interval * 1e-3 + start = time.time() + for _ in range(count): + m1.get_position() + m2.get_position() + end = time.time() + diff = abs((end - start) - expected_dur) + self.assertLess(diff, expected_dur * 0.1) + if __name__ == '__main__': unittest.main() From 1ac33cf50fb5849187eadff41e2e1c3f28404a16 Mon Sep 17 00:00:00 2001 From: chrisruk Date: Fri, 31 Mar 2023 12:23:56 +0100 Subject: [PATCH 05/32] New firmware support (#190) * New firmware * Changes for new PID * Remove bias test, as no longer exists * Update pwm params * Test down to 10ms interval * Lower speed to improve positioning accuracy --- buildhat/data/firmware.bin | Bin 54168 -> 54360 bytes buildhat/data/signature.bin | 2 +- buildhat/data/version | 2 +- buildhat/motors.py | 77 +++++++++++++++++++++++++++++------- test/motors.py | 17 ++------ 5 files changed, 69 insertions(+), 29 deletions(-) diff --git a/buildhat/data/firmware.bin b/buildhat/data/firmware.bin index 3d1cdb279e6e4d1e208fecabdda50aa65ce2e45d..64358ee52512bf324a5201140895c78b0a7f6556 100644 GIT binary patch delta 26705 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 H7mWV{TfuoU delta 26359 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 diff --git a/buildhat/data/signature.bin b/buildhat/data/signature.bin index 364b0d4..27ef00d 100644 --- a/buildhat/data/signature.bin +++ b/buildhat/data/signature.bin @@ -1 +1 @@ -ktCr>_iۼ*c=Y옢3#lw!HwFZ2 y49d鱾o \ No newline at end of file +Q#E]]-.T~駶e[yEV}A5L}A$I!u9Nzw`l@eK;{E \ No newline at end of file diff --git a/buildhat/data/version b/buildhat/data/version index a2e93cb..e9bf96c 100644 --- a/buildhat/data/version +++ b/buildhat/data/version @@ -1 +1 @@ -1670596313 +1674818421 diff --git a/buildhat/motors.py b/buildhat/motors.py index 0027fff..102a159 100644 --- a/buildhat/motors.py +++ b/buildhat/motors.py @@ -27,7 +27,6 @@ def __init__(self, port): self._default_speed = 20 self._currentspeed = 0 self.plimit(0.7) - self.bias(0.3) def set_default_speed(self, default_speed): """Set the default speed of the motor @@ -79,10 +78,10 @@ def bias(self, bias): :param bias: Value 0 to 1 :raises MotorError: Occurs if invalid bias value passed - """ - if not (bias >= 0 and bias <= 1): - raise MotorError("bias should be 0 to 1") - self._write(f"port {self.port} ; bias {bias}\r") + + .. deprecated:: 0.6.0 + """ # noqa: RST303 + raise MotorError("Bias no longer available") class MotorRunmode(Enum): @@ -118,7 +117,8 @@ def __init__(self, port): self._combi = "1 0 2 0 3 0" self._noapos = False self.plimit(0.7) - self.bias(0.3) + self.pwmparams(0.65, 0.01) + self._rpm = False self._release = True self._bqueue = deque(maxlen=5) self._cvqueue = Condition() @@ -126,6 +126,13 @@ def __init__(self, port): self._oldpos = None self._runmode = MotorRunmode.NONE + def set_speed_unit_rpm(self, rpm=False): + """Set whether to use RPM for speed units or not + + :param rpm: Boolean to determine whether to use RPM for units + """ + self._rpm = rpm + def set_default_speed(self, default_speed): """Set the default speed of the motor @@ -200,11 +207,13 @@ def _run_positional_ramp(self, pos, newpos, speed): :param newpos: New motor postion in decimal rotations (from preset position) :param speed: -100 to 100 """ - # Collapse speed range to -5 to 5 - speed *= 0.05 + if self._rpm: + speed = self._speed_process(speed) + else: + speed *= 0.05 # Collapse speed range to -5 to 5 dur = abs((newpos - pos) / speed) cmd = (f"port {self.port}; select 0 ; selrate {self._interval}; " - f"pid {self.port} 0 1 s4 0.0027777778 0 5 0 .1 3; " + f"pid {self.port} 0 1 s4 0.0027777778 0 5 0 .1 3 0.01; " f"set ramp {pos} {newpos} {dur} 0\r") ftr = Future() self._hat.rampftr[self.port].append(ftr) @@ -258,9 +267,14 @@ def run_to_position(self, degrees, speed=None, blocking=True, direction="shortes self._run_to_position(degrees, speed, direction) def _run_for_seconds(self, seconds, speed): + speed = self._speed_process(speed) self._runmode = MotorRunmode.SECONDS + if self._rpm: + pid = f"pid_diff {self.port} 0 5 s2 0.0027777778 1 0 2.5 0 .4 0.01; " + else: + pid = f"pid {self.port} 0 0 s1 1 0 0.003 0.01 0 100 0.01;" cmd = (f"port {self.port} ; select 0 ; selrate {self._interval}; " - f"pid {self.port} 0 0 s1 1 0 0.003 0.01 0 100; " + f"{pid}" f"set pulse {speed} 0.0 {seconds} 0\r") ftr = Future() self._hat.pulseftr[self.port].append(ftr) @@ -309,10 +323,15 @@ def start(self, speed=None): else: if not (speed >= -100 and speed <= 100): raise MotorError("Invalid Speed") + speed = self._speed_process(speed) cmd = f"port {self.port} ; set {speed}\r" if self._runmode == MotorRunmode.NONE: + if self._rpm: + pid = f"pid_diff {self.port} 0 5 s2 0.0027777778 1 0 2.5 0 .4 0.01; " + else: + pid = f"pid {self.port} 0 0 s1 1 0 0.003 0.01 0 100 0.01; " cmd = (f"port {self.port} ; select 0 ; selrate {self._interval}; " - f"pid {self.port} 0 0 s1 1 0 0.003 0.01 0 100; " + f"{pid}" f"set {speed}\r") self._runmode = MotorRunmode.FREE self._currentspeed = speed @@ -401,10 +420,23 @@ def bias(self, bias): :param bias: Value 0 to 1 :raises MotorError: Occurs if invalid bias value passed + + .. deprecated:: 0.6.0 + """ # noqa: RST303 + raise MotorError("Bias no longer available") + + def pwmparams(self, pwmthresh, minpwm): + """PWM thresholds + + :param pwmthresh: Value 0 to 1, threshold below, will switch from fast to slow, PWM + :param minpwm: Value 0 to 1, threshold below which it switches off the drive altogether + :raises MotorError: Occurs if invalid values are passed """ - if not (bias >= 0 and bias <= 1): - raise MotorError("bias should be 0 to 1") - self._write(f"port {self.port} ; bias {bias}\r") + if not (pwmthresh >= 0 and pwmthresh <= 1): + raise MotorError("pwmthresh should be 0 to 1") + if not (minpwm >= 0 and minpwm <= 1): + raise MotorError("minpwm should be 0 to 1") + self._write(f"port {self.port} ; pwmparams {pwmthresh} {minpwm}\r") def pwm(self, pwmv): """PWM motor @@ -453,6 +485,13 @@ def _wait_for_nonblocking(self): """Wait for nonblocking commands to finish""" Device._instance.motorqueue[self.port].join() + def _speed_process(self, speed): + """Lower speed value""" + if self._rpm: + return speed / 60 + else: + return speed + class MotorPair: """Pair of motors @@ -473,6 +512,7 @@ def __init__(self, leftport, rightport): self._rightmotor = Motor(rightport) self.default_speed = 20 self._release = True + self._rpm = False def set_default_speed(self, default_speed): """Set the default speed of the motor @@ -481,6 +521,15 @@ def set_default_speed(self, default_speed): """ self.default_speed = default_speed + def set_speed_unit_rpm(self, rpm=False): + """Set whether to use RPM for speed units or not + + :param rpm: Boolean to determine whether to use RPM for units + """ + self._rpm = rpm + self._leftmotor.set_speed_unit_rpm(rpm) + self._rightmotor.set_speed_unit_rpm(rpm) + def run_for_rotations(self, rotations, speedl=None, speedr=None): """Run pair of motors for N rotations diff --git a/test/motors.py b/test/motors.py index fad9c65..46d01ec 100644 --- a/test/motors.py +++ b/test/motors.py @@ -24,6 +24,7 @@ def test_rotations(self): def test_nonblocking(self): """Test motor nonblocking mode""" m = Motor('A') + m.set_default_speed(10) last = 0 for delay in [1, 0]: for _ in range(3): @@ -44,7 +45,9 @@ def test_nonblocking(self): def test_nonblocking_multiple(self): """Test motor nonblocking mode""" m1 = Motor('A') + m1.set_default_speed(10) m2 = Motor('B') + m2.set_default_speed(10) last = 0 for delay in [1, 0]: for _ in range(3): @@ -118,13 +121,6 @@ def test_plimit(self): self.assertRaises(MotorError, m.plimit, -1) self.assertRaises(MotorError, m.plimit, 2) - def test_bias(self): - """Test setting motor bias""" - m = Motor('A') - m.bias(0.5) - self.assertRaises(MotorError, m.bias, -1) - self.assertRaises(MotorError, m.bias, 2) - def test_pwm(self): """Test PWMing motor""" m = Motor('A') @@ -157,11 +153,6 @@ def handle_motor(speed, pos, apos): m.run_for_seconds(5) self.assertGreater(handle_motor.evt, 0.8 * ((1 / ((m.interval) * 1e-3)) * 5)) - handle_motor.evt = 0 - m.interval = 5 - m.run_for_seconds(5) - self.assertGreater(handle_motor.evt, 0.8 * ((1 / ((m.interval) * 1e-3)) * 5)) - def test_none_callback(self): """Test setting empty callback""" m = Motor('A') @@ -234,7 +225,7 @@ def test_dual_interval(self): """Test dual motor interval""" m1 = Motor('A') m2 = Motor('B') - for interval in [10, 5]: + for interval in [20, 10]: m1.interval = interval m2.interval = interval count = 1000 From eeffd9d7b75cc2a4826095016b8eb2ad1557a46b Mon Sep 17 00:00:00 2001 From: chrisruk Date: Fri, 31 Mar 2023 14:56:41 +0100 Subject: [PATCH 06/32] Per port power limiting (#193) --- buildhat/color.py | 2 +- buildhat/colordistance.py | 2 +- buildhat/devices.py | 4 ++-- buildhat/motors.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/buildhat/color.py b/buildhat/color.py index edf6030..5959263 100644 --- a/buildhat/color.py +++ b/buildhat/color.py @@ -210,4 +210,4 @@ def wait_for_new_color(self): def on(self): """Turn on the sensor and LED""" - self._write(f"port {self.port} ; plimit 1 ; set -1\r") + self.reverse() diff --git a/buildhat/colordistance.py b/buildhat/colordistance.py index da85c3a..3447e17 100644 --- a/buildhat/colordistance.py +++ b/buildhat/colordistance.py @@ -199,4 +199,4 @@ def wait_for_new_color(self): def on(self): """Turn on the sensor and LED""" - self._write(f"port {self.port} ; plimit 1 ; set -1\r") + self.reverse() diff --git a/buildhat/devices.py b/buildhat/devices.py index e8bf086..0db16b1 100644 --- a/buildhat/devices.py +++ b/buildhat/devices.py @@ -188,7 +188,7 @@ def isconnected(self): def reverse(self): """Reverse polarity""" - self._write(f"port {self.port} ; plimit 1 ; set -1\r") + self._write(f"port {self.port} ; port_plimit 1 ; set -1\r") def get(self): """Extract information from device @@ -253,7 +253,7 @@ def select(self): def on(self): """Turn on sensor""" - self._write(f"port {self.port} ; plimit 1 ; on\r") + self._write(f"port {self.port} ; port_plimit 1 ; on\r") def off(self): """Turn off sensor""" diff --git a/buildhat/motors.py b/buildhat/motors.py index 102a159..38786c0 100644 --- a/buildhat/motors.py +++ b/buildhat/motors.py @@ -71,7 +71,7 @@ def plimit(self, plimit): """ if not (plimit >= 0 and plimit <= 1): raise MotorError("plimit should be 0 to 1") - self._write(f"port {self.port} ; plimit {plimit}\r") + self._write(f"port {self.port} ; port_plimit {plimit}\r") def bias(self, bias): """Bias motor @@ -413,7 +413,7 @@ def plimit(self, plimit): """ if not (plimit >= 0 and plimit <= 1): raise MotorError("plimit should be 0 to 1") - self._write(f"port {self.port} ; plimit {plimit}\r") + self._write(f"port {self.port} ; port_plimit {plimit}\r") def bias(self, bias): """Bias motor From a0981fca71b1baa6569247614a4118fdaf9f0445 Mon Sep 17 00:00:00 2001 From: mutesplash <49622611+mutesplash@users.noreply.github.com> Date: Tue, 12 Sep 2023 00:20:40 -0400 Subject: [PATCH 07/32] Add movement counter to WeDo 2.0 Motion Sensor --- buildhat/wedo.py | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/buildhat/wedo.py b/buildhat/wedo.py index df99dd6..fc874c2 100644 --- a/buildhat/wedo.py +++ b/buildhat/wedo.py @@ -35,6 +35,7 @@ class MotionSensor(Device): :param port: Port of device :raises DeviceError: Occurs if there is no motion sensor attached to port """ + default_mode = 0 def __init__(self, port): """ @@ -43,7 +44,18 @@ def __init__(self, port): :param port: Port of device """ super().__init__(port) - self.mode(0) + self.mode(self.default_mode) + + def set_default_data_mode(self, mode): + """ + Set the mode most often queried from this device to significantly + improve performance when repeatedly accessing data + + :param mode: 0 for distance (default), 1 for movement count + """ + if mode == 1 or mode == 0: + self.default_mode = mode + self.mode(mode) def get_distance(self): """ @@ -52,4 +64,24 @@ def get_distance(self): :return: Distance from motion sensor :rtype: int """ - return self.get()[0] + return self._get_data_from_mode(0) + + def get_movement_count(self): + """ + Return the movement counter: The count of how many times the sensor has + detected an object that moved within 4 blocks of the sensor since the + sensor has been plugged in or the BuildHAT reset + + :return: Count of objects detected + :rtype: int + """ + return self._get_data_from_mode(1) + + def _get_data_from_mode(self, mode): + if self.default_mode == mode: + return self.get()[0] + else: + self.mode(mode) + retval = self.get()[0] + self.mode(self.default_mode) + return retval From e2b9a7910cadcfabff47df7d022bd39afceb925d Mon Sep 17 00:00:00 2001 From: mutesplash <49622611+mutesplash@users.noreply.github.com> Date: Tue, 12 Sep 2023 00:36:32 -0400 Subject: [PATCH 08/32] Didn't have all the flake8 checkers installed --- buildhat/wedo.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/buildhat/wedo.py b/buildhat/wedo.py index fc874c2..372524e 100644 --- a/buildhat/wedo.py +++ b/buildhat/wedo.py @@ -35,6 +35,7 @@ class MotionSensor(Device): :param port: Port of device :raises DeviceError: Occurs if there is no motion sensor attached to port """ + default_mode = 0 def __init__(self, port): @@ -48,8 +49,9 @@ def __init__(self, port): def set_default_data_mode(self, mode): """ - Set the mode most often queried from this device to significantly - improve performance when repeatedly accessing data + Set the mode most often queried from this device. + + This significantly improves performance when repeatedly accessing data :param mode: 0 for distance (default), 1 for movement count """ @@ -68,9 +70,11 @@ def get_distance(self): def get_movement_count(self): """ - Return the movement counter: The count of how many times the sensor has - detected an object that moved within 4 blocks of the sensor since the - sensor has been plugged in or the BuildHAT reset + Return the movement counter + + This is the count of how many times the sensor has detected an object + that moved within 4 blocks of the sensor since the sensor has been + plugged in or the BuildHAT reset :return: Count of objects detected :rtype: int From 4f0ac084488bbf7eab04fb58633a4a52800580c9 Mon Sep 17 00:00:00 2001 From: Andrew Scheller Date: Thu, 23 Nov 2023 15:22:32 +0000 Subject: [PATCH 09/32] Fix Build HAT library to work on Raspberry Pi 5 --- buildhat/serinterface.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/buildhat/serinterface.py b/buildhat/serinterface.py index af1ab1b..fbed704 100644 --- a/buildhat/serinterface.py +++ b/buildhat/serinterface.py @@ -5,6 +5,7 @@ import tempfile import threading import time +import os from enum import Enum from threading import Condition, Timer @@ -105,6 +106,11 @@ def __init__(self, firmware, signature, version, device="/dev/serial0", debug=Fa self.rampftr.append([]) self.motorqueue.append(queue.Queue()) + # On a Pi 5 /dev/serial0 will point to /dev/ttyAMA10 (which *only* + # exists on a Pi 5, and is the 3-pin debug UART connector) + # The UART on the Pi 5 GPIO header is /dev/ttyAMA0 + if device == "/dev/serial0" and os.readlink(device) == "ttyAMA10": + device = "/dev/ttyAMA0" self.ser = serial.Serial(device, 115200, timeout=5) # Check if we're in the bootloader or the firmware self.write(b"version\r") From e6e642d21aaab7d327a41dbd21f6bd38d446d3c9 Mon Sep 17 00:00:00 2001 From: Greg Annandale Date: Thu, 23 Nov 2023 17:03:01 +0000 Subject: [PATCH 10/32] Release version 0.6.0 --- CHANGELOG.md | 6 ++++++ VERSION | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e070036..2d3d92c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 0.6.0 + +### Added + +* Support for Raspberry Pi 5 (https://github.com/RaspberryPiFoundation/python-build-hat/pull/203) + ## 0.5.12 ### Added diff --git a/VERSION b/VERSION index 9d6c175..a918a2a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.5.12 +0.6.0 From 84e31014568fa4c35dab5bbf74b39cb9d091559e Mon Sep 17 00:00:00 2001 From: Greg Annandale Date: Thu, 23 Nov 2023 17:15:34 +0000 Subject: [PATCH 11/32] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f6d7f91..7112796 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ share/python-wheels/ .installed.cfg *.egg MANIFEST +.pypirc # Installer logs pip-log.txt From d60686f6c353918eac29f10ea54c9ce4dc751188 Mon Sep 17 00:00:00 2001 From: Greg Annandale Date: Thu, 23 Nov 2023 17:23:16 +0000 Subject: [PATCH 12/32] Fix linting error --- buildhat/serinterface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildhat/serinterface.py b/buildhat/serinterface.py index fbed704..c8e5063 100644 --- a/buildhat/serinterface.py +++ b/buildhat/serinterface.py @@ -1,11 +1,11 @@ """Build HAT handling functionality""" import logging +import os import queue import tempfile import threading import time -import os from enum import Enum from threading import Condition, Timer From ea87bafc957380c8d2a6092e5769eac155e2e049 Mon Sep 17 00:00:00 2001 From: mutesplash <49622611+mutesplash@users.noreply.github.com> Date: Wed, 29 Nov 2023 09:14:46 -0500 Subject: [PATCH 13/32] Public access to the filename of the debug log --- buildhat/hat.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/buildhat/hat.py b/buildhat/hat.py index 395a3d6..227b054 100644 --- a/buildhat/hat.py +++ b/buildhat/hat.py @@ -41,6 +41,9 @@ def get(self): "description": desc} return devices + def get_logfile(self): + return Device._instance.debug_filename + def get_vin(self): """Get the voltage present on the input power jack From 338296334f65ffad2e9b04d20de991bebb447cad Mon Sep 17 00:00:00 2001 From: mutesplash <49622611+mutesplash@users.noreply.github.com> Date: Wed, 29 Nov 2023 09:15:39 -0500 Subject: [PATCH 14/32] Save debug log filename vs throwing it away in the logging framework --- buildhat/serinterface.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/buildhat/serinterface.py b/buildhat/serinterface.py index c8e5063..77ee53e 100644 --- a/buildhat/serinterface.py +++ b/buildhat/serinterface.py @@ -94,8 +94,10 @@ def __init__(self, firmware, signature, version, device="/dev/serial0", debug=Fa self.motorqueue = [] self.fin = False self.running = True + self.debug_filename = None if debug: tmp = tempfile.NamedTemporaryFile(suffix=".log", prefix="buildhat-", delete=False) + self.debug_filename = tmp.name logging.basicConfig(filename=tmp.name, format='%(asctime)s %(message)s', level=logging.DEBUG) From 057e98662990c54ed0d8593d2f048f1e947da2fe Mon Sep 17 00:00:00 2001 From: mutesplash <49622611+mutesplash@users.noreply.github.com> Date: Wed, 29 Nov 2023 09:31:07 -0500 Subject: [PATCH 15/32] Flake8 compliance --- buildhat/hat.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/buildhat/hat.py b/buildhat/hat.py index 227b054..0a277eb 100644 --- a/buildhat/hat.py +++ b/buildhat/hat.py @@ -42,6 +42,11 @@ def get(self): return devices def get_logfile(self): + """Get the filename of the debug log (If enabled, None otherwise) + + :return: Path of the debug logfile + :rtype: str or None + """ return Device._instance.debug_filename def get_vin(self): From 3670a2f19fde8df221093e612d8082b0e9204ea0 Mon Sep 17 00:00:00 2001 From: mutesplash <49622611+mutesplash@users.noreply.github.com> Date: Mon, 4 Dec 2023 15:09:03 -0500 Subject: [PATCH 16/32] Support Mode 7 IR Transmission As described in "LEGO Power Functions RC" PDF https://www.philohome.com/pf/pf.htm --- buildhat/colordistance.py | 334 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 334 insertions(+) diff --git a/buildhat/colordistance.py b/buildhat/colordistance.py index 3447e17..52cae03 100644 --- a/buildhat/colordistance.py +++ b/buildhat/colordistance.py @@ -25,6 +25,9 @@ def __init__(self, port): self.mode(6) self.avg_reads = 4 self._old_color = None + self._ir_channel = 0x0 + self._ir_address = 0x0 + self._ir_toggle = 0x0 def segment_color(self, r, g, b): """Return the color name from HSV @@ -197,6 +200,337 @@ def wait_for_new_color(self): self.callback(None) return self._old_color + @property + def ir_channel(self): + return self._ir_channel + + @ir_channel.setter + def ir_channel(self, channel=1): + """ + Set the IR channel for RC Tx + + :param channel: 1-4 indicating the selected IR channel on the reciever + """ + check_chan = channel + if check_chan > 4: + check_chan = 4 + elif check_chan < 1: + check_chan = 1 + # Internally: 0-3 + self._ir_channel = int(check_chan)-1 + + @property + def ir_address(self): + """ + IR Address space of 0x0 for default PoweredUp or 0x1 for extra space + """ + return self._ir_address + + def toggle_ir_toggle(self): + """ + Toggle the IR toggle bit + + """ + # IYKYK, because the RC documents are not clear + if self._ir_toggle: + self._ir_toggle = 0x0 + else: + self._ir_toggle = 0x1 + return self._ir_toggle + + def send_ir_sop(self, port, mode): + """ + Send an IR message via Power Functions RC Protocol in Single Output PWM mode + https://www.philohome.com/pf/pf.htm + + Port B is blue + + Valid values for mode are: + 0x0: Float output + 0x1: Forward/Clockwise at speed 1 + 0x2: Forward/Clockwise at speed 2 + 0x3: Forward/Clockwise at speed 3 + 0x4: Forward/Clockwise at speed 4 + 0x5: Forward/Clockwise at speed 5 + 0x6: Forward/Clockwise at speed 6 + 0x7: Forward/Clockwise at speed 7 + 0x8: Brake (then float v1.20) + 0x9: Backwards/Counterclockwise at speed 7 + 0xA: Backwards/Counterclockwise at speed 6 + 0xB: Backwards/Counterclockwise at speed 5 + 0xC: Backwards/Counterclockwise at speed 4 + 0xD: Backwards/Counterclockwise at speed 3 + 0xE: Backwards/Counterclockwise at speed 2 + 0xF: Backwards/Counterclockwise at speed 1 + + :param port: 'A' or 'B' + :param mode: 0-15 indicating the port's mode to set + """ + escape_modeselect = 0x0 + escape = escape_modeselect + + ir_mode_single_output = 0x4 + ir_mode = ir_mode_single_output + + so_mode_pwm = 0x0 + so_mode = so_mode_pwm + + output_port_a = 0x0 + output_port_b = 0x1 + + output_port = None + if port == 'A' or port == 'a': + output_port = output_port_a + elif port == 'B' or port == 'b': + output_port = output_port_b + else: + return False + + ir_mode = ir_mode | (so_mode << 1) | output_port + + nibble1 = (self._ir_toggle << 3) | (escape << 2) | self._ir_channel + nibble2 = (self._ir_address << 3) | ir_mode + + # Mode range checked here + return self._send_ir_nibbles(nibble1, nibble2, mode) + + def send_ir_socstid(self, port, mode): + """ + Send an IR message via Power Functions RC Protocol in Single Output Clear/Set/Toggle/Increment/Decrement mode + https://www.philohome.com/pf/pf.htm + + Valid values for mode are: + 0x0: Toggle full Clockwise/Forward (Stop to Clockwise, Clockwise to Stop, Counterclockwise to Clockwise) + 0x1: Toggle direction + 0x2: Increment numerical PWM + 0x3: Decrement numerical PWM + 0x4: Increment PWM + 0x5: Decrement PWM + 0x6: Full Clockwise/Forward + 0x7: Full Counterclockwise/Backward + 0x8: Toggle full (defaults to Forward, first) + 0x9: Clear C1 (C1 to High) + 0xA: Set C1 (C1 to Low) + 0xB: Toggle C1 + 0xC: Clear C2 (C2 to High) + 0xD: Set C2 (C2 to Low) + 0xE: Toggle C2 + 0xF: Toggle full Counterclockwise/Backward (Stop to Clockwise, Counterclockwise to Stop, Clockwise to Counterclockwise) + + :param port: 'A' or 'B' + :param mode: 0-15 indicating the port's mode to set + """ + + escape_modeselect = 0x0 + escape = escape_modeselect + + ir_mode_single_output = 0x4 + ir_mode = ir_mode_single_output + + so_mode_cstid = 0x1 + so_mode = so_mode_cstid + + output_port_a = 0x0 + output_port_b = 0x1 + + output_port = None + if port == 'A' or port == 'a': + output_port = output_port_a + elif port == 'B' or port == 'b': + output_port = output_port_b + else: + return False + + ir_mode = ir_mode | (so_mode << 1) | output_port + + nibble1 = (self._ir_toggle << 3) | (escape << 2) | self._ir_channel + nibble2 = (self._ir_address << 3) | ir_mode + + # Mode range checked here + return self._send_ir_nibbles(nibble1, nibble2, mode) + + def send_ir_combo_pwm(self, port_b_mode, port_a_mode): + """ + Send an IR message via Power Functions RC Protocol in Combo PWM mode + https://www.philohome.com/pf/pf.htm + + Valid values for the modes are: + 0x0 Float + 0x1 PWM Forward step 1 + 0x2 PWM Forward step 2 + 0x3 PWM Forward step 3 + 0x4 PWM Forward step 4 + 0x5 PWM Forward step 5 + 0x6 PWM Forward step 6 + 0x7 PWM Forward step 7 + 0x8 Brake (then float v1.20) + 0x9 PWM Backward step 7 + 0xA PWM Backward step 6 + 0xB PWM Backward step 5 + 0xC PWM Backward step 4 + 0xD PWM Backward step 3 + 0xE PWM Backward step 2 + 0xF PWM Backward step 1 + + :param port_b_mode: 0-15 indicating the command to send to port B + :param port_a_mode: 0-15 indicating the command to send to port A + """ + + escape_combo_pwm = 0x1 + escape = escape_combo_pwm + + nibble1 = (self._ir_toggle << 3) | (escape << 2) | self._ir_channel + + # Port modes are range checked here + return self._send_ir_nibbles(nibble1, port_b_mode, port_a_mode) + + def send_ir_combo_direct(self, port_b_output, port_a_output): + """ + Send an IR message via Power Functions RC Protocol in Combo Direct mode + https://www.philohome.com/pf/pf.htm + + Valid values for the output variables are: + 0x0: Float output + 0x1: Clockwise/Forward + 0x2: Counterclockwise/Backwards + 0x3: Brake then float + + :param port_b_output: 0-3 indicating the output to send to port B + :param port_a_output: 0-3 indicating the output to send to port A + """ + escape_modeselect = 0x0 + escape = escape_modeselect + + ir_mode_combo_direct = 0x1 + ir_mode = ir_mode_combo_direct + + nibble1 = (self._ir_toggle << 3) | (escape << 2) | self._ir_channel + nibble2 = (self._ir_address << 3) | ir_mode + + if port_b_output > 0x3 or port_a_output > 0x3: + return False + if port_b_output < 0x0 or port_a_output < 0x0: + return False + + nibble3 = (port_b_output << 2) | port_a_output + + return self._send_ir_nibbles(nibble1, nibble2, nibble3) + + def send_ir_extended(self, mode): + """ + Send an IR message via Power Functions RC Protocol in Extended mode + https://www.philohome.com/pf/pf.htm + + Valid values for the mode are: + 0x0: Brake Port A (timeout) + 0x1: Increment Speed on Port A + 0x2: Decrement Speed on Port A + + 0x4: Toggle Forward/Clockwise/Float on Port B + + 0x6: Toggle Address bit + 0x7: Align toggle bit + + :param mode: 0-2,4,6-7 + """ + escape_modeselect = 0x0 + escape = escape_modeselect + + ir_mode_extended = 0x0 + ir_mode = ir_mode_extended + + nibble1 = (self._ir_toggle << 3) | (escape << 2) | self._ir_channel + nibble2 = (self._ir_address << 3) | ir_mode + + if mode < 0x0 or mode == 0x3 or mode == 0x5 or mode > 0x7: + return False + + return self._send_ir_nibbles(nibble1, nibble2, mode) + + def send_ir_single_pin(self, port, pin, mode, timeout): + """ + Send an IR message via Power Functions RC Protocol in Single Pin mode + https://www.philohome.com/pf/pf.htm + + Valid values for the mode are: + 0x0: No-op + 0x1: Clear + 0x2: Set + 0x3: Toggle + + Note: The unlabeled IR receiver (vs the one labeled V2) has a "firmware bug in Single Pin mode" + https://www.philohome.com/pfrec/pfrec.htm + + :param port: 'A' or 'B' + :param pin: 1 or 2 + :param mode: 0-3 indicating the pin's mode to set + :param timeout: True or False + """ + escape_mode = 0x0 + escape = escape_mode + + ir_mode_single_continuous = 0x2 + ir_mode_single_timeout = 0x3 + ir_mode = None + if timeout: + ir_mode = ir_mode_single_timeout + else: + ir_mode = ir_mode_single_continuous + + output_port_a = 0x0 + output_port_b = 0x1 + + output_port = None + if port == 'A' or port == 'a': + output_port = output_port_a + elif port == 'B' or port == 'b': + output_port = output_port_b + else: + return False + + if pin != 1 and pin != 2: + return False + pin_value = pin-1 + + if mode > 0x3 or mode < 0x0: + return False + + nibble1 = (self._ir_toggle << 3) | (escape << 2) | self._ir_channel + nibble2 = (self._ir_address << 3) | ir_mode + nibble3 = (output_port << 3) | (pin_value << 3) | mode + + return self._send_ir_nibbles(nibble1, nibble2, mode) + + def _send_ir_nibbles(self, nibble1, nibble2, nibble3): + + # M7 IR Tx SI = N/A + # format count=1 type=1 chars=5 dp=0 + # RAW: 00000000 0000FFFF PCT: 00000000 00000064 SI: 00000000 0000FFFF + + mode = 7 + self.mode(mode) + + # The upper bits of data[2] are ignored + if nibble1 > 0xF or nibble2 > 0xF or nibble3 > 0xF: + return False + if nibble1 < 0x0 or nibble2 < 0x0 or nibble3 < 0x0: + return False + + byte_two = (nibble2 << 4) | nibble3 + + data = bytearray(3) + data[0] = (0xc << 4) | mode + data[1] = byte_two + data[2] = nibble1 + + # print(" ".join('{:04b}'.format(nibble1))) + # print(" ".join('{:04b}'.format(nibble2))) + # print(" ".join('{:04b}'.format(nibble3))) + # print(" ".join('{:08b}'.format(n) for n in data)) + + self._write1(data) + return True + def on(self): """Turn on the sensor and LED""" self.reverse() From a306d01951de028d9323d4295905bbcb31a3ceae Mon Sep 17 00:00:00 2001 From: mutesplash <49622611+mutesplash@users.noreply.github.com> Date: Mon, 4 Dec 2023 15:26:40 -0500 Subject: [PATCH 17/32] Flake8 compliance ... and a bugfix, thanks to flake8... so mad that it actually helped --- buildhat/colordistance.py | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/buildhat/colordistance.py b/buildhat/colordistance.py index 52cae03..3c722a3 100644 --- a/buildhat/colordistance.py +++ b/buildhat/colordistance.py @@ -202,6 +202,7 @@ def wait_for_new_color(self): @property def ir_channel(self): + """Get the IR channel for message transmission""" return self._ir_channel @ir_channel.setter @@ -217,20 +218,15 @@ def ir_channel(self, channel=1): elif check_chan < 1: check_chan = 1 # Internally: 0-3 - self._ir_channel = int(check_chan)-1 + self._ir_channel = int(check_chan) - 1 @property def ir_address(self): - """ - IR Address space of 0x0 for default PoweredUp or 0x1 for extra space - """ + """IR Address space of 0x0 for default PoweredUp or 0x1 for extra space""" return self._ir_address def toggle_ir_toggle(self): - """ - Toggle the IR toggle bit - - """ + """Toggle the IR toggle bit""" # IYKYK, because the RC documents are not clear if self._ir_toggle: self._ir_toggle = 0x0 @@ -241,7 +237,8 @@ def toggle_ir_toggle(self): def send_ir_sop(self, port, mode): """ Send an IR message via Power Functions RC Protocol in Single Output PWM mode - https://www.philohome.com/pf/pf.htm + + PF IR RC Protocol documented at https://www.philohome.com/pf/pf.htm Port B is blue @@ -297,7 +294,8 @@ def send_ir_sop(self, port, mode): def send_ir_socstid(self, port, mode): """ Send an IR message via Power Functions RC Protocol in Single Output Clear/Set/Toggle/Increment/Decrement mode - https://www.philohome.com/pf/pf.htm + + PF IR RC Protocol documented at https://www.philohome.com/pf/pf.htm Valid values for mode are: 0x0: Toggle full Clockwise/Forward (Stop to Clockwise, Clockwise to Stop, Counterclockwise to Clockwise) @@ -320,7 +318,6 @@ def send_ir_socstid(self, port, mode): :param port: 'A' or 'B' :param mode: 0-15 indicating the port's mode to set """ - escape_modeselect = 0x0 escape = escape_modeselect @@ -352,7 +349,8 @@ def send_ir_socstid(self, port, mode): def send_ir_combo_pwm(self, port_b_mode, port_a_mode): """ Send an IR message via Power Functions RC Protocol in Combo PWM mode - https://www.philohome.com/pf/pf.htm + + PF IR RC Protocol documented at https://www.philohome.com/pf/pf.htm Valid values for the modes are: 0x0 Float @@ -375,7 +373,6 @@ def send_ir_combo_pwm(self, port_b_mode, port_a_mode): :param port_b_mode: 0-15 indicating the command to send to port B :param port_a_mode: 0-15 indicating the command to send to port A """ - escape_combo_pwm = 0x1 escape = escape_combo_pwm @@ -387,7 +384,8 @@ def send_ir_combo_pwm(self, port_b_mode, port_a_mode): def send_ir_combo_direct(self, port_b_output, port_a_output): """ Send an IR message via Power Functions RC Protocol in Combo Direct mode - https://www.philohome.com/pf/pf.htm + + PF IR RC Protocol documented at https://www.philohome.com/pf/pf.htm Valid values for the output variables are: 0x0: Float output @@ -419,7 +417,8 @@ def send_ir_combo_direct(self, port_b_output, port_a_output): def send_ir_extended(self, mode): """ Send an IR message via Power Functions RC Protocol in Extended mode - https://www.philohome.com/pf/pf.htm + + PF IR RC Protocol documented at https://www.philohome.com/pf/pf.htm Valid values for the mode are: 0x0: Brake Port A (timeout) @@ -450,7 +449,8 @@ def send_ir_extended(self, mode): def send_ir_single_pin(self, port, pin, mode, timeout): """ Send an IR message via Power Functions RC Protocol in Single Pin mode - https://www.philohome.com/pf/pf.htm + + PF IR RC Protocol documented at https://www.philohome.com/pf/pf.htm Valid values for the mode are: 0x0: No-op @@ -490,7 +490,7 @@ def send_ir_single_pin(self, port, pin, mode, timeout): if pin != 1 and pin != 2: return False - pin_value = pin-1 + pin_value = pin - 1 if mode > 0x3 or mode < 0x0: return False @@ -499,7 +499,7 @@ def send_ir_single_pin(self, port, pin, mode, timeout): nibble2 = (self._ir_address << 3) | ir_mode nibble3 = (output_port << 3) | (pin_value << 3) | mode - return self._send_ir_nibbles(nibble1, nibble2, mode) + return self._send_ir_nibbles(nibble1, nibble2, nibble3) def _send_ir_nibbles(self, nibble1, nibble2, nibble3): @@ -516,7 +516,7 @@ def _send_ir_nibbles(self, nibble1, nibble2, nibble3): if nibble1 < 0x0 or nibble2 < 0x0 or nibble3 < 0x0: return False - byte_two = (nibble2 << 4) | nibble3 + byte_two = (nibble2 << 4) | nibble3 data = bytearray(3) data[0] = (0xc << 4) | mode From e73e4153be9e73fde0269f20a37e9efa1b95884a Mon Sep 17 00:00:00 2001 From: Greg Annandale Date: Tue, 19 Dec 2023 07:42:21 -0600 Subject: [PATCH 18/32] Release version 0.7.0 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index a918a2a..faef31a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.6.0 +0.7.0 From 78c0c6988f9c786113b12c4c6309aecccb36b1da Mon Sep 17 00:00:00 2001 From: Greg Annandale Date: Tue, 19 Dec 2023 07:45:38 -0600 Subject: [PATCH 19/32] Adds 0.7.0 changes to Change Log --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d3d92c..7cbdc59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Change Log +## 0.7.0 + +Adds: + +* Mode 7 IR transmission to ColorDistanceSensor https://github.com/RaspberryPiFoundation/python-build-hat/pull/205 +* Debug log filename access https://github.com/RaspberryPiFoundation/python-build-hat/pull/204 +* Movement counter to WeDo 2.0 Motion Sensor https://github.com/RaspberryPiFoundation/python-build-hat/pull/201 + ## 0.6.0 ### Added From aa6c67de0488defd6ca593c49531a79814ef0956 Mon Sep 17 00:00:00 2001 From: chrisruk Date: Sun, 23 Feb 2025 13:06:00 +0000 Subject: [PATCH 20/32] Increase interval for sensor while testing Avoids error that occurs when handling large amount of sensor data. In future, might be interesting to see if we can support faster UART baud rate. --- test/color.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/color.py b/test/color.py index 688e23c..ba23ff2 100644 --- a/test/color.py +++ b/test/color.py @@ -34,7 +34,7 @@ def test_caching(self): """Test to make sure we're not reading cached data""" color = ColorSensor('A') color.avg_reads = 1 - color.interval = 1 + color.interval = 10 for _ in range(100): color.mode(2) From dd6ee6dd32e0f99d1dc0b5812b05ef2b121c1477 Mon Sep 17 00:00:00 2001 From: chrisruk Date: Sun, 23 Feb 2025 13:09:29 +0000 Subject: [PATCH 21/32] Add W503 to ignore This is so that we can handle multiple line if statements. --- .flake8 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.flake8 b/.flake8 index 732c12e..8f58749 100644 --- a/.flake8 +++ b/.flake8 @@ -1,7 +1,7 @@ [flake8] docstring_style=sphinx max-line-length = 127 -ignore = D400, Q000, S311, PLW, PLC, PLR +ignore = D400, Q000, S311, W503, PLW, PLC, PLR per-file-ignores = buildhat/__init__.py:F401 exclude = docs/conf.py, docs/sphinxcontrib/cmtinc-buildhat.py, docs/sphinx_selective_exclude/*.py From b2ca18d617bfd0b58c18a6515164e9e81abf99fb Mon Sep 17 00:00:00 2001 From: chrisruk Date: Sun, 23 Feb 2025 13:13:35 +0000 Subject: [PATCH 22/32] Allow use of own custom firmware If user creates directory called 'data/' where they run their Python script from and places following files in: * firmware.bin * signature.bin * version This custom firmware will then be used instead of bundled firmware. --- buildhat/devices.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/buildhat/devices.py b/buildhat/devices.py index 0db16b1..80dec80 100644 --- a/buildhat/devices.py +++ b/buildhat/devices.py @@ -72,7 +72,15 @@ def __init__(self, port): def _setup(**kwargs): if Device._instance: return - data = os.path.join(os.path.dirname(sys.modules["buildhat"].__file__), "data/") + if ( + os.path.isdir(os.path.join(os.getcwd(), "data/")) + and os.path.isfile(os.path.join(os.getcwd(), "data", "firmware.bin")) + and os.path.isfile(os.path.join(os.getcwd(), "data", "signature.bin")) + and os.path.isfile(os.path.join(os.getcwd(), "data", "version")) + ): + data = os.path.join(os.getcwd(), "data/") + else: + data = os.path.join(os.path.dirname(sys.modules["buildhat"].__file__), "data/") firm = os.path.join(data, "firmware.bin") sig = os.path.join(data, "signature.bin") ver = os.path.join(data, "version") From 1f5e1205dadc56e3a7f7422196fa6b8f1fb4c0f6 Mon Sep 17 00:00:00 2001 From: chrisruk Date: Sun, 23 Feb 2025 14:29:34 +0000 Subject: [PATCH 23/32] Increase timer Bump timer from 8s to 11s, to wait after receiving "Done initialising ports", to avoid geting disconnected message, even though sensor connected to port. --- buildhat/serinterface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildhat/serinterface.py b/buildhat/serinterface.py index 77ee53e..89c9c92 100644 --- a/buildhat/serinterface.py +++ b/buildhat/serinterface.py @@ -378,7 +378,7 @@ def loop(self, cond, uselist, q, listevt): def runit(): with cond: cond.notify() - t = Timer(8.0, runit) + t = Timer(11.0, runit) t.start() if line[0] == "P" and (line[2] == "C" or line[2] == "M"): From 3f33814857c86e8c463910e39857d649d5706377 Mon Sep 17 00:00:00 2001 From: chrisruk Date: Sun, 23 Feb 2025 14:31:55 +0000 Subject: [PATCH 24/32] Add new firmware --- buildhat/data/firmware.bin | Bin 54360 -> 51456 bytes buildhat/data/signature.bin | 2 +- buildhat/data/version | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/buildhat/data/firmware.bin b/buildhat/data/firmware.bin index 64358ee52512bf324a5201140895c78b0a7f6556..f6dcde379adcfa02e6b9ef87f8f0548548174e94 100644 GIT binary patch literal 51456 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 Y3#t@u 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 literal 54360 zcmc${d3;mF`aeD=Nz+ma6bh0qK$^6)v<14c7BHmc&^9auiWFKjEP7KwEsEYEB4rUl zQGp_M0YTs@`>kNTC?FMFa8FpSg@UFmjaOWbfM!pA@0py0l#BQC{qF~_Ip>*~XP$Y_ z%rnodCymcn6@=;KrIpxDOM7cbID2LS&<+KL~k(0?q`wr`;l$nT<(yQa7k_94N ztlF`45$KyY)+~G%7*4~DBSg7=XN(0h|N5U&rtAW~Q^@}lxR3sa`z$E#bI@PHef|Hl zBOl)R@3l_zMh+z(Mm9yBRoT%);L-MERS=pUWLAUbS?g=0y9vD#o57v5JeNUeU3zo}5+0knNe> z43Ui_C9EZy?8=BRY{`l?sEI^~c10eiv_yEtWw$i(=NdVs&^V2sz>nd-=ShOOktDv; z5b@8jgG!?ORQZ`im}^rKE;~$EZVIZR3xZtP9-P>s2`kR!XM61 z5+>Yqo{A+2Br#;Yq*{_nE*v&X>bLN;o=Oc5=grNFe68!I7AZM5>IzXuR+F^Q7GGQo zA?KzetZVgU-S-n}`0o3-YQ;WMeMzU>U(1K>tK*5fo)4*yIN*#4t=91u__O>;zJ|YL zkX9cy$f_mvB$XJwt*L0a+)G>3rP|y)c{Nd|>Pz`PhVp^r^0ihnaBxfc!0hJJg-6NR zl5ISx?_-b-lwOwB%j#s+(v6&2T4PkL)O3?q&nEFi6(;m|N#h+oH+DIy=%}fWL3vEC zT$SQkb#<7Qn4Y@IrG}YgL1{RoiE?r38GNcRPPlW}RhIzC>`lBsCIYqgx`#+*%7!5w%J`2Pm{WNh;M^^ig_=Dwm(E zoiS@N&#CiyAQV=5i;~t*J+d{N-iL%{NQm`HgcD+Y7*HQiTV(P1 zyi~Yg4`+4sAi@@~UQ}w3)t%L}gp%`-SLPVzq>@W|OBu_#%)+Vf)G^1}WtNj5ZjQbb z>Bk{HhDd8))EMJC$X}c-tE<*b=3ko4)yv{z4O8?om9)A@7i);obkq{#^H+XTOKUr7 zzC|nk)@oLlag3bJK>w&`myNythIykeqDzo*?Ocg zKF~^1h;e@F6uurhM@&z)9)+ZeR-&HFSL;dL6g{mscbiYkxXZG7X>~pyBE;LEXW9_s zQ4Lqu)zC3lwjoNtGBosj6?7puj6>g&)`ST+9HgEb(nl{*heC@bSyXB`^xD`))Lb2j zk3wG3nkeMPG!7bg#zE6I7{Y|F9hCDZI4LG-Nr>?xLrB|RCM@<)U5(Q3ZENE5qB%AW zRlLV3Bi|#+1qp;KdY35IG!o?^ID&X@1R+y+PSsLAS*v9&S!18eT2hLZc%hB9?R9%7 z$&DkX0^~DCPlVsxvVp|7xb0z<(^KFH6$;!`=YDcfo%_!D4K(IMXFksfsUDwR%z!kS z`oT7;cR+nHq}3NwhL|aBrw2Y@{2Wpzv`H9>cWgc_<=avW(PR@JTCXz9$y>+I&Qs}U z8&dVN^JeRJ7-r|~&|ksxivH*8sl=*s^zE3lk-SH$wMoqff#F$aK!5H@X$wyymv&v>hV*655aB-iR$bTXE!Q3D z7P3`KF1~!dR`;voTMMn34`Cr`&HQGjWm7}T<(Fshu=$yDbJVa?G5TCSmbTHwE7x=R z7Eqk&s{Yb-B0T9J$;9*>LLVwW3JGVfFTt|~7LAxrw_Y`D(~`@lujlh#tIb8!SM?{a zcZ{!5%QisMlUfMX@ZD|DR#QXU-+}Wd2T@9d@#x*ZJKk>VNHqN(G=AG}o>3Mr+dxu( zGEvUzwguok?~uWs3pU=Xrhf}z` z-jDItcnR^s<>e9b@yCRG^%WuGFsbv#5fX-eoD6pjZWnl|%p@kWyW=6qylj`!_C{Nm zVq7hwR+Gr8W$GBsoa?Z{u&i{JGG%u4taaD^+bVCiEvKOYt|_w?Uz4h(Xo>Rc;p%X_ zAHFV8OYoj`o#yGF+jee^SCmGX36rNyX3`%{dOgZ+!rhrnVCx<=H~U=jN1oR}+3 z>0LKEibyw3=Fb@Fv^`8ycxl~5z4{ukzTSmltYH|Qx6`(oU>}Nk)H_azFx7sZAzaI1 zpSJhZDe}<=jXl9N-QI%U?zLt)bNL_iUaQ*~h0t{S9Q3(rq#a^E0vqVHs-6C^*lWGv zY~tTVdr%vjkG44mXN1en@_N5QIBP?^^>DO}BO617C@&F~+T_ANJyPL>*K3W2jA&hC zV_kVydDMADL4-k35N24VO)!QT?i;5pYdWORcQkNW%{`kUVihg2mWbGxj{39msB;mz zE(Hn$jl0he*v(L_l^Meg;p3VPZ7=Sqk36o7pFa0G=8i-o)z}oA(gkO; z5v2fko4%WgvSUGHhbCcN&12MK6;fdofJs= zpocoVl6X!TDx~90@J{nas&CHgu8h*e6vh@r0mWf&y87>P4$sLlD0E!>lXI!W7cg&` zW9xx-d%{M9k3CTn!p&;Ktupmpb)?pZL#EA>Vo;-eznE+&53Lhc7pM<^J*5>%HMLli zc|!>~@-JUK5f!J4(N~N;@*Po1buyIOSeC0T8BNPb!f$z?nHEm-R)N zGR0xcNwFC*9vIbVkvqIXqi-fZjn9iWH}=-8;^Pdx@@x2B6Qnb5mUYz*G?GMV(pr8e zKU~9QOJL)bIXBBT@-bu*pFX^cmNO2?i6I5NbY_2UYBWvpACs2vu2-gZFqQJ1@+Dc( zgv&a~5ajaF`Z+wO?4=iu9pw-4d-(0VaGWTu&<#KKcd)KJfiehk>*)->@M1dA_MJNtZOf4!wUl|8rStn#M>}bY9Rut-n#0 zq?$mKnKO^@lS4DkkL6cl{X`Ma+!fIa%CctmLH<)vLS4& zz4_P&{5$+^;K|kgVUZiC?QEBRE+E9=euQ%4&;i3dUkq-u?m3fJ>z$|i@BbDPXE73sTy zA78jxdw=t2zL0MqtD~PL>7%&pKNUGMKQB{|aP4oV0m(X}M_nIM%cqkLSkd&0>mXck z7VsT}v(8dpfiXNzpEFaL`j}}%0hg`9imxMl4kPAy%9x<`wH@cOb7p3bACP*?#AOfE z4WC)V*O!%s7M&-3mFDEQ0Rnf&S9f6p=3t>OGvYpFSPEB|F7w`pF_uc{3&YAA#>S zaNZ1Hq}VDN6UQe4(-lYwHO+{7q<0j0!#iclehy=*zDm^;Ga0kj9+xD2PnLNHlaxqf zYuN^ECvYCph8y1mUk4-Ucb_q*Ur%EcIax+henqdWIPOOlYU(n6ei(E-d-itE1s%(t3ilk)J=n9Gdp77U>>1&n1-b)!%H4X!Ja?5N*c)Xl&z%>P-$6S-~e4jnvb4>z$m^~X@MW7F`=U&%D z&~LNnF4qLmZ?fkbuJNGj*|Wwq4)km6xzVKq{VIE|a}|PK%bu%U1)x{5XO$}-^iuYG z#x)l7-`R7KiwC`sJs)$80X?5R4X!-U53}cNS1#y>u(x*ZUqtyMT=yeHnGg3Y+!45I za0%}dWii|qxUb+`aN|C}Tn;YcDB|EY!TkuAevBwr!TkuQfR)h0?S}gqF7HF4tc3d! zuH*#f@Nms=VNFCi^edtqfp;Fda{CyCMkw;S$9xP()X1-Blq z2`=(u_~B~cjBsI}zz??$?qfLeDb^TpRdC1P%y1(aU_2a*|S+MZqW}>8frs3c193eEWYzQT^JfAx0%KULNx0UoC-O{DM z5vmqf0b#vMW2{id-K^2ep4!lM)yNU4I0uP0X(+p`L^(fTF5w~d|~zf2pY7?%43 zX^EyWeaM!qSVO~*VfuaAj>djiZ}hU?NliJnWcAW_Qy57%#YVD2(UlN2+A?E*W=HJq zNm0w#71~XJz-z5U{wIvsBTC6`S(YNXyiC1J#Z{{(B#g^2aJ#<*8j8q=Ck88(=gvKzHSABtE(yaYi5t#~or2aV8=9t-5e`(1y z_;~A)!!?DPaH=g?leIDWjfxqw_Gd?ADfJ3Hm;Minzg{aE--6D_P;YhE?NA#fX>nM$ zm1;w({mJqW8XukbDL=}vVB8;MUo_n@rzMXeB%U-HQHuUqe=A(o;LWPSJ`8QWok%?t zwI@xHT)-+8W!e|*udg0iUg!+vS`V&7aCHF}TD~1ubbxCqxVjm+`emuyg=K24`atxr z4jMZS`M;4M0<5L4aXTkaLuXE!tZ7t%Z}2k^|A)v+&7g->*ASo+LO$X ze$}Xi+*Bf|OALgcL-=<_NqxUT-a)A>hc6NGjd8d^y8BKJR~6E_43@@gJ&O1b?-4&p zjNga&gZGFZCdStz{*8OYj}+ryLOe0OY#DCQ@OKbU?U2@uW;|T_x*P>|k&b0!-J@TP zvC*#|Pp$aJbD>z@(hRe)YM;89DB?LSA-XS^q_uFz%x(Jjm z-k7Kx&HG!gPLUiKue$0z@oPgK&~Mh8FkWeSI$CXQqqRK_H9<_Tw~fS%9s2CDT9P5{ zI`XJ6b_=v;ouWrTR!@C2@u-?JHim@j3-vGRW5}_rYtWBA`bBLL$WXh^TWBB3KaIUX zxMDckpSEC*F&Fd#xbpaw$aH*?PlRf3Slh?oD+S*iBW;;Nmdma-nqwZL z-)IZjM&gIYg$b)rpA#^b7>c<>m{1L$gsq%Ist@X3#aYt9iM7zTLZ)vYtJd>Q#pfX* zH8pu*LWV00bKreCdS>{UElfzW=VE0ougB?VTpb@KOoOaGCc45c;-lkg5Hc4boH3V| z;|$Oz(X04s$gA=4n$MAU2gv)Ei>NkZ#bK4RI365Ry$4A=^f zwbe`OEvHQ`mLeXKRe@5Z2TCEY_DjEr)7mwVz6{bUApN}Ui0bdUnxZiniTuxJ3Tuky zA;)VL+Fy^_sD{$q_Oje$ZN9S6lKcnutEpPNW>6VdL!;)P6 zM6cq=ko6Y%=G&G>AZ-({ON~Pb+QqrGZ8kz)LWrDMmB}=m8eUqN-o0e2?`y>M5Dk9U&BD8*hwgGm;d#QH zeXSe9UqCo9oxc`|u-Dy@y2yz&JjOfs&`NGd(;*^!>mEDiPZJSdbH`m%0Fx3f8IJCo zr{g^oZX8@X);^2y)_5^S!pRFG8;f#fBqX22Cn%IySFCo@xutqmAf^|MBOIYcCT&EzohaeQXvoH@Jrg>Km8@QbC)Uf7q;Jx47U?9Q{ z?8@_Q{gte8E{m9^Ul~HO$sEL{yVJ{w@VJ`@L$Ky1LNfFJ4gP`dneZ#!6^LJn)Jg6c z@GIO6@GpVCk9!*Yk!}J0C*kkuE`~qMoj#8UTamttdlLM_T><}V@Q1o5=tyEcc3hH& z(0!Nv*)_b}e0<39Y;KTuh&Ma3|0BknN|O4Au>@tg=1NA{#zL;uH5Ak(+yfw3!z)Ql zIuXvh2<(~$Ax4)T^0U0dk=GZl?V$U6hl2jtWd@z(9Sr(ImvTN4;vwr3R~G#5xs*7A z9`7JH?*3?!)4J0T`?hO4{G;F>j@TnEGkinf%X0TcY>i7<9wrQgKNYciTzdGD z9llZ^54U#+5jMJLX=+Td*ju_)f)=^7w>4Tvl4^J^u_3R@+{F9ac0F_OHeM+{bsyJV&IA=A9yS*?d)`eln{&`uMEoIU*%% z-6>LCTd#E_#@?+&=;)H*wn-^!f*WGa!(D_+>egI-M}FKIBK+>8ZCmUqLQCFoCTh1E z$16S76eoI)P~hRxwi{F7S2=xUdX2@#vbpAPkB;Y3*Rl9+PG4EqviOc{7MjB1iD{XI zO5mJgEtsCQ&>ZXzI!9HAJWnv5-yCB7m>y?576u%uM4q0EXQ4xERa1Ay zQ|{;po;4y*IOBQ9A-1v_CG)C7~Gjn?xRMV;yCLLTM>pLciZs$y zXN65PJGifiFw^4npCr|Z5{j5e%MR1t+j zgPWG~ja`%yVlfQYtmlVfpL=rQcEcwAb55T(TA}5qE4bZZ!WNrE?ajiOtZ9vz!+Yi# z3GPZPHOmH0GI2Gb!X9rtZYxdozFl6$)43n9jV{#cLIo}6OH$0s#_~I7rRs;pX>ck% z%sZz%G>*>YYT+x#7&Xb{!o8EVA)!LHcUWAg@Hd3Xj6>rJ5U=uv##O0Z@v(AU-r5ia zp&^uKeh^O-c*uYxW8Z?Ii0S6d<&&}VzH^qeq3PKBd;?CCxPiS)+&)ERs8Hjz6e1C@8p^KbZGvA}K*Fd4tH(DDxU zuC_ql8hQe{9I>Du2o+MjODxnzhIjCd^R!LgdGo%Mv)`NZnzllyaKTGkVT*R8k;Ajk zMM+wwQI;0PTHr^IEUpN;Yq8N0=$u^%4ccl~#t~sQMxFQU671q|X$r9xOyIV#df24x z#p+=bt%og_EnBqmKs}rS*EtJGI3t$zJ@~&0l=fZtK4G=-Pf$(JN&omj%TLQ*1AV0D zsB|VU273n)xIGJ9OePmzG*^$`q#Kj_i{eZ2P|W?@vX~nFD0UR~@MiUeJY8NNoY;|p zvrR8yhOjqpGXJ9PEJmcqu}|T#K5a`!3Fp{pPpz=gxx}l^l_+gryN*AH-9D}_mxH^7 z&EJ}z#Yi@VpTXCc)W=Rdhuc80v0Q9yJ&7U7nR^vv2ujh?} zdYHLXdKx*WGO{vO(xf1mCoRY9W(L0nvg%7tM4R(=MZc3*AA3}r*ph0<9izI8`GS4F ziqzMaNK&(m>PsZ`l1axymmUC4rx|w}Jl0r-I?SF6x(7p@?VJkKU)p@9E{T`W&u!Ro z3_1aprk*s-sZ>_(01|1gxw4xqDwoI=xkNbQZnWrlePwJV0mhw`v9cz`)yh~o@Cqr; zw=EB$R9CrcL$>fo_znDsLXtE)kE`CDM>cjhb14y~p#?K4*WvzTH^~i!E^khrqOyvW zG{Q;Ia6>L&#Cz{D&J$JN&#dNelbxu!deqzv1*>7yue3QC`x>ZOjDgE-`&GMQ6S%ix z-y;X3NqzF*LnXSr8wyE^(inM(qxL$dV3kaV{-4C&MHmU$iMPfQLr?mHxhvsk;Qi&6<*pYNu}$NQ@odb^pX zw0Le=Du6vCoXBWO1xtAeJjqDu^PTtL>UtpV8!^2v?lVsXLvTt=(Z-}66I1wnjbaL) z@1W;8u(CWO4dHH<9J*QDlTO-#m_cAvu zgmg*b6J9%z#{q<=5a2GC!6<^6ktegyO<-q8MCLC*z!!W-7VBk0+n-}jP)4yqen z_G-&U&(+E_#4N+yzJ_ne)A4!yB@J$*O}O<$$H7Vx9xuoGjvJMpp(Gg<)K~9Kg3@*v zC^SzVmx0N2m~hNS`jbn~n+K@k;w2S@7)92`ysYWZrF1wDc0f^1q_G{kq-U5Fqbnxy zb#YujgYi+WAL1TG9F2LicoN?nC+SyqS9mHBWlRSBJQf)R$Hv z7cO~*KzBL}{H+<^DGYL3PrA}7Nt(?>&l;O}p^>I1$$eYmO;o!NyGUOuHCW=&yCfdI zOJZ%1#8k-G;JR$-kN3;2r?qrW8`+3`ZBMY>t#bM8F5ORPLRbZa3?z*v%vzV zx;MEvwUxsTq8GQ4&V)d5lDcZ5Fjp;Y-GFD}6;Hepdyt_*jw3XY?(6R_P-0%^vD2Mk zSzOr2P+ zp)Xn7>FK8qtu4ss>v_3gcE5!4McjPl_r;}=(9|cI(KjxJwJwRv=c@{^D}nu-^mJkB zQt-=#&qeN99s^;2V(i?i}7Gy74w*Hr(1&EomzfmuUE-3#WFHKZYY^kFpjT9KCdCqimCd7p-AX1&51iY?b?|==Aj66;m1SV)q)NQE12{AbAopeymtvCa_o;S@rxl9XF^} zEakcjrM#BEHFbb`EaVg+$1?XHOipeA^-UGasUY{WIvCGnjVRC{ZfqcKD2uxvamfn@ zvAAJ@xC|DTfjGm$0W2<6#FheViS8USMg`mp!?J!_h*Dj&nX!weP8i>icjJkUgM4;u zlpFmBcB~(>V?*42JJyfT70)scHh%*1I@|I7s!Z%zc9U=7B>}C)eZ-`>c}#DOJKMrk zoeI(wp@P}oihl7d`mplYhvSGQ4qp~6DxhnLs?7==!96QN^0oBt747%5zc*kmlCB;Q zuR)!C5SLNBCyp53F-wOWE?yE>f|jOwa@0ljWGZX-MwhSMb9p5xDtIVPF6?(vy%37W zv$p?-7|-?Bm$~)IZR?{%ZQGLF~oNn``ZR&J;~6<7FZ!+Gia?CigB<8mTK z@*NE;_y|(fH>IX3RdVV3+&_Ev)^Mp0UY4XTFv?ONyBvlcy2s6tcvhH|crG++@O;8N zG=5!fMDDdi+@3XjL`7BnTArkgYB_UgG+(SqB$A}U3X(ePvX&?DXQK1Uii^37hc&Ng z?UzVAHv~0zd13EwqsahriR=6Gp$TImE^?{~V`5vdJ09O09V%?FflEDMOy`y#qI3Cc zxuY?fU7Gq#oJ?5nRyEuinXtE%Qy=>?Rw;F{W=kUBl0Ly*kY)>)NaFYKLlZwI`-4!D zl!F&3&pQ?g@N&a`wa5}B;QPrEmMF!$%@Ua?!~2RwnHYli_ZCf}9Pf*m`wUH>yB%-l zMkZ7>M&#~npe2u}ASr5;xmZ(J@g{m%FWG?3gUGx54~Je+yjuKJMK4KO=XWa-NnGdK z6>2h<|C?qUzf?0{Ge^^s8gG9n-x-3Jl6Fx>>cN-Le;8@EX@nU=TyX#DVk(5Hgji%Opu^%DvsM4$MWCsA>_rv zmvq|Ps>T;59waa3*G@k=y_C zgPS#YuQiWOi#CO-J1maYOqw;CFIe1BQ#}h`xKv70M>I=SN^*2Mz3aEOOQ|ACQyO!0 zl5ZEpc3hVBo3R34A=JxnoG-$-Yr+7u%MoNiSPe%`P#)@)ILm~GfYP; zRjGq;E48JPOFe|!rCk@_ugd4cHM`@rMOuE+VyP-zvmZ3h@SFdM-PJmzmu19gXzZEA zQJQyED~mQ;zC^jIFBBNM&JG3NE0$b*AvR_9i!7WpueRt6XJ@UnbX*)YtI9Hrm5!WS zf;G3>I*XNWf_*T`*ArzbLHVZIv*C+`??R=b7s4H0YY$f5S&)3WGP+j^Zpz99wU=P* znlkkP7AoyenkV5q1eq$j*Sd7M&>gd1(wvVFoKC62GS(w3+^Y%j=7_wj5bp6%-pl~+ z8id{Uq=2`jGBWiarmi@tHY#Kpkt(_Hqo=nRUTccb(YYPnQ%N|$DYN0|`Zf=5dOmXr z_T^`|CO9J;-S?yWfJD8vU`i2{jlOKMwrTjPH3X+XH`}tl+i)~VbFGjc=;b#%9Vd3Aj$q>G%plC9+KcBUY=GU6Kzj!0one3UDiavx$U zXdWzPep>`$3Xnp9@VPV(7XDCMCxquAJO<$<2ybHHQ`$Ns{C?yehwyR8J8T`zVQgD} zmiKU^=#7~BX$~xAcv~b=3_>`y%~@^G+I6%=2DEim``QP4@6AaFFW_SqZ&80yoXgKx zycO?W2)kSv)}LHZ;LDcLdQJx&>^Y0WYIJ9YIb|DcFZ1=itNseGVIUh z@Vz9bR_nLJzPPLz3|((;PvEh5a`_sd>*?(bpV{L>0hT>m8`888gXPrEZbb z@lookzO{U}q`^{SVuysG7?HN)PBj-#smI;@<8KFD}75u@RIVkY#yhf1k*4<*ug8O~n`T8{&j>!wl{;7P+1 z@TNJ+a4s8;Ji58>%m}K0j;>y;IrsuS={>UFQiO7)pTnJ(7&&QB*&5#*zgxYhL^k|j z$-d#)Br=pL_zN~#)53=(a;X`QleTob9{l_exu-=e{LY}|5Q!zUG4lk)i!|;vA&>M zJz*ocP%VsERLj>EmilDfEZ;|zQwT5FbOO*GzCZ&B3~Z)R)B zA$%V&wB-eLJ-JopMelyo{HJOo*;&G6?JT;jmVj%r_DYdV=awK^^ zH<^5u_=5UTQZaQ8UzbRf^~72ns`d`5m}-yY21RQ==$fOcm?B9TZ8uwKDul(dq!6ImqP=R~=3*ti?wo!;<=FzQ#?Yn`J$7 zt>7o9pDRU=GwznqQRiMzdl7Q8SelqH(aF9**x03B(MuDrI}p=i*sASk%pgufZH9tt zQuNX_ku|C&g%_4_ywj~8Yk6|&{pxMGI^J6RZV(-ekG8;2%NF&E8gJH3+i3-4s*&y(Ax^uC!fO|SV#HM^6vN=ic>{q z?|@d3#*ydTU3Nq!?R6?rW?9KGiUL#jJ4!+(I!<*d_&2? zk~R3ImrIBzVT1NCy-YUkgP* zZ<{#vO3jT5dZXk?SXwHJ=0QBbGRnVWGilqs0 z_-clJze2SqP7|Zs6%%XNf^)7M!x|OUo%^6W{YVp5h3SqF_{D^M6;yKuR-6>6Q8_C$ z11oM+EY#1ktkC>~_e@JTv?I#(fTaW8zdJv-_;uuN{h;$Rzkb*nYsPb`Ul)ACQ)@{sQ_Dh-(|+OpAi*|I^*maSGT zoiI7bmOUfdvMkds^;6?YnJs&mY0d^{j?TDC{gSGb_$=ArmeuH;K1=qxc{ff;eU@w& z>_@3+$@bXkUBbI9S(k*L9^L&YwPNowecIvNrJf!--fMn$b>~KC!Y;Fd4Xc!5`8FwZP?=uzYSZHxLRE?l_*Qe9@qeGkUYnm zbGHS9Zl@GN*QP?(dPCPXL)Tt{{W@H{OTBJ*DXh`U0UT7{G>)}uzvX&SP3;y@S&Ki+ zx>B@Q)Vks4RzvGx(_pof6PE<^u9v1atX3(kRwAs{5>fMFpm|M-HLzK?Y)kc%gY47? zLG&F=w5IbcFTrBnEIyuPE*i!3@LR_qriTX{O@FC}bN?BvhdahsseXNYPf0deHrzk1 z?t9!fuBuF*DeK2oiJCa8ohH8ZKgQM5%Fk3)WaIxFS-(&{-%cCvjI2A=&$Sy_>)6P; zQ{BDY$ofSAZZD@Agzku|}7M=QS&N7il_Sxbp`khS=gduZl=kFD!5wssfC)}87e z;@J9pJN*=F-Gau}wc^-Hb#w`CZ(k{TOw`g*=e`JNse0nkAT3owOY6n4^{AmM8(ZU` zrM|H>MA+s%uIGbvv;Ek52Ok|vhgiCcW9u`>-CQ(~jj5IPzD#Q$um_K=ciYWX@7&SX zipJFq^Y{U}>J>8FiNOs{(ut%Nc8i`rx>l=e&NoUnO7UAn$rI+qlj@MYlB{&>E`M#G z$0x89d18t_Vv4lpeqxGX{zmXeGyXJ@KT729+}tsUKb=UjLiWmWmqLymob}q@FL0{K z+Os`>v8>mg73pt5zbn!Wm_J17387~)bZ*-fbhm7*u^Q>`LeU$M=EhEOGP1VetAgaX z5bXP59cfgxdg7CDMiC--so#`&rAn!hgpjKwCi>sxWWmPhjpRkeSNJWey||}Kph+~` z7kVH0c<^AuL2c*Dr!#vP#zY^)FK65~r;`J*cX$w^kBL4{CNnulz&B630>7ssA$!4{ zLV8H{#w^Q>jqcH%e#z5>KKP~Ljgm&~qsFa8H+vpdOwrWwKlR*)cRgP>hFt#IO!`04 zvQ@{8dP7St%xJwhrsPuSz8d~bd_NFXft4&j1LL=r*OTfBS@b4d0v8vZSl)N3G-2aJ z8Ge67LOSA0q4oJyis_?Yf`q=U#1!93)ROhm4fvdu&p;(ev*f}Rn>0DqxDqq(hg);; ze5^HG$&HW-Z61<#(L{Hzoi`av#qfvoUWpN?Rcv)V7~7-WSoSgtL)BvUL;pYqtHT0W#& zs@{X!QN750SlR6nv80Mv@|s5*{WvnoP!dv0+am^bh#PKZTC!bc+9F%cDA`D<(91*c zC2Ac+S^)93HuPvB^(|$JYAwoF(N(=p+7HD5tNVIyOAJ!(@tnf$qTBip&zHDu=(fJ; z`K+xQ-djB{vv;j$KwBjIuX)sMo$=n_S>84trwnmz>tS{9)rC-r-&*?B$)%s_f;)kc zr+R*(n5k*HK5iKy!S!6l);#{-xHC7t04rUo8%wHJRMJyRK1UJD<>vJ&@4fUXoiw2i z{pC5EPs5)GX!zS^V%mX|K3`9#eUsYP5aAb(B)LE9oo_L{+D7#XJ^go=ucv#g&v|9S z1ERbK1M;>&-s|^}_X*_rdV4*Sx6ZsnJ&*ThP0XuaaX zBF^mm4@Au$-8?EtQW5Wy^aLbHA!!UG-H-3?n@N7&Da;U)bX7#5=&Nf=m+C{?S*aD; z_e5fTf%(rqp9D z>9)S$e#{bz_e%Fs=3fR$)W5_%+cNI)_Ig13AUfN!_WDqTk?k`w7Q?Ss@uuzso=s{; z$qNk|aQ^flelf&RG>`9-Q@vt4>b(wquAa}uZcuJsELWNrSKe!>6!$)>~n4m zSXulEl<5_-&z|=qq*^Jq!a288)R^n6g}n{{Fcv1CX>WJ7tokb zA@8Gr#`ME=0bg6}hdiIgTwwCfn05RUg*shH z{<9dCf%~bx@S{bt%=WImR#1EKsKsa3$}RJ7CO1~6fu@f|Kg4M@SCSV}E=LW&=kfK& z9I-YgH0uIdd8aQXxv5QCh0!J-dRa&{96b^9q_oQzyFAwS-BMu#ljLiwB(bgfH>Zde zG6U`8@7aV&n+a(dkcMAYwZucOCc28577iD+aIhr~p?R)2ODx_Rmjbcxi28Z}cKeKI zJHN3+AwJ#pF7D&v-j%DtLM>XdE7{Ts?*vx@=A3S;(v^sL7}Zm1zvvi2$Ba5C7S&t& z?Hsjjbo}t?F#VOAe`9BLb%PF8;_OouIvKut%*APSENq%CuQz4@3v^Q03aQXtti$qv zM)@=pzulqwTB}E!j{98Sn$MWS_XC_$J1Ot-pv((>(H?k}@EPTCx$4G}t(w zZ(dr}K^}A-Oh+$hC>y=X(YF5*+tz2N@|{mGJC)~rjE!U(=Mf7X!A3aWWm>XWO!*Xi zC&ZLTnSDxiK4%F5XOi<{_EtNk%9G+Ao_~Ho+qg+hchKwj3LS0hTHN4xe0)=54L^{q zZrGbQkC!Gc(2Xb`wX}MLR8Wg$HU!$k*TT8Hf7Ftwm4uF38=d}9>qXWYE5-KRY$yFM zG8?2u9Xefiw=dcjx99&`-aN?jweAuoZqDF?r)#@ciw+H{C9eaN&i#8_@=->P?7qkRk0`N&Mi-!0K-o9T`x zS*uQU9KknrZtEn6uT`HDTlJZCtxEf`#<9X8$9sh16dR3(I!;(5@Mk-oM%&VMrTuzb zg>Rij`?7y5@_nfur8YONZJ3{jU(2DtRaa4kU9gaS^=S98yfk4X+F2^#TTb3~Trg8%evwtYIeVGaRxE0a^PX>#cjpS^`(m}G+Yo_)qEILY!7e}cAHWz zaKbz%zNEmtfBzhp&Ko6Jp|BQ9?2_bc)HscQ#hJ_A&pYX?1;3Z+rLz_~e+)+pUJ+Nw zp9JQQRC0owrtIauBe@8_QcERIfaFk7a!-#>a<+TEnwH>jVMzTZHqW{n{Q;mq;L9n1 zUc%5jvCj3`x;yCOg3u?5=sSt%dk4_(vrC1QBKp_bp+D@R==YJ=;h68}=*AAcAAS_jQ#<2~>nLoWW>Z*ur@`Ip02`yXOm?yG$r@T*1q z54Xc#$nfXh1OKZ*_zOh*D;WOj6;u8a{RE)@)weAAB(H*!R^qyS-bujq?P{KPY*)h;_}r?vMWhPbtmC3(f0=WSMNgqxt-Si zNvxXvE$l-t6FgWY=ZO0GcM<(#B6<=)|1!`&ETWgUL!ZOYXFEg8{`p*)l3$SKb5tMXc)mbzdN&SBU5@VFga>-fb-h`q3i#N7|v!Vd%5(f&Ks_`!#2iXx-j- z%wXt?{}TN~ppUok zHD^K)`r#t_ha9O4ed1rD?=wy!9Ps5NS|I!;j+et1sBhu++ufQIhdgq{KD=B+zd}Uc zJAi(lM}qI}ee>PWcIaPa=-1dgnP*CU`w4VB-rAWE#Rx*8pCJU-Kln%<>@gb42u8>_-^-cke>Kk?lf}#E1Ca;9F@1+U9Ey#r3Jj z-!{XDKbHwX_@5Q=r;7NC7=F4NbQ!-q-YBkKH|HM}cc!SlILmbQt9#&&55j+2>~W** zYZ?B#dv!4W=YW6NUHIoR{4?)?|5w+Y`tK>??=0dMupVtTQT$7Rf2xT8k#_jsW%v)= z1OG>m?AM-J5r3QQ|L)fk{OyI{c^wg4n0*o9kD%F?-=@AAg#I@+`@%0jczoKkfuX0` z(;Mjj6jzqP{hA1Nrk6K^^wQt1vxCq}MfB@z|1bUeBVSGd-Py#@zkCn$>Bz&^ukpJP zBKm)d=raT8Pq-wwJNDoGnh3uz^ljcw=8d0tz5emJ$4#&IK6=QLhoATT5Ige$d*UzK zZLBW+EmJ4j6SeK${W@5)XwRN}SLxzd>3Rf8cc*83gk1bR`!<`K5g|t`9W1G+Sw$#a zvRJxLgER}j&cSr{R6u8^V{eVlxn;tpApEHeKM_uQeWUgN)w6H<*585Fp>t#W0zzPJ zc?bWjApAP9b+jV>IRX3vehK}wxTX*8*+iJa@Q=F({_FsL{Dy~o`YU4X@50SMy5f;B^t5Me0QzzfeeigVzoWv?<80}_wCB9TuRZv} zT|^({{eS7#*>|D8in%5FYtYQt->-Kf55Mk|i0CJZ=&}A6XB(M7A1;7`t!xZv z2vC*0cZEQA1gr8l$ znjZ`77nwyp=a2~dee&YN+Ub1-!(Q(0jNg4pjl45nr-9ZsdA)U*e?Qv$UgLFGP}yD* zHMYoeiH#VS{>O;-ZJY}`G{_FZ5lO0bjV8?E=Svf=k~u@d!_?Gco1lvuVC z?aJ1Xl}&b!cG(+PM-w4a#BOyrFzg5J!hUBL;B6r9&<^>ID>xa&-(R|W4)shB@@x_L zAQAbq3_0x&9e})5T!XF;nrEOlvz~@ustZW=Tchzo$UhZzb(?!7L;l=fB3}&T`-4`{ z_k}`WpU@8b1)Rs93)~{O(-%%4hjjD>V%l((?wVCyrTu^tVATg)V*Q{9 zxRXWP*#TW47j{4cw+1xuPXFB;ggHdSeACsBVOIYS%)Nm5cF;-_7xjSo$$Maa7&&Md zX2%tZ`PLO5=JEjMKW!4>8=tKB&>+l}!2IM@+HX4q&9fuA;6AyYkWO$T?z>mNO$#d1 zc2PH9cAaNsI)7K0?&$uvDA7ypN)(0?NdqPFj|Eb}h4rDY71pv6Z5B&J*Z-H;y6t6@ z$iM!7KcGG2f)_gJ2+~QP=17IJffXnH^(vno80Ol~5byp=#5;jFvmN5y*st9gI1#*q zcqil!WNmPY-nlRxW4Ac)d)Zhg`w(XXafH|gF9#vs2E=vuK>Q3O%0;U@3W#y9V z_gP&?%@$shNASBl(xsv0HT<(UBdz6g2uU1^v-PKd`n#Z&=Gf-Ez{%Y(d}T`KBvPRe z@-rCXs|@i&BH{@^{5`(JQjjBjAL6n$V~yG1lNSF_;FP0%J=0SnpVU4j&_WqdpPCF0J$?~KnT-debgyBMT-jp$p&d`Y5D&3q@X`%Z{HyiPeE6n!(8 z&vDHc_vbYl_q^y^4jzU)%gVr)@r-2Ld6JY)$-o__o78SElC}{_{x6OMivZ zm)c{!!WVKIaYgt%B; zdaSi#?Ru=6T(|O_)(W@RYP1QU=7XAV`wi41pdPXP3~H9!V?E^l52!Lwzq;B$O#`*t zeGSxPPq^V;$iA7pOl$_4B?9>US6Yg76TipAq()_aG<>sKwp` zpss*=&$9>AWl%4>cY|sMwZ^>*)LEo$vhM)(4XAPU?VvsfHP-$HsE=KXuGiz;uc*WBxy@NPhgXyLW4(essGuM{ky%r%cuIE6l0@cH{1k^Iuf@_s{FG0v;=ifm+ z4Qjk|5vYZrdN~$?S^(;O`{SVg2I^f=_ZtHJY#;g=`mxwoXNY|jd(rG17=K?)?1v43 z{<9AzA`M(!?ML5<_?6|#E{d0RHs-fuQfbi3qqK6@K{#{QavZ8 znh;1;Am)`G=<%sykAGjRNsskiR+Ap9-^O{YJ{#w;V&=ta8^0$gmf{Dor08=F|M?fK z^}FjiF@RDjqMR5&Q0S$aw!rDJraITq_6PNuV>PJR`2AF>XAe4mzP1AI8Hiczq?vpoc=JE&J|WuUr%`q4H6R5&QDeHy3` zQ2XptK@m_g$7E1Wd>>rqC<65-sCOOXLH!P@gHs3UXHbth^Fdiaopg=`bp@2ekq7EB zsE3?dP|cu*IqnB_7SsmENKoH^T4Wy%>T^(E+lPYs7}QqVAW$bjS#4RMK5%%f=e+5l z8WA?oHULxu!d#wIQ2RhV?Cl5YEl>&0zM$R&^}I6y)D}>sj(AY7gL===2h>JLdDIyP z>Sa((&KOW@5c{LOC#aPO>+0$O>N$jU^mGHY7}O)4NKlJFY2BSbJq~J%yCbN1pys>6 zK>dI1y?J~T$F(nBJ&QEj&>*pl7NAFiK}alG0D~l?*0g9X16VA!g=FFxZH%y%K?{iB z*kj{3fPE7{Tt`U6CdSEgZ0ClnSaueKl-O~Mmt+R)L~OAz%OrLjPrRUoW#0F6&uFo^ zdGB|B@AG;8ywRss-PKj6&Qf*iRCQIID%}0tBitY`=ZCc-`$Zoh^#Po3o=a{eZW2` zSYrcwXJ``7c^3@3W;;!@gU)f0d__6uy<^x3kBNHueirV_RJ14YXt^F~KOUNl|5Fg# zsj4WC$0>oFH-ge=kCs!ac)UF`)K}P7p!VjBoG9!?m^q?0>JZ;Oq&9{i{7-~Y2!Dz& z8sSfdC?}=Bc}2LRG_T5clnUpfBaYJjbm*rzPqhOpZeOw-UW${`;LnLmZ^sVcpTLK% zySP+Jw7ZKr@9&00DamfPw+R*HgkKMyx7eS@^HEUu3Z7oD|JDEWGdz8S6Wio>*fEuE zJ!fdLtz4S2#J5zl$PF%nPVD96yd6VoelzkvHKa3D%DfSWo&-nt4zs!Uz}CACzj@dE z-(1scXx&))w^m}s1pL7GN70XrT8jH%-##&F-xf-=){E@VP>lVR^-kZBZG0!$_*tBH z64ocsdhHv7ogH6~$t?6f`I{%t`c4iwOZv9YM7ud%h4X%j?@;qc^FIMx9={DPk0HNBw&^?M{B3?*Do1ho=P>6z z30N_suzmbK3yr=IrI{3$-Hcjh2u-7B69czOMaT0&Zome%~hO-1*nV#!|_94dK99)lH=5=zlP)8fy#^mw6J=J0_!FbF{SBkF%l_3hOu~_8+CjY!oFJ}d?mu; zw<-$JGVtAi*nCKVUqLb!z5?#@O=d%gWG)*M~k!$D9^vTU69whPxr6viI&&- zVDu>OvDqL$wfIwRy;>rT45s^*Uo8O4MZP6i13xmD>IS#+gT6-lDvHiEcc_j|vd$+b- zA?o*DQObS%%2dRjymj!37UA!1?Yk0%@Pk{_Q|WJH`P&$^H-=-gv7@N?WyG;x!}}h> z%fI4y4eo!L$M`c6<86hE*P5}W74K+0TrEOx_eIki(n_|#5L>W-3uml*fuW4N315h3 zT9i_UuTpPaf6C;P{-fs8nmFz-b_K<=0eDJ!{W^#9dR5-d*S}{xsg>tuF-z_e1K1GXZPk(p5M+kxoa#7iD;d z;EZ(l(v3WeGhr4ytyrYVZ9}GYsxsB21CcUb`nE|g#xQ+C=<_P?8AWBVMmTtO zPqRNEM2no!z9rqtd5_+Rp0F1|o&0w>eUzO3s^=+Flrd7iACT|Ecu>luM6_%h+xq)*5SnRE)LK>H9Np6k!&vP4=}cNNwfV zkKmnx6TqGka7M4uy5$+zL`!d|7R0-v&$9)yT-*ZU0y8R9scG-Cxd~kSe)?yl=QpSq zd?u#}l_~r?sAb0BQ7)))_kQukadazfmwdMdp#`#})C;fUv=M3tCbWa82p>yIJAcr0 zsYI1-avel3C`y-b;_>yfJloFVEWPk-mBZ;%dGPw0Kj($V7f^ee?9TYkMI)~Z?< z?+QE57Dl_cg^C3AGA@dDEA_$=SxVt@+!vn5?s!X_Ho7jf2+>#2)3 zWPf%lZ=sTdPmOm%pM5zL6yI<1lxWfiN(k~NL#g=x`4ETyAL0KpgjbQqWx9$s6DRwq z=o>f=IvAjCyZEx{VhN5BaQ6%(KzXQ^hontSv6wgJ`qF$UBZDglfA`M^c3bpw$a8Ki zOB1*Q>F7)UGT=haWhhnU{rM0xI>Cj13}M6)B<961%9Se}p*{9^4^NfgLdO8<;d_Rg zi<&H)xA-Rc^4pCRozle;db4jx<;@?iD|!HLmx0^AgdcUYgmXtSACU;k^5Ha=$;h`Q z=Y8>}GvRR)ojpi<%Tv-lkjn8qn{)|qUPNgc*W;#A=^J?aVR5R=gCmGlBeslrinHT6 zcvWF8{$dB7FP4y8N{8Cz`yjcuSP#xoY~j#he=K<=FYw1^%XGeONMy$N8-pz)rU9~7 zem5{)4)bI=Eb>d>jv<#RD1KA|u51r8k}Y@dh<1R@`SfZcO9iCjk%=c~_nWSkB#`gC zIpn{>j?;RE21`d0*$8}+UD`;sM0e4Io;KtHREh9 z_?Ny`^2l{>K6udJChh(TJjV}V9Xi1>!Wa1ne1$y*N(V|t27h`J{r?E{_X8s?w1E-q z#~Q&L3!SN*E^uus|DKe<89x`l`B)D5Mx^t+-a}tKfa|Zg`c2<4UMe}MO~DEI9L5|f zZwbbbrI4<0gY*9AhR<}%_}l4Glbi;%&>4F-PSI{J*`tMDPTzU(st_{gypQ`x4xIN1 z4MaZ3gBM4`kMWTf4&!=%tlCGd>vaRk;am`VulH&=;7%Ih<3}(b zJA&`6BZJW+FJBEtta0SatLUS0^zXukfh>k_G2H8Ly^ec*x5^tm9BG_^k>^jrS0>-} zp&e7qdqaJukfKg*vfUyK7*BFxLQr9tRbz~{hl$Zv_<^{twiv7%;c1-pX$0od2qieP zGjM)$UpMn!xxN^CDLbSx`)=t8bJ*!1S1LT?jh&)IH0&~eacaIL)L3SxbNTwPUl9Ig zN9I}P!J9x_ShO{AU0YeXLyQn>__~+&Wlv@m4iFW57QJ z_^9dQ1SW7osKv<+OB$0kkImy;;U=At!6$ndQ`y%G;R&$FwzKkcZN7CRf5PAu6b?yv;U-Qw%UF68emj{TRi_7->Lv$V^JOH-t8730r!O@|Lfy(!v3?+!vPD%3bGK3ySpY341$c?~E3w@sgp`ygt_i+oF&iS{H=& zvDAlicc62AJp5XS(Jjp7jB}Xt4qG|q-c0kC{*+rHCR-^Te0(q`(j6GKnm*jJUa0p= zFda6X-eRiIo06>&nMW&!dLmG()n}L~{H}{uhjH4oTrJ#H zW!e|Ss8?)^v9$iW34N#gdJ{7};jlQu9GZc#w_Wxm%R%(om$2Uu`!uXYQmHkxqr)23 zaTcQrCH>Bwm@`aHb|tWl(l6{g?d|y)@ZU1cRfgG#-FQKCZ|3GKvHinL*l2b;_om!y1)c|RN0&J2RjdOGLU z$WrXgYrHWDJ^xwP3`f6Azv2^={CY6#mHD@0p=}RX9%ZLX!uHYLx=z3eZe!kuhJz22 zwNh?Jt^Ri7D@sd9V8I8eM$WcZ>_qD1&8`FX`z-q;3wGGFWA6~@BHCH%jQeFSn|J?( zuy**Y!u%e?4o8|l4L)39r~IYxE&FZMd%{!aW{GdOBPNVbLOXLMgUTWqL|49UU&#K6 zSSUE?4u!y@Z^60AqS_VQ8CS!SH@Oko&+KILf?Y*yogv2lB*vTNdFqU{ID!6b6DaH8 zGTrvK?&H$l+E9k4XxsV;_&h3G9ty6E{~sP`Bw*L+W=0-u=+=lR&9vWd@3aSZ(D{Vt zFd95Yp)(%&F5|nu$9RFU#ROYdB-+<*fo8We{t2_X|G*Y5JXR=H(u$&=>D(@~aFC8G4SxL%qm>rMt;LfVrh zU3Kltzw8NvJ+%+}rDE-|R)Q6H|7TAk=~Li52|DEO=+A1=kGXQRg1(z$svG7fu_eBUx^*pGq445E~*`2H>VXo0xwK- zA90g2Qs}wCnT`sR;kR}&#$H#7Wvmjyplu(g{-Y<%@+`)iT*9+LC{C~rYi$#uTOhIN zHyJxkd49LyH2vTccAKi*>7@)O|tgjv@Q&nNsC z2fNIJXMpwI##iPPaCSD&FNr&P1XCdoNrbfSk>-gfyPm|^qgF}hSUzKB!5&ss5xVx5 zEUehLh4J&uF)rH{om|Evav3qOC%Vkj`rlbO!xd?n#m^|;RKb~ogx?O9>ddH^w+-ry!;amCz4x)CsqP>L&m27!EH0)IZx6_C zpZ331xiyZ(>>r*|2ZbLY403opDc!r!7rfIYHgV~iCbM-{)9X#>mF?X8pfl!pF8y3r zv_qYC*gWj^8RrQWw$Z>$fZqjV4nc7}wP_HPBxwn-q(ztmR zupV}>lQHV$yU_wRLZ-~P(XrOiWY1tm<^)aYplOQBEOi(TIyT0XZ6eLF-Vy1Tiy68U zaOQ3F+$xML@NLtlVW&N2)Q3$mKXY^#%KW|36vy9S6ME5qQt!D2{bvMw0G`UqVm_Fu zVQ+5Qb&NakRt!sAryG>+U%68Q>X}UaZ#MllhRs{!vf$irb>?e+y5X}6bPMC? zel0-vyj0Mg$ksb-O}C@nX)w#Qn`PR0(C))oF}J6D`Y7e?x42H!ayxKIe%;OHy*r8z zj7^9l1pm)2gusCFhA9tG!(E}BPH zXMS|wKMZ@#DtFufOVat{K89@eny;DuYZoeFQWZ&{l-o(=qK@Ei+cnxT|Ax>E_tYXRCY6 zVW-#}F6ZGmpN>wX`sk)^Njl?}{@Og~_6_F1=jL~?n!s`+YIW3YN_rY44MB(Wz6(o~ zY}7s`+o-`_d!GsBdsPg;yVDGwL=D)dvAeFeBr=?>?29b?|DIP+Z#&n1&w9hQ!wSZk@Z z!+O>lYb&*N*v{Hw%bzMLEmt2)>G}6nM>b=%{V^wYTWWf}nJEka_PO)|&PfamKv`^t1-MiAwFhBvn5~ zcdXRvOlV#8EsL=xirck1)=J|uYYp@M8%`(Umfds@7$35z_*lC#@=6jXTVw16g4)Ol zlQGWjpwm@d&IG6N0GA|&aFYRzU^ZGzwnocFJBADuvS@&TcWt4aj+(h!pi%vVF7L#vsjZXr&Ol$ zEWm^Jn^VNk42uo$mW_jVb;QrjgtUJF@0`kFoo=ZpPyUL05-mRjt#ih~J2486{}=EG zyUCWLw*ISq(wqILzqRL8aj!Clp`MU+j0q{Yl5}% zkK`3e_gJ1{I5l$P-8fg{2WGA7N9Jj+^>=+7wMgtEwTo7h9mfK@{@ zcH*qSmGIgSE3gBlQXbM0hSvrc4|Dry-uaQ0M8R`6Ia3nr5Tn_-xU$^b!i{1`L>tcZ z)wxRt$V%vB*A4CVBj(AjSQfhhFI^AY}sCx|>VCK}I~ z*<`~W_8|Og{Lp^LdU3~@twBcHJ%_fkd9%(vw|h9YQagKUUS3Gd!0ZrhVZ>~eQ9R^~ z!pYMISg#?r)!_KmR`Lc4&rWV%%D%0Jf_=_um{FPRXy4lB=`n_=Jf0~N1K| zLIg*-3)ksF#k0<1z(IdcG-mscpXD(A{K&mh8t{ylMIXjg%wHF%UPQoNXFWdO2*tzv zWJ5mR3okI7!_VCTZ-3&o!OkJY-ZhSs*Q_-O>{a{B!gaCha*dYtVqC=z|vUC*22 zT|o(Z@997oC5H*FnR3Xxk`Su-4zu1hxoC20wBv`eoy+9$291-3dj8WMUa-6PQxD_K zEXr_z?J@Wg_HNY`?R8%9aQqh@75~{NEkp<5PW6lMC!EP2nR>Qwu5%90RpJ>0eRoKanoY4k zc8tx}xwr(jKd4}xS^bkE`S>n=jMDK~mw<65&hVah1Z!0aG)B&1$k(Iq{1H8)rW*1w z|A^-)@WaS4!){G_-0t>#ea|%iv&#Rh_-=7_N6w6KO~H2nO}Yjv^Eu2&V18k@C>iaP zk9%F5*`CWS5zIlt&&>bC2~Pp5nwv9(+R)a}shfIgU$wiM`Zb@2{?<0&Nb7t}x6>FB z^xNrc1<>evQ{=o^!K3M;L{k;v-8hnETzJt~8SKqsN+GcMLqn|5U zh8ulKU%mD=a9JWpGgyEaEko>+z7F%lufZ%3a)koH@bY9!nf->uwdr~^AAIj9e44%- zf!3G~ld~M)$#&&q4C6Ayf7F=w=*FE*F zRje{X?K*DOxWm;`pr=D3LbUgd#>J~c`&mLAP34@(8Et z6jm^tiY?PVl%&7#bVl^qcczD^3w1H#I8W=AiTn<^?Qj#gZ%FjZDm9<97oX{xHI zI$Cw9%2ZuZeYE;gwP|z3=A)Z0Z9ZE0C(rskh2NVXo#1Y(#O)3ZRi%88G~zzl^{7sB zTD4o)%{1_@I)JfbQE{<)UvLk}C-PI9w`8tUCT$Xt3*Y9OEBmJL2~Q?}>6|%~sEd_#m<#={w`TQ04RNOVU;1Y=A#%QZVj$Sus}G9n^|U6m~8G?zlyvH6@^&rFmH(g-DiT)+x4({7u;n<)OsleH-P zET1FD@sw*w(1<$rpF+6!o9Q2UH zc=?usN4I{s>a7 z6*Wi|7k(dLm55P(ShEd!O?0?msK{Ml_(m?{^K*la@9t_titz9XF)Dn6xCc4m@1fK@6;(8H1m`N$edf6+OOjifs%8Ag;H(dG z``+%aa*k>%cst5D*BuLj*qm$P!s?kJ_0@lfycYAaRLJgv6dpoKHKf3dLQal48y<~c z&coPFdor$2x3Ga*x=nXj_3sgPh%MqL(JkUnqq!w@`fKWKap5n&qgoj;UwpRf=Ut`Z z-0+>^5=i5@u39mU_&j^ICMPCdGb8*t!?$v0$JFJrj{RLPb=kz}kVbO&{o(=SdXBi4 z5T)rgBxp?G>kK<{xxWuXQ!EY`zx4h1LIG4>=y$8Og ztj!g4Zw2eoepJWGr9THB(CaLx3_44PS$!%>yT@=c2JMY!N!3J5mPLuVxuqkl4hPgrF}c{TyU)T8WSR+g+q0sUMyqWY0kx^IPyi^^;3>~l?$3T z*pVNqj*8~ehTP`J;G{3zumA>p32)*9x6{z4b>uq%U+Kq!(2E)JHR!nv`D*l5hWtR^ zU-Bo?kxD8KYhi=CVbe@fyL?G%w^?QA?~CZu7LCRfsUrJNMx4}g3AuvyyUGEZo73a^qIbX27xjMS`Bgb>VJu4wcIzZZ{%;~r2DA5} zd*QqE3rYQQ80cuv4?6yc@bogl>`V zM=onsxl=Q#d-vLKv{Owd{nlp`asfzS6^^Con#Mo=7f5fj~)B>F=Bt%=Z#}Fe5 zsBc)G10JagPZ~y`Yw)9C-@^gBk{`7zsh%Mzp23sRdv7;+cKj1&2K%}vvFS5&E?D5t z(rDrCV6z$DAw}TC*|<#+ne^2(ZPU4~(4;U|GOQ17$us8I3vB+U*uj~$Del40q|e5B|u4BRYUV#~WjrOiXf!woOP=dW z!WtBs%iD?>S(f}aoE(a;dRyt;43`Q2O)k!8vUAV3=RY9D?NBf`H50@?xv_pPInq6G zTsaRS*_RJLlm1+NSQY#w_VauU&ytz~tY*$X>!EqHFJ9pI8@PyWefOjA#R+>pp??N^ zS2=ux-}GTc7e)|@Uvr#?be&7!7`hl>TI z1J6pUFpE8dsoV4~8d^>E$==ClCg@clN9`y(8?OBN#A@Z;61z!$HpLp<1 zJ@HHTRnJiO9TqKnEtd2_Fjk?qi3vq|c%U;DO=8BvXlp#%Yd?rN?5YH{G1)$U>-p+f zt10b#g_b{Daj2MfwFWbXE|v9RUCk#$L8x3>qZ9X@Ax%Ph>|rU{e!e=!UL*wh*RjN6 zHnm)`N&U9iaxNiq9y=2na>Q7QFN>*YRcEVxgKrJd{1xpBVdLto*3uU%noDwnO@g^y zRAUaxfH|$PbYA~@-aW@=F+EmE(Amlwcv_goqB^2G1t|ijUS7vOCRH0&Ebn7&v|17S z642JZc`Ibp3oF$R`iAAi$*%u3oIl?d&Z)1!`4jZ8B<+SC*3J%MH?b!ezWH{<-mv9NZ=~u$JI4*Te-Z^+-J19`{h`Qn*{4Rk+&9chnAoAN$kRe6ix z<^BAf>b6Y#G^X0ewXyjz6t&>E|if3BWS&{Ub5G|lb z5tFSIGvkGSxK*8MoM%vy*T=X{Yu@=3wRK{h|Nn-AVH^%omi{H}5@9gw{}%>*l5+qZ z)~FxLy%+(|w@9aSH#o5}58ty{$V+ot&!;xe{Bf?%-fVtMno=-p&Ms+=@C~eV4nK2* zpEXy|_AlAMY2Y^*%iLkT!#a6Y#Ed6}$E@7!x$I}c3Z_eaQkZ27vWE&wM6Fe8pD${x zeTM!NmDSthocplqPU{fK^KK;{$S@LwsdEI@{6DTW#FK=mXZm8!Qt7@xkO;wdJ(njMPja5xb6sxSNZMHS9 zV8soM&Ag*wdsQP}ZELA*Msl{gp}C>)E0Gy|Wld8>Lw$W!MKb}-;PaO7O*NbAZOtu> zRVx@@Q@MijX7Hnl_~r&4UuK(XwpHgZ)v zV`D?(_#zQ0=eJi?JkZoqcl!hxd~;1*RYOZNU)j<~V4A9$TO7Q@4nBMp7+&$^ww6l1 z(bil=$jIeaH8tDHYipY9Rh7uxSYxXNIdyd{^)(f?<{EHydrh-FfVZEf=7t8o&Q`xe z!KsN)Wb?jSm<;f)%i?ckg3HBEeTRckZSI;dr4(f{mm$um$dYtaUIR31u@ zSXm{vTRsIA1COfPJT9^SHbaRYYnV!lYeKa|o^3VtU(dp4K!L~w)Y-^C$|^p~f1_YZI5zXER_5{5E%j*Q4fT|^0{W=&ei_-Ou>zqlD4wsi zHCIrptCMw)LJ|v;ZB24HnKH#GGEbl-Zu7fc`cyf+ zqoxvKuBmj`p!}Kwvc5f@#n3t6N^@gFt%9hmkH+O@pu48>{>mC4JuaGuJV_>BrTc#g zk4GH_cdDzah+j1}>KH(vqp_g^T?xO<1|13J6Tb<57XCRJYU;Y_{guTnyVU{ zWIRavf$Br~YisIiK$08;#Ny{8B}G3&%|XtA21aSA_1kJ|oGp?a+L!V`cCYe<-85!Qs!{Op>LX zew-TWE1+K{5Sl^FZmxpwT(WmiC2MyC6jnn$ObWSkwN*S&MFx#bn*wJ7asMqivb~T+ z|3{gm$WPIiguMfnLqjE>*wO?AQH}aFiY)097!>6-d{#|;WmRi7pH)FRdt$w``P0aJ zRV)rggEv&mt$q=aM{TIdQB_e>U4shzHMju|LqAq67_0K*K_k!h#+v4;R6YyRDc=E# zlH_cDnFp$mj6v!GN&qq1DyteZVb`H+tEuK;4pJp++%caTHI$)kM{R?x5*P-V%Kn<(C-@mW zz)H=syzTm%)$g^J>vFH^aVxuKvTp<`t<;)IO0f0FNI0KP4Ze zR<*eiqXH#`qV^dj87tbK0fCZ-aN-FmMv%YSuTg5Wx2-KCSR}yk2XxfgNHr*Wgl=dA zqtHa@zxsO+H3CYHe*ewv4<`a{X?T^jQr&^n0KgvQVG!Q2Qo2V%8L31=nMkyfU3;o8=9f_w%4H2p_6Q>ZIE?{ z+)Vt*6h2P08v-y%{;~oOh#b|f0aZtlJ@I~2y$7-o?uaB)s=5kd8?}7F!iKs4?*1Ay znh7@>!mb9__ zO?Jo(1C_KuekH<>`&gdwcP)#@@&jcQ&o?a&cuOnVGGE%pV45`(|ZBVP_Rn6O}^HFRmSQzzuT~j8yDAZLYPG-c|lo84gSWp3|N|?d#3<%k8 zEoFZzNv3tw*qM+agHK(+qiaiBP+7AXraUYnNDhrk$@7(7KTyV9>!{^GBFbD5wTevi zps=@So{a1Z|7|9(uuYyV%lwx-U$!DIcSCN#lFH;$m#&!DWo7c|B-RJ&8@7Wzij6Oi z<>iS7z|O%nezc88MpmXADkF9V-NNJ%H6qd3(j|`?DGjtBQ+g!aqi3PGKo3FpvbIM^ zTAQAsWEtu;!%PC>@4@?k@}#u#$Oz%s$OrIgq(SA>l2uM2+0(!WmND`uR;fQsW7tJy zZ=iByAt3bIO-u?HME-~jp0U)WcP?I*x@>V;Dxa2;mcAe*bwS#l8&Xp<(o!0b#Omjql z$Mf~n+?#mJlHz__P1SZ@xtC`l{rC3m)Ey}CP2=uYGk$#`zXE`$<|(RzuV`qg#|$pw zD(tq#Cd|QBI^;Q6O0zEaUSbEM`(km!=>7Ua`Bsjn@dF4%?V`CU5tpN(NuF>~;_g9G zn3k9cXH4V2B0_Nr)4~+`vBLUlw#K%Gp&f(D70XiiiaMGF1{`G=K=*-Bgq(!2-C2x( z3)j=Q7U$sGGp^6jWvl}FMTnzo67H`d#2hoV1mc@Q*BDroNzHnJqVmU=!84ZI+*}1S zo@{Y+)zB|!qlLfi5;0Vi+exlyVXN2j)b%#D)EimW=Bj!$;EK^i%2Y0TcAOO+$QYPg zrvAPdm6>0h$1h&VFUA@Sp68XigW_zJRID<8Rb6Y_Oa&IgYQ|J@4cZb0hscY_b%dBQ z)|F?hn!xLI{@ltWglzO9y1WYVu9EZJkM6%&d7FqyX2S3Gd6ZG7+~&TLB^Mt68s(|% zYRpT@p9feT4Vfw3SCBmN4%VY$e>l(2{fNVo-Z$Blz9T4=cQSSy?=*OKZW{>@^-Y!|#!QMlYy4qAz7=vZ%Fg0-C$o;+ELd^m$4{+z z<;0sS3|%8DDurp;CjFx9mwuO@-B)JK?)zhF_MP9|o&E369L#>9<4E?I2Vcr|<^3Z2 z*?+v2{q)X1X5aC*v)KjdSF`Ue`)79LrW@Ijht)Z+eW}a&)AC6$UCb7_5R&NqL*BWKdoojG@9KA7Xl_*PDG^g}r_Uwu z@=pMd6Ts&L@HzqfP5{pnz_%B8_X7W3(9sKedO=q&=<5ZYy`Z-jboYY(Uhtt8{OAQ= zdcmJw@TnL4>IL6=!M|Scu^0U81z&r?-(K*!7yRx8-+RITUdW*r^5}(JdLf@)$f+0d z>V@2TA-`V8u@~~}g>TN0KMWmE=ouCV4~t#|vzYmGVS~trjCX{tirO z){HH9LNyHJL1iBUd`YDmFBFUp@kS?a>ng)kvt-&Dvp z1bxn+`n9HE8?V?1X;{#=uK3r!tXC?rrhx8Gdt1yKa8G^R8ub2+4H)y+L3`Qc&y=#l zht9_@k$%eU`2V~06Mve~pFciV{?WJQul|$-X8EVO&5z#mM8N;$-|OJQFQ_X3pI$=v zB*Fkn*prQY){qe{xWJOnQMDpOGBT|ghbtJ@e2y=U^4|v`BpXAEi`LiJ5mI`xJ+O=w z-{P^@5a|mL#^H*^Wx$0MiEJ6t%Z4bZ-qU7I{arcx?`4LrtA7LH%5%z%}I!yX?7vOB6 z?@BD+A%!gC3U}wTo)yoG@vk@?{t$3&z_kw7>h-up#?~)pOvJ?mjX+?S(;vzYjQs}d SKWmOK{=&12|JzR(|33j#kSRj| diff --git a/buildhat/data/signature.bin b/buildhat/data/signature.bin index 27ef00d..8f495fa 100644 --- a/buildhat/data/signature.bin +++ b/buildhat/data/signature.bin @@ -1 +1 @@ -Q#E]]-.T~駶e[yEV}A5L}A$I!u9Nzw`l@eK;{E \ No newline at end of file +N.2wSmQ/IӍm )io\~IAz4ĘɅPw>\w LV \ No newline at end of file diff --git a/buildhat/data/version b/buildhat/data/version index e9bf96c..1f3109b 100644 --- a/buildhat/data/version +++ b/buildhat/data/version @@ -1 +1 @@ -1674818421 +1737564117 From 31b746ac1895681a443d9b61b93515c8c0fdb2c4 Mon Sep 17 00:00:00 2001 From: chrisruk Date: Thu, 6 Mar 2025 20:17:25 +0000 Subject: [PATCH 25/32] Update documentation Add information on how to make use of custom firmware. --- docs/buildhat/index.rst | 43 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/docs/buildhat/index.rst b/docs/buildhat/index.rst index 91b48b0..0b9c94e 100644 --- a/docs/buildhat/index.rst +++ b/docs/buildhat/index.rst @@ -28,10 +28,51 @@ power supply. For best results, use the `official Raspberry Pi Build HAT power s .. _official Raspberry Pi Build HAT power supply: http://raspberrypi.com/products/build-hat-power-supply +It is now possible to use custom firmware with the library. To do this you can follow the steps below. + +.. code-block:: + :caption: Using custom firmware + + sudo apt install cmake python3 build-essential gcc-arm-none-eabi libnewlib-arm-none-eabi libstdc++-arm-none-eabi-newlib + + git clone https://github.com/raspberrypi/pico-sdk.git --recursive + git clone https://github.com/raspberrypi/buildhat.git --recursive + + cd buildhat + export PICO_SDK_PATH="$(pwd)/../pico-sdk/" + make + + cd .. + mkdir test + cd test + mkdir data + + cp ../buildhat/firmware-pico/build/main.bin data/firmware.bin + cp ../buildhat/bhbl-pico/signature.bin data/signature.bin + cat ../buildhat/firmware-pico/version.h | sed 's/#define FWVERSION "//g; s/ .*//g' > data/version + +Then place your script, such as the following, within the test/ directory. + +.. code-block:: + :caption: Create test.py in test directory + + import time + from buildhat import Motor + + m = Motor('A') + m.start() + + time.sleep(5) + +Then use: ``python test.py`` in the test directory, to run your script with your custom firmware. + +Note if you want python to always reload the firmware from your **data/** directory each time +you run your script, simply write the value: -1 to **data/version**. + .. warning:: The API for the Build HAT is undergoing active development and is subject - to change. + to change. .. toctree:: :maxdepth: 2 From 6a763de85852f29100ad692aaa732720eb68b191 Mon Sep 17 00:00:00 2001 From: Greg Annandale Date: Wed, 9 Jul 2025 09:06:37 +0100 Subject: [PATCH 26/32] Whitespace fix --- docs/buildhat/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/buildhat/index.rst b/docs/buildhat/index.rst index 0b9c94e..f4f7fc4 100644 --- a/docs/buildhat/index.rst +++ b/docs/buildhat/index.rst @@ -28,7 +28,7 @@ power supply. For best results, use the `official Raspberry Pi Build HAT power s .. _official Raspberry Pi Build HAT power supply: http://raspberrypi.com/products/build-hat-power-supply -It is now possible to use custom firmware with the library. To do this you can follow the steps below. +It is now possible to use custom firmware with the library. To do this you can follow the steps below. .. code-block:: :caption: Using custom firmware From 04ce41ceaa639785846c7d0623c3a40f48ab6390 Mon Sep 17 00:00:00 2001 From: Greg Annandale Date: Wed, 9 Jul 2025 14:44:15 +0100 Subject: [PATCH 27/32] Fix Buildthedocs config --- .readthedocs.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 3f5c924..121204f 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -6,10 +6,11 @@ version: 2 build: - image: latest + os: ubuntu-22.04 + tools: + python: "3.8" python: - version: 3.8 install: - method: setuptools path: . From 7cb54ca9d57e23f0879a5b47245e7fdc9fcae87a Mon Sep 17 00:00:00 2001 From: Greg Annandale Date: Wed, 9 Jul 2025 14:45:45 +0100 Subject: [PATCH 28/32] Update copyright dates --- LICENSE | 2 +- docs/conf.py | 2 +- docs/license.rst | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/LICENSE b/LICENSE index 78d99a6..a43a2e8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2020-2021 - Raspberry Pi Foundation +Copyright (c) 2020-2025 - Raspberry Pi Foundation Copyright (c) 2017-2021 - LEGO System A/S - Aastvej 1, 7190 Billund, DK Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/docs/conf.py b/docs/conf.py index e17f1fd..aae1e0d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -59,7 +59,7 @@ # General information about the project. project = 'Raspberry Pi Build HAT' -copyright = '''2020-2021 - Raspberry Pi Foundation; +copyright = '''2020-2025 - Raspberry Pi Foundation; 2017-2020 - LEGO System A/S - Aastvej 1, 7190 Billund, DK.''' # The version info for the project you're documenting, acts as replacement for diff --git a/docs/license.rst b/docs/license.rst index 2ad53fd..af5c040 100644 --- a/docs/license.rst +++ b/docs/license.rst @@ -3,7 +3,7 @@ License information The MIT License (MIT) -Copyright (c) 2020-2021 - Raspberry Pi Foundation +Copyright (c) 2020-2025 - Raspberry Pi Foundation Copyright (c) 2017-2021 - LEGO System A/S - Aastvej 1, 7190 Billund, DK diff --git a/setup.py b/setup.py index fe788ea..38ae4f1 100755 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ """Setup file""" -# Copyright (c) 2020-2021 Raspberry Pi Foundation +# Copyright (c) 2020-2025 Raspberry Pi Foundation # # SPDX-License-Identifier: MIT From ea80469800ed4164e6d5798cd2accf118b1997ce Mon Sep 17 00:00:00 2001 From: Greg Annandale Date: Wed, 9 Jul 2025 14:46:25 +0100 Subject: [PATCH 29/32] Release version 0.8.0 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index faef31a..a3df0a6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.7.0 +0.8.0 From 7e27ff2a1752503d2796e4073a99d462d0af57a0 Mon Sep 17 00:00:00 2001 From: Greg Annandale Date: Wed, 9 Jul 2025 14:49:18 +0100 Subject: [PATCH 30/32] Update CHANGELOG.md --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cbdc59..53d91df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Change Log +## 0.8.0 + +Adds: + +* Support for custom firmware (see: https://www.raspberrypi.com/news/build-hat-firmware-now-fully-open-source/) +* Add W503 to Flake8 ignore +* Increase interval for sensor while testing - avoids error that occurs when handling large amount of sensor data. In future, might be interesting to see if we can support faster UART baud rate. + ## 0.7.0 Adds: From ab7ff577b6df8f0a57735250f97d35fde1102bbc Mon Sep 17 00:00:00 2001 From: chrisruk Date: Thu, 12 Jun 2025 22:11:10 +0100 Subject: [PATCH 31/32] Resend 'version' if we get empty data back --- buildhat/serinterface.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/buildhat/serinterface.py b/buildhat/serinterface.py index 89c9c92..901dc2e 100644 --- a/buildhat/serinterface.py +++ b/buildhat/serinterface.py @@ -117,17 +117,9 @@ def __init__(self, firmware, signature, version, device="/dev/serial0", debug=Fa # Check if we're in the bootloader or the firmware self.write(b"version\r") - emptydata = 0 incdata = 0 while True: line = self.read() - if len(line) == 0: - # Didn't receive any data - emptydata += 1 - if emptydata > 3: - break - else: - continue if cmp(line, BuildHAT.FIRMWARE): self.state = HatState.FIRMWARE ver = line[len(BuildHAT.FIRMWARE):].split(' ') From 535de8782326eabc7c5f620ac3baf8c9dd40f339 Mon Sep 17 00:00:00 2001 From: Greg Annandale Date: Thu, 10 Jul 2025 08:58:30 +0100 Subject: [PATCH 32/32] Release version 0.9.0 --- CHANGELOG.md | 6 ++++++ VERSION | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53d91df..8bf1d81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 0.9.0 + +Fixes: + +* Resend 'version' if empty data received from the Build HAT + ## 0.8.0 Adds: diff --git a/VERSION b/VERSION index a3df0a6..ac39a10 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.8.0 +0.9.0