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:
- CircuitPython firmware (UF2): https://circuitpython.org
- Adafruit CircuitPython Libraries: https://circuitpython.org/libraries
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
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:
- blinks = the code has been loaded and is running
- blinks = the code has stopped (either from a code error or CTRL-C)
- 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
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
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
.
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:
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)
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
.
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:
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
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
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
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
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
.
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
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
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)
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
.
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:
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
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
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
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()
.
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
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
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
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')
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')
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)
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)
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)
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)
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
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
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.
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.
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
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
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()
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.
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:
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
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
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
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.
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.
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.
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)