Module: explorer

This CircuitPython library provides sample interface classes for the DC NextGen badge hardware.

The library provides classes for each of the badge hardware:

  • expLed - the simple on-board LED
  • expNeopixels - 9 RGB addressable LEDs
  • expTouch - 9 touch sensors
  • expTone - simple square wave tone
  • expSynth - polyphonic synthesizer
  • expAccelerometer - 3-axis accelerometer
  • expIR - IR receiver and emitter
  • expDisplay - 200x200 mono (BW) or tri-color (RWB) ePaper display

Implementation Notes

Hardware:

  • one of the Explorer Badge designs

Software and Dependencies:

   1# explorer.py (Explorer Hardware Library)
   2#
   3# Open Source License: MIT
   4# Copyright: 2024 Bradán Lane STUDIO
   5
   6'''
   7This CircuitPython library provides sample interface classes for the DC NextGen badge hardware.
   8
   9The library provides classes for each of the badge hardware:
  10
  11 - expLed            - the simple on-board LED
  12 - expNeopixels      - 9 RGB addressable LEDs
  13 - expTouch          - 9 touch sensors
  14 - expTone           - simple square wave tone
  15 - expSynth          - polyphonic synthesizer
  16 - expAccelerometer  - 3-axis accelerometer
  17 - expIR             - IR receiver and emitter
  18 - expDisplay        - 200x200 mono (BW) or tri-color (RWB) ePaper display
  19
  20Implementation Notes
  21--------------------
  22**Hardware:**
  23
  24 - one of the Explorer Badge designs
  25
  26**Software and Dependencies:**
  27
  28 - CircuitPython firmware (UF2): https://circuitpython.org
  29 - Adafruit CircuitPython Libraries: https://circuitpython.org/libraries
  30'''
  31
  32DEVICE_COUNT = 9    # represents the number of Neopixels and Touch sensors available
  33
  34__display_en_pin = None # global for a few static methods
  35
  36# attempt to add more typing but don't worry if it does not work
  37try:
  38    from typing import ClassVar
  39except:
  40    pass
  41
  42# this try:except section is to allow PyDoc to skip loading these modules
  43try:
  44    import board            # provides access to all of the board IO pins and hardware
  45    import time             # convenient for timing tasks and for 'sleep' delays
  46    import digitalio        # used for the red LED and to control the audio amplifier
  47    import neopixel         # controls the Neopixel LEDs
  48    import touchio          # controls the touch sensors
  49    import pwmio            # used for simple tones
  50    import audiopwmio       # used for synth sounds
  51    import synthio          # one option for making sound with the on-board speaker
  52    import displayio
  53    import terminalio       # only used if we use the simple built-in font (and as a fallback if our bitmap font is missing)
  54    import pulseio          # used for the IR receiver
  55    import adafruit_irremote
  56    import ulab.numpy as np # provides lots of numeric data processing
  57except Exception as e:
  58    print("ERROR: Missing built-in libraries. This occurs when attempting to use this module outside of a CircuitPython environment.", e)
  59    pass
  60
  61try:
  62    __display_en_pin = digitalio.DigitalInOut(board.DISPLAY_EN)
  63    __display_en_pin.direction = digitalio.Direction.OUTPUT
  64    __display_en_pin.value = True
  65except Exception as e:
  66    print("ERROR: unable to access display enable pin.", e)
  67    pass
  68
  69try:
  70    from adafruit_bitmap_font import bitmap_font
  71except Exception as e:
  72    print("ERROR: Missing 'adafruit_bitmap_font' in 'lib/' folder. The ZIP is available from: https://circuitpython.org/libraries", e)
  73    pass
  74
  75try:
  76    from adafruit_display_text import label
  77except Exception as e:
  78    print("ERROR: Missing 'adafruit_display_text' in 'lib/' folder. The ZIP is available from: https://circuitpython.org/libraries", e)
  79    pass
  80
  81
  82class expLed:
  83    '''
  84        The board has a simple status LED on the back.
  85        CircuitPython will use this LED to indicate any errors.
  86
  87        Blink Code:
  88        1. blinks = the code has been loaded and is running
  89        2. blinks = the code has stopped (either from a code error or CTRL-C)
  90        3. blinks = there is a hardware problem and CircuitPython did not load correctly
  91
  92        The LED is also available to user code.
  93        The LED is controlled from a pin available from `board.LED` or `board.GP4`
  94    '''
  95    def __init__(self):
  96        ''' setup the on-board LED '''
  97        self.__led = digitalio.DigitalInOut(board.LED)
  98        self.__led.direction = digitalio.Direction.OUTPUT
  99        self.__led.value = 0 # the initial status LED is OFF
 100
 101    def __repr__(self):
 102        return ("expLed: state={}".format("ON" if self.__led.value else "OFF"))
 103
 104    def on(self):
 105        ''' turn LED on '''
 106        self.__led.value = 1 # the status LED is ON
 107    def off(self):
 108        ''' turn LED off '''
 109        self.__led.value = 0 # the status LED is OFF
 110    def blink(self, duration:float=0.5):
 111        ''' blink LED for a duration '''
 112        self.__led.value = 1
 113        time.sleep(duration)
 114        self.__led.value = 0
 115
 116
 117class expNeopixels:
 118    '''
 119        The board has a chain of Neopixels - controllable RGB LEDs which are capable of displaying any color.
 120        The Neopixels are controlled from a pin defined from `board.NEOPIXEL` or `board.GP5`.
 121    '''
 122    COUNT:ClassVar[int] = DEVICE_COUNT  # number of neopixels on the badge
 123
 124    def __init__(self):
 125        '''
 126        Setup the six on-board Neopixel LEDs
 127
 128        Available Class Constants:
 129        '''
 130        #print("initializing the {:d} neopixels".format(count))
 131        self.__size = expNeopixels.COUNT
 132        self.__neopixels = neopixel.NeoPixel(board.NEOPIXEL, self.__size, brightness=0.1, auto_write=True)
 133        self.clear() # start with neopixels as BLACK aka OFF
 134
 135    def __repr__(self):
 136        return ("dckNeopixel: size={:d}  values=[{}]".format(self.__size, repr(self.__neopixels)))
 137
 138    def set(self, index:int, color:int):
 139        ''' set one Neopixel LED (index) to an RGB color (0xRRGGBB) '''
 140        index = index % self.__size # prevent overflow
 141        self.__neopixels[index] = color
 142    def fill(self, color:int):
 143        ''' set all Neopixel LEDs to an RGB color (0xRRGGBB) '''
 144        self.__neopixels.fill(color)
 145    def clear(self):
 146        ''' set all Neopixel LEDs to Black '''
 147        self.__neopixels.fill(0x000000)
 148
 149
 150class expTouch:
 151    '''
 152        The letters on the face of the board (D-C-K-i-d-s) are touch sensitive.
 153        The touch sensors are controlled from pins available
 154        from `board.TOUCH1` thru `board.TOUCH9` or as `board.GP19` thru `board.GP27`.
 155    '''
 156    COUNT:ClassVar[int] = DEVICE_COUNT  # number of touch pads on the badge
 157
 158    def __init__(self):
 159        '''
 160        Initialize the touch pads and setup lists (arrays) to maintain data about each touch pad
 161
 162        Available Class Constants:
 163        '''
 164
 165        #print("initializing the {:d} touch sensors".format(count))
 166        PINS: list = [board.TOUCH1, board.TOUCH2, board.TOUCH3, board.TOUCH4, board.TOUCH5, board.TOUCH6, board.TOUCH7, board.TOUCH8, board.TOUCH9]
 167        self.__size = expTouch.COUNT
 168        # we track current and previous state so we can support 'press' and 'release'
 169        self.__new_states = [0 for i in range(self.__size)]  # current states
 170        self.__old_states = [0 for i in range(self.__size)]  # previous states
 171        self.__raws =       [0 for i in range(self.__size)]  # raw data values corresponding to current state
 172        self.__sensors = [None for i in range(self.__size)]
 173        for i in range(self.__size):
 174            self.__sensors[i] = touchio.TouchIn(PINS[i])
 175            # the minimum sensitivity (aka threshold) is set when the touch sensor is created
 176            # print("initialized touch {:d} with threshold {:d}".format(i, self.__touch_sensors[i].threshold))
 177
 178    def __repr__(self):
 179        vals = [str(sensor.threshold) for sensor in self.__sensors]
 180        return ("expTouch: size={:d}  threshold=[{}]".format(self.__size, ", ".join(vals)))
 181
 182    def update(self):
 183        ''' update should be called within the main loop to compute the current state of each touch sensor '''
 184        for i in range(self.__size):
 185            self.__old_states[i] = self.__new_states[i]
 186            self.__new_states[i] = self.__sensors[i].value
 187            self.__raws[i] = self.__sensors[i].raw_value if self.__new_states[i] else 0
 188
 189    def touching(self, index:int) -> bool:
 190        ''' return a boolean (True) if the touch sensor (index) is currently being touched '''
 191        # 'touching' means the sensor is being touched
 192        index = index % self.__size # prevent overflow
 193        if self.__new_states[index]:
 194            return True
 195        return False
 196
 197    def touched(self, index:int) -> bool:
 198        ''' return a boolean (True) if the touch sensor (index) is newly being touched '''
 199        # 'touched' means the sensor is being touched but was not in the prior update
 200        index = index % self.__size # prevent overflow
 201        if self.__new_states[index] and not self.__old_states[index]:
 202            return True
 203        return False
 204
 205    def released(self, index:int) -> bool:
 206        ''' return a boolean (True) if the touch sensor (index) is newly no longer being touched '''
 207        # 'released' means the sensor is not being touched but was in the prior update
 208        index = index % self.__size # prevent overflow
 209        if not self.__new_states[index] and self.__old_states[index]:
 210            return True
 211        return False
 212
 213    def raw(self, index) -> int:
 214        ''' return the raw data value of the touch sensor (index) '''
 215        index = index % self.__size # prevent overflow
 216        return self.__raws[index]
 217
 218    def threshold(self, index:int) -> int:
 219        ''' return the threshold value of the touch sensor (index) '''
 220        index = index % self.__size # prevent overflow
 221        return self.__sensors[index].threshold
 222
 223
 224class expTone:
 225    '''
 226        CircuitPython supports simple square wave PWM (pulse width modulation) tones.
 227        The badge has a small speaker with a mono amplifier.
 228        The sound is generated on a single pin from `board.SPEAKER` or `board.GP6`.
 229        The amplifier is controlled with a pin from `board.SPEAKER_EN` or `board.GP7`.
 230    '''
 231    def __init__(self):
 232        ''' initialize PWM tone generation and the automatic amplifier control '''
 233        # initialize the sound amplifyer as OFF
 234        self.__amplifier = digitalio.DigitalInOut(board.SPEAKER_EN)
 235        self.__amplifier.direction = digitalio.Direction.OUTPUT
 236        self.__amplifier.value = 0 # we start with the amplifier OFF
 237        # create a basic synth
 238        # simple tones
 239        self.__pwm = None
 240
 241    def __repr__(self):
 242        if self.__pwm:
 243            return "expTone: frequency={:d}".format(self.__pwm.frequency)
 244        return "expTone: frequency=OFF"
 245
 246
 247    def note_on(self, frequency:float=440.0):
 248        ''' produce the given frequency (float) as a PWM signal '''
 249        frequency = int(frequency)
 250        if self.__pwm is None:
 251            self.__amplifier.value = True
 252            self.__pwm = pwmio.PWMOut(board.SPEAKER, frequency=frequency, variable_frequency=True)
 253            self.__pwm.duty_cycle = 0x8000
 254        else:
 255            self.__pwm.frequency = frequency
 256        #print("frequency = %d" % (self.__pwm.frequency))
 257
 258    def note_off(self, frequency:float=440.0):
 259        ''' stop producing sound (the frequency parameter is not used but included for consistency with the expSynth class) '''
 260        # the frequency is not used but is included to maintain a compatible API to expSynth
 261        if self.__pwm is not None:
 262            self.__pwm.deinit()
 263            self.__pwm = None
 264            self.__amplifier.value = False
 265
 266    def note_off_all(self):
 267        ''' stop producing sound '''
 268        # there is only one sound at a time so we can just call `note_off``
 269        self.note_off()
 270
 271
 272class expSynth:
 273    '''
 274        CircuitPython supports polyphonic synth sounds.
 275        The badge has a small speaker with a mono amplifier.
 276        The synth sounds are generated on a single pin from `board.SPEAKER` or `board.GP6`.
 277        The amplifier is controlled with a pin from `board.SPEAKER_EN` or `board.GP7`.
 278    '''
 279    TRIANGLE:ClassVar[int]   = 0
 280    SAWTOOTH:ClassVar[int]   = 1
 281    SQUARE:ClassVar[int]     = 2
 282    SINE:ClassVar[int]       = 3
 283
 284    __WAVE_NAMES = ["TRIANGLE", "SAWTOOTH", "SQUARE", "SINE"]
 285
 286    def __init__(self, wave=None):
 287        '''
 288            Initialize synthesizer generation with a waveform and the automatic amplifier control.
 289
 290            Warning: The sine wave suffers from approximation and will have less volume than other waveforms.
 291
 292            Available Class Constants:
 293        '''
 294
 295        self.__amplifier = digitalio.DigitalInOut(board.SPEAKER_EN)
 296        self.__amplifier.direction = digitalio.Direction.OUTPUT
 297        self.__amplifier.value = False # we start with the amplifier OFF
 298        # create a basic synth
 299        SAMPLE_SIZE = 1024
 300        SAMPLE_VOLUME = 32767 # largest positive number in a 16bit integer
 301
 302        self.__wave_type = wave
 303        if wave == expSynth.SAWTOOTH:
 304            # sawtooth wave
 305            self.__wave = np.linspace(SAMPLE_VOLUME, -SAMPLE_VOLUME, num=SAMPLE_SIZE, dtype=np.int16)
 306        elif wave == expSynth.SQUARE:
 307            # full square wave
 308            self.__wave = np.concatenate((np.full((SAMPLE_SIZE // 2), SAMPLE_VOLUME, dtype=np.int16), np.full((SAMPLE_SIZE // 2), -SAMPLE_VOLUME, dtype=np.int16)))
 309        elif wave == expSynth.SINE:
 310            # sine wave : IMPORTANT, without an RC filter, this will be faint
 311            self.__wave = np.array(np.sin(np.linspace(0, 2*np.pi, SAMPLE_SIZE, endpoint=False)) * SAMPLE_VOLUME, dtype=np.int16)
 312        else:
 313            #the synth will default to a triangle waveform
 314            self.__wave = None # a triangle waveform is the default
 315            self.__wave_type = 0
 316
 317        self.__synth = synthio.Synthesizer(sample_rate=22050)
 318        self.__audio = audiopwmio.PWMAudioOut(board.SPEAKER)
 319        self.__audio.play(self.__synth)
 320        # some data to track notes and control the amplifier
 321        self.__notes = {} # dict used to track notes
 322        self.__note_count = 0
 323
 324    def __repr__(self):
 325        notes = [repr(note) for note in self.__notes]
 326        return "expSynth: wave={} count={:d} notes=[{}]".format(expSynth.__WAVE_NAMES[self.__wave_type], self.__note_count, ", ".join(notes))
 327        pass
 328
 329    def note_on(self, frequency:float=440.0):
 330        ''' add a new note (frequency) to the polyphonic output '''
 331        # add to active notes
 332        if not self.__note_count:
 333            self.__amplifier.value = True
 334        synth_note = synthio.Note(frequency, waveform=self.__wave)
 335        self.__notes[frequency] = synth_note
 336        self.__synth.press(synth_note)
 337        self.__note_count += 1
 338
 339    def note_off(self,  frequency:float=440.0):
 340        ''' remove an active note (frequency) from the polyphonic output '''
 341        synth_note = self.__notes.get(frequency, None)
 342        if synth_note is not None:
 343            self.__synth.release(synth_note)
 344            self.__notes.pop(frequency)
 345            self.__note_count -= 1
 346        if not self.__note_count:
 347            self.__amplifier.value = False
 348
 349    def note_off_all(self):
 350        ''' remove all existing notes from the polyphonic output '''
 351        for note in self.__notes:
 352            synth_note = self.__notes.get(note, None)
 353            self.__synth.release(synth_note)
 354            self.__notes.pop(note)
 355        self.__note_count = 0
 356        self.__amplifier.value = False
 357
 358
 359class expAccelerometer:
 360    '''
 361        DISCLAIMER: The badge *was* to include the MXC4005XC 3-axis accelerometer.
 362                    Unfortunately, the accelerometer proved unreliable and has been omitted or disabled.
 363                    For completeness, the code is included doe historical completeness.
 364
 365        It has 12bit values for each axis and defaults to +/- 2G.
 366        The accelerometer provides 'acceleration' data for X, Y, and Z.
 367        It does not provide 'position' data such as angles.
 368        The accelerometer has an interrupt pin to quickly indicate orientation changes and shaking.
 369
 370        The accelerometer is controlled using I2C.
 371            - I2C is a 2-wire serial interface which allows many devices to share the same wires.
 372            - Each I2C device, sharing the I2C wires, has its own 1-byte address.
 373            - CircuitPython provides a means to scan I2C for all attached devices.
 374
 375        The accelerometer has an I2C address of `0x15` and is accessed using `board.I2C()`.
 376    '''
 377    __DEFAULT_ADDR        = 0x15
 378    __SETTING_SHAKE       = 0b00001111 # shake -Y +Y -X +X
 379    __SETTING_ORIENT      = 0b11000000 # orientation Z and XY
 380    __INTERRUPT_CHANGE    = 0x00
 381    __INTERRUPT_READY     = 0x01
 382    __READ_STATUS         = 0x02
 383    __STATUS_READY        = 0b00010000
 384    __READ_X              = 0x03
 385    __READ_Y              = 0x05
 386    __READ_Z              = 0x07
 387    __READ_T              = 0x09
 388    __SET_SHAKE_INT       = 0x0A
 389    __SET_TILT_INT        = 0x0B
 390    __READ_SETTINGS       = 0x0D
 391    __WRITE_SETTINGS      = 0x0D
 392    # settings are a bit field
 393    __SETTING_POWERDOWN   = 0b00000001
 394    __SETTING_POWERUP     = 0b00000000
 395    __SETTING_RANGE       = 0b01100000
 396    __SETTING_2G          = 0b00000000
 397    __SETTING_4G          = 0b00100000
 398    __SETTING_8G          = 0b01000000
 399
 400    def __init__(self, reserve_interrupt:bool=False):
 401        ''' Initialize the accelerometer '''
 402        time.sleep(0.5)
 403        self.__i2c = board.I2C()
 404        '''
 405        while not self.__i2c.try_lock():
 406            pass
 407        '''
 408        self.__addr = expAccelerometer.__DEFAULT_ADDR
 409        self.__range = 0
 410        self.__power = 0
 411        self.__data = bytearray(2)
 412        self.__x = 0    # x axis (raw)
 413        self.__y = 0    # y axis (raw)
 414        self.__z = 0    # z axis (raw)
 415        self.__t = 0    # temperature (celsius)
 416        time.sleep(0.2) # this may not be 100% necessary but we give the device some time to settle
 417        self.__i2c.unlock()
 418        val = self.__read_byte(expAccelerometer.__READ_SETTINGS)
 419        self.__power = val & expAccelerometer.__SETTING_POWERDOWN
 420        self.__range = (0x01 << (((val & expAccelerometer.__SETTING_RANGE) >> 5) + 1)) # 2, 4, or 8
 421        self.update()   # get initial data to 'prime the pump'
 422        #print ("Data: X:{:+5d}   Y:{:+5d}   Z:{:+5d}   T:{:2d}".format(gyro.x, gyro.y, gyro.z, gyro.temperature))
 423        if reserve_interrupt:
 424            self.__interrupt = None
 425        else:
 426            self.__interrupt = digitalio.DigitalInOut(board.GP28)
 427            self.__interrupt.direction = digitalio.Direction.INPUT
 428            self.__interrupt.pull = digitalio.Pull.UP
 429        val = self.__write_byte(expAccelerometer.__SET_SHAKE_INT, (expAccelerometer.__SETTING_SHAKE)) # option:  | expAccelerometer.__SETTING_ORIENT
 430        # there are several things which can be detected; we simplify the API by generalizing to just one action
 431        self.__orientation = 0
 432        self.__shake = 0
 433
 434    def __repr__(self):
 435        return "MXC4005XC: address:0x{:02X}: range:+/-{:d}g power:{} x:{:+05d} y:{:+05d} z:{:+05d} temperature:{:02d}".format(self.__addr, self.__range, ("DN" if self.__power else "UP"), self.__x, self.__y, self.__z, self.__t)
 436
 437    def __write_byte(self, cmd, val):
 438        ''' internal method to write a command and a byte '''
 439        count = 0
 440        try:
 441            # print ("I2C Write: %02X %02X" % (cmd, val))
 442            if self.__i2c.try_lock():
 443                # command and data must be sent together for proper timing
 444                self.__i2c.writeto(self.__addr, bytes([cmd, val]))
 445                self.__i2c.unlock()
 446            else:
 447                self.__i2c.unlock()
 448                print ("********  I2C Write lock error  ********")
 449        except: # OSError as e:
 450            print ("********  I2C Write error  ********")
 451            self.__i2c.unlock()
 452        return val
 453
 454
 455    def __read_byte(self, cmd):
 456        ''' internal method to write a command and then read a byte '''
 457        val = 0
 458        try:
 459            # print ("I2C Read: %02X" % (cmd))
 460            if self.__i2c.try_lock():
 461                self.__i2c.writeto(self.__addr, bytes([cmd]))
 462                self.__i2c.readfrom_into(self.__addr, self.__data)
 463                val = self.__data[0]
 464                self.__i2c.unlock()
 465            else:
 466                self.__i2c.unlock()
 467                print ("********  I2C Read lock error  ********")
 468        except: # OSError as e:
 469            print ("********  I2C Read error  ********")
 470            self.__i2c.unlock()
 471        return val
 472
 473    def __read_doublebyte(self, cmd):
 474        ''' internal method to write a command and read two successive bytes; assumes only high 12 bits are significant '''
 475        val = self.__read_byte(cmd)                 # read MSB
 476        val = (val << 8) + self.__read_byte(cmd+1)  # add MSB and LSB
 477        val = val // 16                             # shift out the empty 4 bits, preserving the sign
 478        if val > 2047:
 479            val = val - 4096                        # adjust to 2's compliment
 480        return val
 481
 482    def update(self):
 483        ''' update should be called within the main loop to compute the read the current accelerometer data '''
 484        self.__x = self.__read_doublebyte(expAccelerometer.__READ_X)
 485        self.__y = self.__read_doublebyte(expAccelerometer.__READ_Y)
 486        self.__z = self.__read_doublebyte(expAccelerometer.__READ_Z)
 487
 488        self.__t = 0
 489        self.__t = self.__read_byte(expAccelerometer.__READ_T)
 490        if self.__t > 127:
 491            self.__t = self.__t - 256
 492        self.__t += 25  # the base temperature reading is 25C
 493
 494        RANGE = 1024    # the datasheet implies this should be 2048
 495        self.__p = self.__x / (RANGE * (-90.0))
 496        self.__r = self.__y / (RANGE * (-90.0))
 497        if (self.__z < 0):
 498            if (self.__y < 0):
 499                self.__r = 180.0 - self.__r
 500            else:
 501                self.__r = -180.0 - self.__r
 502        # print ("Data: X:%+5d    Y:%+5d    Z:%+5d    P:%+5.1f    R:%+5.1f    T:%2d" % (self.__x, self.__y, self.__z, self.__p, self.__r, self.__t))
 503
 504
 505    @property
 506    def interrupt(self) -> bool:
 507        if self.__interrupt is None:
 508            # clear the interrupt, regardless of the pin being controlled or not
 509            val = self.__read_byte(expAccelerometer.__INTERRUPT_CHANGE) # read the data
 510            self.__write_byte(expAccelerometer.__INTERRUPT_CHANGE, val) # clear the data
 511            return val != 0
 512        ''' return a boolean (True) if the interrupt has been triggered '''
 513        if self.__interrupt.value == 0: # pin is pulled LOW to indicate the interrupt has occurred
 514            self.__shake = 0
 515            self.__orientation = 0
 516            
 517            val = self.__read_byte(expAccelerometer.__INTERRUPT_CHANGE) # read the data
 518            self.__write_byte(expAccelerometer.__INTERRUPT_CHANGE, val) # clear the data
 519
 520            #print("interrupt: shake=0x{:02X} tilt=0x{:02X}".format((val & expAccelerometer.__SETTING_SHAKE), (val & expAccelerometer.__SETTING_ORIENT) >> 6))
 521            self.__shake = val & expAccelerometer.__SETTING_SHAKE
 522            self.__orientation = val >> 6
 523            return True
 524        self.__shake = 0
 525        self.__orientation = 0
 526        return False
 527
 528    @property
 529    def shake(self) -> bool:
 530        ''' return a boolean (True) if 'shake' has been detected (use after checking 'interrupt') '''
 531        shake = self.__shake
 532        self.__shake = 0
 533        return shake
 534
 535    @property
 536    def orientation(self) -> int:
 537        ''' return the axis of the orientation change (use after checking 'interrupt') '''
 538        orientation = self.__orientation
 539        self.__orientation = 0
 540        return orientation
 541
 542    @property
 543    def x(self) -> int:
 544        ''' return the current X axis acceleration value (int) '''
 545        # the datasheet says this is +/- 2048 but I'm only seeing +/- 1024
 546        return self.__x
 547
 548    @property
 549    def y(self) -> int:
 550        ''' return the current Y axis acceleration value (int) '''
 551        # the datasheet says this is +/- 2048 but I'm only seeing +/- 1024
 552        return self.__y
 553
 554    @property
 555    def z(self) -> int:
 556        ''' return the current Z axis acceleration value (int) '''
 557        return self.__z
 558
 559    @property
 560    def temperature(self) -> int:
 561        ''' return the current celsius temperature value (int) '''
 562        return self.__t
 563
 564    @property
 565    def range(self) -> int:
 566        ''' return the current acceleration g-force range (int) : 2, 4, or 8 '''
 567        val = self.__read_byte(expAccelerometer.__READ_SETTINGS)
 568        power = val & expAccelerometer.__SETTING_POWERDOWN
 569        # return 2, 4, or 8
 570        return (0x01 << (((val & expAccelerometer.__SETTING_RANGE) >> 5) + 1))
 571    @range.setter
 572    def range(self, range):
 573        ''' change the acceleration g-force range (int) : 2, 4, or 8 '''
 574        if range <= 2:
 575            range = 0
 576        elif range <= 4:
 577            range = 1
 578        else:
 579            range = 2
 580        range = range << 5
 581        self.__write_byte(expAccelerometer.__WRITE_SETTINGS, range)
 582
 583    @property
 584    def power(self) -> bool:
 585        ''' return boolean (True) when the accelerometer is powered up and active '''
 586        val = self.__read_byte(expAccelerometer.__READ_SETTINGS)
 587        return (val & expAccelerometer.__SETTING_POWERDOWN)
 588    @power.setter
 589    def power(self, value:int):
 590        ''' set the accelerometer powered state (1=on 0=off) '''
 591        if value:
 592            ''' power up the chip after it hs been powered down '''
 593            self.__write_byte(expAccelerometer.__WRITE_SETTINGS, expAccelerometer.__SETTING_POWERUP)
 594        else:
 595            ''' power down the chip '''
 596            self.__write_byte(expAccelerometer.__WRITE_SETTINGS, expAccelerometer.__SETTING_POWERDOWN)
 597
 598
 599class expEEPROM:
 600    '''
 601        The CAT24C16W is a 2KB EEPROM with I2C interface.
 602            - I2C is a 2-wire serial interface which allows many devices to share the same wires.
 603            - Each I2C device, sharing the I2C wires, has its own 1-byte address.
 604            - CircuitPython provides a means to scan I2C for all attached devices.
 605
 606        The EEPROM has a series of I2C addresses starting with `0x50` and is accessed using `board.I2C()`.
 607        The 2KB is broken into 8 256 byte pages.
 608        
 609        Reads and writes can be to specific addresses or - by setting 'position' they can be sequential.
 610    '''
 611    
 612    def __init__(self) -> None:
 613        self._max_size = 0x800  # 2KB
 614        self._page_count = 8
 615        self._current_position = 0  # only support sequential reads or writes
 616    
 617        self._i2c = board.I2C()
 618        self._base_address = 0x50    # each 'address' is 256 bytes; a total of 8 addresses are used
 619        # thee are created here to avoid repeated memory allocations
 620        self._data = bytearray(1)
 621        self._read_cmd = bytearray(1)
 622        self._write_cmd = bytearray(2)
 623        
 624    def __len__(self) -> int:
 625        return self._max_size
 626
 627    def _read_eeprom(self, count: int, fixed=None) -> int:
 628        ''' internal method to read a byte or word '''
 629        if count not in (1, 2):
 630            raise ValueError("Read 1 or 2 bytes only")
 631        if fixed is not None:
 632            address = fixed
 633        else:
 634            address = self._current_position
 635        try:
 636            if self._i2c.try_lock():
 637                block = address // 256
 638                if block >= self._page_count:
 639                    raise ValueError("Block out of range")
 640                offset = address % 256
 641                self._read_cmd[0] = offset
 642                self._i2c.writeto(block + self._base_address, self._read_cmd)
 643                self._i2c.readfrom_into(self._base_address, self._data)
 644                val = self._data[0]
 645                if count == 2:
 646                    address += 1
 647                    block = address // 256
 648                    if block >= self._page_count:
 649                        raise ValueError("Block out of range")
 650                    offset = address % 256
 651                    self._read_cmd[0] = offset
 652                    self._i2c.writeto(block + self._base_address, self._read_cmd)
 653                    self._i2c.readfrom_into(self._base_address, self._data)
 654                    val |= self._data[0] << 8
 655                self._i2c.unlock()
 656            else:
 657                self._i2c.unlock()
 658                print ("Rats! The storage is locked and I can't find the key")
 659                val = -1
 660        except Exception as e:
 661            self._i2c.unlock()
 662            print ("EEEK! I can't read what was written. Something about ", e)
 663            val = -1
 664        if fixed is None:
 665            self._current_position += count
 666        #print (f"Read {count} bytes = {val}")   # DELETEME
 667        return val
 668
 669    def __write_eeprom(self, count: int, val: int, fixed=None) -> int:
 670        ''' internal method to write a byte or word '''
 671        if count not in (1, 2):
 672            raise ValueError("Read 1 or 2 bytes only")
 673
 674        if fixed is not None:
 675            address = fixed
 676        else:
 677            address = self._current_position
 678        try:
 679            if self._i2c.try_lock():
 680                # command and data must be sent together for proper timing
 681                block = address // 256
 682                if block >= self._page_count:
 683                    raise ValueError("Block out of range")
 684                offset = address % 256
 685                self._write_cmd[0] = offset
 686                self._write_cmd[1] = val & 0xFF
 687                self._i2c.writeto(block + self._base_address, self._write_cmd)
 688                time.sleep(0.006) # 5ms write cycle time plus a little extra
 689                if count == 2:
 690                    next_address = address + 1
 691                    block = next_address // 256
 692                    if block >= self._page_count:
 693                        raise ValueError("Block out of range")
 694                    offset = next_address % 256
 695                    self._write_cmd[0] = offset
 696                    self._write_cmd[1] = (val >> 8) & 0xFF
 697                    self._i2c.writeto(block + self._base_address, self._write_cmd)
 698                    time.sleep(0.006)
 699                self._i2c.unlock()
 700                result = self._read_eeprom(count, fixed=address)
 701            else:
 702                print ("What?! The storage is locked and I can't find the key")
 703                self._i2c.unlock()
 704                result = -1
 705        except Exception as e:
 706            print ("EEEK! I have forgotten how to write. Something about ", e)
 707            self._i2c.unlock()
 708            result = -1
 709        if fixed is None:
 710            self._current_position += count
 711        #print (f"Write {count} bytes = {val} == {result}") # DELETEME
 712        return val
 713
 714    def readByte(self, addr=None) -> int:
 715        if addr is not None:
 716            return self._read_eeprom(1, fixed=addr)
 717        return self._read_eeprom(1)
 718        
 719    def readWord(self, addr=None) -> int:
 720        if addr is not None:
 721            return self._read_eeprom(2, fixed=addr)
 722        return self._read_eeprom(2)
 723
 724    def writeByte(self, val: int, addr=None) -> int:
 725        if addr is not None:
 726            return self.__write_eeprom(1, val, fixed=addr)
 727        return self.__write_eeprom(1, val)
 728    
 729    def writeWord(self, val: int, addr=None) -> int:
 730        if addr is not None:
 731            return self.__write_eeprom(2, val, fixed=addr)
 732        return self.__write_eeprom(2, val)
 733
 734    @property
 735    def position(self) -> int:
 736        return self._current_position
 737    @position.setter
 738    def position(self, value: int) -> None:
 739        self._current_position = value
 740
 741
 742class expIR:
 743    '''
 744    The badge has both an IR receiver and emitter. It can support a range of IR protocols.
 745    For the purposed of this library, the pulse sizes are close to but not exactly NEC format.
 746
 747    IR Data Format:
 748        The header has a very long pulse and a long gap.
 749        Each bit of the binary data starts with a marker pulse and then a long or short gap.
 750        Most formats include a stop bit at the end which is the same as a 'zero' bit.
 751        
 752    CircuitPython does an excellent job of decoding arbitrary IR signals.
 753    Other implementations may only support fixed size messages.
 754    
 755    The below pulse durations are from the NEC protocol. Other protocols and devices may have
 756    different pulse durations. The NEC protocol is a common format used by many devices.
 757    These pulse durations are also found in most Adafruit IR examples.
 758    '''
 759
 760    IR_HEADER = [9000, 4500]
 761    IR_START = 560
 762    IR_SHORT = 560
 763    IR_LONG = 1700
 764    IR_ZERO = [IR_START, IR_SHORT]
 765    IR_ONE  = [IR_START, IR_LONG]
 766
 767    def __init__(self):
 768        ''' Initialize the IR receiver and transmitter '''
 769        self.__ir_rx = pulseio.PulseIn(board.GP1, maxlen=200, idle_state=True)
 770        self.__ir_tx = pulseio.PulseOut(board.GP0, frequency=38000, duty_cycle=2**15)
 771        self.__decoder = adafruit_irremote.GenericDecode()
 772        self.__encoder = adafruit_irremote.GenericTransmit(header=expIR.IR_HEADER, one=expIR.IR_ONE, zero=expIR.IR_ZERO, trail=255, debug=False)
 773        self.__active = True
 774        
 775        #message = [0, 3, 5] # cmd, brightness, color (we are only interested in the color)
 776    def __repr__(self):
 777        return "expIR: receiver=board.RX transmitter=board.TX"
 778    
 779    def read(self) -> list:
 780        if not self.__active:
 781            return None
 782        ''' return a list of the received IR data '''
 783        pulses = self.__decoder.read_pulses(self.__ir_rx)
 784        count = len(pulses)
 785        if count < 4:
 786            print("Insufficient data:", count, pulses)
 787            return None
 788        try:
 789            code = list(self.__decoder.decode_bits(pulses))
 790            #print("Received:", code)
 791            return code
 792        except Exception as e:
 793            print("Failed to decode:", e, "raw:", count, pulses)
 794            return None
 795
 796    def write(self, value):
 797        if not self.__active:
 798            return
 799        ''' transmit the IR data '''
 800        if type(value) is not list:
 801            value = [value]
 802        #print("Sending: ", value)
 803        self.__ir_rx.pause()
 804        self.__encoder.transmit(self.__ir_tx, value)
 805        self.__ir_rx.resume()
 806        self.__ir_rx.clear()
 807        
 808    @property
 809    def available(self) -> int:
 810        count = len(self.__ir_rx)
 811        if count <= 2:
 812            self.__ir_rx.clear()
 813            count = 0
 814        return count
 815
 816    @property
 817    def active(self) -> bool:
 818        return self.__active
 819    @active.setter
 820    def active(self, value: bool):
 821        self.__active = value
 822        if value:
 823            self.__ir_rx.clear()
 824            self.__ir_rx.resume()
 825        else:
 826            self.__ir_rx.pause()
 827            self.__ir_rx.clear()
 828
 829
 830class expDisplay:
 831    '''
 832        The badge has an 200x200 tri-color (RBW) ePaper display.
 833        Tri-color ePaper displays are very slow to refresh - taking 15-20 seconds.
 834
 835        The display is accessed using `board.DISPLAY`.
 836
 837        **Note** CircuitPython will treat the ePaper display as an output device for REPL and error messages.
 838        To prevent this behavior, use `expDisplay.disable()` to disable the display.
 839        This will preserve any core content from changing the display
 840        It will also prevent any intended content from appearing on the display.
 841        Use `expDisplay.disable()` to reenable the display.
 842        At anytime, use `expDisplay.available()` to check the availability of the display.
 843    '''
 844    
 845    WIDTH:ClassVar[int] = 200
 846    HEIGHT:ClassVar[int] = 200
 847    BLACK:ClassVar[int] = 0x000000
 848    WHITE:ClassVar[int] = 0xFFFFFF
 849    RED:ClassVar[int]   = 0xFF0000
 850
 851    def __init__(self):
 852        '''
 853        Setup the on-board Display (will generate an error message if the display is not available for output)
 854
 855        Available Class Constants:
 856        '''
 857        self.__display = board.DISPLAY
 858        self.__display.root_group = displayio.Group()
 859        
 860        # determine Explorer Badge variation
 861        try:
 862            self._type = board.VID()    # will be a 1 or a 2
 863            if self._type not in [1, 2]:
 864                raise ValueError
 865            if self._type == 1:
 866                self._refresh_rate = 20 # this is the minimum refresh rate but a longer one is advices
 867            else:
 868                self._refresh_rate = 5  # this is the minimum refresh rate but a longer one is advices
 869        except:
 870            self._type = 0
 871            print("ERROR: unrecognized board")
 872            return
 873        
 874    def __repr__(self):
 875        if self._type == 1:
 876            driver = "SSD1681 BWR"
 877        elif self._type == 2:
 878            driver = "SSD1608 BW"
 879        else:
 880            driver = "unknown"
 881        return f"expDisplay: {driver} ePaper with refresh rate={(self._refresh_rate):d}"
 882
 883    @staticmethod
 884    def disable():
 885        ''' disable the display for all output - affects REPL, error messages, and user code '''
 886        if __display_en_pin:
 887            __display_en_pin.value = False # set to OFF
 888    @staticmethod
 889    def enable():
 890        ''' enable the display for all output - affects REPL, error messages, and user code '''
 891        if __display_en_pin:
 892            __display_en_pin.value = True # set to ON
 893    @staticmethod
 894    def available() -> bool:
 895        ''' return boolean (True) if the on-board display is available for user '''
 896        if __display_en_pin:
 897            return (__display_en_pin.value)
 898        return False
 899
 900    def background(self, bg):
 901        '''
 902            Assign a background
 903
 904            Use the class constants for colors or provide a filename for an image.
 905        '''
 906        
 907        if type(bg) is int:
 908            # simple white background
 909            background_bitmap = displayio.Bitmap(expDisplay.WIDTH, expDisplay.HEIGHT, 1)
 910            palette = displayio.Palette(1)
 911            palette[0] = bg
 912            background_tile = displayio.TileGrid(background_bitmap, pixel_shader=palette)
 913            self.__display.root_group.append(background_tile)
 914        elif type(bg) is str:
 915            try:
 916                pic = displayio.OnDiskBitmap(bg)
 917                tile = displayio.TileGrid(pic, pixel_shader=pic.pixel_shader)
 918                #print("shader =", pic.pixel_shader, dir(pic), dir(pic.pixel_shader))
 919                self.__display.root_group.append(tile)
 920            except:
 921                print(f"Error: unable to access '{bg}' file")
 922        else:
 923            print("ValueError: unsupported type {type(bg)} for background")
 924
 925    def text(self, fontname:str, msg:str, fgcolor:int, bgcolor:int=None, x:int=0, y:int=0):
 926        """
 927        Add text to the display
 928
 929        Use the class constants for colors
 930
 931        The justification of the text is automatically determined based on the `X` and `Y` values as follows:
 932
 933         * X in left 1/3rd then left-justified
 934         * X in middle 1/3rd then center-justified
 935         * X in right 1/3rd then right-justified
 936         * Y in upper 1/3rd then top-justified
 937         * Y in middle 1/3rd then center-justified
 938         * Y in lower 1/3rd then bottom-justified
 939
 940        The `fontname` assumes the `/fonts/` folder but does not assume the font extension *(.bdf or .pcf)*.
 941        Choose a font with will fit the length of the longest line in the `msg`.
 942
 943        To use a multi-line messages, add `\\n` between the lines: e.g. 'Hello\\nWorld'.
 944
 945        For more control, the sky is the limit when using the various CircuitPython display libraries.
 946        """
 947        # FYI: in the doc above the use of '\\n' is because the doc generator barfs on '\n'
 948
 949        x = int(x)
 950        y = int(y)
 951        try:
 952            font_file_name = "/fonts/" + fontname
 953            font = bitmap_font.load_font(font_file_name)
 954            scaling=1
 955            font_height = (font.ascent + font.descent)
 956            box = font.get_bounding_box()
 957            box_height = box[1] - box[3]
 958            # use the larger of font_height and box_height
 959            if font_height < box_height:
 960                font_height = box_height
 961        except:
 962            print(f"Warning: unable to locate font '{font_file_name}. Using 'terminalio' font")
 963            font = terminalio.FONT
 964            scaling=3
 965            font_height = 8 * scaling
 966
 967        if font_height > 32:
 968            font_height = int(font_height * 0.85)   # tighten up big text just a bit
 969
 970        #bgcolor = expDisplay.BLACK if color == expDisplay.WHITE else expDisplay.WHITE
 971
 972        # we will compute the bounding box anchor based on the screen coordinates
 973        anchor_x = 0.0
 974        anchor_y = 0.0
 975        if y < (expDisplay.HEIGHT / 3):
 976            # top third: anchor to the bottom of the bounding box
 977            anchor_y = 1.0
 978        elif y < ((expDisplay.HEIGHT / 3) * 2):
 979            # middle third: anchor to the vertical middle of the bounding box
 980            anchor_y = 0.5
 981        else:
 982            # bottom third: anchor to the top of the bounding box
 983            anchor_y = 0.0
 984        if x < (expDisplay.WIDTH / 3):
 985            # left third: anchor to the left of the bounding box
 986            anchor_x = 0.0
 987        elif x < ((expDisplay.WIDTH / 3) * 2):
 988            # middle third: anchor to the horizontal middle of the bounding box
 989            anchor_x = 0.5
 990        else:
 991            # right third: anchor to the right of the bounding box
 992            anchor_x = 1.0
 993
 994        #print("Text anchor", (anchor_x, anchor_y), "with origin", (x,y))
 995        texts = msg.split("\n")             # break up multiple lines
 996        #print(msg,texts)
 997        offset = 0 - int((font_height * (len(texts)-1)) / 2)  # starting position is number of lines above and below center
 998
 999        # add all the lines
1000        for i in range (len(texts)):
1001            text = label.Label(font,
1002                            text=texts[i],
1003                            color=fgcolor,
1004                            background_color=bgcolor,
1005                            #padding_left=4,
1006                            #padding_top=4,
1007                            #padding_right=4,
1008                            #line_spacing=0.75,
1009                            scale=scaling)
1010            text.anchor_point = (anchor_x, anchor_y)
1011            text.anchored_position = (x,y+offset)
1012
1013            '''
1014            # if we want to support a tight background, then we need to clear the background by offset rendering message using a background color in multiple directions
1015            # we could also support a drop shadow by first offset rendering the message with a background color and then rendering hte message on top
1016            '''
1017            self.__display.root_group.append(text)
1018            offset += font_height
1019
1020    def image(self, file_name:str, x:int=0, y:int=0):
1021        '''
1022            Add an image to the display
1023
1024            The `file_name` must be fully qualified with a leading slash ("/").
1025            The `X` and `Y` refer to the placement of the upper left of the image.
1026        '''
1027        try:
1028            pic = displayio.OnDiskBitmap(file_name)
1029            tile = displayio.TileGrid(pic, pixel_shader=pic.pixel_shader)
1030            #print("shader =", pic.pixel_shader, dir(pic), dir(pic.pixel_shader))
1031            self.__display.root_group.append(tile)
1032        except:
1033            print("Error: image missing: {}".format(file_name))
1034
1035    def refresh(self, wait:bool=False):
1036        ''' refresh the display after adding background, text, images, or shapes (ePaper do not auto-refresh) '''
1037        remaining = self.__display.time_to_refresh
1038        if remaining > 0.5:
1039            print (f"waiting {remaining:4.2f} ...")
1040        time.sleep(remaining)
1041        try:
1042            self.__display.refresh()
1043            #print("ePaper display updated")
1044            if wait:
1045                remaining = self.__display.time_to_refresh
1046                time.sleep(remaining)
1047                while self.__display.busy:
1048                    time.sleep(0.1)
1049                print("ePaper display refresh complete")
1050        except Exception as e:
1051            print("Error: unable to update display -", e)
1052        return self.__display.time_to_refresh
1053    
1054    @property
1055    def busy(self) -> bool:
1056        ''' return boolean (True) if the display is currently refreshing '''
1057        return self.__display.busy
DEVICE_COUNT = 9
class expLed:
 83class expLed:
 84    '''
 85        The board has a simple status LED on the back.
 86        CircuitPython will use this LED to indicate any errors.
 87
 88        Blink Code:
 89        1. blinks = the code has been loaded and is running
 90        2. blinks = the code has stopped (either from a code error or CTRL-C)
 91        3. blinks = there is a hardware problem and CircuitPython did not load correctly
 92
 93        The LED is also available to user code.
 94        The LED is controlled from a pin available from `board.LED` or `board.GP4`
 95    '''
 96    def __init__(self):
 97        ''' setup the on-board LED '''
 98        self.__led = digitalio.DigitalInOut(board.LED)
 99        self.__led.direction = digitalio.Direction.OUTPUT
100        self.__led.value = 0 # the initial status LED is OFF
101
102    def __repr__(self):
103        return ("expLed: state={}".format("ON" if self.__led.value else "OFF"))
104
105    def on(self):
106        ''' turn LED on '''
107        self.__led.value = 1 # the status LED is ON
108    def off(self):
109        ''' turn LED off '''
110        self.__led.value = 0 # the status LED is OFF
111    def blink(self, duration:float=0.5):
112        ''' blink LED for a duration '''
113        self.__led.value = 1
114        time.sleep(duration)
115        self.__led.value = 0

The board has a simple status LED on the back. CircuitPython will use this LED to indicate any errors.

Blink Code:

  1. blinks = the code has been loaded and is running
  2. blinks = the code has stopped (either from a code error or CTRL-C)
  3. blinks = there is a hardware problem and CircuitPython did not load correctly

The LED is also available to user code. The LED is controlled from a pin available from board.LED or board.GP4

expLed()
 96    def __init__(self):
 97        ''' setup the on-board LED '''
 98        self.__led = digitalio.DigitalInOut(board.LED)
 99        self.__led.direction = digitalio.Direction.OUTPUT
100        self.__led.value = 0 # the initial status LED is OFF

setup the on-board LED

def on(self):
105    def on(self):
106        ''' turn LED on '''
107        self.__led.value = 1 # the status LED is ON

turn LED on

def off(self):
108    def off(self):
109        ''' turn LED off '''
110        self.__led.value = 0 # the status LED is OFF

turn LED off

class expNeopixels:
118class expNeopixels:
119    '''
120        The board has a chain of Neopixels - controllable RGB LEDs which are capable of displaying any color.
121        The Neopixels are controlled from a pin defined from `board.NEOPIXEL` or `board.GP5`.
122    '''
123    COUNT:ClassVar[int] = DEVICE_COUNT  # number of neopixels on the badge
124
125    def __init__(self):
126        '''
127        Setup the six on-board Neopixel LEDs
128
129        Available Class Constants:
130        '''
131        #print("initializing the {:d} neopixels".format(count))
132        self.__size = expNeopixels.COUNT
133        self.__neopixels = neopixel.NeoPixel(board.NEOPIXEL, self.__size, brightness=0.1, auto_write=True)
134        self.clear() # start with neopixels as BLACK aka OFF
135
136    def __repr__(self):
137        return ("dckNeopixel: size={:d}  values=[{}]".format(self.__size, repr(self.__neopixels)))
138
139    def set(self, index:int, color:int):
140        ''' set one Neopixel LED (index) to an RGB color (0xRRGGBB) '''
141        index = index % self.__size # prevent overflow
142        self.__neopixels[index] = color
143    def fill(self, color:int):
144        ''' set all Neopixel LEDs to an RGB color (0xRRGGBB) '''
145        self.__neopixels.fill(color)
146    def clear(self):
147        ''' set all Neopixel LEDs to Black '''
148        self.__neopixels.fill(0x000000)

The board has a chain of Neopixels - controllable RGB LEDs which are capable of displaying any color. The Neopixels are controlled from a pin defined from board.NEOPIXEL or board.GP5.

expNeopixels()
125    def __init__(self):
126        '''
127        Setup the six on-board Neopixel LEDs
128
129        Available Class Constants:
130        '''
131        #print("initializing the {:d} neopixels".format(count))
132        self.__size = expNeopixels.COUNT
133        self.__neopixels = neopixel.NeoPixel(board.NEOPIXEL, self.__size, brightness=0.1, auto_write=True)
134        self.clear() # start with neopixels as BLACK aka OFF

Setup the six on-board Neopixel LEDs

Available Class Constants:

expNeopixels.COUNT: ClassVar[int] = 9
def set(self, index: int, color: int):
139    def set(self, index:int, color:int):
140        ''' set one Neopixel LED (index) to an RGB color (0xRRGGBB) '''
141        index = index % self.__size # prevent overflow
142        self.__neopixels[index] = color

set one Neopixel LED (index) to an RGB color (0xRRGGBB)

def fill(self, color: int):
143    def fill(self, color:int):
144        ''' set all Neopixel LEDs to an RGB color (0xRRGGBB) '''
145        self.__neopixels.fill(color)

set all Neopixel LEDs to an RGB color (0xRRGGBB)

def clear(self):
146    def clear(self):
147        ''' set all Neopixel LEDs to Black '''
148        self.__neopixels.fill(0x000000)

set all Neopixel LEDs to Black

class expTouch:
151class expTouch:
152    '''
153        The letters on the face of the board (D-C-K-i-d-s) are touch sensitive.
154        The touch sensors are controlled from pins available
155        from `board.TOUCH1` thru `board.TOUCH9` or as `board.GP19` thru `board.GP27`.
156    '''
157    COUNT:ClassVar[int] = DEVICE_COUNT  # number of touch pads on the badge
158
159    def __init__(self):
160        '''
161        Initialize the touch pads and setup lists (arrays) to maintain data about each touch pad
162
163        Available Class Constants:
164        '''
165
166        #print("initializing the {:d} touch sensors".format(count))
167        PINS: list = [board.TOUCH1, board.TOUCH2, board.TOUCH3, board.TOUCH4, board.TOUCH5, board.TOUCH6, board.TOUCH7, board.TOUCH8, board.TOUCH9]
168        self.__size = expTouch.COUNT
169        # we track current and previous state so we can support 'press' and 'release'
170        self.__new_states = [0 for i in range(self.__size)]  # current states
171        self.__old_states = [0 for i in range(self.__size)]  # previous states
172        self.__raws =       [0 for i in range(self.__size)]  # raw data values corresponding to current state
173        self.__sensors = [None for i in range(self.__size)]
174        for i in range(self.__size):
175            self.__sensors[i] = touchio.TouchIn(PINS[i])
176            # the minimum sensitivity (aka threshold) is set when the touch sensor is created
177            # print("initialized touch {:d} with threshold {:d}".format(i, self.__touch_sensors[i].threshold))
178
179    def __repr__(self):
180        vals = [str(sensor.threshold) for sensor in self.__sensors]
181        return ("expTouch: size={:d}  threshold=[{}]".format(self.__size, ", ".join(vals)))
182
183    def update(self):
184        ''' update should be called within the main loop to compute the current state of each touch sensor '''
185        for i in range(self.__size):
186            self.__old_states[i] = self.__new_states[i]
187            self.__new_states[i] = self.__sensors[i].value
188            self.__raws[i] = self.__sensors[i].raw_value if self.__new_states[i] else 0
189
190    def touching(self, index:int) -> bool:
191        ''' return a boolean (True) if the touch sensor (index) is currently being touched '''
192        # 'touching' means the sensor is being touched
193        index = index % self.__size # prevent overflow
194        if self.__new_states[index]:
195            return True
196        return False
197
198    def touched(self, index:int) -> bool:
199        ''' return a boolean (True) if the touch sensor (index) is newly being touched '''
200        # 'touched' means the sensor is being touched but was not in the prior update
201        index = index % self.__size # prevent overflow
202        if self.__new_states[index] and not self.__old_states[index]:
203            return True
204        return False
205
206    def released(self, index:int) -> bool:
207        ''' return a boolean (True) if the touch sensor (index) is newly no longer being touched '''
208        # 'released' means the sensor is not being touched but was in the prior update
209        index = index % self.__size # prevent overflow
210        if not self.__new_states[index] and self.__old_states[index]:
211            return True
212        return False
213
214    def raw(self, index) -> int:
215        ''' return the raw data value of the touch sensor (index) '''
216        index = index % self.__size # prevent overflow
217        return self.__raws[index]
218
219    def threshold(self, index:int) -> int:
220        ''' return the threshold value of the touch sensor (index) '''
221        index = index % self.__size # prevent overflow
222        return self.__sensors[index].threshold

The letters on the face of the board (D-C-K-i-d-s) are touch sensitive. The touch sensors are controlled from pins available from board.TOUCH1 thru board.TOUCH9 or as board.GP19 thru board.GP27.

expTouch()
159    def __init__(self):
160        '''
161        Initialize the touch pads and setup lists (arrays) to maintain data about each touch pad
162
163        Available Class Constants:
164        '''
165
166        #print("initializing the {:d} touch sensors".format(count))
167        PINS: list = [board.TOUCH1, board.TOUCH2, board.TOUCH3, board.TOUCH4, board.TOUCH5, board.TOUCH6, board.TOUCH7, board.TOUCH8, board.TOUCH9]
168        self.__size = expTouch.COUNT
169        # we track current and previous state so we can support 'press' and 'release'
170        self.__new_states = [0 for i in range(self.__size)]  # current states
171        self.__old_states = [0 for i in range(self.__size)]  # previous states
172        self.__raws =       [0 for i in range(self.__size)]  # raw data values corresponding to current state
173        self.__sensors = [None for i in range(self.__size)]
174        for i in range(self.__size):
175            self.__sensors[i] = touchio.TouchIn(PINS[i])
176            # the minimum sensitivity (aka threshold) is set when the touch sensor is created
177            # print("initialized touch {:d} with threshold {:d}".format(i, self.__touch_sensors[i].threshold))

Initialize the touch pads and setup lists (arrays) to maintain data about each touch pad

Available Class Constants:

expTouch.COUNT: ClassVar[int] = 9
def update(self):
183    def update(self):
184        ''' update should be called within the main loop to compute the current state of each touch sensor '''
185        for i in range(self.__size):
186            self.__old_states[i] = self.__new_states[i]
187            self.__new_states[i] = self.__sensors[i].value
188            self.__raws[i] = self.__sensors[i].raw_value if self.__new_states[i] else 0

update should be called within the main loop to compute the current state of each touch sensor

def touching(self, index: int) -> bool:
190    def touching(self, index:int) -> bool:
191        ''' return a boolean (True) if the touch sensor (index) is currently being touched '''
192        # 'touching' means the sensor is being touched
193        index = index % self.__size # prevent overflow
194        if self.__new_states[index]:
195            return True
196        return False

return a boolean (True) if the touch sensor (index) is currently being touched

def touched(self, index: int) -> bool:
198    def touched(self, index:int) -> bool:
199        ''' return a boolean (True) if the touch sensor (index) is newly being touched '''
200        # 'touched' means the sensor is being touched but was not in the prior update
201        index = index % self.__size # prevent overflow
202        if self.__new_states[index] and not self.__old_states[index]:
203            return True
204        return False

return a boolean (True) if the touch sensor (index) is newly being touched

def released(self, index: int) -> bool:
206    def released(self, index:int) -> bool:
207        ''' return a boolean (True) if the touch sensor (index) is newly no longer being touched '''
208        # 'released' means the sensor is not being touched but was in the prior update
209        index = index % self.__size # prevent overflow
210        if not self.__new_states[index] and self.__old_states[index]:
211            return True
212        return False

return a boolean (True) if the touch sensor (index) is newly no longer being touched

def raw(self, index) -> int:
214    def raw(self, index) -> int:
215        ''' return the raw data value of the touch sensor (index) '''
216        index = index % self.__size # prevent overflow
217        return self.__raws[index]

return the raw data value of the touch sensor (index)

def threshold(self, index: int) -> int:
219    def threshold(self, index:int) -> int:
220        ''' return the threshold value of the touch sensor (index) '''
221        index = index % self.__size # prevent overflow
222        return self.__sensors[index].threshold

return the threshold value of the touch sensor (index)

class expTone:
225class expTone:
226    '''
227        CircuitPython supports simple square wave PWM (pulse width modulation) tones.
228        The badge has a small speaker with a mono amplifier.
229        The sound is generated on a single pin from `board.SPEAKER` or `board.GP6`.
230        The amplifier is controlled with a pin from `board.SPEAKER_EN` or `board.GP7`.
231    '''
232    def __init__(self):
233        ''' initialize PWM tone generation and the automatic amplifier control '''
234        # initialize the sound amplifyer as OFF
235        self.__amplifier = digitalio.DigitalInOut(board.SPEAKER_EN)
236        self.__amplifier.direction = digitalio.Direction.OUTPUT
237        self.__amplifier.value = 0 # we start with the amplifier OFF
238        # create a basic synth
239        # simple tones
240        self.__pwm = None
241
242    def __repr__(self):
243        if self.__pwm:
244            return "expTone: frequency={:d}".format(self.__pwm.frequency)
245        return "expTone: frequency=OFF"
246
247
248    def note_on(self, frequency:float=440.0):
249        ''' produce the given frequency (float) as a PWM signal '''
250        frequency = int(frequency)
251        if self.__pwm is None:
252            self.__amplifier.value = True
253            self.__pwm = pwmio.PWMOut(board.SPEAKER, frequency=frequency, variable_frequency=True)
254            self.__pwm.duty_cycle = 0x8000
255        else:
256            self.__pwm.frequency = frequency
257        #print("frequency = %d" % (self.__pwm.frequency))
258
259    def note_off(self, frequency:float=440.0):
260        ''' stop producing sound (the frequency parameter is not used but included for consistency with the expSynth class) '''
261        # the frequency is not used but is included to maintain a compatible API to expSynth
262        if self.__pwm is not None:
263            self.__pwm.deinit()
264            self.__pwm = None
265            self.__amplifier.value = False
266
267    def note_off_all(self):
268        ''' stop producing sound '''
269        # there is only one sound at a time so we can just call `note_off``
270        self.note_off()

CircuitPython supports simple square wave PWM (pulse width modulation) tones. The badge has a small speaker with a mono amplifier. The sound is generated on a single pin from board.SPEAKER or board.GP6. The amplifier is controlled with a pin from board.SPEAKER_EN or board.GP7.

expTone()
232    def __init__(self):
233        ''' initialize PWM tone generation and the automatic amplifier control '''
234        # initialize the sound amplifyer as OFF
235        self.__amplifier = digitalio.DigitalInOut(board.SPEAKER_EN)
236        self.__amplifier.direction = digitalio.Direction.OUTPUT
237        self.__amplifier.value = 0 # we start with the amplifier OFF
238        # create a basic synth
239        # simple tones
240        self.__pwm = None

initialize PWM tone generation and the automatic amplifier control

def note_on(self, frequency: float = 440.0):
248    def note_on(self, frequency:float=440.0):
249        ''' produce the given frequency (float) as a PWM signal '''
250        frequency = int(frequency)
251        if self.__pwm is None:
252            self.__amplifier.value = True
253            self.__pwm = pwmio.PWMOut(board.SPEAKER, frequency=frequency, variable_frequency=True)
254            self.__pwm.duty_cycle = 0x8000
255        else:
256            self.__pwm.frequency = frequency
257        #print("frequency = %d" % (self.__pwm.frequency))

produce the given frequency (float) as a PWM signal

def note_off(self, frequency: float = 440.0):
259    def note_off(self, frequency:float=440.0):
260        ''' stop producing sound (the frequency parameter is not used but included for consistency with the expSynth class) '''
261        # the frequency is not used but is included to maintain a compatible API to expSynth
262        if self.__pwm is not None:
263            self.__pwm.deinit()
264            self.__pwm = None
265            self.__amplifier.value = False

stop producing sound (the frequency parameter is not used but included for consistency with the expSynth class)

def note_off_all(self):
267    def note_off_all(self):
268        ''' stop producing sound '''
269        # there is only one sound at a time so we can just call `note_off``
270        self.note_off()

stop producing sound

class expSynth:
273class expSynth:
274    '''
275        CircuitPython supports polyphonic synth sounds.
276        The badge has a small speaker with a mono amplifier.
277        The synth sounds are generated on a single pin from `board.SPEAKER` or `board.GP6`.
278        The amplifier is controlled with a pin from `board.SPEAKER_EN` or `board.GP7`.
279    '''
280    TRIANGLE:ClassVar[int]   = 0
281    SAWTOOTH:ClassVar[int]   = 1
282    SQUARE:ClassVar[int]     = 2
283    SINE:ClassVar[int]       = 3
284
285    __WAVE_NAMES = ["TRIANGLE", "SAWTOOTH", "SQUARE", "SINE"]
286
287    def __init__(self, wave=None):
288        '''
289            Initialize synthesizer generation with a waveform and the automatic amplifier control.
290
291            Warning: The sine wave suffers from approximation and will have less volume than other waveforms.
292
293            Available Class Constants:
294        '''
295
296        self.__amplifier = digitalio.DigitalInOut(board.SPEAKER_EN)
297        self.__amplifier.direction = digitalio.Direction.OUTPUT
298        self.__amplifier.value = False # we start with the amplifier OFF
299        # create a basic synth
300        SAMPLE_SIZE = 1024
301        SAMPLE_VOLUME = 32767 # largest positive number in a 16bit integer
302
303        self.__wave_type = wave
304        if wave == expSynth.SAWTOOTH:
305            # sawtooth wave
306            self.__wave = np.linspace(SAMPLE_VOLUME, -SAMPLE_VOLUME, num=SAMPLE_SIZE, dtype=np.int16)
307        elif wave == expSynth.SQUARE:
308            # full square wave
309            self.__wave = np.concatenate((np.full((SAMPLE_SIZE // 2), SAMPLE_VOLUME, dtype=np.int16), np.full((SAMPLE_SIZE // 2), -SAMPLE_VOLUME, dtype=np.int16)))
310        elif wave == expSynth.SINE:
311            # sine wave : IMPORTANT, without an RC filter, this will be faint
312            self.__wave = np.array(np.sin(np.linspace(0, 2*np.pi, SAMPLE_SIZE, endpoint=False)) * SAMPLE_VOLUME, dtype=np.int16)
313        else:
314            #the synth will default to a triangle waveform
315            self.__wave = None # a triangle waveform is the default
316            self.__wave_type = 0
317
318        self.__synth = synthio.Synthesizer(sample_rate=22050)
319        self.__audio = audiopwmio.PWMAudioOut(board.SPEAKER)
320        self.__audio.play(self.__synth)
321        # some data to track notes and control the amplifier
322        self.__notes = {} # dict used to track notes
323        self.__note_count = 0
324
325    def __repr__(self):
326        notes = [repr(note) for note in self.__notes]
327        return "expSynth: wave={} count={:d} notes=[{}]".format(expSynth.__WAVE_NAMES[self.__wave_type], self.__note_count, ", ".join(notes))
328        pass
329
330    def note_on(self, frequency:float=440.0):
331        ''' add a new note (frequency) to the polyphonic output '''
332        # add to active notes
333        if not self.__note_count:
334            self.__amplifier.value = True
335        synth_note = synthio.Note(frequency, waveform=self.__wave)
336        self.__notes[frequency] = synth_note
337        self.__synth.press(synth_note)
338        self.__note_count += 1
339
340    def note_off(self,  frequency:float=440.0):
341        ''' remove an active note (frequency) from the polyphonic output '''
342        synth_note = self.__notes.get(frequency, None)
343        if synth_note is not None:
344            self.__synth.release(synth_note)
345            self.__notes.pop(frequency)
346            self.__note_count -= 1
347        if not self.__note_count:
348            self.__amplifier.value = False
349
350    def note_off_all(self):
351        ''' remove all existing notes from the polyphonic output '''
352        for note in self.__notes:
353            synth_note = self.__notes.get(note, None)
354            self.__synth.release(synth_note)
355            self.__notes.pop(note)
356        self.__note_count = 0
357        self.__amplifier.value = False

CircuitPython supports polyphonic synth sounds. The badge has a small speaker with a mono amplifier. The synth sounds are generated on a single pin from board.SPEAKER or board.GP6. The amplifier is controlled with a pin from board.SPEAKER_EN or board.GP7.

expSynth(wave=None)
287    def __init__(self, wave=None):
288        '''
289            Initialize synthesizer generation with a waveform and the automatic amplifier control.
290
291            Warning: The sine wave suffers from approximation and will have less volume than other waveforms.
292
293            Available Class Constants:
294        '''
295
296        self.__amplifier = digitalio.DigitalInOut(board.SPEAKER_EN)
297        self.__amplifier.direction = digitalio.Direction.OUTPUT
298        self.__amplifier.value = False # we start with the amplifier OFF
299        # create a basic synth
300        SAMPLE_SIZE = 1024
301        SAMPLE_VOLUME = 32767 # largest positive number in a 16bit integer
302
303        self.__wave_type = wave
304        if wave == expSynth.SAWTOOTH:
305            # sawtooth wave
306            self.__wave = np.linspace(SAMPLE_VOLUME, -SAMPLE_VOLUME, num=SAMPLE_SIZE, dtype=np.int16)
307        elif wave == expSynth.SQUARE:
308            # full square wave
309            self.__wave = np.concatenate((np.full((SAMPLE_SIZE // 2), SAMPLE_VOLUME, dtype=np.int16), np.full((SAMPLE_SIZE // 2), -SAMPLE_VOLUME, dtype=np.int16)))
310        elif wave == expSynth.SINE:
311            # sine wave : IMPORTANT, without an RC filter, this will be faint
312            self.__wave = np.array(np.sin(np.linspace(0, 2*np.pi, SAMPLE_SIZE, endpoint=False)) * SAMPLE_VOLUME, dtype=np.int16)
313        else:
314            #the synth will default to a triangle waveform
315            self.__wave = None # a triangle waveform is the default
316            self.__wave_type = 0
317
318        self.__synth = synthio.Synthesizer(sample_rate=22050)
319        self.__audio = audiopwmio.PWMAudioOut(board.SPEAKER)
320        self.__audio.play(self.__synth)
321        # some data to track notes and control the amplifier
322        self.__notes = {} # dict used to track notes
323        self.__note_count = 0

Initialize synthesizer generation with a waveform and the automatic amplifier control.

Warning: The sine wave suffers from approximation and will have less volume than other waveforms.

Available Class Constants:

expSynth.TRIANGLE: ClassVar[int] = 0
expSynth.SAWTOOTH: ClassVar[int] = 1
expSynth.SQUARE: ClassVar[int] = 2
expSynth.SINE: ClassVar[int] = 3
def note_on(self, frequency: float = 440.0):
330    def note_on(self, frequency:float=440.0):
331        ''' add a new note (frequency) to the polyphonic output '''
332        # add to active notes
333        if not self.__note_count:
334            self.__amplifier.value = True
335        synth_note = synthio.Note(frequency, waveform=self.__wave)
336        self.__notes[frequency] = synth_note
337        self.__synth.press(synth_note)
338        self.__note_count += 1

add a new note (frequency) to the polyphonic output

def note_off(self, frequency: float = 440.0):
340    def note_off(self,  frequency:float=440.0):
341        ''' remove an active note (frequency) from the polyphonic output '''
342        synth_note = self.__notes.get(frequency, None)
343        if synth_note is not None:
344            self.__synth.release(synth_note)
345            self.__notes.pop(frequency)
346            self.__note_count -= 1
347        if not self.__note_count:
348            self.__amplifier.value = False

remove an active note (frequency) from the polyphonic output

def note_off_all(self):
350    def note_off_all(self):
351        ''' remove all existing notes from the polyphonic output '''
352        for note in self.__notes:
353            synth_note = self.__notes.get(note, None)
354            self.__synth.release(synth_note)
355            self.__notes.pop(note)
356        self.__note_count = 0
357        self.__amplifier.value = False

remove all existing notes from the polyphonic output

class expAccelerometer:
360class expAccelerometer:
361    '''
362        DISCLAIMER: The badge *was* to include the MXC4005XC 3-axis accelerometer.
363                    Unfortunately, the accelerometer proved unreliable and has been omitted or disabled.
364                    For completeness, the code is included doe historical completeness.
365
366        It has 12bit values for each axis and defaults to +/- 2G.
367        The accelerometer provides 'acceleration' data for X, Y, and Z.
368        It does not provide 'position' data such as angles.
369        The accelerometer has an interrupt pin to quickly indicate orientation changes and shaking.
370
371        The accelerometer is controlled using I2C.
372            - I2C is a 2-wire serial interface which allows many devices to share the same wires.
373            - Each I2C device, sharing the I2C wires, has its own 1-byte address.
374            - CircuitPython provides a means to scan I2C for all attached devices.
375
376        The accelerometer has an I2C address of `0x15` and is accessed using `board.I2C()`.
377    '''
378    __DEFAULT_ADDR        = 0x15
379    __SETTING_SHAKE       = 0b00001111 # shake -Y +Y -X +X
380    __SETTING_ORIENT      = 0b11000000 # orientation Z and XY
381    __INTERRUPT_CHANGE    = 0x00
382    __INTERRUPT_READY     = 0x01
383    __READ_STATUS         = 0x02
384    __STATUS_READY        = 0b00010000
385    __READ_X              = 0x03
386    __READ_Y              = 0x05
387    __READ_Z              = 0x07
388    __READ_T              = 0x09
389    __SET_SHAKE_INT       = 0x0A
390    __SET_TILT_INT        = 0x0B
391    __READ_SETTINGS       = 0x0D
392    __WRITE_SETTINGS      = 0x0D
393    # settings are a bit field
394    __SETTING_POWERDOWN   = 0b00000001
395    __SETTING_POWERUP     = 0b00000000
396    __SETTING_RANGE       = 0b01100000
397    __SETTING_2G          = 0b00000000
398    __SETTING_4G          = 0b00100000
399    __SETTING_8G          = 0b01000000
400
401    def __init__(self, reserve_interrupt:bool=False):
402        ''' Initialize the accelerometer '''
403        time.sleep(0.5)
404        self.__i2c = board.I2C()
405        '''
406        while not self.__i2c.try_lock():
407            pass
408        '''
409        self.__addr = expAccelerometer.__DEFAULT_ADDR
410        self.__range = 0
411        self.__power = 0
412        self.__data = bytearray(2)
413        self.__x = 0    # x axis (raw)
414        self.__y = 0    # y axis (raw)
415        self.__z = 0    # z axis (raw)
416        self.__t = 0    # temperature (celsius)
417        time.sleep(0.2) # this may not be 100% necessary but we give the device some time to settle
418        self.__i2c.unlock()
419        val = self.__read_byte(expAccelerometer.__READ_SETTINGS)
420        self.__power = val & expAccelerometer.__SETTING_POWERDOWN
421        self.__range = (0x01 << (((val & expAccelerometer.__SETTING_RANGE) >> 5) + 1)) # 2, 4, or 8
422        self.update()   # get initial data to 'prime the pump'
423        #print ("Data: X:{:+5d}   Y:{:+5d}   Z:{:+5d}   T:{:2d}".format(gyro.x, gyro.y, gyro.z, gyro.temperature))
424        if reserve_interrupt:
425            self.__interrupt = None
426        else:
427            self.__interrupt = digitalio.DigitalInOut(board.GP28)
428            self.__interrupt.direction = digitalio.Direction.INPUT
429            self.__interrupt.pull = digitalio.Pull.UP
430        val = self.__write_byte(expAccelerometer.__SET_SHAKE_INT, (expAccelerometer.__SETTING_SHAKE)) # option:  | expAccelerometer.__SETTING_ORIENT
431        # there are several things which can be detected; we simplify the API by generalizing to just one action
432        self.__orientation = 0
433        self.__shake = 0
434
435    def __repr__(self):
436        return "MXC4005XC: address:0x{:02X}: range:+/-{:d}g power:{} x:{:+05d} y:{:+05d} z:{:+05d} temperature:{:02d}".format(self.__addr, self.__range, ("DN" if self.__power else "UP"), self.__x, self.__y, self.__z, self.__t)
437
438    def __write_byte(self, cmd, val):
439        ''' internal method to write a command and a byte '''
440        count = 0
441        try:
442            # print ("I2C Write: %02X %02X" % (cmd, val))
443            if self.__i2c.try_lock():
444                # command and data must be sent together for proper timing
445                self.__i2c.writeto(self.__addr, bytes([cmd, val]))
446                self.__i2c.unlock()
447            else:
448                self.__i2c.unlock()
449                print ("********  I2C Write lock error  ********")
450        except: # OSError as e:
451            print ("********  I2C Write error  ********")
452            self.__i2c.unlock()
453        return val
454
455
456    def __read_byte(self, cmd):
457        ''' internal method to write a command and then read a byte '''
458        val = 0
459        try:
460            # print ("I2C Read: %02X" % (cmd))
461            if self.__i2c.try_lock():
462                self.__i2c.writeto(self.__addr, bytes([cmd]))
463                self.__i2c.readfrom_into(self.__addr, self.__data)
464                val = self.__data[0]
465                self.__i2c.unlock()
466            else:
467                self.__i2c.unlock()
468                print ("********  I2C Read lock error  ********")
469        except: # OSError as e:
470            print ("********  I2C Read error  ********")
471            self.__i2c.unlock()
472        return val
473
474    def __read_doublebyte(self, cmd):
475        ''' internal method to write a command and read two successive bytes; assumes only high 12 bits are significant '''
476        val = self.__read_byte(cmd)                 # read MSB
477        val = (val << 8) + self.__read_byte(cmd+1)  # add MSB and LSB
478        val = val // 16                             # shift out the empty 4 bits, preserving the sign
479        if val > 2047:
480            val = val - 4096                        # adjust to 2's compliment
481        return val
482
483    def update(self):
484        ''' update should be called within the main loop to compute the read the current accelerometer data '''
485        self.__x = self.__read_doublebyte(expAccelerometer.__READ_X)
486        self.__y = self.__read_doublebyte(expAccelerometer.__READ_Y)
487        self.__z = self.__read_doublebyte(expAccelerometer.__READ_Z)
488
489        self.__t = 0
490        self.__t = self.__read_byte(expAccelerometer.__READ_T)
491        if self.__t > 127:
492            self.__t = self.__t - 256
493        self.__t += 25  # the base temperature reading is 25C
494
495        RANGE = 1024    # the datasheet implies this should be 2048
496        self.__p = self.__x / (RANGE * (-90.0))
497        self.__r = self.__y / (RANGE * (-90.0))
498        if (self.__z < 0):
499            if (self.__y < 0):
500                self.__r = 180.0 - self.__r
501            else:
502                self.__r = -180.0 - self.__r
503        # print ("Data: X:%+5d    Y:%+5d    Z:%+5d    P:%+5.1f    R:%+5.1f    T:%2d" % (self.__x, self.__y, self.__z, self.__p, self.__r, self.__t))
504
505
506    @property
507    def interrupt(self) -> bool:
508        if self.__interrupt is None:
509            # clear the interrupt, regardless of the pin being controlled or not
510            val = self.__read_byte(expAccelerometer.__INTERRUPT_CHANGE) # read the data
511            self.__write_byte(expAccelerometer.__INTERRUPT_CHANGE, val) # clear the data
512            return val != 0
513        ''' return a boolean (True) if the interrupt has been triggered '''
514        if self.__interrupt.value == 0: # pin is pulled LOW to indicate the interrupt has occurred
515            self.__shake = 0
516            self.__orientation = 0
517            
518            val = self.__read_byte(expAccelerometer.__INTERRUPT_CHANGE) # read the data
519            self.__write_byte(expAccelerometer.__INTERRUPT_CHANGE, val) # clear the data
520
521            #print("interrupt: shake=0x{:02X} tilt=0x{:02X}".format((val & expAccelerometer.__SETTING_SHAKE), (val & expAccelerometer.__SETTING_ORIENT) >> 6))
522            self.__shake = val & expAccelerometer.__SETTING_SHAKE
523            self.__orientation = val >> 6
524            return True
525        self.__shake = 0
526        self.__orientation = 0
527        return False
528
529    @property
530    def shake(self) -> bool:
531        ''' return a boolean (True) if 'shake' has been detected (use after checking 'interrupt') '''
532        shake = self.__shake
533        self.__shake = 0
534        return shake
535
536    @property
537    def orientation(self) -> int:
538        ''' return the axis of the orientation change (use after checking 'interrupt') '''
539        orientation = self.__orientation
540        self.__orientation = 0
541        return orientation
542
543    @property
544    def x(self) -> int:
545        ''' return the current X axis acceleration value (int) '''
546        # the datasheet says this is +/- 2048 but I'm only seeing +/- 1024
547        return self.__x
548
549    @property
550    def y(self) -> int:
551        ''' return the current Y axis acceleration value (int) '''
552        # the datasheet says this is +/- 2048 but I'm only seeing +/- 1024
553        return self.__y
554
555    @property
556    def z(self) -> int:
557        ''' return the current Z axis acceleration value (int) '''
558        return self.__z
559
560    @property
561    def temperature(self) -> int:
562        ''' return the current celsius temperature value (int) '''
563        return self.__t
564
565    @property
566    def range(self) -> int:
567        ''' return the current acceleration g-force range (int) : 2, 4, or 8 '''
568        val = self.__read_byte(expAccelerometer.__READ_SETTINGS)
569        power = val & expAccelerometer.__SETTING_POWERDOWN
570        # return 2, 4, or 8
571        return (0x01 << (((val & expAccelerometer.__SETTING_RANGE) >> 5) + 1))
572    @range.setter
573    def range(self, range):
574        ''' change the acceleration g-force range (int) : 2, 4, or 8 '''
575        if range <= 2:
576            range = 0
577        elif range <= 4:
578            range = 1
579        else:
580            range = 2
581        range = range << 5
582        self.__write_byte(expAccelerometer.__WRITE_SETTINGS, range)
583
584    @property
585    def power(self) -> bool:
586        ''' return boolean (True) when the accelerometer is powered up and active '''
587        val = self.__read_byte(expAccelerometer.__READ_SETTINGS)
588        return (val & expAccelerometer.__SETTING_POWERDOWN)
589    @power.setter
590    def power(self, value:int):
591        ''' set the accelerometer powered state (1=on 0=off) '''
592        if value:
593            ''' power up the chip after it hs been powered down '''
594            self.__write_byte(expAccelerometer.__WRITE_SETTINGS, expAccelerometer.__SETTING_POWERUP)
595        else:
596            ''' power down the chip '''
597            self.__write_byte(expAccelerometer.__WRITE_SETTINGS, expAccelerometer.__SETTING_POWERDOWN)

DISCLAIMER: The badge was to include the MXC4005XC 3-axis accelerometer. Unfortunately, the accelerometer proved unreliable and has been omitted or disabled. For completeness, the code is included doe historical completeness.

It has 12bit values for each axis and defaults to +/- 2G. The accelerometer provides 'acceleration' data for X, Y, and Z. It does not provide 'position' data such as angles. The accelerometer has an interrupt pin to quickly indicate orientation changes and shaking.

The accelerometer is controlled using I2C. - I2C is a 2-wire serial interface which allows many devices to share the same wires. - Each I2C device, sharing the I2C wires, has its own 1-byte address. - CircuitPython provides a means to scan I2C for all attached devices.

The accelerometer has an I2C address of 0x15 and is accessed using board.I2C().

expAccelerometer(reserve_interrupt: bool = False)
401    def __init__(self, reserve_interrupt:bool=False):
402        ''' Initialize the accelerometer '''
403        time.sleep(0.5)
404        self.__i2c = board.I2C()
405        '''
406        while not self.__i2c.try_lock():
407            pass
408        '''
409        self.__addr = expAccelerometer.__DEFAULT_ADDR
410        self.__range = 0
411        self.__power = 0
412        self.__data = bytearray(2)
413        self.__x = 0    # x axis (raw)
414        self.__y = 0    # y axis (raw)
415        self.__z = 0    # z axis (raw)
416        self.__t = 0    # temperature (celsius)
417        time.sleep(0.2) # this may not be 100% necessary but we give the device some time to settle
418        self.__i2c.unlock()
419        val = self.__read_byte(expAccelerometer.__READ_SETTINGS)
420        self.__power = val & expAccelerometer.__SETTING_POWERDOWN
421        self.__range = (0x01 << (((val & expAccelerometer.__SETTING_RANGE) >> 5) + 1)) # 2, 4, or 8
422        self.update()   # get initial data to 'prime the pump'
423        #print ("Data: X:{:+5d}   Y:{:+5d}   Z:{:+5d}   T:{:2d}".format(gyro.x, gyro.y, gyro.z, gyro.temperature))
424        if reserve_interrupt:
425            self.__interrupt = None
426        else:
427            self.__interrupt = digitalio.DigitalInOut(board.GP28)
428            self.__interrupt.direction = digitalio.Direction.INPUT
429            self.__interrupt.pull = digitalio.Pull.UP
430        val = self.__write_byte(expAccelerometer.__SET_SHAKE_INT, (expAccelerometer.__SETTING_SHAKE)) # option:  | expAccelerometer.__SETTING_ORIENT
431        # there are several things which can be detected; we simplify the API by generalizing to just one action
432        self.__orientation = 0
433        self.__shake = 0

Initialize the accelerometer

def update(self):
483    def update(self):
484        ''' update should be called within the main loop to compute the read the current accelerometer data '''
485        self.__x = self.__read_doublebyte(expAccelerometer.__READ_X)
486        self.__y = self.__read_doublebyte(expAccelerometer.__READ_Y)
487        self.__z = self.__read_doublebyte(expAccelerometer.__READ_Z)
488
489        self.__t = 0
490        self.__t = self.__read_byte(expAccelerometer.__READ_T)
491        if self.__t > 127:
492            self.__t = self.__t - 256
493        self.__t += 25  # the base temperature reading is 25C
494
495        RANGE = 1024    # the datasheet implies this should be 2048
496        self.__p = self.__x / (RANGE * (-90.0))
497        self.__r = self.__y / (RANGE * (-90.0))
498        if (self.__z < 0):
499            if (self.__y < 0):
500                self.__r = 180.0 - self.__r
501            else:
502                self.__r = -180.0 - self.__r
503        # print ("Data: X:%+5d    Y:%+5d    Z:%+5d    P:%+5.1f    R:%+5.1f    T:%2d" % (self.__x, self.__y, self.__z, self.__p, self.__r, self.__t))

update should be called within the main loop to compute the read the current accelerometer data

expAccelerometer.interrupt: bool
506    @property
507    def interrupt(self) -> bool:
508        if self.__interrupt is None:
509            # clear the interrupt, regardless of the pin being controlled or not
510            val = self.__read_byte(expAccelerometer.__INTERRUPT_CHANGE) # read the data
511            self.__write_byte(expAccelerometer.__INTERRUPT_CHANGE, val) # clear the data
512            return val != 0
513        ''' return a boolean (True) if the interrupt has been triggered '''
514        if self.__interrupt.value == 0: # pin is pulled LOW to indicate the interrupt has occurred
515            self.__shake = 0
516            self.__orientation = 0
517            
518            val = self.__read_byte(expAccelerometer.__INTERRUPT_CHANGE) # read the data
519            self.__write_byte(expAccelerometer.__INTERRUPT_CHANGE, val) # clear the data
520
521            #print("interrupt: shake=0x{:02X} tilt=0x{:02X}".format((val & expAccelerometer.__SETTING_SHAKE), (val & expAccelerometer.__SETTING_ORIENT) >> 6))
522            self.__shake = val & expAccelerometer.__SETTING_SHAKE
523            self.__orientation = val >> 6
524            return True
525        self.__shake = 0
526        self.__orientation = 0
527        return False
expAccelerometer.shake: bool
529    @property
530    def shake(self) -> bool:
531        ''' return a boolean (True) if 'shake' has been detected (use after checking 'interrupt') '''
532        shake = self.__shake
533        self.__shake = 0
534        return shake

return a boolean (True) if 'shake' has been detected (use after checking 'interrupt')

expAccelerometer.orientation: int
536    @property
537    def orientation(self) -> int:
538        ''' return the axis of the orientation change (use after checking 'interrupt') '''
539        orientation = self.__orientation
540        self.__orientation = 0
541        return orientation

return the axis of the orientation change (use after checking 'interrupt')

expAccelerometer.x: int
543    @property
544    def x(self) -> int:
545        ''' return the current X axis acceleration value (int) '''
546        # the datasheet says this is +/- 2048 but I'm only seeing +/- 1024
547        return self.__x

return the current X axis acceleration value (int)

expAccelerometer.y: int
549    @property
550    def y(self) -> int:
551        ''' return the current Y axis acceleration value (int) '''
552        # the datasheet says this is +/- 2048 but I'm only seeing +/- 1024
553        return self.__y

return the current Y axis acceleration value (int)

expAccelerometer.z: int
555    @property
556    def z(self) -> int:
557        ''' return the current Z axis acceleration value (int) '''
558        return self.__z

return the current Z axis acceleration value (int)

expAccelerometer.temperature: int
560    @property
561    def temperature(self) -> int:
562        ''' return the current celsius temperature value (int) '''
563        return self.__t

return the current celsius temperature value (int)

expAccelerometer.range: int
565    @property
566    def range(self) -> int:
567        ''' return the current acceleration g-force range (int) : 2, 4, or 8 '''
568        val = self.__read_byte(expAccelerometer.__READ_SETTINGS)
569        power = val & expAccelerometer.__SETTING_POWERDOWN
570        # return 2, 4, or 8
571        return (0x01 << (((val & expAccelerometer.__SETTING_RANGE) >> 5) + 1))

return the current acceleration g-force range (int) : 2, 4, or 8

expAccelerometer.power: bool
584    @property
585    def power(self) -> bool:
586        ''' return boolean (True) when the accelerometer is powered up and active '''
587        val = self.__read_byte(expAccelerometer.__READ_SETTINGS)
588        return (val & expAccelerometer.__SETTING_POWERDOWN)

return boolean (True) when the accelerometer is powered up and active

class expEEPROM:
600class expEEPROM:
601    '''
602        The CAT24C16W is a 2KB EEPROM with I2C interface.
603            - I2C is a 2-wire serial interface which allows many devices to share the same wires.
604            - Each I2C device, sharing the I2C wires, has its own 1-byte address.
605            - CircuitPython provides a means to scan I2C for all attached devices.
606
607        The EEPROM has a series of I2C addresses starting with `0x50` and is accessed using `board.I2C()`.
608        The 2KB is broken into 8 256 byte pages.
609        
610        Reads and writes can be to specific addresses or - by setting 'position' they can be sequential.
611    '''
612    
613    def __init__(self) -> None:
614        self._max_size = 0x800  # 2KB
615        self._page_count = 8
616        self._current_position = 0  # only support sequential reads or writes
617    
618        self._i2c = board.I2C()
619        self._base_address = 0x50    # each 'address' is 256 bytes; a total of 8 addresses are used
620        # thee are created here to avoid repeated memory allocations
621        self._data = bytearray(1)
622        self._read_cmd = bytearray(1)
623        self._write_cmd = bytearray(2)
624        
625    def __len__(self) -> int:
626        return self._max_size
627
628    def _read_eeprom(self, count: int, fixed=None) -> int:
629        ''' internal method to read a byte or word '''
630        if count not in (1, 2):
631            raise ValueError("Read 1 or 2 bytes only")
632        if fixed is not None:
633            address = fixed
634        else:
635            address = self._current_position
636        try:
637            if self._i2c.try_lock():
638                block = address // 256
639                if block >= self._page_count:
640                    raise ValueError("Block out of range")
641                offset = address % 256
642                self._read_cmd[0] = offset
643                self._i2c.writeto(block + self._base_address, self._read_cmd)
644                self._i2c.readfrom_into(self._base_address, self._data)
645                val = self._data[0]
646                if count == 2:
647                    address += 1
648                    block = address // 256
649                    if block >= self._page_count:
650                        raise ValueError("Block out of range")
651                    offset = address % 256
652                    self._read_cmd[0] = offset
653                    self._i2c.writeto(block + self._base_address, self._read_cmd)
654                    self._i2c.readfrom_into(self._base_address, self._data)
655                    val |= self._data[0] << 8
656                self._i2c.unlock()
657            else:
658                self._i2c.unlock()
659                print ("Rats! The storage is locked and I can't find the key")
660                val = -1
661        except Exception as e:
662            self._i2c.unlock()
663            print ("EEEK! I can't read what was written. Something about ", e)
664            val = -1
665        if fixed is None:
666            self._current_position += count
667        #print (f"Read {count} bytes = {val}")   # DELETEME
668        return val
669
670    def __write_eeprom(self, count: int, val: int, fixed=None) -> int:
671        ''' internal method to write a byte or word '''
672        if count not in (1, 2):
673            raise ValueError("Read 1 or 2 bytes only")
674
675        if fixed is not None:
676            address = fixed
677        else:
678            address = self._current_position
679        try:
680            if self._i2c.try_lock():
681                # command and data must be sent together for proper timing
682                block = address // 256
683                if block >= self._page_count:
684                    raise ValueError("Block out of range")
685                offset = address % 256
686                self._write_cmd[0] = offset
687                self._write_cmd[1] = val & 0xFF
688                self._i2c.writeto(block + self._base_address, self._write_cmd)
689                time.sleep(0.006) # 5ms write cycle time plus a little extra
690                if count == 2:
691                    next_address = address + 1
692                    block = next_address // 256
693                    if block >= self._page_count:
694                        raise ValueError("Block out of range")
695                    offset = next_address % 256
696                    self._write_cmd[0] = offset
697                    self._write_cmd[1] = (val >> 8) & 0xFF
698                    self._i2c.writeto(block + self._base_address, self._write_cmd)
699                    time.sleep(0.006)
700                self._i2c.unlock()
701                result = self._read_eeprom(count, fixed=address)
702            else:
703                print ("What?! The storage is locked and I can't find the key")
704                self._i2c.unlock()
705                result = -1
706        except Exception as e:
707            print ("EEEK! I have forgotten how to write. Something about ", e)
708            self._i2c.unlock()
709            result = -1
710        if fixed is None:
711            self._current_position += count
712        #print (f"Write {count} bytes = {val} == {result}") # DELETEME
713        return val
714
715    def readByte(self, addr=None) -> int:
716        if addr is not None:
717            return self._read_eeprom(1, fixed=addr)
718        return self._read_eeprom(1)
719        
720    def readWord(self, addr=None) -> int:
721        if addr is not None:
722            return self._read_eeprom(2, fixed=addr)
723        return self._read_eeprom(2)
724
725    def writeByte(self, val: int, addr=None) -> int:
726        if addr is not None:
727            return self.__write_eeprom(1, val, fixed=addr)
728        return self.__write_eeprom(1, val)
729    
730    def writeWord(self, val: int, addr=None) -> int:
731        if addr is not None:
732            return self.__write_eeprom(2, val, fixed=addr)
733        return self.__write_eeprom(2, val)
734
735    @property
736    def position(self) -> int:
737        return self._current_position
738    @position.setter
739    def position(self, value: int) -> None:
740        self._current_position = value

The CAT24C16W is a 2KB EEPROM with I2C interface. - I2C is a 2-wire serial interface which allows many devices to share the same wires. - Each I2C device, sharing the I2C wires, has its own 1-byte address. - CircuitPython provides a means to scan I2C for all attached devices.

The EEPROM has a series of I2C addresses starting with 0x50 and is accessed using board.I2C(). The 2KB is broken into 8 256 byte pages.

Reads and writes can be to specific addresses or - by setting 'position' they can be sequential.

def readByte(self, addr=None) -> int:
715    def readByte(self, addr=None) -> int:
716        if addr is not None:
717            return self._read_eeprom(1, fixed=addr)
718        return self._read_eeprom(1)
def readWord(self, addr=None) -> int:
720    def readWord(self, addr=None) -> int:
721        if addr is not None:
722            return self._read_eeprom(2, fixed=addr)
723        return self._read_eeprom(2)
def writeByte(self, val: int, addr=None) -> int:
725    def writeByte(self, val: int, addr=None) -> int:
726        if addr is not None:
727            return self.__write_eeprom(1, val, fixed=addr)
728        return self.__write_eeprom(1, val)
def writeWord(self, val: int, addr=None) -> int:
730    def writeWord(self, val: int, addr=None) -> int:
731        if addr is not None:
732            return self.__write_eeprom(2, val, fixed=addr)
733        return self.__write_eeprom(2, val)
expEEPROM.position: int
735    @property
736    def position(self) -> int:
737        return self._current_position
class expIR:
743class expIR:
744    '''
745    The badge has both an IR receiver and emitter. It can support a range of IR protocols.
746    For the purposed of this library, the pulse sizes are close to but not exactly NEC format.
747
748    IR Data Format:
749        The header has a very long pulse and a long gap.
750        Each bit of the binary data starts with a marker pulse and then a long or short gap.
751        Most formats include a stop bit at the end which is the same as a 'zero' bit.
752        
753    CircuitPython does an excellent job of decoding arbitrary IR signals.
754    Other implementations may only support fixed size messages.
755    
756    The below pulse durations are from the NEC protocol. Other protocols and devices may have
757    different pulse durations. The NEC protocol is a common format used by many devices.
758    These pulse durations are also found in most Adafruit IR examples.
759    '''
760
761    IR_HEADER = [9000, 4500]
762    IR_START = 560
763    IR_SHORT = 560
764    IR_LONG = 1700
765    IR_ZERO = [IR_START, IR_SHORT]
766    IR_ONE  = [IR_START, IR_LONG]
767
768    def __init__(self):
769        ''' Initialize the IR receiver and transmitter '''
770        self.__ir_rx = pulseio.PulseIn(board.GP1, maxlen=200, idle_state=True)
771        self.__ir_tx = pulseio.PulseOut(board.GP0, frequency=38000, duty_cycle=2**15)
772        self.__decoder = adafruit_irremote.GenericDecode()
773        self.__encoder = adafruit_irremote.GenericTransmit(header=expIR.IR_HEADER, one=expIR.IR_ONE, zero=expIR.IR_ZERO, trail=255, debug=False)
774        self.__active = True
775        
776        #message = [0, 3, 5] # cmd, brightness, color (we are only interested in the color)
777    def __repr__(self):
778        return "expIR: receiver=board.RX transmitter=board.TX"
779    
780    def read(self) -> list:
781        if not self.__active:
782            return None
783        ''' return a list of the received IR data '''
784        pulses = self.__decoder.read_pulses(self.__ir_rx)
785        count = len(pulses)
786        if count < 4:
787            print("Insufficient data:", count, pulses)
788            return None
789        try:
790            code = list(self.__decoder.decode_bits(pulses))
791            #print("Received:", code)
792            return code
793        except Exception as e:
794            print("Failed to decode:", e, "raw:", count, pulses)
795            return None
796
797    def write(self, value):
798        if not self.__active:
799            return
800        ''' transmit the IR data '''
801        if type(value) is not list:
802            value = [value]
803        #print("Sending: ", value)
804        self.__ir_rx.pause()
805        self.__encoder.transmit(self.__ir_tx, value)
806        self.__ir_rx.resume()
807        self.__ir_rx.clear()
808        
809    @property
810    def available(self) -> int:
811        count = len(self.__ir_rx)
812        if count <= 2:
813            self.__ir_rx.clear()
814            count = 0
815        return count
816
817    @property
818    def active(self) -> bool:
819        return self.__active
820    @active.setter
821    def active(self, value: bool):
822        self.__active = value
823        if value:
824            self.__ir_rx.clear()
825            self.__ir_rx.resume()
826        else:
827            self.__ir_rx.pause()
828            self.__ir_rx.clear()

The badge has both an IR receiver and emitter. It can support a range of IR protocols. For the purposed of this library, the pulse sizes are close to but not exactly NEC format.

IR Data Format: The header has a very long pulse and a long gap. Each bit of the binary data starts with a marker pulse and then a long or short gap. Most formats include a stop bit at the end which is the same as a 'zero' bit.

CircuitPython does an excellent job of decoding arbitrary IR signals. Other implementations may only support fixed size messages.

The below pulse durations are from the NEC protocol. Other protocols and devices may have different pulse durations. The NEC protocol is a common format used by many devices. These pulse durations are also found in most Adafruit IR examples.

expIR()
768    def __init__(self):
769        ''' Initialize the IR receiver and transmitter '''
770        self.__ir_rx = pulseio.PulseIn(board.GP1, maxlen=200, idle_state=True)
771        self.__ir_tx = pulseio.PulseOut(board.GP0, frequency=38000, duty_cycle=2**15)
772        self.__decoder = adafruit_irremote.GenericDecode()
773        self.__encoder = adafruit_irremote.GenericTransmit(header=expIR.IR_HEADER, one=expIR.IR_ONE, zero=expIR.IR_ZERO, trail=255, debug=False)
774        self.__active = True
775        
776        #message = [0, 3, 5] # cmd, brightness, color (we are only interested in the color)

Initialize the IR receiver and transmitter

expIR.IR_HEADER = [9000, 4500]
expIR.IR_START = 560
expIR.IR_SHORT = 560
expIR.IR_LONG = 1700
expIR.IR_ZERO = [560, 560]
expIR.IR_ONE = [560, 1700]
def read(self) -> list:
780    def read(self) -> list:
781        if not self.__active:
782            return None
783        ''' return a list of the received IR data '''
784        pulses = self.__decoder.read_pulses(self.__ir_rx)
785        count = len(pulses)
786        if count < 4:
787            print("Insufficient data:", count, pulses)
788            return None
789        try:
790            code = list(self.__decoder.decode_bits(pulses))
791            #print("Received:", code)
792            return code
793        except Exception as e:
794            print("Failed to decode:", e, "raw:", count, pulses)
795            return None
def write(self, value):
797    def write(self, value):
798        if not self.__active:
799            return
800        ''' transmit the IR data '''
801        if type(value) is not list:
802            value = [value]
803        #print("Sending: ", value)
804        self.__ir_rx.pause()
805        self.__encoder.transmit(self.__ir_tx, value)
806        self.__ir_rx.resume()
807        self.__ir_rx.clear()
expIR.available: int
809    @property
810    def available(self) -> int:
811        count = len(self.__ir_rx)
812        if count <= 2:
813            self.__ir_rx.clear()
814            count = 0
815        return count
expIR.active: bool
817    @property
818    def active(self) -> bool:
819        return self.__active
class expDisplay:
 831class expDisplay:
 832    '''
 833        The badge has an 200x200 tri-color (RBW) ePaper display.
 834        Tri-color ePaper displays are very slow to refresh - taking 15-20 seconds.
 835
 836        The display is accessed using `board.DISPLAY`.
 837
 838        **Note** CircuitPython will treat the ePaper display as an output device for REPL and error messages.
 839        To prevent this behavior, use `expDisplay.disable()` to disable the display.
 840        This will preserve any core content from changing the display
 841        It will also prevent any intended content from appearing on the display.
 842        Use `expDisplay.disable()` to reenable the display.
 843        At anytime, use `expDisplay.available()` to check the availability of the display.
 844    '''
 845    
 846    WIDTH:ClassVar[int] = 200
 847    HEIGHT:ClassVar[int] = 200
 848    BLACK:ClassVar[int] = 0x000000
 849    WHITE:ClassVar[int] = 0xFFFFFF
 850    RED:ClassVar[int]   = 0xFF0000
 851
 852    def __init__(self):
 853        '''
 854        Setup the on-board Display (will generate an error message if the display is not available for output)
 855
 856        Available Class Constants:
 857        '''
 858        self.__display = board.DISPLAY
 859        self.__display.root_group = displayio.Group()
 860        
 861        # determine Explorer Badge variation
 862        try:
 863            self._type = board.VID()    # will be a 1 or a 2
 864            if self._type not in [1, 2]:
 865                raise ValueError
 866            if self._type == 1:
 867                self._refresh_rate = 20 # this is the minimum refresh rate but a longer one is advices
 868            else:
 869                self._refresh_rate = 5  # this is the minimum refresh rate but a longer one is advices
 870        except:
 871            self._type = 0
 872            print("ERROR: unrecognized board")
 873            return
 874        
 875    def __repr__(self):
 876        if self._type == 1:
 877            driver = "SSD1681 BWR"
 878        elif self._type == 2:
 879            driver = "SSD1608 BW"
 880        else:
 881            driver = "unknown"
 882        return f"expDisplay: {driver} ePaper with refresh rate={(self._refresh_rate):d}"
 883
 884    @staticmethod
 885    def disable():
 886        ''' disable the display for all output - affects REPL, error messages, and user code '''
 887        if __display_en_pin:
 888            __display_en_pin.value = False # set to OFF
 889    @staticmethod
 890    def enable():
 891        ''' enable the display for all output - affects REPL, error messages, and user code '''
 892        if __display_en_pin:
 893            __display_en_pin.value = True # set to ON
 894    @staticmethod
 895    def available() -> bool:
 896        ''' return boolean (True) if the on-board display is available for user '''
 897        if __display_en_pin:
 898            return (__display_en_pin.value)
 899        return False
 900
 901    def background(self, bg):
 902        '''
 903            Assign a background
 904
 905            Use the class constants for colors or provide a filename for an image.
 906        '''
 907        
 908        if type(bg) is int:
 909            # simple white background
 910            background_bitmap = displayio.Bitmap(expDisplay.WIDTH, expDisplay.HEIGHT, 1)
 911            palette = displayio.Palette(1)
 912            palette[0] = bg
 913            background_tile = displayio.TileGrid(background_bitmap, pixel_shader=palette)
 914            self.__display.root_group.append(background_tile)
 915        elif type(bg) is str:
 916            try:
 917                pic = displayio.OnDiskBitmap(bg)
 918                tile = displayio.TileGrid(pic, pixel_shader=pic.pixel_shader)
 919                #print("shader =", pic.pixel_shader, dir(pic), dir(pic.pixel_shader))
 920                self.__display.root_group.append(tile)
 921            except:
 922                print(f"Error: unable to access '{bg}' file")
 923        else:
 924            print("ValueError: unsupported type {type(bg)} for background")
 925
 926    def text(self, fontname:str, msg:str, fgcolor:int, bgcolor:int=None, x:int=0, y:int=0):
 927        """
 928        Add text to the display
 929
 930        Use the class constants for colors
 931
 932        The justification of the text is automatically determined based on the `X` and `Y` values as follows:
 933
 934         * X in left 1/3rd then left-justified
 935         * X in middle 1/3rd then center-justified
 936         * X in right 1/3rd then right-justified
 937         * Y in upper 1/3rd then top-justified
 938         * Y in middle 1/3rd then center-justified
 939         * Y in lower 1/3rd then bottom-justified
 940
 941        The `fontname` assumes the `/fonts/` folder but does not assume the font extension *(.bdf or .pcf)*.
 942        Choose a font with will fit the length of the longest line in the `msg`.
 943
 944        To use a multi-line messages, add `\\n` between the lines: e.g. 'Hello\\nWorld'.
 945
 946        For more control, the sky is the limit when using the various CircuitPython display libraries.
 947        """
 948        # FYI: in the doc above the use of '\\n' is because the doc generator barfs on '\n'
 949
 950        x = int(x)
 951        y = int(y)
 952        try:
 953            font_file_name = "/fonts/" + fontname
 954            font = bitmap_font.load_font(font_file_name)
 955            scaling=1
 956            font_height = (font.ascent + font.descent)
 957            box = font.get_bounding_box()
 958            box_height = box[1] - box[3]
 959            # use the larger of font_height and box_height
 960            if font_height < box_height:
 961                font_height = box_height
 962        except:
 963            print(f"Warning: unable to locate font '{font_file_name}. Using 'terminalio' font")
 964            font = terminalio.FONT
 965            scaling=3
 966            font_height = 8 * scaling
 967
 968        if font_height > 32:
 969            font_height = int(font_height * 0.85)   # tighten up big text just a bit
 970
 971        #bgcolor = expDisplay.BLACK if color == expDisplay.WHITE else expDisplay.WHITE
 972
 973        # we will compute the bounding box anchor based on the screen coordinates
 974        anchor_x = 0.0
 975        anchor_y = 0.0
 976        if y < (expDisplay.HEIGHT / 3):
 977            # top third: anchor to the bottom of the bounding box
 978            anchor_y = 1.0
 979        elif y < ((expDisplay.HEIGHT / 3) * 2):
 980            # middle third: anchor to the vertical middle of the bounding box
 981            anchor_y = 0.5
 982        else:
 983            # bottom third: anchor to the top of the bounding box
 984            anchor_y = 0.0
 985        if x < (expDisplay.WIDTH / 3):
 986            # left third: anchor to the left of the bounding box
 987            anchor_x = 0.0
 988        elif x < ((expDisplay.WIDTH / 3) * 2):
 989            # middle third: anchor to the horizontal middle of the bounding box
 990            anchor_x = 0.5
 991        else:
 992            # right third: anchor to the right of the bounding box
 993            anchor_x = 1.0
 994
 995        #print("Text anchor", (anchor_x, anchor_y), "with origin", (x,y))
 996        texts = msg.split("\n")             # break up multiple lines
 997        #print(msg,texts)
 998        offset = 0 - int((font_height * (len(texts)-1)) / 2)  # starting position is number of lines above and below center
 999
1000        # add all the lines
1001        for i in range (len(texts)):
1002            text = label.Label(font,
1003                            text=texts[i],
1004                            color=fgcolor,
1005                            background_color=bgcolor,
1006                            #padding_left=4,
1007                            #padding_top=4,
1008                            #padding_right=4,
1009                            #line_spacing=0.75,
1010                            scale=scaling)
1011            text.anchor_point = (anchor_x, anchor_y)
1012            text.anchored_position = (x,y+offset)
1013
1014            '''
1015            # if we want to support a tight background, then we need to clear the background by offset rendering message using a background color in multiple directions
1016            # we could also support a drop shadow by first offset rendering the message with a background color and then rendering hte message on top
1017            '''
1018            self.__display.root_group.append(text)
1019            offset += font_height
1020
1021    def image(self, file_name:str, x:int=0, y:int=0):
1022        '''
1023            Add an image to the display
1024
1025            The `file_name` must be fully qualified with a leading slash ("/").
1026            The `X` and `Y` refer to the placement of the upper left of the image.
1027        '''
1028        try:
1029            pic = displayio.OnDiskBitmap(file_name)
1030            tile = displayio.TileGrid(pic, pixel_shader=pic.pixel_shader)
1031            #print("shader =", pic.pixel_shader, dir(pic), dir(pic.pixel_shader))
1032            self.__display.root_group.append(tile)
1033        except:
1034            print("Error: image missing: {}".format(file_name))
1035
1036    def refresh(self, wait:bool=False):
1037        ''' refresh the display after adding background, text, images, or shapes (ePaper do not auto-refresh) '''
1038        remaining = self.__display.time_to_refresh
1039        if remaining > 0.5:
1040            print (f"waiting {remaining:4.2f} ...")
1041        time.sleep(remaining)
1042        try:
1043            self.__display.refresh()
1044            #print("ePaper display updated")
1045            if wait:
1046                remaining = self.__display.time_to_refresh
1047                time.sleep(remaining)
1048                while self.__display.busy:
1049                    time.sleep(0.1)
1050                print("ePaper display refresh complete")
1051        except Exception as e:
1052            print("Error: unable to update display -", e)
1053        return self.__display.time_to_refresh
1054    
1055    @property
1056    def busy(self) -> bool:
1057        ''' return boolean (True) if the display is currently refreshing '''
1058        return self.__display.busy

The badge has an 200x200 tri-color (RBW) ePaper display. Tri-color ePaper displays are very slow to refresh - taking 15-20 seconds.

The display is accessed using board.DISPLAY.

Note CircuitPython will treat the ePaper display as an output device for REPL and error messages. To prevent this behavior, use expDisplay.disable() to disable the display. This will preserve any core content from changing the display It will also prevent any intended content from appearing on the display. Use expDisplay.disable() to reenable the display. At anytime, use expDisplay.available() to check the availability of the display.

expDisplay()
852    def __init__(self):
853        '''
854        Setup the on-board Display (will generate an error message if the display is not available for output)
855
856        Available Class Constants:
857        '''
858        self.__display = board.DISPLAY
859        self.__display.root_group = displayio.Group()
860        
861        # determine Explorer Badge variation
862        try:
863            self._type = board.VID()    # will be a 1 or a 2
864            if self._type not in [1, 2]:
865                raise ValueError
866            if self._type == 1:
867                self._refresh_rate = 20 # this is the minimum refresh rate but a longer one is advices
868            else:
869                self._refresh_rate = 5  # this is the minimum refresh rate but a longer one is advices
870        except:
871            self._type = 0
872            print("ERROR: unrecognized board")
873            return

Setup the on-board Display (will generate an error message if the display is not available for output)

Available Class Constants:

expDisplay.WIDTH: ClassVar[int] = 200
expDisplay.HEIGHT: ClassVar[int] = 200
expDisplay.BLACK: ClassVar[int] = 0
expDisplay.WHITE: ClassVar[int] = 16777215
expDisplay.RED: ClassVar[int] = 16711680
@staticmethod
def disable():
884    @staticmethod
885    def disable():
886        ''' disable the display for all output - affects REPL, error messages, and user code '''
887        if __display_en_pin:
888            __display_en_pin.value = False # set to OFF

disable the display for all output - affects REPL, error messages, and user code

@staticmethod
def enable():
889    @staticmethod
890    def enable():
891        ''' enable the display for all output - affects REPL, error messages, and user code '''
892        if __display_en_pin:
893            __display_en_pin.value = True # set to ON

enable the display for all output - affects REPL, error messages, and user code

@staticmethod
def available() -> bool:
894    @staticmethod
895    def available() -> bool:
896        ''' return boolean (True) if the on-board display is available for user '''
897        if __display_en_pin:
898            return (__display_en_pin.value)
899        return False

return boolean (True) if the on-board display is available for user

def background(self, bg):
901    def background(self, bg):
902        '''
903            Assign a background
904
905            Use the class constants for colors or provide a filename for an image.
906        '''
907        
908        if type(bg) is int:
909            # simple white background
910            background_bitmap = displayio.Bitmap(expDisplay.WIDTH, expDisplay.HEIGHT, 1)
911            palette = displayio.Palette(1)
912            palette[0] = bg
913            background_tile = displayio.TileGrid(background_bitmap, pixel_shader=palette)
914            self.__display.root_group.append(background_tile)
915        elif type(bg) is str:
916            try:
917                pic = displayio.OnDiskBitmap(bg)
918                tile = displayio.TileGrid(pic, pixel_shader=pic.pixel_shader)
919                #print("shader =", pic.pixel_shader, dir(pic), dir(pic.pixel_shader))
920                self.__display.root_group.append(tile)
921            except:
922                print(f"Error: unable to access '{bg}' file")
923        else:
924            print("ValueError: unsupported type {type(bg)} for background")

Assign a background

Use the class constants for colors or provide a filename for an image.

def text( self, fontname: str, msg: str, fgcolor: int, bgcolor: int = None, x: int = 0, y: int = 0):
 926    def text(self, fontname:str, msg:str, fgcolor:int, bgcolor:int=None, x:int=0, y:int=0):
 927        """
 928        Add text to the display
 929
 930        Use the class constants for colors
 931
 932        The justification of the text is automatically determined based on the `X` and `Y` values as follows:
 933
 934         * X in left 1/3rd then left-justified
 935         * X in middle 1/3rd then center-justified
 936         * X in right 1/3rd then right-justified
 937         * Y in upper 1/3rd then top-justified
 938         * Y in middle 1/3rd then center-justified
 939         * Y in lower 1/3rd then bottom-justified
 940
 941        The `fontname` assumes the `/fonts/` folder but does not assume the font extension *(.bdf or .pcf)*.
 942        Choose a font with will fit the length of the longest line in the `msg`.
 943
 944        To use a multi-line messages, add `\\n` between the lines: e.g. 'Hello\\nWorld'.
 945
 946        For more control, the sky is the limit when using the various CircuitPython display libraries.
 947        """
 948        # FYI: in the doc above the use of '\\n' is because the doc generator barfs on '\n'
 949
 950        x = int(x)
 951        y = int(y)
 952        try:
 953            font_file_name = "/fonts/" + fontname
 954            font = bitmap_font.load_font(font_file_name)
 955            scaling=1
 956            font_height = (font.ascent + font.descent)
 957            box = font.get_bounding_box()
 958            box_height = box[1] - box[3]
 959            # use the larger of font_height and box_height
 960            if font_height < box_height:
 961                font_height = box_height
 962        except:
 963            print(f"Warning: unable to locate font '{font_file_name}. Using 'terminalio' font")
 964            font = terminalio.FONT
 965            scaling=3
 966            font_height = 8 * scaling
 967
 968        if font_height > 32:
 969            font_height = int(font_height * 0.85)   # tighten up big text just a bit
 970
 971        #bgcolor = expDisplay.BLACK if color == expDisplay.WHITE else expDisplay.WHITE
 972
 973        # we will compute the bounding box anchor based on the screen coordinates
 974        anchor_x = 0.0
 975        anchor_y = 0.0
 976        if y < (expDisplay.HEIGHT / 3):
 977            # top third: anchor to the bottom of the bounding box
 978            anchor_y = 1.0
 979        elif y < ((expDisplay.HEIGHT / 3) * 2):
 980            # middle third: anchor to the vertical middle of the bounding box
 981            anchor_y = 0.5
 982        else:
 983            # bottom third: anchor to the top of the bounding box
 984            anchor_y = 0.0
 985        if x < (expDisplay.WIDTH / 3):
 986            # left third: anchor to the left of the bounding box
 987            anchor_x = 0.0
 988        elif x < ((expDisplay.WIDTH / 3) * 2):
 989            # middle third: anchor to the horizontal middle of the bounding box
 990            anchor_x = 0.5
 991        else:
 992            # right third: anchor to the right of the bounding box
 993            anchor_x = 1.0
 994
 995        #print("Text anchor", (anchor_x, anchor_y), "with origin", (x,y))
 996        texts = msg.split("\n")             # break up multiple lines
 997        #print(msg,texts)
 998        offset = 0 - int((font_height * (len(texts)-1)) / 2)  # starting position is number of lines above and below center
 999
1000        # add all the lines
1001        for i in range (len(texts)):
1002            text = label.Label(font,
1003                            text=texts[i],
1004                            color=fgcolor,
1005                            background_color=bgcolor,
1006                            #padding_left=4,
1007                            #padding_top=4,
1008                            #padding_right=4,
1009                            #line_spacing=0.75,
1010                            scale=scaling)
1011            text.anchor_point = (anchor_x, anchor_y)
1012            text.anchored_position = (x,y+offset)
1013
1014            '''
1015            # if we want to support a tight background, then we need to clear the background by offset rendering message using a background color in multiple directions
1016            # we could also support a drop shadow by first offset rendering the message with a background color and then rendering hte message on top
1017            '''
1018            self.__display.root_group.append(text)
1019            offset += font_height

Add text to the display

Use the class constants for colors

The justification of the text is automatically determined based on the X and Y values as follows:

  • X in left 1/3rd then left-justified
  • X in middle 1/3rd then center-justified
  • X in right 1/3rd then right-justified
  • Y in upper 1/3rd then top-justified
  • Y in middle 1/3rd then center-justified
  • Y in lower 1/3rd then bottom-justified

The fontname assumes the /fonts/ folder but does not assume the font extension (.bdf or .pcf). Choose a font with will fit the length of the longest line in the msg.

To use a multi-line messages, add \n between the lines: e.g. 'Hello\nWorld'.

For more control, the sky is the limit when using the various CircuitPython display libraries.

def image(self, file_name: str, x: int = 0, y: int = 0):
1021    def image(self, file_name:str, x:int=0, y:int=0):
1022        '''
1023            Add an image to the display
1024
1025            The `file_name` must be fully qualified with a leading slash ("/").
1026            The `X` and `Y` refer to the placement of the upper left of the image.
1027        '''
1028        try:
1029            pic = displayio.OnDiskBitmap(file_name)
1030            tile = displayio.TileGrid(pic, pixel_shader=pic.pixel_shader)
1031            #print("shader =", pic.pixel_shader, dir(pic), dir(pic.pixel_shader))
1032            self.__display.root_group.append(tile)
1033        except:
1034            print("Error: image missing: {}".format(file_name))

Add an image to the display

The file_name must be fully qualified with a leading slash ("/"). The X and Y refer to the placement of the upper left of the image.

def refresh(self, wait: bool = False):
1036    def refresh(self, wait:bool=False):
1037        ''' refresh the display after adding background, text, images, or shapes (ePaper do not auto-refresh) '''
1038        remaining = self.__display.time_to_refresh
1039        if remaining > 0.5:
1040            print (f"waiting {remaining:4.2f} ...")
1041        time.sleep(remaining)
1042        try:
1043            self.__display.refresh()
1044            #print("ePaper display updated")
1045            if wait:
1046                remaining = self.__display.time_to_refresh
1047                time.sleep(remaining)
1048                while self.__display.busy:
1049                    time.sleep(0.1)
1050                print("ePaper display refresh complete")
1051        except Exception as e:
1052            print("Error: unable to update display -", e)
1053        return self.__display.time_to_refresh

refresh the display after adding background, text, images, or shapes (ePaper do not auto-refresh)

expDisplay.busy: bool
1055    @property
1056    def busy(self) -> bool:
1057        ''' return boolean (True) if the display is currently refreshing '''
1058        return self.__display.busy

return boolean (True) if the display is currently refreshing