class _HeartRateMeasurement(ComplexCharacteristic):
    """Notify-only characteristic of streaming heart rate data."""

    uuid = StandardUUID(0x2A37)

    def __init__(self) -> None:
        super().__init__(properties=Characteristic.NOTIFY)

    def bind(self, service: "HeartRateService") -> _bleio.PacketBuffer:
        """Bind to a HeartRateService."""
        bound_characteristic = super().bind(service)
        bound_characteristic.set_cccd(notify=True)
        # Use a PacketBuffer that can store one packet to receive the HRM data.
        return _bleio.PacketBuffer(bound_characteristic, buffer_size=1)
class _CSCMeasurement(ComplexCharacteristic):
    """Notify-only characteristic of speed and cadence data."""

    uuid = StandardUUID(0x2A5B)

    def __init__(self):
        super().__init__(properties=Characteristic.NOTIFY)

    def bind(self, service):
        """Bind to a CyclingSpeedAndCadenceService."""
        bound_characteristic = super().bind(service)
        bound_characteristic.set_cccd(notify=True)
        # Use a PacketBuffer that can store one packet to receive the SCS data.
        return _bleio.PacketBuffer(bound_characteristic, buffer_size=1)
class ReportOut:
    """A single HID report that receives HID data from a client."""
    # pylint: disable=too-few-public-methods
    uuid = StandardUUID(_REPORT_UUID_NUM)
    def __init__(self, service, report_id, usage_page, usage, *, max_length):
        self._characteristic = _bleio.Characteristic.add_to_service(
            service.bleio_service,
            self.uuid.bleio_uuid,
            max_length=max_length,
            fixed_length=True,
            properties=(Characteristic.READ | Characteristic.WRITE |
                        Characteristic.WRITE_NO_RESPONSE),
            read_perm=Attribute.ENCRYPT_NO_MITM, write_perm=Attribute.ENCRYPT_NO_MITM
        )
        self._report_id = report_id
        self.usage_page = usage_page
        self.usage = usage

        _bleio.Descriptor.add_to_characteristic(
            self._characteristic, _REPORT_REF_DESCR_UUID,
            read_perm=Attribute.ENCRYPT_NO_MITM, write_perm=Attribute.NO_ACCESS,
            initial_value=struct.pack('<BB', self._report_id, _REPORT_TYPE_OUTPUT))
class ReportIn:
    """A single HID report that transmits HID data into a client."""
    uuid = StandardUUID(_REPORT_UUID_NUM)
    def __init__(self, service, report_id, usage_page, usage, *, max_length):
        self._characteristic = _bleio.Characteristic.add_to_service(
            service.bleio_service,
            self.uuid.bleio_uuid,
            properties=Characteristic.READ | Characteristic.NOTIFY,
            read_perm=Attribute.ENCRYPT_NO_MITM, write_perm=Attribute.NO_ACCESS,
            max_length=max_length, fixed_length=True)
        self._report_id = report_id
        self.usage_page = usage_page
        self.usage = usage

        _bleio.Descriptor.add_to_characteristic(
            self._characteristic, _REPORT_REF_DESCR_UUID,
            read_perm=Attribute.ENCRYPT_NO_MITM, write_perm=Attribute.NO_ACCESS,
            initial_value=struct.pack('<BB', self._report_id, _REPORT_TYPE_INPUT))

    def send_report(self, report):
        """Send a report to the peers"""
        self._characteristic.value = report
