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 diff --git a/CHANGELOG.md b/CHANGELOG.md index e070036..7cbdc59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # 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 + +* 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..faef31a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.5.12 +0.7.0 diff --git a/buildhat/colordistance.py b/buildhat/colordistance.py index 3447e17..3c722a3 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): + """Get the IR channel for message transmission""" + 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 + + PF IR RC Protocol documented at 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 + + 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) + 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 + + PF IR RC Protocol documented at 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 + + PF IR RC Protocol documented at 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 + + PF IR RC Protocol documented at 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 + + PF IR RC Protocol documented at 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, nibble3) + + 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() diff --git a/buildhat/hat.py b/buildhat/hat.py index 395a3d6..0a277eb 100644 --- a/buildhat/hat.py +++ b/buildhat/hat.py @@ -41,6 +41,14 @@ def get(self): "description": desc} 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): """Get the voltage present on the input power jack diff --git a/buildhat/serinterface.py b/buildhat/serinterface.py index af1ab1b..77ee53e 100644 --- a/buildhat/serinterface.py +++ b/buildhat/serinterface.py @@ -1,6 +1,7 @@ """Build HAT handling functionality""" import logging +import os import queue import tempfile import threading @@ -93,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) @@ -105,6 +108,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") diff --git a/buildhat/wedo.py b/buildhat/wedo.py index df99dd6..372524e 100644 --- a/buildhat/wedo.py +++ b/buildhat/wedo.py @@ -36,6 +36,8 @@ class MotionSensor(Device): :raises DeviceError: Occurs if there is no motion sensor attached to port """ + default_mode = 0 + def __init__(self, port): """ Initialise motion sensor @@ -43,7 +45,19 @@ 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. + + This significantly improves 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 +66,26 @@ 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 + + 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 + """ + 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