示例#5
0
class CatPrinter(Service):

    uuid = StandardUUID(0xAE30)

    _tx = StreamIn(uuid=StandardUUID(0xAE01), timeout=1.0, buffer_size=256)

    def _write_data(self, buf):
        self._tx.write(buf)

    @property
    def bitmap_width(self):
        return 384

    def __init__(self, service=None):
        super().__init__(service=service)
        self._mode = None

    @property
    def mode(self):
        return self._mode

    @mode.setter
    def mode(self, value):
        if value == self.mode:
            return

        if value == MODE_TEXT:
            self._write_data(printtext)
        elif value == MODE_BITMAP:
            self._write_data(printimage)
        else:
            raise ValueError("Invalid mode %r" % value)

        self._mode = value

    def feed_lines(self, lines):
        buf = bytearray(paperfeed)
        buf[6] = lines & 0xFF
        buf[7] = lines >> 8
        buf[8] = checksum(buf, 6, 2)
        self._write_data(buf)

    def _print_common(self, text, reverse_bits=True):
        data = memoryview(text)
        while data:
            sz = min(112, len(data))
            sub_data = data[:sz]
            data = data[sz:]
            buf = bytearray(sz + 8)
            buf[0] = 0x51
            buf[1] = 0x78
            buf[2] = 0xA2
            buf[3] = 0x0
            buf[4] = sz
            buf[5] = 0
            if reverse_bits:
                buf[6:6 + sz] = bytes(mirrortable[c] for c in sub_data)
            else:
                buf[6:6 + sz] = sub_data
            buf[6 + sz] = checksum(buf, 6, len(sub_data))
            buf[6 + sz + 1] = 0xFF

            self._write_data(buf)

    def print_text(self, text):
        self.mode = MODE_TEXT
        self._print_common(text.encode("utf-8"))

    def print_line(self, text):
        self.print_text(text)
        self._print_common(b"\n")

    def print_bitmap_row(self, data, reverse_bits=True):
        self.mode = MODE_BITMAP
        self._print_common(data, reverse_bits)
class _EddystoneService:
    """Placeholder service. Not implemented."""

    # pylint: disable=too-few-public-methods
    uuid = StandardUUID(0xFEAA)
示例#7
0
class HIDService(Service):
    """
    Provide devices for HID over BLE.

    :param str hid_descriptor: USB HID descriptor that describes the structure of the reports. Known
        as the report map in BLE HID.

    Example::

        from adafruit_ble.hid_server import HIDServer

        hid = HIDServer()
    """

    uuid = StandardUUID(0x1812)

    boot_keyboard_in = Characteristic(
        uuid=StandardUUID(0x2A22),
        properties=(Characteristic.READ | Characteristic.NOTIFY),
        read_perm=Attribute.ENCRYPT_NO_MITM,
        write_perm=Attribute.NO_ACCESS,
        max_length=8,
        fixed_length=True,
    )

    boot_keyboard_out = Characteristic(
        uuid=StandardUUID(0x2A32),
        properties=(Characteristic.READ
                    | Characteristic.WRITE
                    | Characteristic.WRITE_NO_RESPONSE),
        read_perm=Attribute.ENCRYPT_NO_MITM,
        write_perm=Attribute.ENCRYPT_NO_MITM,
        max_length=1,
        fixed_length=True,
    )

    protocol_mode = Uint8Characteristic(
        uuid=StandardUUID(0x2A4E),
        properties=(Characteristic.READ | Characteristic.WRITE_NO_RESPONSE),
        read_perm=Attribute.OPEN,
        write_perm=Attribute.OPEN,
        initial_value=1,
        max_value=1,
    )
    """Protocol mode: boot (0) or report (1)"""

    # bcdHID (version), bCountryCode (0 not localized), Flags: RemoteWake, NormallyConnectable
    # bcd1.1, country = 0, flag = normal connect
    # TODO: Make this a struct.
    hid_information = Characteristic(
        uuid=StandardUUID(0x2A4A),
        properties=Characteristic.READ,
        read_perm=Attribute.ENCRYPT_NO_MITM,
        write_perm=Attribute.NO_ACCESS,
        initial_value=b"\x01\x01\x00\x02",
    )
    """Hid information including version, country code and flags."""

    report_map = Characteristic(
        uuid=StandardUUID(0x2A4B),
        properties=Characteristic.READ,
        read_perm=Attribute.ENCRYPT_NO_MITM,
        write_perm=Attribute.NO_ACCESS,
        fixed_length=True,
    )
    """This is the USB HID descriptor (not to be confused with a BLE Descriptor). It describes
       which report characteristic are what."""

    suspended = Uint8Characteristic(
        uuid=StandardUUID(0x2A4C),
        properties=Characteristic.WRITE_NO_RESPONSE,
        read_perm=Attribute.NO_ACCESS,
        write_perm=Attribute.ENCRYPT_NO_MITM,
        max_value=1,
    )
    """Controls whether the device should be suspended (0) or not (1)."""
    def __init__(self, hid_descriptor=DEFAULT_HID_DESCRIPTOR, service=None):
        super().__init__(report_map=hid_descriptor)
        if service:
            # TODO: Add support for connecting to a remote hid server.
            pass
        self._init_devices()

    def _init_devices(self):
        # pylint: disable=too-many-branches,too-many-statements,too-many-locals
        self.devices = []
        hid_descriptor = self.report_map

        global_table = [None] * 10
        local_table = [None] * 3
        collections = []
        top_level_collections = []

        i = 0
        while i < len(hid_descriptor):
            b = hid_descriptor[i]
            tag = (b & 0xF0) >> 4
            _type = (b & 0b1100) >> 2
            size = b & 0b11
            size = 4 if size == 3 else size
            i += 1
            data = hid_descriptor[i:i + size]
            if _type == _ITEM_TYPE_GLOBAL:
                global_table[tag] = data
            elif _type == _ITEM_TYPE_MAIN:
                if tag == _MAIN_ITEM_TAG_START_COLLECTION:
                    collections.append({
                        "type": data,
                        "locals": list(local_table),
                        "globals": list(global_table),
                        "mains": [],
                    })
                elif tag == _MAIN_ITEM_TAG_END_COLLECTION:
                    collection = collections.pop()
                    # This is a top level collection if the collections list is now empty.
                    if not collections:
                        top_level_collections.append(collection)
                    else:
                        collections[-1]["mains"].append(collection)
                elif tag == _MAIN_ITEM_TAG_INPUT:
                    collections[-1]["mains"].append({
                        "tag":
                        "input",
                        "locals":
                        list(local_table),
                        "globals":
                        list(global_table),
                    })
                elif tag == _MAIN_ITEM_TAG_OUTPUT:
                    collections[-1]["mains"].append({
                        "tag":
                        "output",
                        "locals":
                        list(local_table),
                        "globals":
                        list(global_table),
                    })
                else:
                    raise RuntimeError(
                        "Unsupported main item in HID descriptor")
                local_table = [None] * 3
            else:
                local_table[tag] = data

            i += size

        def get_report_info(collection, reports):
            """ Gets info about hid reports """
            for main in collection["mains"]:
                if "type" in main:
                    get_report_info(main, reports)
                else:
                    report_size, report_id, report_count = [
                        x[0] for x in main["globals"][7:10]
                    ]
                    if report_id not in reports:
                        reports[report_id] = {
                            "input_size": 0,
                            "output_size": 0
                        }
                    if main["tag"] == "input":
                        reports[report_id][
                            "input_size"] += report_size * report_count
                    elif main["tag"] == "output":
                        reports[report_id][
                            "output_size"] += report_size * report_count

        for collection in top_level_collections:
            if collection["type"][0] != 1:
                raise NotImplementedError(
                    "Only Application top level collections supported.")
            usage_page = collection["globals"][0][0]
            usage = collection["locals"][0][0]
            reports = {}
            get_report_info(collection, reports)
            if len(reports) > 1:
                raise NotImplementedError(
                    "Only one report id per Application collection supported")

            report_id, report = list(reports.items())[0]
            output_size = report["output_size"]
            if output_size > 0:
                self.devices.append(
                    ReportOut(self,
                              report_id,
                              usage_page,
                              usage,
                              max_length=output_size // 8))

            input_size = reports[report_id]["input_size"]
            if input_size > 0:
                self.devices.append(
                    ReportIn(self,
                             report_id,
                             usage_page,
                             usage,
                             max_length=input_size // 8))
示例#8
0
class IBBQService(Service):
    """Service for reading from an iBBQ thermometer.
    """

    _CREDENTIALS_MSG = b"\x21\x07\x06\x05\x04\x03\x02\x01\xb8\x22\x00\x00\x00\x00\x00"
    _REALTIME_DATA_ENABLE_MSG = b"\x0B\x01\x00\x00\x00\x00"
    _UNITS_FAHRENHEIT_MSG = b"\x02\x01\x00\x00\x00\x00"
    _UNITS_CELSIUS_MSG = b"\x02\x00\x00\x00\x00\x00"
    _REQUEST_BATTERY_LEVEL_MSG = b"\x08\x24\x00\x00\x00\x00"

    def __init__(self, service=None):
        super().__init__(service=service)
        # Defer creating buffers until needed, since MTU is not known yet.
        self._settings_result_buf = None
        self._realtime_data_buf = None

    uuid = StandardUUID(0xFFF0)

    settings_result = _SettingsResult()

    account_and_verify = Characteristic(
        uuid=StandardUUID(0xFFF2),
        properties=Characteristic.WRITE,
        read_perm=Attribute.NO_ACCESS,
    )
    """Send credentials to this characteristic."""

    # Not yet understood, not clear if available.
    # history_data = Characteristic(uuid=StandardUUID(0xFFF3),
    #                               properties=Characteristic.NOTIFY,
    #                               write_perm=Attribute.NO_ACCESS)

    realtime_data = _RealtimeData()
    """Real-time temperature values."""

    settings_data = Characteristic(
        uuid=StandardUUID(0xFFF5),
        properties=Characteristic.WRITE,
        read_perm=Attribute.NO_ACCESS,
    )
    """Send control messages here."""

    def init(self):
        """Perform initial "pairing", which is not regular BLE pairing."""
        self.account_and_verify = self._CREDENTIALS_MSG
        self.settings_data = self._REALTIME_DATA_ENABLE_MSG

    def display_fahrenheit(self):
        """Display temperatures on device in degrees Fahrenheit.

        Note: This does not change the units returned by `temperatures`.
        """
        self.settings_data = self._UNITS_FAHRENHEIT_MSG

    def display_celsius(self):
        """Display temperatures on device in degrees Celsius.

        Note: This does not change the units returned by `temperatures`.
        """
        self.settings_data = self._UNITS_CELSIUS_MSG

    @property
    def temperatures(self):
        """Return a tuple of temperatures for all the possible temperature probes on the device.
        Temperatures are in degrees Celsius. Unconnected probes return 0.0.
        """
        if self._realtime_data_buf is None:
            self._realtime_data_buf = bytearray(
                self.realtime_data.packet_size  # pylint: disable=no-member
            )
        data = self._realtime_data_buf
        length = self.realtime_data.readinto(data)  # pylint: disable=no-member
        if length > 0:
            return tuple(
                struct.unpack_from("<H", data, offset=offset)[0] / 10
                for offset in range(0, length, 2)
            )
        # No data.
        return None

    @property
    def battery_level(self):
        """Get current battery level in volts as ``(current_voltage, max_voltage)``.
        Results are approximate and may differ from the
        actual battery voltage by 0.1v or so.
        """
        if self._settings_result_buf is None:
            self._settings_result_buf = bytearray(
                self.settings_result.packet_size  # pylint: disable=no-member
            )

        self.settings_data = self._REQUEST_BATTERY_LEVEL_MSG
        results = self._settings_result_buf
        length = self.settings_result.readinto(results)  # pylint: disable=no-member
        if length >= 5:
            header, current_voltage, max_voltage = struct.unpack_from("<BHH", results)
            if header == 0x24:
                # Calibration was determined empirically, by comparing
                # the returned values with actual measurements of battery voltage,
                # on one sample each of two different products.
                return (
                    current_voltage / 2000 - 0.3,
                    (6550 if max_voltage == 0 else max_voltage) / 2000,
                )
        # Unexpected response or no data.
        return None
示例#9
0
class CyclingSpeedAndCadenceService(Service):
    """Service for reading from a Cycling Speed and Cadence sensor."""

    # 0x180D is the standard HRM 16-bit, on top of standard base UUID
    uuid = StandardUUID(0x1816)

    # Mandatory.
    csc_measurement = _CSCMeasurement()

    csc_feature = Uint8Characteristic(uuid=StandardUUID(0x2A5C),
                                      properties=Characteristic.READ)
    sensor_location = Uint8Characteristic(uuid=StandardUUID(0x2A5D),
                                          properties=Characteristic.READ)

    sc_control_point = Characteristic(uuid=StandardUUID(0x2A39),
                                      properties=Characteristic.WRITE)

    _SENSOR_LOCATIONS = (
        "Other",
        "Top of shoe",
        "In shoe",
        "Hip",
        "Front Wheel",
        "Left Crank",
        "Right Crank",
        "Left Pedal",
        "Right Pedal",
        "Front Hub",
        "Rear Dropout",
        "Chainstay",
        "Rear Wheel",
        "Rear Hub",
        "Chest",
        "Spider",
        "Chain Ring",
    )

    def __init__(self, service=None):
        super().__init__(service=service)
        # Defer creating buffer until we're definitely connected.
        self._measurement_buf = None

    @property
    def measurement_values(self):
        """All the measurement values, returned as a CSCMeasurementValues
        namedtuple.

        Return ``None`` if no packet has been read yet.
        """
        # uint8: flags
        #  bit 0 = 1: Wheel Revolution Data is present
        #  bit 1 = 1: Crank Revolution Data is present
        #
        # The next two fields are present only if bit 0 above is 1:
        #   uint32: Cumulative Wheel Revolutions
        #   uint16: Last Wheel Event Time, in 1024ths of a second
        #
        # The next two fields are present only if bit 10 above is 1:
        #   uint16: Cumulative Crank Revolutions
        #   uint16: Last Crank Event Time, in 1024ths of a second
        #

        if self._measurement_buf is None:
            self._measurement_buf = bytearray(
                self.csc_measurement.incoming_packet_length  # pylint: disable=no-member
            )
        buf = self._measurement_buf
        packet_length = self.csc_measurement.readinto(buf)  # pylint: disable=no-member
        if packet_length == 0:
            return None
        flags = buf[0]
        next_byte = 1

        if flags & 0x1:
            wheel_revs = struct.unpack_from("<L", buf, next_byte)[0]
            wheel_time = struct.unpack_from("<H", buf, next_byte + 4)[0]
            next_byte += 6
        else:
            wheel_revs = wheel_time = None

        if flags & 0x2:
            # Note that wheel revs is uint32 and and crank revs is uint16.
            crank_revs = struct.unpack_from("<H", buf, next_byte)[0]
            crank_time = struct.unpack_from("<H", buf, next_byte + 2)[0]
        else:
            crank_revs = crank_time = None

        return CSCMeasurementValues(wheel_revs, wheel_time, crank_revs,
                                    crank_time)

    @property
    def location(self):
        """The location of the sensor on the cycle, as a string.

        Possible values are:
        "Other", "Top of shoe", "In shoe", "Hip",
        "Front Wheel", "Left Crank", "Right Crank",
        "Left Pedal", "Right Pedal", "Front Hub",
        "Rear Dropout", "Chainstay", "Rear Wheel",
        "Rear Hub", "Chest", "Spider", "Chain Ring")
        "Other", "Chest", "Wrist", "Finger", "Hand", "Ear Lobe", "Foot",
        and "InvalidLocation" (if value returned does not match the specification).
        """

        try:
            return self._SENSOR_LOCATIONS[self.sensor_location]
        except IndexError:
            return "InvalidLocation"
示例#10
0
class HeartRateService(Service):
    """Service for reading from a Heart Rate sensor."""

    # 0x180D is the standard HRM 16-bit, on top of standard base UUID
    uuid = StandardUUID(0x180D)

    # uint8: flags
    #  bit 0 = 0: Heart Rate Value is uint8
    #  bit 0 = 1: Heart Rate Value is uint16
    #  bits 2:1 = 0 or 1: Sensor Contact Feature not supported
    #  bits 2:1 = 2: Sensor Contact Feature supported, contact is not detected
    #  bits 2:1 = 3: Sensor Contact Feature supported, contacted is detected
    #  bit 3 = 0: Energy Expended field is not present
    #  bit 3 = 1: Energy Expended field is present. Units: kilo Joules
    #  bit 4 = 0: RR-Interval values are not present
    #  bit 4 = 1: One or more RR-Interval values are present
    #
    # next uint8 or uint16: Heart Rate Value
    # next uint16: Energy Expended, if present
    # next uint16 (multiple): RR-Interval values, resolution of 1/1024 second
    #   in order of oldest to newest
    #
    # Mandatory for Heart Rate Service
    heart_rate_measurement = _HeartRateMeasurement()
    # Optional for Heart Rate Service.
    body_sensor_location = Uint8Characteristic(uuid=StandardUUID(0x2A38),
                                               properties=Characteristic.READ)

    # Mandatory only if Energy Expended features is supported.
    heart_rate_control_point = Uint8Characteristic(
        uuid=StandardUUID(0x2A39), properties=Characteristic.WRITE)

    _BODY_LOCATIONS = ("Other", "Chest", "Wrist", "Finger", "Hand", "Ear Lobe",
                       "Foot")

    def __init__(self, service=None):
        super().__init__(service=service)
        # Defer creating buffer until needed.
        self._measurement_buf = None

    @property
    def measurement_values(self):
        """All the measurement values, returned as a HeartRateMeasurementValues
        namedtuple.

        Return ``None`` if no packet has been read yet.
        """
        if self._measurement_buf is None:
            self._measurement_buf = bytearray(
                self.heart_rate_measurement.packet_size  # pylint: disable=no-member
            )
        buf = self._measurement_buf
        packet_length = self.heart_rate_measurement.readinto(  # pylint: disable=no-member
            buf)
        if packet_length == 0:
            return None
        flags = buf[0]
        next_byte = 1

        if flags & 0x1:
            bpm = struct.unpack_from("<H", buf, next_byte)[0]
            next_byte += 2
        else:
            bpm = struct.unpack_from("<B", buf, next_byte)[0]
            next_byte += 1

        if flags & 0x4:
            # True or False if Sensor Contact Feature is supported.
            contact = bool(flags & 0x2)
        else:
            # None (meaning we don't know) if Sensor Contact Feature is not supported.
            contact = None

        if flags & 0x8:
            energy_expended = struct.unpack_from("<H", buf, next_byte)[0]
            next_byte += 2
        else:
            energy_expended = None

        rr_values = []
        if flags & 0x10:
            for offset in range(next_byte, packet_length, 2):
                rr_val = struct.unpack_from("<H", buf, offset)[0]
                rr_values.append(rr_val)

        return HeartRateMeasurementValues(bpm, contact, energy_expended,
                                          rr_values)

    @property
    def location(self):
        """The location of the sensor on the human body, as a string.

        Note that the specification describes a limited number of locations.
        But the sensor manufacturer may specify using a non-standard location.
        For instance, some armbands are meant to be worn just below the inner elbow,
        but that is not a prescribed location. So the sensor will report something
        else, such as "Wrist".

        Possible values are:
        "Other", "Chest", "Wrist", "Finger", "Hand", "Ear Lobe", "Foot", and
        "InvalidLocation" (if value returned does not match the specification).
        """

        try:
            return self._BODY_LOCATIONS[self.body_sensor_location]
        except IndexError:
            return "InvalidLocation"
示例#11
0
class NameService(Service):
    uuid = StandardUUID(0xfeef)
    _disp_rx = StreamIn(uuid=StandardUUID(0xfeee), timeout=1.0, buffer_size=8192)

    def __init__(self):
        super().__init__()
        self._bitmap = displayio.Bitmap(badge.display.width, badge.display.height, 2)
        self._palette = displayio.Palette(2)
        self._palette[0] = 0x000000
        self._palette[1] = 0xffffff
        self._offset = 0
        self._bufsize = 0
        self._ledstate = False
     
    def update(self):
        while self._disp_rx.in_waiting > 0:
            if self._bufsize == 0:
                value = int.from_bytes(self._disp_rx.read(1), 'little')
                if value == 0:
                    self._finish_update()
                    continue
                self._bufsize = value & 0x7f
                if value & 0x80:
                    self._offset = None
            if self._offset is None and self._disp_rx.in_waiting >= 2:
                self._offset = int.from_bytes(self._disp_rx.read(2), 'little')
            if self._bufsize > 0 and self._offset is not None:
                data = self._disp_rx.read(min(self._bufsize, self._disp_rx.in_waiting))
                self._bufsize -= len(data)
                for i in range(len(data)):
                    for bit in range(8):
                        self._bitmap[self._offset*8+bit] = 1 if data[i] & (1 << bit) else 0
                    self._offset += 1
                self._ledstate = not self._ledstate
                badge.pixels.fill((0, 0, 0x10 * self._ledstate))
                # TODO: once we have partial refresh, it'd be nice to draw the new pixels
                # on screen as we receive them

    def _store_bitmap(self):
        try:
            storage.remount('/', False)
        except:
            pass
        try:
            bitmap_save('/nametag.bmp', self._bitmap)
        except Exception as err:
            print("Couldn't save file: {}".format(err))
        try:
            storage.remount('/', True)
        except:
            pass
    
    def _finish_update(self):
        print("Update done!")
        self._offset = 0
        self._bufsize = 0
        self._ledstate = False
        badge.pixels.fill((0, 0x10, 0))
        frame = displayio.Group()
        frame.append(displayio.TileGrid(self._bitmap, pixel_shader=self._palette))
        badge.display.show(frame)
        while badge.display.time_to_refresh > 0:
            pass
        badge.display.refresh()
        self._store_bitmap()