Merge remote-tracking branch 'origin/master'

# Conflicts:
#	Vertical_Garden_Barometer/code.py
This commit is contained in:
firepixie 2020-06-12 11:27:35 -07:00
commit eb84830c4b
68 changed files with 173745 additions and 122 deletions

50
Activity_Generator/code.py Executable file
View file

@ -0,0 +1,50 @@
"""ACTIVITY GENERATOR for Adafruit CLUE"""
import time
import random
from adafruit_clue import clue
from things import activities
from things import subjects
screen = clue.simple_text_display(text_scale=4, colors=(clue.WHITE,))
screen[1].text = "ACTIVITY"
screen[2].text = "GENERATOR"
screen.show()
time.sleep(1.5)
screen[0].text = "make a"
screen[2].text = "about"
screen[1].color = clue.RED
screen[3].color = clue.GREEN
screen[4].color = clue.BLUE
activity = "???"
subject_a = "???"
subject_b = "???"
two_subjects = True
def random_pick(items):
index = random.randint(0, len(items)-1)
return items[index]
while True:
if clue.button_a:
activity = random_pick(activities)
subject_a = random_pick(subjects)
subject_b = random_pick(subjects)
time.sleep(0.25)
if clue.button_b:
two_subjects = not two_subjects
time.sleep(0.5)
screen[1].text = activity
screen[3].text = subject_a
if two_subjects:
screen[4].text = subject_b
else:
screen[4].text = ""
screen.show()

57
Activity_Generator/things.py Executable file
View file

@ -0,0 +1,57 @@
activities = [
"DRAWING",
"SONG",
"STORY",
"VIDEO",
]
subjects = [
"COMPUTER",
"ALIEN",
"LASER",
"FOOD",
"TREE",
"DREAM",
"WEATHER",
"DOG",
"CAT",
"BIRD",
"HORSE",
"BLANKET",
"TIME",
"PLANT",
"LEAF",
"EAR",
"HAND",
"FEET",
"TEETH",
"PHONE",
"SPACE",
"ROBOT",
"GHOST",
"FUTURE",
"PROBLEM",
"MUSIC",
"NOISE",
"METAL",
"ROCK",
"AIR",
"HOPE",
"FEAR",
"LOVE",
"DANGER",
"COOKIE",
"BREAD",
"WATER",
"HAT",
"ROUND",
"SQUARE",
"SUCCESS",
"LIGHT",
"RUNNING",
"TALKING",
"SLEEPING",
"FLYING",
"SINGING",
"ACTING",
]

View file

@ -37,7 +37,7 @@ while True:
print("\nFeather Sense Sensor Demo")
print("---------------------------------------------")
print("Proximity:", apds9960.proximity())
print("Proximity:", apds9960.proximity)
print("Red: {}, Green: {}, Blue: {}, Clear: {}".format(*apds9960.color_data))
print("Temperature: {:.1f} C".format(bmp280.temperature))
print("Barometric pressure:", bmp280.pressure)

209
Baudot_TTY/baudot_tty.py Normal file
View file

@ -0,0 +1,209 @@
### Baudot TTY Message Transmitter
### The 5-bit mode is defined in ANSI TIA/EIA-825 (2000)
### "A Frequency Shift Keyed Modem for use on the Public Switched Telephone Network"
import time
import math
import array
import board
from audiocore import RawSample
import audiopwmio
# constants for sine wave generation
SIN_LENGTH = 100 # more is less choppy
SIN_AMPLITUDE = 2 ** 12 # 0 (min) to 32768 (max) 8192 is nice
SIN_OFFSET = 32767.5 # for 16bit range, (2**16 - 1) / 2
DELTA_PI = 2 * math.pi / SIN_LENGTH # happy little constant
sine_wave = [
int(SIN_OFFSET + SIN_AMPLITUDE * math.sin(DELTA_PI * i)) for i in range(SIN_LENGTH)
]
tones = (
RawSample(array.array("H", sine_wave), sample_rate=1800 * SIN_LENGTH), # Bit 0
RawSample(array.array("H", sine_wave), sample_rate=1400 * SIN_LENGTH), # Bit 1
)
bit_0 = tones[0]
bit_1 = tones[1]
carrier = tones[1]
char_pause = 0.1 # pause time between chars, set to 0 for fastest rate possible
dac = audiopwmio.PWMAudioOut(
board.A2
) # the CLUE edge connector marked "#0" to STEMMA speaker
# The CLUE's on-board speaker works OK, not great, just crank amplitude to full before trying.
# dac = audiopwmio.PWMAudioOut(board.SPEAKER)
LTRS = (
"\b",
"E",
"\n",
"A",
" ",
"S",
"I",
"U",
"\r",
"D",
"R",
"J",
"N",
"F",
"C",
"K",
"T",
"Z",
"L",
"W",
"H",
"Y",
"P",
"Q",
"O",
"B",
"G",
"FIGS",
"M",
"X",
"V",
"LTRS",
)
FIGS = (
"\b",
"3",
"\n",
"-",
" ",
"-",
"8",
"7",
"\r",
"$",
"4",
"'",
",",
"!",
":",
"(",
"5",
'"',
")",
"2",
"=",
"6",
"0",
"1",
"9",
"?",
"+",
"FIGS",
".",
"/",
";",
"LTRS",
)
char_count = 0
current_mode = LTRS
# The 5-bit Baudot text telephone (TTY) mode is a Frequency Shift Keyed modem
# for use on the Public Switched Telephone network.
#
# Definitions:
# Carrier tone is a 1400Hz tone.
# Binary 0 is an 1800Hz tone.
# Binary 1 is a 1400Hz tone.
# Bit duration is 20ms.
# Two modes exist: Letters, aka LTRS, for alphabet characters
# and Figures aka FIGS for numbers and symbols. These modes are switched by
# sending the appropriate 5-bit LTRS or FIGS character.
#
# Character transmission sequence:
# Carrier tone transmits for 150ms before each character.
# Start bit is a binary 0 (sounded for one bit duration of 20ms).
# 5-bit character code can be a combination of binary 0s and binary 1s.
# Stop bit is a binary 1 with a minimum duration of 1-1/2 bits (30ms)
#
#
def baudot_bit(pitch=bit_1, duration=0.022): # spec says 20ms, but adjusted as needed
dac.play(pitch, loop=True)
time.sleep(duration)
# dac.stop()
def baudot_carrier(duration=0.15): # Carrier tone is transmitted for 150 ms before the
# first character is transmitted
baudot_bit(carrier, duration)
dac.stop()
def baudot_start():
baudot_bit(bit_0)
def baudot_stop():
baudot_bit(bit_1, 0.04) # minimum duration is 30ms
dac.stop()
def send_character(value):
baudot_carrier() # send carrier tone
baudot_start() # send start bit tone
for i in range(5): # send each bit of the character
bit = (value >> i) & 0x01 # bit shift and bit mask to get value of each bit
baudot_bit(tones[bit]) # send each bit, either 0 or 1, of a character
baudot_stop() # send stop bit
baudot_carrier() # not to spec, but works better to extend carrier
def send_message(text):
global char_count, current_mode # pylint: disable=global-statement
for char in text:
if char not in LTRS and char not in FIGS: # just skip unknown characters
print("Unknown character:", char)
continue
if char not in current_mode: # switch mode
if current_mode == LTRS:
print("Switching mode to FIGS")
current_mode = FIGS
send_character(current_mode.index("FIGS"))
elif current_mode == FIGS:
print("Switching mode to LTRS")
current_mode = LTRS
send_character(current_mode.index("LTRS"))
# Send char mode at beginning of message and every 72 characters
if char_count >= 72 or char_count == 0:
print("Resending mode")
if current_mode == LTRS:
send_character(current_mode.index("LTRS"))
elif current_mode == FIGS:
send_character(current_mode.index("FIGS"))
# reset counter
char_count = 0
print(char)
send_character(current_mode.index(char))
time.sleep(char_pause)
# increment counter
char_count += 1
while True:
send_message("\nADAFRUIT 1234567890 -$!+='()/:;?,. ")
time.sleep(2)
send_message("\nWELCOME TO JOHN PARK'S WORKSHOP!")
time.sleep(3)
send_message("\nWOULD YOU LIKE TO PLAY A GAME?")
time.sleep(5)
# here's an example of sending a character
# send_character(current_mode.index("A"))
# time.sleep(char_pause)

View file

@ -0,0 +1,283 @@
### Baudot TTY Message Transmitter with CLUE GUI
### Pick from four phrases to send from the CLUE screen with buttons
### The 5-bit mode is defined in ANSI TIA/EIA-825 (2000)
### "A Frequency Shift Keyed Modem for use on the Public Switched Telephone Network"
import time
import math
import array
import board
from audiocore import RawSample
import audiopwmio
import displayio
from adafruit_display_shapes.circle import Circle
from adafruit_clue import clue
from adafruit_display_text import label
import terminalio
# Enter your messages here no more than 34 characters including spaces per line
messages = [
"HELLO FROM ADAFRUIT INDUSTRIES",
"12345678910 -$!+='()/:;?",
"WOULD YOU LIKE TO PLAY A GAME?",
"WELCOME TO JOHN PARK'S WORKSHOP",
]
clue.display.brightness = 1.0
screen = displayio.Group(max_size=5)
VFD_GREEN = 0x00FFD2
VFD_BG = 0x000505
# setup screen
# BG
color_bitmap = displayio.Bitmap(240, 240, 1)
color_palette = displayio.Palette(1)
color_palette[0] = VFD_BG
bg_sprite = displayio.TileGrid(color_bitmap, x=0, y=0, pixel_shader=color_palette)
screen.append(bg_sprite)
# title
title_label = label.Label(
terminalio.FONT, text="TTY CLUE", scale=4, color=VFD_GREEN, max_glyphs=11
)
title_label.x = 20
title_label.y = 16
screen.append(title_label)
# footer
footer_label = label.Label(
terminalio.FONT, text="<PICK SEND>", scale=2, color=VFD_GREEN, max_glyphs=40
)
footer_label.x = 4
footer_label.y = 220
screen.append(footer_label)
# message configs
messages_config = [
(0, messages[0], VFD_GREEN, 2, 60),
(1, messages[1], VFD_GREEN, 2, 90),
(2, messages[2], VFD_GREEN, 2, 120),
(3, messages[3], VFD_GREEN, 2, 150),
]
messages_labels = {} # dictionary of configured messages_labels
message_group = displayio.Group(max_size=5, scale=1)
for message_config in messages_config:
(name, textline, color, x, y) = message_config # unpack tuple into five var names
message_label = label.Label(terminalio.FONT, text=textline, color=color, max_glyphs=50)
message_label.x = x
message_label.y = y
messages_labels[name] = message_label
message_group.append(message_label)
screen.append(message_group)
# selection dot
dot_y = [52, 82, 112, 142]
dot = Circle(220, 60, 8, outline=VFD_GREEN, fill=VFD_BG)
screen.append(dot)
clue.display.show(screen)
# constants for sine wave generation
SIN_LENGTH = 100 # more is less choppy
SIN_AMPLITUDE = 2 ** 12 # 0 (min) to 32768 (max) 8192 is nice
SIN_OFFSET = 32767.5 # for 16bit range, (2**16 - 1) / 2
DELTA_PI = 2 * math.pi / SIN_LENGTH # happy little constant
sine_wave = [
int(SIN_OFFSET + SIN_AMPLITUDE * math.sin(DELTA_PI * i)) for i in range(SIN_LENGTH)
]
tones = (
RawSample(array.array("H", sine_wave), sample_rate=1800 * SIN_LENGTH), # Bit 0
RawSample(array.array("H", sine_wave), sample_rate=1400 * SIN_LENGTH), # Bit 1
)
bit_0 = tones[0]
bit_1 = tones[1]
carrier = tones[1]
char_pause = 0.1 # pause time between chars, set to 0 for fastest rate possible
dac = audiopwmio.PWMAudioOut(
board.A2
) # the CLUE edge connector marked "#0" to STEMMA speaker
# The CLUE's on-board speaker works OK, not great, just crank amplitude to full before trying.
# dac = audiopwmio.PWMAudioOut(board.SPEAKER)
LTRS = (
"\b",
"E",
"\n",
"A",
" ",
"S",
"I",
"U",
"\r",
"D",
"R",
"J",
"N",
"F",
"C",
"K",
"T",
"Z",
"L",
"W",
"H",
"Y",
"P",
"Q",
"O",
"B",
"G",
"FIGS",
"M",
"X",
"V",
"LTRS",
)
FIGS = (
"\b",
"3",
"\n",
"-",
" ",
"-",
"8",
"7",
"\r",
"$",
"4",
"'",
",",
"!",
":",
"(",
"5",
'"',
")",
"2",
"=",
"6",
"0",
"1",
"9",
"?",
"+",
"FIGS",
".",
"/",
";",
"LTRS",
)
char_count = 0
current_mode = LTRS
# The 5-bit Baudot text telephone (TTY) mode is a Frequency Shift Keyed modem
# for use on the Public Switched Telephone network.
#
# Definitions:
# Carrier tone is a 1400Hz tone.
# Binary 0 is an 1800Hz tone.
# Binary 1 is a 1400Hz tone.
# Bit duration is 20ms.
# Two modes exist: Letters, aka LTRS, for alphabet characters
# and Figures aka FIGS for numbers and symbols. These modes are switched by
# sending the appropriate 5-bit LTRS or FIGS character.
#
# Character transmission sequence:
# Carrier tone transmits for 150ms before each character.
# Start bit is a binary 0 (sounded for one bit duration of 20ms).
# 5-bit character code can be a combination of binary 0s and binary 1s.
# Stop bit is a binary 1 with a minimum duration of 1-1/2 bits (30ms)
#
#
def baudot_bit(pitch=bit_1, duration=0.022): # spec says 20ms, but adjusted as needed
dac.play(pitch, loop=True)
time.sleep(duration)
# dac.stop()
def baudot_carrier(duration=0.15): # Carrier tone is transmitted for 150 ms before the
# first character is transmitted
baudot_bit(carrier, duration)
dac.stop()
def baudot_start():
baudot_bit(bit_0)
def baudot_stop():
baudot_bit(bit_1, 0.04) # minimum duration is 30ms
dac.stop()
def send_character(value):
baudot_carrier() # send carrier tone
baudot_start() # send start bit tone
for i in range(5): # send each bit of the character
bit = (value >> i) & 0x01 # bit shift and bit mask to get value of each bit
baudot_bit(tones[bit]) # send each bit, either 0 or 1, of a character
baudot_stop() # send stop bit
baudot_carrier() # not to spec, but works better to extend carrier
def send_message(text):
global char_count, current_mode # pylint: disable=global-statement
for char in text:
if char not in LTRS and char not in FIGS: # just skip unknown characters
print("Unknown character:", char)
continue
if char not in current_mode: # switch mode
if current_mode == LTRS:
print("Switching mode to FIGS")
current_mode = FIGS
send_character(current_mode.index("FIGS"))
elif current_mode == FIGS:
print("Switching mode to LTRS")
current_mode = LTRS
send_character(current_mode.index("LTRS"))
# Send char mode at beginning of message and every 72 characters
if char_count >= 72 or char_count == 0:
print("Resending mode")
if current_mode == LTRS:
send_character(current_mode.index("LTRS"))
elif current_mode == FIGS:
send_character(current_mode.index("FIGS"))
# reset counter
char_count = 0
print(char)
send_character(current_mode.index(char))
time.sleep(char_pause)
# increment counter
char_count += 1
message_pick = 0
while True:
if clue.button_a:
message_pick = (message_pick + 1) % 4 # loop through the lines
dot.y = dot_y[message_pick]
time.sleep(0.4) # debounce
if clue.button_b:
dot.fill = VFD_GREEN
send_message(messages[message_pick])
dot.fill = VFD_BG

View file

@ -0,0 +1,230 @@
### Baudot TTY Message Transmitter
### Bluefruit Connect UART mode to send messages to CLUE for audio
### tramsission to TTY machine.
### The 5-bit mode is defined in ANSI TIA/EIA-825 (2000)
### "A Frequency Shift Keyed Modem for use on the Public Switched Telephone Network"
import time
import math
import array
import board
import audiopwmio
from audiocore import RawSample
from adafruit_ble import BLERadio
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
from adafruit_ble.services.nordic import UARTService
# BLE radio setup
ble = BLERadio()
uart_server = UARTService()
advertisement = ProvideServicesAdvertisement(uart_server)
ble._adapter.name = "TTY_MACHINE" # pylint: disable=protected-access
# constants for sine wave generation
SIN_LENGTH = 100 # more is less choppy
SIN_AMPLITUDE = 2 ** 12 # 0 (min) to 32768 (max) 8192 is nice
SIN_OFFSET = 32767.5 # for 16bit range, (2**16 - 1) / 2
DELTA_PI = 2 * math.pi / SIN_LENGTH # happy little constant
sine_wave = [
int(SIN_OFFSET + SIN_AMPLITUDE * math.sin(DELTA_PI * i)) for i in range(SIN_LENGTH)
]
tones = (
RawSample(array.array("H", sine_wave), sample_rate=1800 * SIN_LENGTH), # Bit 0
RawSample(array.array("H", sine_wave), sample_rate=1400 * SIN_LENGTH), # Bit 1
)
bit_0 = tones[0]
bit_1 = tones[1]
carrier = tones[1]
char_pause = 0.0 # pause time between chars, set to 0 for fastest rate possible
dac = audiopwmio.PWMAudioOut(
board.A2
) # the CLUE edge connector marked "#0" to STEMMA speaker
# The CLUE's on-board speaker works OK, not great, just crank amplitude to full before trying.
# dac = audiopwmio.PWMAudioOut(board.SPEAKER)
LTRS = (
"\b",
"E",
"\n",
"A",
" ",
"S",
"I",
"U",
"\r",
"D",
"R",
"J",
"N",
"F",
"C",
"K",
"T",
"Z",
"L",
"W",
"H",
"Y",
"P",
"Q",
"O",
"B",
"G",
"FIGS",
"M",
"X",
"V",
"LTRS",
)
FIGS = (
"\b",
"3",
"\n",
"-",
" ",
"-",
"8",
"7",
"\r",
"$",
"4",
"'",
",",
"!",
":",
"(",
"5",
'"',
")",
"2",
"=",
"6",
"0",
"1",
"9",
"?",
"+",
"FIGS",
".",
"/",
";",
"LTRS",
)
char_count = 0
current_mode = LTRS
# The 5-bit Baudot text telephone (TTY) mode is a Frequency Shift Keyed modem
# for use on the Public Switched Telephone network.
#
# Definitions:
# Carrier tone is a 1400Hz tone.
# Binary 0 is an 1800Hz tone.
# Binary 1 is a 1400Hz tone.
# Bit duration is 20ms.
#
# Two modes exist: Letters, aka LTRS, for alphabet characters
# and Figures aka FIGS for numbers and symbols. These modes are switched by
# sending the appropriate 5-bit LTRS or FIGS character.
#
# Character transmission sequence:
# Carrier tone transmits for 150ms before each character.
# Start bit is a binary 0 (sounded for one bit duration of 20ms).
# 5-bit character code can be a combination of binary 0s and binary 1s.
# Stop bit is a binary 1 with a minimum duration of 1-1/2 bits (30ms)
def baudot_bit(pitch=bit_1, duration=0.022): # spec says 20ms, but adjusted as needed
dac.play(pitch, loop=True)
time.sleep(duration)
# dac.stop()
def baudot_carrier(duration=0.15): # Carrier is transmitted 150 ms before first character is sent
baudot_bit(carrier, duration)
dac.stop()
def baudot_start():
baudot_bit(bit_0)
def baudot_stop():
baudot_bit(bit_1, 0.04) # minimum duration is 30ms
dac.stop()
def send_character(value):
baudot_carrier() # send carrier tone
baudot_start() # send start bit tone
for i in range(5): # send each bit of the character
bit = (value >> i) & 0x01 # bit shift and bit mask to get value of each bit
baudot_bit(tones[bit]) # send each bit, either 0 or 1, of a character
baudot_stop() # send stop bit
baudot_carrier() # not to spec, but works better to extend carrier
def send_message(text):
global char_count, current_mode # pylint: disable=global-statement
for char in text:
if char not in LTRS and char not in FIGS: # just skip unknown characters
print("Unknown character:", char)
continue
if char not in current_mode: # switch mode
if current_mode == LTRS:
print("Switching mode to FIGS")
current_mode = FIGS
send_character(current_mode.index("FIGS"))
elif current_mode == FIGS:
print("Switching mode to LTRS")
current_mode = LTRS
send_character(current_mode.index("LTRS"))
# Send char mode at beginning of message and every 72 characters
if char_count >= 72 or char_count == 0:
print("Resending mode")
if current_mode == LTRS:
send_character(current_mode.index("LTRS"))
elif current_mode == FIGS:
send_character(current_mode.index("FIGS"))
# reset counter
char_count = 0
print(char)
send_character(current_mode.index(char))
time.sleep(char_pause)
# increment counter
char_count += 1
while True:
print("WAITING...")
send_message("\nWAITING...\n")
ble.start_advertising(advertisement)
while not ble.connected:
pass
# Connected
ble.stop_advertising()
print("CONNECTED")
send_message("\nCONNECTED\n")
# Loop and read packets
while ble.connected:
if uart_server.in_waiting:
raw_bytes = uart_server.read(uart_server.in_waiting)
textmsg = raw_bytes.decode().strip()
print("received text =", textmsg)
send_message("\n")
send_message(textmsg.upper())
# Disconnected
print("DISCONNECTED")
send_message("\nDISCONNECTED\n")

View file

@ -0,0 +1,46 @@
import time
import board
import digitalio
import analogio
from adafruit_clue import clue
# Turn off the NeoPixel
clue.pixel.fill(0)
# Motor setup
motor = digitalio.DigitalInOut(board.P2)
motor.direction = digitalio.Direction.OUTPUT
# Soil sense setup
analog = analogio.AnalogIn(board.P1)
def read_and_average(analog_in, times, wait):
analog_sum = 0
for _ in range(times):
analog_sum += analog_in.value
time.sleep(wait)
return analog_sum / times
clue_display = clue.simple_text_display(title=" CLUE Plant", title_scale=1, text_scale=3)
clue_display.show()
while True:
# Take 100 readings and average them
analog_value = read_and_average(analog, 100, 0.01)
# Calculate a percentage (analog_value ranges from 0 to 65535)
percentage = analog_value / 65535 * 100
# Display the percentage
clue_display[0].text = "Soil: {} %".format(int(percentage))
# Print the values to the serial console
print((analog_value, percentage))
if percentage < 50:
motor.value = True
clue_display[1].text = "Motor ON"
clue_display[1].color = (0, 255, 0)
time.sleep(0.5)
# always turn off quickly
motor.value = False
clue_display[1].text = "Motor OFF"
clue_display[1].color = (255, 0, 0)

View file

@ -0,0 +1,99 @@
# Bonsai Buckaroo + CLUE Plant Care Bot
import time
import board
from digitalio import DigitalInOut, Direction
from analogio import AnalogIn
from adafruit_clue import clue
from adafruit_display_text import label
import displayio
import terminalio
import pulseio
moist_level = 50 # adjust this value as needed for your plant
board.DISPLAY.brightness = 0.8
clue.pixel.fill(0) # turn off NeoPixel
clue_display = displayio.Group(max_size=4)
# draw the dry plant
dry_plant_file = open("dry.bmp", "rb")
dry_plant_bmp = displayio.OnDiskBitmap(dry_plant_file)
dry_plant_sprite = displayio.TileGrid(dry_plant_bmp, pixel_shader=displayio.ColorConverter())
clue_display.append(dry_plant_sprite)
# draw the happy plant on top (so it can be moved out of the way when needed)
happy_plant_file = open("happy.bmp", "rb")
happy_plant_bmp = displayio.OnDiskBitmap(happy_plant_file)
happy_plant_sprite = displayio.TileGrid(happy_plant_bmp, pixel_shader=displayio.ColorConverter())
clue_display.append(happy_plant_sprite)
# Create text
# first create the group
text_group = displayio.Group(max_size=3, scale=3)
# Make a label
title_label = label.Label(terminalio.FONT, text="CLUE Plant", color=0x00FF22)
# Position the label
title_label.x = 10
title_label.y = 4
# Append label to group
text_group.append(title_label)
soil_label = label.Label(terminalio.FONT, text="Soil: ", color=0xFFAA88, max_glyphs=10)
soil_label.x = 4
soil_label.y = 64
text_group.append(soil_label)
motor_label = label.Label(terminalio.FONT, text="Motor off", color=0xFF0000, max_glyphs=9)
motor_label.x = 4
motor_label.y = 74
text_group.append(motor_label)
clue_display.append(text_group)
board.DISPLAY.show(clue_display)
motor = DigitalInOut(board.P2)
motor.direction = Direction.OUTPUT
buzzer = pulseio.PWMOut(board.SPEAKER, variable_frequency=True)
buzzer.frequency = 1000
sense_pin = board.P1
analog = AnalogIn(board.P1)
def read_and_average(ain, times, wait):
asum = 0
for _ in range(times):
asum += ain.value
time.sleep(wait)
return asum / times
time.sleep(5)
while True:
# take 100 readings and average them
aval = read_and_average(analog, 100, 0.01)
# calculate a percentage (aval ranges from 0 to 65535)
aperc = aval / 65535 * 100
# display the percentage
soil_label.text = "Soil: {} %".format(int(aperc))
print((aval, aperc))
if aperc < moist_level:
happy_plant_sprite.x = 300 # move the happy sprite away
time.sleep(1)
motor.value = True
motor_label.text = "Motor ON"
motor_label.color = 0x00FF00
buzzer.duty_cycle = 2**15
time.sleep(0.5)
# always turn off quickly
motor.value = False
motor_label.text = "Motor off"
motor_label.color = 0xFF0000
buzzer.duty_cycle = 0
if aperc >= moist_level:
happy_plant_sprite.x = 0 # bring back the happy sprite

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

150
CLUE_BBQ/clue_bbq.py Normal file
View file

@ -0,0 +1,150 @@
# Adafruit BBQ display works with ibbq protocol-based BLE temperature probes
import time
import displayio
import _bleio
import adafruit_ble
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
from adafruit_ble_ibbq import IBBQService
from adafruit_clue import clue
from adafruit_display_shapes.circle import Circle
from adafruit_display_text import label
from adafruit_bitmap_font import bitmap_font
clue.display.brightness = 1.0
homescreen_screen = displayio.Group(max_size=3)
temperatures_screen = displayio.Group(max_size=2)
# define custom colors
GREEN = 0x00D929
BLUE = 0x0000FF
RED = 0xFF0000
ORANGE = 0xFF6A00
YELLOW = 0xFFFF00
PURPLE = 0xE400FF
BLACK = 0x000000
WHITE = 0xFFFFFF
BURNT = 0xBB4E00
unit_mode = False # set the temperature unit_mode. True = centigrade, False = farenheit
# Setup homescreen
color_bitmap = displayio.Bitmap(120, 120, 1)
color_palette = displayio.Palette(1)
color_palette[0] = BURNT
bg_sprite = displayio.TileGrid(color_bitmap, x=120, y=0, pixel_shader=color_palette)
homescreen_screen.append(bg_sprite)
clue_color = [GREEN, BLUE, RED, ORANGE, YELLOW, PURPLE]
outer_circle = Circle(120, 120, 119, fill=BLACK, outline=BURNT)
homescreen_screen.append(outer_circle)
title_font = bitmap_font.load_font("/font/GothamBlack-50.bdf")
title_font.load_glyphs("BQLUE".encode("utf-8"))
title_label = label.Label(title_font, text="BBQLUE", color=clue.ORANGE, max_glyphs=15)
title_label.x = 12
title_label.y = 120
homescreen_screen.append(title_label)
clue.display.show(homescreen_screen)
# Setup temperatures screen
temp_font = bitmap_font.load_font("/font/GothamBlack-25.bdf")
temp_font.load_glyphs("0123456789FC.-<".encode("utf-8"))
my_labels_config = [
(0, "", GREEN, 2, 100),
(1, "", BLUE, 2, 150),
(2, "", RED, 2, 200),
(3, "", ORANGE, 135, 100),
(4, "", YELLOW, 135, 150),
(5, "", PURPLE, 135, 200),
]
my_labels = {} # dictionary of configured my_labels
text_group = displayio.Group(max_size=8, scale=1)
for label_config in my_labels_config:
(name, text, color, x, y) = label_config # unpack a tuple into five var names
templabel = label.Label(temp_font, text=text, color=color, max_glyphs=15)
templabel.x = x
templabel.y = y
my_labels[name] = templabel
text_group.append(templabel)
temperatures_screen.append(text_group)
temp_title_label = label.Label(
title_font, text="BBQLUE", color=clue.ORANGE, max_glyphs=15
)
temp_title_label.x = 12
temp_title_label.y = 30
temperatures_screen.append(temp_title_label)
# PyLint can't find BLERadio for some reason so special case it here.
ble = adafruit_ble.BLERadio() # pylint: disable=no-member
ibbq_connection = None
while True:
# re-display homescreen here
clue.display.show(homescreen_screen)
print("Scanning...")
for adv in ble.start_scan(ProvideServicesAdvertisement, timeout=5):
clue.pixel.fill((50, 50, 0))
if IBBQService in adv.services:
print("found an IBBq advertisement")
ibbq_connection = ble.connect(adv)
print("Connected")
break
# Stop scanning whether or not we are connected.
ble.stop_scan()
try:
if ibbq_connection and ibbq_connection.connected:
clue.pixel.fill((0, 0, 50))
ibbq_service = ibbq_connection[IBBQService]
ibbq_service.init()
while ibbq_connection.connected:
if clue.button_a: # hold a to swap between C and F
print("unit_mode swapped")
unit_mode = not unit_mode
clue.red_led = True
clue.play_tone(1200, 0.1)
clue.red_led = False
time.sleep(0.1) # debounce
temps = ibbq_service.temperatures
batt = ibbq_service.battery_level
if temps is not None:
probe_count = len(temps) # check how many probes there are
for i in range(probe_count):
if temps[i] != 0 and temps[i] < 1000: # unplugged probes
if unit_mode:
clue.pixel.fill((50, 0, 0))
temp = temps[i]
my_labels[i].text = "{} C".format(temp)
clue.pixel.fill((0, 0, 0))
print("Probe", i + 1, "Temperature:", temp, "C")
else: # F
clue.pixel.fill((50, 0, 0))
temp = temps[i] * 9 / 5 + 32
my_labels[i].text = "{} F".format(temp)
clue.pixel.fill((0, 0, 0))
print("Probe", i + 1, "Temperature:", temp, "F")
else:
print(
"Probe", i + 1, "is unplugged",
)
my_labels[i].text = " ---"
clue.display.show(temperatures_screen)
except _bleio.ConnectionError:
continue

5424
CLUE_BBQ/font/GothamBlack-25.bdf Executable file

File diff suppressed because it is too large Load diff

9236
CLUE_BBQ/font/GothamBlack-50.bdf Executable file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,88 @@
"""
Start a 20 second hand washing timer via proximity sensor.
Countdown the seconds with text and beeps.
Display a bitmaps for waiting and washing modes.
"""
import time
import board
from adafruit_clue import clue
from adafruit_display_text import label
from adafruit_bitmap_font import bitmap_font
import displayio
import pulseio
clue.display.brightness = 0.8
clue_display = displayio.Group(max_size=4)
# draw the background image
wash_on_file = open("wash_on.bmp", "rb")
wash_on_bmp = displayio.OnDiskBitmap(wash_on_file)
wash_on_sprite = displayio.TileGrid(wash_on_bmp, pixel_shader=displayio.ColorConverter())
clue_display.append(wash_on_sprite)
# draw the foreground image
wash_off_file = open("wash_off.bmp", "rb")
wash_off_bmp = displayio.OnDiskBitmap(wash_off_file)
wash_off_sprite = displayio.TileGrid(wash_off_bmp, pixel_shader=displayio.ColorConverter())
clue_display.append(wash_off_sprite)
# Create text
# first create the group
text_group = displayio.Group(max_size=5, scale=1)
# Make a label
title_font = bitmap_font.load_font("/font/RacingSansOne-Regular-38.bdf")
title_font.load_glyphs("HandWashTimer".encode('utf-8'))
title_label = label.Label(title_font, text="Hand Wash", color=0x001122)
# Position the label
title_label.x = 10
title_label.y = 16
# Append label to group
text_group.append(title_label)
title2_label = label.Label(title_font, text="Timer", color=0x001122)
# Position the label
title2_label.x = 6
title2_label.y = 52
# Append label to group
text_group.append(title2_label)
timer_font = bitmap_font.load_font("/font/RacingSansOne-Regular-29.bdf")
timer_font.load_glyphs("0123456789ADSWabcdefghijklmnopqrstuvwxyz:!".encode('utf-8'))
timer_label = label.Label(timer_font, text="Wave to start", color=0x4f3ab1, max_glyphs=15)
timer_label.x = 24
timer_label.y = 100
text_group.append(timer_label)
clue_display.append(text_group)
clue.display.show(clue_display)
def countdown(seconds):
for i in range(seconds):
buzzer.duty_cycle = 2**15
timer_label.text = ("Scrub time: {}".format(seconds-i))
buzzer.duty_cycle = 0
time.sleep(1)
timer_label.text = ("Done!")
wash_off_sprite.x = 0
buzzer.duty_cycle = 2**15
time.sleep(0.3)
buzzer.duty_cycle = 0
timer_label.x = 24
timer_label.y = 100
timer_label.text = ("Wave to start")
# setup buzzer
buzzer = pulseio.PWMOut(board.SPEAKER, variable_frequency=True)
buzzer.frequency = 1000
while True:
# print("Distance: {}".format(clue.proximity)) # use to test the sensor
if clue.proximity > 1:
timer_label.x = 12
timer_label.y = 226
timer_label.text = "Scrub Away!"
wash_off_sprite.x = 300
time.sleep(2)
countdown(20)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

BIN
CLUE_Hand_Wash_Timer/wash_off.bmp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

BIN
CLUE_Hand_Wash_Timer/wash_on.bmp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

File diff suppressed because it is too large Load diff

130
CLUE_I_Ching/clue_iching.py Executable file
View file

@ -0,0 +1,130 @@
import time
import random
import displayio
from adafruit_bitmap_font import bitmap_font
from adafruit_display_text import label
from adafruit_clue import clue
#--| User Config |-------------------------------
BACKGROUND_COLOR = 0xCFBC17
HEXAGRAM_COLOR = 0xBB0000
FONT_COLOR = 0x005500
SHAKE_THRESHOLD = 20
MELODY = ( (1000, 0.1), # (freq, duration)
(1200, 0.1),
(1400, 0.1),
(1600, 0.2))
#--| User Config |-------------------------------
# Defined in order treating each hexagram as a 6 bit value.
HEXAGRAMS = (
"EARTH", "RETURN", "THE ARMY", "PREVAILING", "MODESTY", " CRYING\nPHEASANT",
"ASCENDANCE", "PEACE", "WEARINESS", "THUNDER", "LETTING\n LOOSE",
"MARRYING\n MAIDEN", " SMALL\nEXCESS", "ABUNDANCE", "STEADFASTNESS",
" GREAT\nINJURY", "SUPPORT", "RETRENCHMENT", "WATER", "FRUGALITY",
"ADMONISHMENT", "FULFILLMENT", "THE WELL", "WAITING", "ILLNESS",
"THE CHASE", "TRAPPED", "LAKE", "CUTTING", "REVOLUTION", " GREAT\nEXCESS",
"STRIDE", "LOSS", "THE CHEEKS", "BLINDNESS", "DECREASE", "MOUNTAIN",
"DECORATION", "WORK", " BIG\nCATTLE", "ADVANCE", "BITING", "UNFULFILLMENT",
"ABANDONED", "TRAVELER", "FIRE", " THE\nCAULDRON", " GREAT\nHARVEST",
"VIEW", "INCREASE", "FLOWING", "SINCERITY", "PROGRESS", "FAMILY", "WIND",
" SMALL\nCATTLE", "OBSTRUCTION", "PROPRIETY", "THE COURT", "TREADING",
"LITTLE\n PIG", "GATHERING", "RENDEZVOUS", "HEAVEN",
)
# Grab the CLUE's display
display = clue.display
# Background fill
bg_bitmap = displayio.Bitmap(display.width, display.height, 1)
bg_palette = displayio.Palette(1)
bg_palette[0] = BACKGROUND_COLOR
background = displayio.TileGrid(bg_bitmap, pixel_shader=bg_palette)
# Hexagram setup
sprite_sheet = displayio.Bitmap(11, 4, 2)
palette = displayio.Palette(2)
palette.make_transparent(0)
palette[0] = 0x000000
palette[1] = HEXAGRAM_COLOR
for x in range(11):
sprite_sheet[x, 0] = 1 # - - 0 YIN
sprite_sheet[x, 1] = 0
sprite_sheet[x, 2] = 1 # --- 1 YANG
sprite_sheet[x, 3] = 0
sprite_sheet[5, 0] = 0
tile_grid = displayio.TileGrid(sprite_sheet, pixel_shader=palette,
width = 1,
height = 6,
tile_width = 11,
tile_height = 2)
hexagram = displayio.Group(max_size=1, x=60, y=15, scale=10)
hexagram.append(tile_grid)
# Hexagram name label
# font credit: https://www.instagram.com/cove703/
font = bitmap_font.load_font("/christopher_done_24.bdf")
font.load_glyphs(b'ABCDEFGHIJKLMNOPQRSTUVWXYZ')
hexname = label.Label(font, text=" "*40, color=FONT_COLOR)
# this will initially hold the "shake for reading" message
hexname.text = " SHAKE\n FOR\nREADING"
hexname.anchor_point = (0.5, 0.0)
hexname.anchored_position = (120, 120)
# Set up main display group (splash)
splash = displayio.Group()
display.show(splash)
# Add background and text label
splash.append(background)
splash.append(hexname)
def show_hexagram(number):
for i in range(6):
tile_grid[5-i] = (number >> i) & 0x01
def show_name(number):
hexname.text = HEXAGRAMS[number]
hexname.anchored_position = (120, 180)
#===================================
# MAIN CODE
#===================================
print("shake")
# wait for shake
while not clue.shake(shake_threshold=SHAKE_THRESHOLD):
pass
# calibrate the mystic universe
x, y, z = clue.acceleration
random.seed(int(time.monotonic() + abs(x) + abs(y) + abs(z)))
# cast a reading
reading = random.randrange(64)
print("reading = ", reading, HEXAGRAMS[reading])
# play a melody
for note, duration in MELODY:
clue.play_tone(note, duration)
# prompt to show
display.auto_refresh = False
hexname.text = " GOT IT\n\nPRESS BUTTON\n TO SEE"
hexname.anchored_position = (120, 120)
display.auto_refresh = True
while not clue.button_a and not clue.button_b:
pass
# and then show it
display.auto_refresh = False
splash.append(hexagram)
show_hexagram(reading)
show_name(reading)
display.auto_refresh = True
# hold here until reset
while True:
pass

View file

@ -0,0 +1,263 @@
# clue-plotter v1.14
# Sensor and input plotter for Adafruit CLUE in CircuitPython
# This plots the sensors and three of the analogue inputs on
# the LCD display either with scrolling or wrap mode which
# approximates a slow timebase oscilloscope, left button selects
# next source or with long press changes palette or longer press
# turns on output for Mu plotting, right button changes plot style
# Tested with an Adafruit CLUE (Alpha) and CircuitPython and 5.0.0
# copy this file to CLUE board as code.py
# needs companion plot_sensor.py and plotter.py files
# MIT License
# Copyright (c) 2020 Kevin J. Walters
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import time
import gc
import board
from plotter import Plotter
from plot_source import PlotSource, TemperaturePlotSource, PressurePlotSource, \
HumidityPlotSource, ColorPlotSource, ProximityPlotSource, \
IlluminatedColorPlotSource, VolumePlotSource, \
AccelerometerPlotSource, GyroPlotSource, \
MagnetometerPlotSource, PinPlotSource
from adafruit_clue import clue
debug = 1
# A list of all the data sources for plotting
sources = [TemperaturePlotSource(clue, mode="Celsius"),
TemperaturePlotSource(clue, mode="Fahrenheit"),
PressurePlotSource(clue, mode="Metric"),
PressurePlotSource(clue, mode="Imperial"),
HumidityPlotSource(clue),
ColorPlotSource(clue),
ProximityPlotSource(clue),
IlluminatedColorPlotSource(clue, mode="Red"),
IlluminatedColorPlotSource(clue, mode="Green"),
IlluminatedColorPlotSource(clue, mode="Blue"),
IlluminatedColorPlotSource(clue, mode="Clear"),
VolumePlotSource(clue),
AccelerometerPlotSource(clue),
GyroPlotSource(clue),
MagnetometerPlotSource(clue),
PinPlotSource([board.P0, board.P1, board.P2])
]
# The first source to select when plotting starts
current_source_idx = 0
# The various plotting styles - scroll is currently a jump scroll
stylemodes = (("lines", "scroll"), # draws lines between points
("lines", "wrap"),
("dots", "scroll"), # just points - slightly quicker
("dots", "wrap")
)
current_sm_idx = 0
def d_print(level, *args, **kwargs):
"""A simple conditional print for debugging based on global debug level."""
if not isinstance(level, int):
print(level, *args, **kwargs)
elif debug >= level:
print(*args, **kwargs)
def select_colors(plttr, src, def_palette):
"""Choose the colours based on the particular PlotSource
or forcing use of default palette."""
# otherwise use defaults
channel_colidx = []
palette = plttr.get_colors()
colors = PlotSource.DEFAULT_COLORS if def_palette else src.colors()
for col in colors:
try:
channel_colidx.append(palette.index(col))
except ValueError:
channel_colidx.append(PlotSource.DEFAULT_COLORS.index(col))
return channel_colidx
def ready_plot_source(plttr, srcs, def_palette, index=0):
"""Select the plot source by index from srcs list and then setup the
plot parameters by retrieving meta-data from the PlotSource object."""
src = srcs[index]
# Put the description of the source on screen at the top
source_name = str(src)
d_print(1, "Selecting source:", source_name)
plttr.clear_all()
plttr.title = source_name
plttr.y_axis_lab = src.units()
# The range on graph will start at this value
plttr.y_range = (src.initial_min(), src.initial_max())
plttr.y_min_range = src.range_min()
# Sensor/data source is expected to produce data between these values
plttr.y_full_range = (src.min(), src.max())
channels_from_src = src.values()
plttr.channels = channels_from_src # Can be between 1 and 3
plttr.channel_colidx = select_colors(plttr, src, def_palette)
src.start()
return (src, channels_from_src)
def wait_release(func, menu):
"""Calls func repeatedly waiting for it to return a false value
and goes through menu list as time passes.
The menu is a list of menu entries where each entry is a
two element list of time passed in seconds and text to display
for that period.
The entries must be in ascending time order."""
start_t_ns = time.monotonic_ns()
menu_option = None
selected = False
for menu_option, menu_entry in enumerate(menu):
menu_time_ns = start_t_ns + int(menu_entry[0] * 1e9)
menu_text = menu_entry[1]
if menu_text:
plotter.info = menu_text
while time.monotonic_ns() < menu_time_ns:
if not func():
selected = True
break
if menu_text:
plotter.info = ""
if selected:
break
return (menu_option, (time.monotonic_ns() - start_t_ns) * 1e-9)
def popup_text(plttr, text, duration=1.0):
"""Place some text on the screen using info property of Plotter object
for duration seconds."""
plttr.info = text
time.sleep(duration)
plttr.info = None
mu_plotter_output = False
range_lock = False
initial_title = "CLUE Plotter"
# displayio has some static limits on text - pre-calculate the maximum
# length of all of the different PlotSource objects
max_title_len = max(len(initial_title), max([len(str(so)) for so in sources]))
plotter = Plotter(board.DISPLAY,
style=stylemodes[current_sm_idx][0],
mode=stylemodes[current_sm_idx][1],
title=initial_title,
max_title_len=max_title_len,
mu_output=mu_plotter_output,
debug=debug)
# If set to true this forces use of colour blindness friendly colours
use_def_pal = False
clue.pixel[0] = clue.BLACK # turn off the NeoPixel on the back of CLUE board
plotter.display_on()
# Using left and right here in case the CLUE is cased hiding A/B labels
popup_text(plotter,
"\n".join(["Button Guide",
"Left: next source",
" 2secs: palette",
" 4s: Mu plot",
" 6s: range lock",
"Right: style change"]), duration=10)
count = 0
while True:
# Set the source and start items
(source, channels) = ready_plot_source(plotter, sources,
use_def_pal,
current_source_idx)
while True:
# Read data from sensor or voltage from pad
all_data = source.data()
# Check for left (A) and right (B) buttons
if clue.button_a:
# Wait for button release with time-based menu
opt, _ = wait_release(lambda: clue.button_a,
[(2, "Next\nsource"),
(4,
("Source" if use_def_pal else "Default")
+ "\npalette"),
(6,
"Mu output "
+ ("off" if mu_plotter_output else "on")),
(8,
"Range lock\n" + ("off" if range_lock else "on"))
])
if opt == 0: # change plot source
current_source_idx = (current_source_idx + 1) % len(sources)
break # to leave inner while and select the new source
elif opt == 1: # toggle palette
use_def_pal = not use_def_pal
plotter.channel_colidx = select_colors(plotter, source,
use_def_pal)
elif opt == 2: # toggle Mu output
mu_plotter_output = not mu_plotter_output
plotter.mu_output = mu_plotter_output
else: # toggle range lock
range_lock = not range_lock
plotter.y_range_lock = range_lock
if clue.button_b: # change plot style and mode
current_sm_idx = (current_sm_idx + 1) % len(stylemodes)
(new_style, new_mode) = stylemodes[current_sm_idx]
wait_release(lambda: clue.button_b,
[(2, new_style + "\n" + new_mode)])
d_print(1, "Graph change", new_style, new_mode)
plotter.change_stylemode(new_style, new_mode)
# Display it
if channels == 1:
plotter.data_add((all_data,))
else:
plotter.data_add(all_data)
# An occasional print of free heap
if debug >=3 and count % 15 == 0:
gc.collect() # must collect() first to measure free memory
print("Free memory:", gc.mem_free())
count += 1
source.stop()
plotter.display_off()

View file

@ -0,0 +1,378 @@
# MIT License
# Copyright (c) 2020 Kevin J. Walters
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""
`plot_source`
================================================================================
CircuitPython library for the clue-plotter application.
* Author(s): Kevin J. Walters
Implementation Notes
--------------------
**Hardware:**
* Adafruit CLUE <https://www.adafruit.com/product/4500>
**Software and Dependencies:**
* Adafruit's CLUE library: https://github.com/adafruit/Adafruit_CircuitPython_CLUE
"""
import math
import analogio
class PlotSource():
"""An abstract class for a sensor which returns the data from the sensor
and provides some metadata useful for plotting.
Sensors returning vector quanities like a 3-axis accelerometer are supported.
When the source is used start() will be called and when it's not needed stop() will
be called.
:param values: Number of values returned by data method, between 1 and 3.
:param name: Name of the sensor used to title the graph, only 17 characters fit on screen.
:param units: Units for data used for y axis label.
:param abs_min: Absolute minimum value for data, defaults to 0.
:param abs_max: Absolute maximum value for data, defaults to 65535.
:param initial_min: The initial minimum value suggested for y axis on graph,
defaults to abs_min.
:param initial_max: The initial maximum value suggested for y axis on graph,
defaults to abs_max.
:param range_min: A suggested minimum range to aid automatic y axis ranging.
:param rate: The approximate rate in Hz that that data method returns in a tight loop.
:param colors: A list of the suggested colors for data.
:param debug: A numerical debug level, defaults to 0.
"""
DEFAULT_COLORS = (0xffff00, 0x00ffff, 0xff0080)
RGB_COLORS = (0xff0000, 0x00ff00, 0x0000ff)
def __init__(self, values, name, units="",
abs_min=0, abs_max=65535, initial_min=None, initial_max=None,
range_min=None,
rate=None, colors=None, debug=0):
if type(self) == PlotSource: # pylint: disable=unidiomatic-typecheck
raise TypeError("PlotSource must be subclassed")
self._values = values
self._name = name
self._units = units
self._abs_min = abs_min
self._abs_max = abs_max
self._initial_min = initial_min if initial_min is not None else abs_min
self._initial_max = initial_max if initial_max is not None else abs_max
if range_min is None:
self._range_min = (abs_max - abs_min) / 100 # 1% of full range
else:
self._range_min = range_min
self._rate = rate
if colors is not None:
self._colors = colors
else:
self._colors = self.DEFAULT_COLORS[:values]
self._debug = debug
def __str__(self):
return self._name
def data(self):
"""Data sample from the sensor.
:return: A single numerical value or an array or tuple for vector values.
"""
raise NotImplementedError()
def min(self):
return self._abs_min
def max(self):
return self._abs_max
def initial_min(self):
return self._initial_min
def initial_max(self):
return self._initial_max
def range_min(self):
return self._range_min
def start(self):
pass
def stop(self):
pass
def values(self):
return self._values
def units(self):
return self._units
def rate(self):
return self._rate
def colors(self):
return self._colors
# This over-reads presumably due to electronics warming the board
# It also looks odd on close inspection as it climbs about 0.1C if
# it's read frequently
# Data sheet say operating temperature is -40C to 85C
class TemperaturePlotSource(PlotSource):
def _convert(self, value):
return value * self._scale + self._offset
def __init__(self, my_clue, mode="Celsius"):
self._clue = my_clue
range_min = 0.8
if mode[0].lower() == "f":
mode_name = "Fahrenheit"
self._scale = 1.8
self._offset = 32.0
range_min = 1.6
elif mode[0].lower() == "k":
mode_name = "Kelvin"
self._scale = 1.0
self._offset = 273.15
else:
mode_name = "Celsius"
self._scale = 1.0
self._offset = 0.0
super().__init__(1, "Temperature",
units=mode_name[0],
abs_min=self._convert(-40),
abs_max=self._convert(85),
initial_min=self._convert(10),
initial_max=self._convert(40),
range_min=range_min,
rate=24)
def data(self):
return self._convert(self._clue.temperature)
# The 300, 1100 values are in adafruit_bmp280 but are private variables
class PressurePlotSource(PlotSource):
def _convert(self, value):
return value * self._scale
def __init__(self, my_clue, mode="M"):
self._clue = my_clue
if mode[0].lower() == "i":
# 29.92 inches mercury equivalent to 1013.25mb in ISA
self._scale = 29.92 / 1013.25
units = "inHg"
range_min = 0.04
else:
self._scale = 1.0
units = "hPa" # AKA millibars (mb)
range_min = 1
super().__init__(1, "Pressure", units=units,
abs_min=self._convert(300), abs_max=self._convert(1100),
initial_min=self._convert(980), initial_max=self._convert(1040),
range_min=range_min,
rate=22)
def data(self):
return self._convert(self._clue.pressure)
class ProximityPlotSource(PlotSource):
def __init__(self, my_clue):
self._clue = my_clue
super().__init__(1, "Proximity",
abs_min=0, abs_max=255,
rate=720)
def data(self):
return self._clue.proximity
class HumidityPlotSource(PlotSource):
def __init__(self, my_clue):
self._clue = my_clue
super().__init__(1, "Rel. Humidity", units="%",
abs_min=0, abs_max=100, initial_min=20, initial_max=60,
rate=54)
def data(self):
return self._clue.humidity
# If clue.touch_N has not been used then it doesn't instantiate
# the TouchIn object so there's no problem with creating an AnalogIn...
class PinPlotSource(PlotSource):
def __init__(self, pin):
try:
pins = [p for p in pin]
except TypeError:
pins = [pin]
self._pins = pins
self._analogin = [analogio.AnalogIn(p) for p in pins]
# Assumption here that reference_voltage is same for all
# 3.3V graphs nicely with rounding up to 4.0V
self._reference_voltage = self._analogin[0].reference_voltage
self._conversion_factor = self._reference_voltage / (2**16 - 1)
super().__init__(len(pins),
"Pad: " + ", ".join([str(p).split('.')[-1] for p in pins]),
units="V",
abs_min=0.0, abs_max=math.ceil(self._reference_voltage),
rate=10000)
def data(self):
if len(self._analogin) == 1:
return self._analogin[0].value * self._conversion_factor
else:
return tuple([ana.value * self._conversion_factor
for ana in self._analogin])
def pins(self):
return self._pins
class ColorPlotSource(PlotSource):
def __init__(self, my_clue):
self._clue = my_clue
super().__init__(3, "Color: R, G, B",
abs_min=0, abs_max=8000, # 7169 looks like max
rate=50,
colors=self.RGB_COLORS,
)
def data(self):
(r, g, b, _) = self._clue.color # fourth value is clear value
return (r, g, b)
def start(self):
# These values will affect the maximum return value
# Set APDS9660 to sample every (256 - 249 ) * 2.78 = 19.46ms
# pylint: disable=protected-access
self._clue._sensor.integration_time = 249 # 19.46ms, ~ 50Hz
self._clue._sensor.color_gain = 0x02 # 16x (library default is 4x)
class IlluminatedColorPlotSource(PlotSource):
def __init__(self, my_clue, mode="Clear"):
self._clue = my_clue
col_fl_lc = mode[0].lower()
if col_fl_lc == "r":
plot_colour = self.RGB_COLORS[0]
elif col_fl_lc == "g":
plot_colour = self.RGB_COLORS[1]
elif col_fl_lc == "b":
plot_colour = self.RGB_COLORS[2]
elif col_fl_lc == "c":
plot_colour = self.DEFAULT_COLORS[0]
else:
raise ValueError("Colour must be Red, Green, Blue or Clear")
self._channel = col_fl_lc
super().__init__(1, "Illum. color: " + self._channel.upper(),
abs_min=0, abs_max=8000,
initial_min=0, initial_max=2000,
colors=(plot_colour,),
rate=50)
def data(self):
(r, g, b, c) = self._clue.color
if self._channel == "r":
return r
elif self._channel == "g":
return g
elif self._channel == "b":
return b
elif self._channel == "c":
return c
else:
return None # This should never happen
def start(self):
# Set APDS9660 to sample every (256 - 249 ) * 2.78 = 19.46ms
# pylint: disable=protected-access
self._clue._sensor.integration_time = 249 # 19.46ms, ~ 50Hz
self._clue._sensor.color_gain = 0x03 # 64x (library default is 4x)
self._clue.white_leds = True
def stop(self):
self._clue.white_leds = False
class VolumePlotSource(PlotSource):
def __init__(self, my_clue):
self._clue = my_clue
super().__init__(1, "Volume", units="dB",
abs_min=0, abs_max=97+3, # 97dB is 16bit dynamic range
initial_min=10, initial_max=60,
rate=41)
# 20 due to conversion of amplitude of signal
_LN_CONVERSION_FACTOR = 20 / math.log(10)
def data(self):
return (math.log(self._clue.sound_level + 1)
* self._LN_CONVERSION_FACTOR)
# This appears not to be a blocking read in terms of waiting for a
# a genuinely newvalue from the sensor
# CP standard says this should be radians per second but library
# currently returns degrees per second
# https://circuitpython.readthedocs.io/en/latest/docs/design_guide.html
# https://github.com/adafruit/Adafruit_CircuitPython_LSM6DS/issues/9
class GyroPlotSource(PlotSource):
def __init__(self, my_clue):
self._clue = my_clue
super().__init__(3, "Gyro", units="dps",
abs_min=-287-13, abs_max=287+13, # 286.703 appears to be max
initial_min=-100, initial_max=100,
colors=self.RGB_COLORS,
rate=500)
def data(self):
return self._clue.gyro
class AccelerometerPlotSource(PlotSource):
def __init__(self, my_clue):
self._clue = my_clue
super().__init__(3, "Accelerometer", units="ms-2",
abs_min=-40, abs_max=40, # 39.1992 approx max
initial_min=-20, initial_max=20,
colors=self.RGB_COLORS,
rate=500)
def data(self):
return self._clue.acceleration
class MagnetometerPlotSource(PlotSource):
def __init__(self, my_clue):
self._clue = my_clue
super().__init__(3, "Magnetometer", units="uT",
abs_min=-479-21, abs_max=479+21, # 478.866 approx max
initial_min=-80, initial_max=80, # Earth around 60uT
colors=self.RGB_COLORS,
rate=500)
def data(self):
return self._clue.magnetic

View file

@ -0,0 +1,871 @@
# MIT License
# Copyright (c) 2020 Kevin J. Walters
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""
`plotter`
================================================================================
CircuitPython library for the clue-plotter application's plotting facilties.
Internally this holds some values in a circular buffer to enable redrawing
and has some basic statistics on data.
Not intended to be a truly general purpose plotter but perhaps could be
developed into one.
* Author(s): Kevin J. Walters
Implementation Notes
--------------------
**Hardware:**
* Adafruit CLUE <https://www.adafruit.com/product/4500>
**Software and Dependencies:**
* Adafruit's CLUE library: https://github.com/adafruit/Adafruit_CircuitPython_CLUE
"""
import time
import array
import displayio
import terminalio
from adafruit_display_text.label import Label
def mapf(value, in_min, in_max, out_min, out_max):
return (value - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
# This creates ('{:.0f}', '{:.1f}', '{:.2f}', etc
_FMT_DEC_PLACES = tuple("{:." + str(x) + "f}" for x in range(10))
def format_width(nchars, value):
"""Simple attempt to generate a value within nchars characters.
Return value can be too long, e.g. for nchars=5, bad things happen
with values > 99999 or < -9999 or < -99.9."""
neg_format = _FMT_DEC_PLACES[nchars - 3]
pos_format = _FMT_DEC_PLACES[nchars - 2]
if value <= -10.0:
text_value = neg_format.format(value) # may overflow width
elif value < 0.0:
text_value = neg_format.format(value)
elif value >= 10.0:
text_value = pos_format.format(value) # may overflow width
else:
text_value = pos_format.format(value) # 0.0 to 9.99999
return text_value
class Plotter():
_DEFAULT_SCALE_MODE = {"lines": "onscroll",
"dots": "screen"}
# Palette for plotting, first one is set transparent
TRANSPARENT_IDX = 0
# Removed one colour to get number down to 8 for more efficient
# bit-packing in displayio's Bitmap
_PLOT_COLORS = (0x000000,
0x0000ff,
0x00ff00,
0x00ffff,
0xff0000,
# 0xff00ff,
0xffff00,
0xffffff,
0xff0080)
POS_INF = float("inf")
NEG_INF = float("-inf")
# Approximate number of seconds to review data for zooming in
# and how often to do that check
ZOOM_IN_TIME = 8
ZOOM_IN_CHECK_TIME_NS = 5 * 1e9
# 20% headroom either side on zoom in/out
ZOOM_HEADROOM = 20 / 100
GRID_COLOR = 0x308030
GRID_DOT_SPACING = 8
_GRAPH_TOP = 30 # y position for the graph placement
INFO_FG_COLOR = 0x000080
INFO_BG_COLOR = 0xc0c000
LABEL_COLOR = 0xc0c0c0
def _display_manual(self):
"""Intention was to disable auto_refresh here but this needs a
simple displayio refresh to work well."""
self._output.auto_refresh = True
def _display_auto(self):
self._output.auto_refresh = True
def _display_refresh(self):
"""Intention was to call self._output.refresh() but this does not work well
as current implementation is designed with a fixed frame rate in mind."""
if self._output.auto_refresh:
return True
else:
return True
def __init__(self, output,
style="lines", mode="scroll", scale_mode=None,
screen_width=240, screen_height=240,
plot_width=192, plot_height=201,
x_divs=4, y_divs=4,
scroll_px=50,
max_channels=3,
est_rate=50,
title="",
max_title_len=20,
mu_output=False,
debug=0):
"""scroll_px of greater than 1 gives a jump scroll."""
# pylint: disable=too-many-locals,too-many-statements
self._output = output
self.change_stylemode(style, mode, scale_mode=scale_mode, clear=False)
self._screen_width = screen_width
self._screen_height = screen_height
self._plot_width = plot_width
self._plot_height = plot_height
self._plot_height_m1 = plot_height - 1
self._x_divs = x_divs
self._y_divs = y_divs
self._scroll_px = scroll_px
self._max_channels = max_channels
self._est_rate = est_rate
self._title = title
self._max_title_len = max_title_len
# These arrays are used to provide a circular buffer
# with _data_values valid values - this needs to be sized
# one larger than screen width to retrieve prior y position
# for line undrawing in wrap mode
self._data_size = self._plot_width + 1
self._data_y_pos = []
self._data_value = []
for _ in range(self._max_channels):
# 'i' is 32 bit signed integer
self._data_y_pos.append(array.array('i', [0] * self._data_size))
self._data_value.append(array.array('f', [0.0] * self._data_size))
# begin-keep-pylint-happy
self._data_mins = None
self._data_maxs = None
self._data_stats_maxlen = None
self._data_stats = None
self._values = None
self._data_values = None
self._x_pos = None
self._data_idx = None
self._plot_lastzoom_ns = None
# end-keep-pylint-happy
self._init_data()
self._mu_output = mu_output
self._debug = debug
self._channels = None
self._channel_colidx = []
# The range the data source generates within
self._abs_min = None
self._abs_max = None
# The current plot min/max
self._plot_min = None
self._plot_max = None
self._plot_min_range = None # Used partly to prevent div by zero
self._plot_range_lock = False
self._plot_dirty = False # flag indicate some data has been plotted
self._font = terminalio.FONT
self._y_axis_lab = ""
self._y_lab_width = 6 # maximum characters for y axis label
self._y_lab_color = self.LABEL_COLOR
self._displayio_graph = None
self._displayio_plot = None
self._displayio_title = None
self._displayio_info = None
self._displayio_y_labs = None
self._displayio_y_axis_lab = None
self._last_manual_refresh = None
def _init_data(self, ranges=True):
# Allocate arrays for each possible channel with plot_width elements
self._data_mins = [self.POS_INF]
self._data_maxs = [self.NEG_INF]
self._data_start_ns = [time.monotonic_ns()]
self._data_stats_maxlen = 10
# When in use the arrays in here are variable length
self._data_stats = [[] * self._max_channels]
self._values = 0 # total data processed
self._data_values = 0 # valid elements in data_y_pos and data_value
self._x_pos = 0
self._data_idx = 0
self._plot_lastzoom_ns = 0 # monotonic_ns() for last zoom in
if ranges:
self._plot_min = None
self._plot_max = None
self._plot_min_range = None # Used partly to prevent div by zero
self._plot_dirty = False # flag indicate some data has been plotted
def _recalc_y_pos(self):
"""Recalculates _data_y_pos based on _data_value for changes in y scale."""
# Check if nothing to do - important since _plot_min _plot_max not yet set
if self._data_values == 0:
return
for ch_idx in range(self._channels):
# intentional use of negative array indexing
for data_idx in range(self._data_idx - 1,
self._data_idx - 1 - self._data_values,
-1):
self._data_y_pos[ch_idx][data_idx] = round(mapf(self._data_value[ch_idx][data_idx],
self._plot_min,
self._plot_max,
self._plot_height_m1,
0))
def get_colors(self):
return self._PLOT_COLORS
def clear_all(self, ranges=True):
if self._values != 0:
self._undraw_bitmap()
self._init_data(ranges=ranges)
# Simple implementation here is to clear the screen on change...
def change_stylemode(self, style, mode, scale_mode=None, clear=True):
if style not in ("lines", "dots"):
raise ValueError("style not lines or dots")
if mode not in ("scroll", "wrap"):
raise ValueError("mode not scroll or wrap")
if scale_mode is None:
scale_mode = self._DEFAULT_SCALE_MODE[style]
elif scale_mode not in ("pixel", "onscroll", "screen", "time"):
raise ValueError("scale_mode not pixel, onscroll, screen or time")
# Clearing everything on screen and everything stored in variables
# apart from plot ranges is simplest approach here - clearing
# involves undrawing which uses the self._style so must not change
# that beforehand
if clear:
self.clear_all(ranges=False)
self._style = style
self._mode = mode
self._scale_mode = scale_mode
if self._mode == "wrap":
self._display_auto()
elif self._mode == "scroll":
self._display_manual()
def _make_empty_tg_plot_bitmap(self):
plot_bitmap = displayio.Bitmap(self._plot_width, self._plot_height,
len(self._PLOT_COLORS))
# Create a colour palette for plot dots/lines
plot_palette = displayio.Palette(len(self._PLOT_COLORS))
for idx in range(len(self._PLOT_COLORS)):
plot_palette[idx] = self._PLOT_COLORS[idx]
plot_palette.make_transparent(0)
tg_plot_data = displayio.TileGrid(plot_bitmap,
pixel_shader=plot_palette)
tg_plot_data.x = self._screen_width - self._plot_width - 1
tg_plot_data.y = self._GRAPH_TOP
return (tg_plot_data, plot_bitmap)
def _make_tg_grid(self):
# pylint: disable=too-many-locals
grid_width = self._plot_width
grid_height = self._plot_height_m1
div_width = self._plot_width // self._x_divs
div_height = self._plot_height // self._y_divs
a_plot_grid = displayio.Bitmap(div_width, div_height, 2)
# Grid colours
grid_palette = displayio.Palette(2)
grid_palette.make_transparent(0)
grid_palette[0] = 0x000000
grid_palette[1] = self.GRID_COLOR
# Horizontal line on grid rectangle
for x in range(0, div_width, self.GRID_DOT_SPACING):
a_plot_grid[x, 0] = 1
# Vertical line on grid rectangle
for y in range(0, div_height, self.GRID_DOT_SPACING):
a_plot_grid[0, y] = 1
right_line = displayio.Bitmap(1, grid_height, 2)
tg_right_line = displayio.TileGrid(right_line,
pixel_shader=grid_palette)
for y in range(0, grid_height, self.GRID_DOT_SPACING):
right_line[0, y] = 1
bottom_line = displayio.Bitmap(grid_width + 1, 1, 2)
tg_bottom_line = displayio.TileGrid(bottom_line,
pixel_shader=grid_palette)
for x in range(0, grid_width + 1, self.GRID_DOT_SPACING):
bottom_line[x, 0] = 1
# Create a TileGrid using the Bitmap and Palette
# and tiling it based on number of divisions required
tg_plot_grid = displayio.TileGrid(a_plot_grid,
pixel_shader=grid_palette,
width=self._x_divs,
height=self._y_divs,
default_tile = 0)
tg_plot_grid.x = self._screen_width - self._plot_width - 1
tg_plot_grid.y = self._GRAPH_TOP
tg_right_line.x = tg_plot_grid.x + grid_width
tg_right_line.y = tg_plot_grid.y
tg_bottom_line.x = tg_plot_grid.x
tg_bottom_line.y = tg_plot_grid.y + grid_height
g_plot_grid = displayio.Group(max_size=3)
g_plot_grid.append(tg_plot_grid)
g_plot_grid.append(tg_right_line)
g_plot_grid.append(tg_bottom_line)
return g_plot_grid
def _make_empty_graph(self, tg_and_plot=None):
font_w, font_h = self._font.get_bounding_box()
self._displayio_title = Label(self._font,
text=self._title,
max_glyphs=self._max_title_len,
scale=2,
line_spacing=1,
color=self._y_lab_color)
self._displayio_title.x = self._screen_width - self._plot_width
self._displayio_title.y = font_h // 2
self._displayio_y_axis_lab = Label(self._font,
text=self._y_axis_lab,
max_glyphs=self._y_lab_width,
line_spacing=1,
color=self._y_lab_color)
self._displayio_y_axis_lab.x = 0 # 0 works here because text is ""
self._displayio_y_axis_lab.y = font_h // 2
plot_y_labels = []
# y increases top to bottom of screen
for y_div in range(self._y_divs + 1):
plot_y_labels.append(Label(self._font,
text=" " * self._y_lab_width,
max_glyphs=self._y_lab_width,
line_spacing=1,
color=self._y_lab_color))
plot_y_labels[-1].x = (self._screen_width - self._plot_width
- self._y_lab_width * font_w - 5)
plot_y_labels[-1].y = (round(y_div * self._plot_height / self._y_divs)
+ self._GRAPH_TOP - 1)
self._displayio_y_labs = plot_y_labels
# Three items (grid, axis label, title) plus the y tick labels
g_background = displayio.Group(max_size=3+len(plot_y_labels))
g_background.append(self._make_tg_grid())
for label in self._displayio_y_labs:
g_background.append(label)
g_background.append(self._displayio_y_axis_lab)
g_background.append(self._displayio_title)
if tg_and_plot is not None:
(tg_plot, plot) = tg_and_plot
else:
(tg_plot, plot) = self._make_empty_tg_plot_bitmap()
self._displayio_plot = plot
# Create the main Group for display with one spare slot for
# popup informational text
main_group = displayio.Group(max_size=3)
main_group.append(g_background)
main_group.append(tg_plot)
self._displayio_info = None
return main_group
def set_y_axis_tick_labels(self, y_min, y_max):
px_per_div = (y_max - y_min) / self._y_divs
for idx, tick_label in enumerate(self._displayio_y_labs):
value = y_max - idx * px_per_div
text_value = format_width(self._y_lab_width, value)
tick_label.text = text_value[:self._y_lab_width]
def display_on(self, tg_and_plot=None):
if self._displayio_graph is None:
self._displayio_graph = self._make_empty_graph(tg_and_plot=tg_and_plot)
self._output.show(self._displayio_graph)
def display_off(self):
pass
def _draw_vline(self, x1, y1, y2, colidx):
"""Draw a clipped vertical line at x1 from pixel one along from y1 to y2.
"""
if y2 == y1:
if 0 <= y2 <= self._plot_height_m1:
self._displayio_plot[x1, y2] = colidx
return
# For y2 above y1, on screen this translates to being below
step = 1 if y2 > y1 else -1
for line_y_pos in range(max(0, min(y1 + step, self._plot_height_m1)),
max(0, min(y2, self._plot_height_m1)) + step,
step):
self._displayio_plot[x1, line_y_pos] = colidx
# def _clear_plot_bitmap(self): ### woz here
def _redraw_all_col_idx(self, col_idx_list):
x_cols = min(self._data_values, self._plot_width)
wrapMode = self._mode == "wrap"
if wrapMode:
x_data_idx = (self._data_idx - self._x_pos) % self._data_size
else:
x_data_idx = (self._data_idx - x_cols) % self._data_size
for ch_idx in range(self._channels):
col_idx = col_idx_list[ch_idx]
data_idx = x_data_idx
for x_pos in range(x_cols):
# "jump" the gap in the circular buffer for wrap mode
if wrapMode and x_pos == self._x_pos:
data_idx = (data_idx + self._data_size - self._plot_width) % self._data_size
# ideally this should inhibit lines between wrapped data
y_pos = self._data_y_pos[ch_idx][data_idx]
if self._style == "lines" and x_pos != 0:
# Python supports negative array index
prev_y_pos = self._data_y_pos[ch_idx][data_idx - 1]
self._draw_vline(x_pos, prev_y_pos, y_pos, col_idx)
else:
if 0 <= y_pos <= self._plot_height_m1:
self._displayio_plot[x_pos, y_pos] = col_idx
data_idx += 1
if data_idx >= self._data_size:
data_idx = 0
# This is almost always going to be quicker
# than the slow _clear_plot_bitmap implemented on 5.0.0 displayio
def _undraw_bitmap(self):
if not self._plot_dirty:
return
self._redraw_all_col_idx([self.TRANSPARENT_IDX] * self._channels)
self._plot_dirty = False
def _redraw_all(self):
self._redraw_all_col_idx(self._channel_colidx)
self._plot_dirty = True
def _undraw_column(self, x_pos, data_idx):
"""Undraw a single column at x_pos based on data from data_idx."""
colidx = self.TRANSPARENT_IDX
for ch_idx in range(self._channels):
y_pos = self._data_y_pos[ch_idx][data_idx]
if self._style == "lines" and x_pos != 0:
# Python supports negative array index
prev_y_pos = self._data_y_pos[ch_idx][data_idx - 1]
self._draw_vline(x_pos, prev_y_pos, y_pos, colidx)
else:
if 0 <= y_pos <= self._plot_height_m1:
self._displayio_plot[x_pos, y_pos] = colidx
# very similar code to _undraw_bitmap although that is now
# more sophisticated as it supports wrap mode
def _redraw_for_scroll(self, x1, x2, x1_data_idx):
"""Redraw data from x1 to x2 inclusive for scroll mode only."""
for ch_idx in range(self._channels):
colidx = self._channel_colidx[ch_idx]
data_idx = x1_data_idx
for x_pos in range(x1, x2 + 1):
y_pos = self._data_y_pos[ch_idx][data_idx]
if self._style == "lines" and x_pos != 0:
# Python supports negative array index
prev_y_pos = self._data_y_pos[ch_idx][data_idx - 1]
self._draw_vline(x_pos, prev_y_pos, y_pos, colidx)
else:
if 0 <= y_pos <= self._plot_height_m1:
self._displayio_plot[x_pos, y_pos] = colidx
data_idx += 1
if data_idx >= self._data_size:
data_idx = 0
self._plot_dirty = True
def _update_stats(self, values):
"""Update the statistics for minimum and maximum."""
for idx, value in enumerate(values):
# Occasionally check if we need to add a new bucket to stats
if idx == 0 and self._values & 0xf == 0:
now_ns = time.monotonic_ns()
if now_ns - self._data_start_ns[-1] > 1e9:
self._data_start_ns.append(now_ns)
self._data_mins.append(value)
self._data_maxs.append(value)
# Remove the first elements if too long
if len(self._data_start_ns) > self._data_stats_maxlen:
self._data_start_ns.pop(0)
self._data_mins.pop(0)
self._data_maxs.pop(0)
continue
if value < self._data_mins[-1]:
self._data_mins[-1] = value
if value > self._data_maxs[-1]:
self._data_maxs[-1] = value
def _data_store(self, values):
"""Store the data values in the circular buffer."""
for ch_idx, value in enumerate(values):
self._data_value[ch_idx][self._data_idx] = value
# Increment the data index for circular buffer
self._data_idx += 1
if self._data_idx >= self._data_size:
self._data_idx = 0
def _data_draw(self, values, x_pos, data_idx):
offscale = False
for ch_idx, value in enumerate(values):
# Last two parameters appear "swapped" - this deals with the
# displayio screen y coordinate increasing downwards
y_pos = round(mapf(value,
self._plot_min, self._plot_max,
self._plot_height_m1, 0))
if y_pos < 0 or y_pos >= self._plot_height:
offscale = True
self._data_y_pos[ch_idx][data_idx] = y_pos
if self._style == "lines" and self._x_pos != 0:
# Python supports negative array index
prev_y_pos = self._data_y_pos[ch_idx][data_idx - 1]
self._draw_vline(x_pos, prev_y_pos, y_pos,
self._channel_colidx[ch_idx])
self._plot_dirty = True # bit wrong if whole line is off screen
else:
if not offscale:
self._displayio_plot[x_pos, y_pos] = self._channel_colidx[ch_idx]
self._plot_dirty = True
def _check_zoom_in(self):
"""Check if recent data warrants zooming in on y axis scale based on checking
minimum and maximum times which are recorded in approximate 1 second buckets.
Returns two element tuple with (min, max) or empty tuple for no zoom required.
Caution is required with min == max."""
start_idx = len(self._data_start_ns) - self.ZOOM_IN_TIME
if start_idx < 0:
return ()
now_ns = time.monotonic_ns()
if now_ns < self._plot_lastzoom_ns + self.ZOOM_IN_CHECK_TIME_NS:
return ()
recent_min = min(self._data_mins[start_idx:])
recent_max = max(self._data_maxs[start_idx:])
recent_range = recent_max - recent_min
headroom = recent_range * self.ZOOM_HEADROOM
# No zoom if the range of data is near the plot range
if (self._plot_min > recent_min - headroom
and self._plot_max < recent_max + headroom):
return ()
new_plot_min = max(recent_min - headroom, self._abs_min)
new_plot_max = min(recent_max + headroom, self._abs_max)
return (new_plot_min, new_plot_max)
def _auto_plot_range(self, redraw_plot=True):
"""Check if we need to zoom out or in based on checking historical
data values unless y_range_lock has been set.
"""
if self._plot_range_lock:
return False
zoom_in = False
zoom_out = False
# Calcuate some new min/max values based on recentish data
# and add some headroom
y_min = min(self._data_mins)
y_max = max(self._data_maxs)
y_range = y_max - y_min
headroom = y_range * self.ZOOM_HEADROOM
new_plot_min = max(y_min - headroom, self._abs_min)
new_plot_max = min(y_max + headroom, self._abs_max)
# set new range if the data does not fit on the screen
# this will also redo y tick labels if necessary
if (new_plot_min < self._plot_min or new_plot_max > self._plot_max):
if self._debug >= 2:
print("Zoom out")
self._change_y_range(new_plot_min, new_plot_max,
redraw_plot=redraw_plot)
zoom_out = True
else: # otherwise check if zoom in is warranted
rescale_zoom_range = self._check_zoom_in()
if rescale_zoom_range:
if self._debug >= 2:
print("Zoom in")
self._change_y_range(rescale_zoom_range[0], rescale_zoom_range[1],
redraw_plot=redraw_plot)
zoom_in = True
if zoom_in or zoom_out:
self._plot_lastzoom_ns = time.monotonic_ns()
return True
return False
def data_add(self, values):
# pylint: disable=too-many-branches
changed = False
data_idx = self._data_idx
x_pos = self._x_pos
self._update_stats(values)
if self._mode == "wrap":
if self._x_pos == 0 or self._scale_mode == "pixel":
changed = self._auto_plot_range(redraw_plot=False)
# Undraw any previous data at current x position
if (not changed and self._data_values >= self._plot_width
and self._values >= self._plot_width):
self._undraw_column(self._x_pos, data_idx - self._plot_width)
elif self._mode == "scroll":
if x_pos >= self._plot_width: # Fallen off x axis range?
changed = self._auto_plot_range(redraw_plot=False)
if not changed:
self._undraw_bitmap() # Need to cls for the scroll
sc_data_idx = ((data_idx + self._scroll_px - self._plot_width)
% self._data_size)
self._data_values -= self._scroll_px
self._redraw_for_scroll(0,
self._plot_width - 1 - self._scroll_px,
sc_data_idx)
x_pos = self._plot_width - self._scroll_px
elif self._scale_mode == "pixel":
changed = self._auto_plot_range(redraw_plot=True)
# Draw the new data
self._data_draw(values, x_pos, data_idx)
# Store the new values in circular buffer
self._data_store(values)
# increment x position dealing with wrap/scroll
new_x_pos = x_pos + 1
if new_x_pos >= self._plot_width:
# fallen off edge so wrap or leave position
# on last column for scroll
if self._mode == "wrap":
self._x_pos = 0
else:
self._x_pos = new_x_pos # this is off screen
else:
self._x_pos = new_x_pos
if self._data_values < self._data_size:
self._data_values += 1
self._values += 1
if self._mu_output:
print(values)
# scrolling mode has automatic refresh in background turned off
if self._mode == "scroll":
self._display_refresh()
def _change_y_range(self, new_plot_min, new_plot_max, redraw_plot=True):
y_min = new_plot_min
y_max = new_plot_max
if self._debug >= 2:
print("Change Y range", new_plot_min, new_plot_max, redraw_plot)
# if values reduce range below the minimum then widen the range
# but keep it within the absolute min/max values
if self._plot_min_range is not None:
range_extend = self._plot_min_range - (y_max - y_min)
if range_extend > 0:
y_max += range_extend / 2
y_min -= range_extend / 2
if y_min < self._abs_min:
y_min = self._abs_min
y_max = y_min + self._plot_min_range
elif y_max > self._abs_max:
y_max = self._abs_max
y_min = y_max - self._plot_min_range
self._plot_min = y_min
self._plot_max = y_max
self.set_y_axis_tick_labels(self._plot_min, self._plot_max)
if self._values:
self._undraw_bitmap()
self._recalc_y_pos() ## calculates new y positions
if redraw_plot:
self._redraw_all()
@property
def title(self):
return self._title
@title.setter
def title(self, value):
self._title = value[:self._max_title_len] # does not show truncation
self._displayio_title.text = self._title
@property
def info(self):
if self._displayio_info is None:
return None
return self._displayio_info.text
@info.setter
def info(self, value):
"""Place some text on the screen.
Multiple lines are supported with newline character.
Font will be 3x standard terminalio font or 2x if that does not fit."""
if self._displayio_info is not None:
self._displayio_graph.pop()
if value is not None and value != "":
font_scale = 3
line_spacing = 1.25
font_w, font_h = self._font.get_bounding_box()
text_lines = value.split("\n")
max_word_chars = max([len(word) for word in text_lines])
# If too large reduce the scale
if (max_word_chars * font_scale * font_w > self._screen_width
or len(text_lines) * font_scale * font_h * line_spacing > self._screen_height):
font_scale -= 1
self._displayio_info = Label(self._font, text=value,
line_spacing=line_spacing,
scale=font_scale,
background_color=self.INFO_FG_COLOR,
color=self.INFO_BG_COLOR)
# centre the (left justified) text
self._displayio_info.x = (self._screen_width
- font_scale * font_w * max_word_chars) // 2
self._displayio_info.y = self._screen_height // 2
self._displayio_graph.append(self._displayio_info)
else:
self._displayio_info = None
if self._mode == "scroll":
self._display_refresh()
@property
def channels(self):
return self._channels
@channels.setter
def channels(self, value):
if value > self._max_channels:
raise ValueError("Exceeds max_channels")
self._channels = value
@property
def y_range(self):
return (self._plot_min, self._plot_max)
@y_range.setter
def y_range(self, minmax):
if minmax[0] != self._plot_min or minmax[1] != self._plot_max:
self._change_y_range(minmax[0], minmax[1], redraw_plot=True)
@property
def y_full_range(self):
return (self._plot_min, self._plot_max)
@y_full_range.setter
def y_full_range(self, minmax):
self._abs_min = minmax[0]
self._abs_max = minmax[1]
@property
def y_min_range(self):
return self._plot_min_range
@y_min_range.setter
def y_min_range(self, value):
self._plot_min_range = value
@property
def y_axis_lab(self):
return self._y_axis_lab
@y_axis_lab.setter
def y_axis_lab(self, text):
self._y_axis_lab = text[:self._y_lab_width]
font_w, _ = self._font.get_bounding_box()
x_pos = (40 - font_w * len(self._y_axis_lab)) // 2
# max() used to prevent negative (off-screen) values
self._displayio_y_axis_lab.x = max(0, x_pos)
self._displayio_y_axis_lab.text = self._y_axis_lab
@property
def channel_colidx(self):
return self._channel_colidx
@channel_colidx.setter
def channel_colidx(self, value):
# tuple() ensures object has a local / read-only copy of data
self._channel_colidx = tuple(value)
@property
def mu_output(self):
return self._mu_output
@mu_output.setter
def mu_output(self, value):
self._mu_output = value
@property
def y_range_lock(self):
return self._plot_range_lock
@y_range_lock.setter
def y_range_lock(self, value):
self._plot_range_lock = value

View file

@ -0,0 +1,111 @@
# The MIT License (MIT)
#
# Copyright (c) 2020 Kevin J. Walters
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import sys
import os
import unittest
from unittest.mock import Mock, MagicMock, PropertyMock
verbose = int(os.getenv('TESTVERBOSE', '2'))
# Mocking libraries which are about to be import'd by Plotter
sys.modules['analogio'] = MagicMock()
# Borrowing the dhalbert/tannewt technique from adafruit/Adafruit_CircuitPython_Motor
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
# import what we are testing or will test in future
# pylint: disable=unused-import,wrong-import-position
from plot_source import PlotSource, TemperaturePlotSource, PressurePlotSource, \
HumidityPlotSource, ColorPlotSource, ProximityPlotSource, \
IlluminatedColorPlotSource, VolumePlotSource, \
AccelerometerPlotSource, GyroPlotSource, \
MagnetometerPlotSource, PinPlotSource
# pylint: disable=protected-access
class Test_TemperaturePlotSource(unittest.TestCase):
SENSOR_DATA = (20, 21.3, 22.0, 0.0, -40, 85)
def test_celsius(self):
"""Create the source in Celsius mode and test with some values."""
# Emulate the clue's temperature sensor by
# returning a temperature from a small tuple
# of test data
mocked_clue = Mock()
expected_data = self.SENSOR_DATA
type(mocked_clue).temperature = PropertyMock(side_effect=self.SENSOR_DATA)
source = TemperaturePlotSource(mocked_clue,
mode="Celsius")
for expected_value in expected_data:
self.assertAlmostEqual(source.data(),
expected_value,
msg="Checking converted temperature is correct")
def test_fahrenheit(self):
"""Create the source in Fahrenheit mode and test with some values."""
# Emulate the clue's temperature sensor by
# returning a temperature from a small tuple
# of test data
mocked_clue = Mock()
expected_data = (68, 70.34, 71.6,
32.0, -40, 185)
type(mocked_clue).temperature = PropertyMock(side_effect=self.SENSOR_DATA)
source = TemperaturePlotSource(mocked_clue,
mode="Fahrenheit")
for expected_value in expected_data:
self.assertAlmostEqual(source.data(),
expected_value,
msg="Checking converted temperature is correct")
def test_kelvin(self):
"""Create the source in Kelvin mode and test with some values."""
# Emulate the clue's temperature sensor by
# returning a temperature from a small tuple
# of test data
mocked_clue = Mock()
expected_data = (293.15, 294.45, 295.15,
273.15, 233.15, 358.15)
type(mocked_clue).temperature = PropertyMock(side_effect=self.SENSOR_DATA)
source = TemperaturePlotSource(mocked_clue,
mode="Kelvin")
for expected_value in expected_data:
data = source.data()
# self.assertEqual(data,
# expected_value,
# msg="An inappropriate check for floating-point")
self.assertAlmostEqual(data,
expected_value,
msg="Checking converted temperature is correct")
if __name__ == '__main__':
unittest.main(verbosity=verbose)

View file

@ -0,0 +1,555 @@
# The MIT License (MIT)
#
# Copyright (c) 2020 Kevin J. Walters
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import sys
import time
import array
import os
import unittest
from unittest.mock import Mock, MagicMock, patch
import numpy
verbose = int(os.getenv('TESTVERBOSE', '2'))
# Mocking libraries which are about to be import'd by Plotter
sys.modules['board'] = MagicMock()
sys.modules['displayio'] = MagicMock()
sys.modules['terminalio'] = MagicMock()
sys.modules['adafruit_display_text.label'] = MagicMock()
# Replicate CircuitPython's time.monotonic_ns() pre 3.5
if not hasattr(time, "monotonic_ns"):
time.monotonic_ns = lambda: int(time.monotonic() * 1e9)
# Borrowing the dhalbert/tannewt technique from adafruit/Adafruit_CircuitPython_Motor
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
# pylint: disable=wrong-import-position
# import what we are testing
from plotter import Plotter
import terminalio # mocked
terminalio.FONT = Mock()
terminalio.FONT.get_bounding_box = Mock(return_value=(6, 14))
# TODO use setup() and tearDown()
# - https://docs.python.org/3/library/unittest.html#unittest.TestCase.tearDown
# pylint: disable=protected-access, no-self-use, too-many-locals
class Test_Plotter(unittest.TestCase):
"""Tests for Plotter.
Very useful but code needs a good tidy particulary around widths,
lots of 200 hard-coded numbers.
Would benefit from testing different widths too."""
# These were the original dimensions of the Bitmap
# Current clue-plotter uses 192 for width and
# scrolling is set to 50
_PLOT_WIDTH = 200
_PLOT_HEIGHT = 201
_SCROLL_PX = 25
def count_nz_rows(self, bitmap):
nz_rows = []
for y_pos in range(self._PLOT_HEIGHT):
count = 0
for x_pos in range(self._PLOT_WIDTH):
if bitmap[x_pos, y_pos] != 0:
count += 1
if count > 0:
nz_rows.append(y_pos)
return nz_rows
def aprint_plot(self, bitmap):
for y in range(self._PLOT_HEIGHT):
for x in range(self._PLOT_WIDTH):
print("X" if bitmap[x][y] else " ", end="")
print()
def make_a_Plotter(self, style, mode, scale_mode=None):
mocked_display = Mock()
plotter = Plotter(mocked_display,
style=style,
mode=mode,
scale_mode=scale_mode,
scroll_px=self._SCROLL_PX,
plot_width=self._PLOT_WIDTH,
plot_height=self._PLOT_HEIGHT,
title="Debugging",
max_title_len=99,
mu_output=False,
debug=0)
return plotter
def ready_plot_source(self, plttr, source):
#source_name = str(source)
plttr.clear_all()
#plttr.title = source_name
#plttr.y_axis_lab = source.units()
plttr.y_range = (source.initial_min(), source.initial_max())
plttr.y_full_range = (source.min(), source.max())
plttr.y_min_range = source.range_min()
channels_from_source = source.values()
plttr.channels = channels_from_source
plttr.channel_colidx = (1, 2, 3)
source.start()
return (source, channels_from_source)
def make_a_PlotSource(self, channels = 1):
ps = Mock()
ps.initial_min = Mock(return_value=-100.0)
ps.initial_max = Mock(return_value=100.0)
ps.min = Mock(return_value=-100.0)
ps.max = Mock(return_value=100.0)
ps.range_min = Mock(return_value=5.0)
if channels == 1:
ps.values = Mock(return_value=channels)
ps.data = Mock(side_effect=list(range(10,90)) * 100)
elif channels == 3:
ps.values = Mock(return_value=channels)
ps.data = Mock(side_effect=list(zip(list(range(10,90)),
list(range(15,95)),
list(range(40,60)) * 4)) * 100)
return ps
def make_a_PlotSource_narrowrange(self):
ps = Mock()
ps.initial_min = Mock(return_value=0.0)
ps.initial_max = Mock(return_value=500.0)
ps.min = Mock(return_value=0.0)
ps.max = Mock(return_value=500.0)
ps.range_min = Mock(return_value=5.0)
ps.values = Mock(return_value=1)
# 24 elements repeated 13 times ranging between 237 and 253
# 5 elements repeated 6000 times
ps.data = Mock(side_effect=(list(range(237, 260 + 1)) * 13
+ list(range(100, 400 + 1, 75)) * 6000))
return ps
def make_a_PlotSource_onespike(self):
ps = Mock()
ps.initial_min = Mock(return_value=-100.0)
ps.initial_max = Mock(return_value=100.0)
ps.min = Mock(return_value=-100.0)
ps.max = Mock(return_value=100.0)
ps.range_min = Mock(return_value=5.0)
ps.values = Mock(return_value=1)
ps.data = Mock(side_effect=([0]*95 + [5,10,20,50,80,90,70,30,20,10]
+ [0] * 95 + [1] * 1000))
return ps
def make_a_PlotSource_bilevel(self, first_v=60, second_v=700):
ps = Mock()
ps.initial_min = Mock(return_value=-100.0)
ps.initial_max = Mock(return_value=100.0)
ps.min = Mock(return_value=-1000.0)
ps.max = Mock(return_value=1000.0)
ps.range_min = Mock(return_value=10.0)
ps.values = Mock(return_value=1)
ps.data = Mock(side_effect=[first_v] * 199 + [second_v] * 1001)
return ps
def test_spike_after_wrap_and_overwrite_one_channel(self):
"""A specific test to check that a spike that appears in wrap mode is
correctly cleared by subsequent flat data."""
plotter = self.make_a_Plotter("lines", "wrap")
(tg, plot) = (Mock(), numpy.zeros((self._PLOT_WIDTH, self._PLOT_HEIGHT), numpy.uint8))
plotter.display_on(tg_and_plot=(tg, plot))
test_source1 = self.make_a_PlotSource_onespike()
self.ready_plot_source(plotter, test_source1)
unique1, _ = numpy.unique(plot, return_counts=True)
self.assertTrue(numpy.alltrue(unique1 == [0]),
"Checking all pixels start as 0")
# Fill screen
for _ in range(200):
plotter.data_add((test_source1.data(),))
unique2, _ = numpy.unique(plot, return_counts=True)
self.assertTrue(numpy.alltrue(unique2 == [0, 1]),
"Checking pixels are now a mix of 0 and 1")
# Rewrite whole screen with new data as we are in wrap mode
for _ in range(190):
plotter.data_add((test_source1.data(),))
non_zero_rows = self.count_nz_rows(plot)
if verbose >= 4:
print("y=99", plot[:, 99])
print("y=100", plot[:, 100])
self.assertTrue(9 not in non_zero_rows,
"Check nothing is just above 90 which plots at 10")
self.assertEqual(non_zero_rows, [99, 100],
"Only pixels left plotted should be from"
+ "values 0 and 1 being plotted at 99 and 100")
self.assertTrue(numpy.alltrue(plot[:, 99] == [1] * 190 + [0] * 10),
"Checking row 99 precisely")
self.assertTrue(numpy.alltrue(plot[:, 100] == [0] * 190 + [1] * 10),
"Checking row 100 precisely")
plotter.display_off()
def test_clearmode_from_lines_wrap_to_dots_scroll(self):
"""A specific test to check that a spike that appears in lines wrap mode is
correctly cleared by a change to dots scroll."""
plotter = self.make_a_Plotter("lines", "wrap")
(tg, plot) = (Mock(), numpy.zeros((self._PLOT_WIDTH, self._PLOT_HEIGHT), numpy.uint8))
plotter.display_on(tg_and_plot=(tg, plot))
test_source1 = self.make_a_PlotSource_onespike()
self.ready_plot_source(plotter, test_source1)
unique1, _ = numpy.unique(plot, return_counts=True)
self.assertTrue(numpy.alltrue(unique1 == [0]),
"Checking all pixels start as 0")
# Fill screen then wrap to write another 20 values
for _ in range(200 + 20):
plotter.data_add((test_source1.data(),))
unique2, _ = numpy.unique(plot, return_counts=True)
self.assertTrue(numpy.alltrue(unique2 == [0, 1]),
"Checking pixels are now a mix of 0 and 1")
plotter.change_stylemode("dots", "scroll")
unique3, _ = numpy.unique(plot, return_counts=True)
self.assertTrue(numpy.alltrue(unique3 == [0]),
"Checking all pixels are now 0 after change_stylemode")
plotter.display_off()
def test_clear_after_scrolling_one_channel(self):
"""A specific test to check screen clears after a scroll to help
investigate a bug with that failing to happen in most cases."""
plotter = self.make_a_Plotter("lines", "scroll")
(tg, plot) = (Mock(), numpy.zeros((self._PLOT_WIDTH, self._PLOT_HEIGHT), numpy.uint8))
plotter.display_on(tg_and_plot=(tg, plot))
test_source1 = self.make_a_PlotSource()
self.ready_plot_source(plotter, test_source1)
unique1, _ = numpy.unique(plot, return_counts=True)
self.assertTrue(numpy.alltrue(unique1 == [0]),
"Checking all pixels start as 0")
# Fill screen
for _ in range(200):
plotter.data_add((test_source1.data(),))
unique2, _ = numpy.unique(plot, return_counts=True)
self.assertTrue(numpy.alltrue(unique2 == [0, 1]),
"Checking pixels are now a mix of 0 and 1")
self.assertEqual(plotter._values, 200)
self.assertEqual(plotter._data_values, 200)
# Force a single scroll of the data
for _ in range(10):
plotter.data_add((test_source1.data(),))
self.assertEqual(plotter._values, 200 + 10)
self.assertEqual(plotter._data_values, 200 + 10 - self._SCROLL_PX)
# This should clear all data and the screen
if verbose >= 3:
print("change_stylemode() to a new mode which will clear screen")
plotter.change_stylemode("dots", "wrap")
unique3, _ = numpy.unique(plot, return_counts=True)
self.assertTrue(numpy.alltrue(unique3 == [0]),
"Checking all pixels are now 0")
plotter.display_off()
def test_check_internal_data_three_channels(self):
width = self._PLOT_WIDTH
plotter = self.make_a_Plotter("lines", "scroll")
(tg, plot) = (Mock(), numpy.zeros((width, self._PLOT_HEIGHT), numpy.uint8))
plotter.display_on(tg_and_plot=(tg, plot))
test_triplesource1 = self.make_a_PlotSource(channels=3)
self.ready_plot_source(plotter, test_triplesource1)
unique1, _ = numpy.unique(plot, return_counts=True)
self.assertTrue(numpy.alltrue(unique1 == [0]),
"Checking all pixels start as 0")
# Three data samples
all_data = []
for d_idx in range(3):
all_data.append(test_triplesource1.data())
plotter.data_add(all_data[-1])
# all_data is now [(10, 15, 40), (11, 16, 41), (12, 17, 42)]
self.assertEqual(plotter._data_y_pos[0][0:3],
array.array('i', [90, 89, 88]),
"channel 0 plotted y positions")
self.assertEqual(plotter._data_y_pos[1][0:3],
array.array('i', [85, 84, 83]),
"channel 1 plotted y positions")
self.assertEqual(plotter._data_y_pos[2][0:3],
array.array('i', [60, 59, 58]),
"channel 2 plotted y positions")
# Fill rest of screen
for d_idx in range(197):
all_data.append(test_triplesource1.data())
plotter.data_add(all_data[-1])
# Three values more values to force a scroll
for d_idx in range(3):
all_data.append(test_triplesource1.data())
plotter.data_add(all_data[-1])
# all_data[-4] is (49, 54, 59)
# all_data[-3:0] is [(50, 55, 40) (51, 56, 41) (52, 57, 42)]
expected_data_size = width - self._SCROLL_PX + 3
st_x_pos = width - self._SCROLL_PX
d_idx = plotter._data_idx - 3
self.assertTrue(self._SCROLL_PX > 3,
"Ensure no scrolling occurred from recent 3 values")
# the data_idx here is 2 because the size is now plot_width + 1
self.assertEqual(plotter._data_idx, 2)
self.assertEqual(plotter._x_pos, st_x_pos + 3)
self.assertEqual(plotter._data_values, expected_data_size)
self.assertEqual(plotter._values, len(all_data))
if verbose >= 4:
print("YP",d_idx, plotter._data_y_pos[0][d_idx:d_idx+3])
print("Y POS", [str(plotter._data_y_pos[ch_idx][d_idx:d_idx+3])
for ch_idx in [0, 1, 2]])
ch0_ypos = [50, 49, 48]
self.assertEqual([plotter._data_y_pos[0][idx] for idx in range(d_idx, d_idx + 3)],
ch0_ypos,
"channel 0 plotted y positions")
ch1_ypos = [45, 44, 43]
self.assertEqual([plotter._data_y_pos[1][idx] for idx in range(d_idx, d_idx + 3)],
ch1_ypos,
"channel 1 plotted y positions")
ch2_ypos = [60, 59, 58]
self.assertEqual([plotter._data_y_pos[2][idx] for idx in range(d_idx, d_idx + 3)],
ch2_ypos,
"channel 2 plotted y positions")
# Check for plot points - fortunately none overlap
total_pixel_matches = 0
for ch_idx, ch_ypos in enumerate((ch0_ypos, ch1_ypos, ch2_ypos)):
expected = plotter.channel_colidx[ch_idx]
for idx, y_pos in enumerate(ch_ypos):
actual = plot[st_x_pos+idx, y_pos]
if actual == expected:
total_pixel_matches += 1
else:
if verbose >= 4:
print("Pixel value for channel",
"{:d}, naive expectation {:d},".format(ch_idx,
expected),
"actual {:d} at {:d}, {:d}, {:d}".format(idx,
actual,
st_x_pos + idx,
y_pos))
# Only 7 out of 9 will match because channel 2 put a vertical
# line at x position 175 over-writing ch0 and ch1
self.assertEqual(total_pixel_matches, 7, "plotted pixels check")
# Check for that line from pixel positions 42 to 60
for y_pos in range(42, 60 + 1):
self.assertEqual(plot[st_x_pos, y_pos],
plotter.channel_colidx[2],
"channel 2 (over-writing) vertical line")
plotter.display_off()
def test_clear_after_scrolling_three_channels(self):
"""A specific test to check screen clears after a scroll with
multiple channels being plotted (three) to help
investigate a bug with that failing to happen in most cases
for the second and third channels."""
plotter = self.make_a_Plotter("lines", "scroll")
(tg, plot) = (Mock(), numpy.zeros((self._PLOT_WIDTH, self._PLOT_HEIGHT), numpy.uint8))
plotter.display_on(tg_and_plot=(tg, plot))
test_triplesource1 = self.make_a_PlotSource(channels=3)
self.ready_plot_source(plotter, test_triplesource1)
unique1, _ = numpy.unique(plot, return_counts=True)
self.assertTrue(numpy.alltrue(unique1 == [0]),
"Checking all pixels start as 0")
# Fill screen
for _ in range(200):
plotter.data_add(test_triplesource1.data())
unique2, _ = numpy.unique(plot, return_counts=True)
self.assertTrue(numpy.alltrue(unique2 == [0, 1, 2, 3]),
"Checking pixels are now a mix of 0, 1, 2, 3")
# Force a single scroll of the data
for _ in range(10):
plotter.data_add(test_triplesource1.data())
# This should clear all data and the screen
if verbose >= 3:
print("change_stylemode() to a new mode which will clear screen")
plotter.change_stylemode("dots", "wrap")
unique3, _ = numpy.unique(plot, return_counts=True)
self.assertTrue(numpy.alltrue(unique3 == [0]),
"Checking all pixels are now 0")
plotter.display_off()
def test_auto_rescale_wrap_mode(self):
"""Ensure the auto-scaling is working and not leaving any remnants of previous plot."""
plotter = self.make_a_Plotter("lines", "wrap")
(tg, plot) = (Mock(), numpy.zeros((self._PLOT_WIDTH, self._PLOT_HEIGHT), numpy.uint8))
plotter.display_on(tg_and_plot=(tg, plot))
test_source1 = self.make_a_PlotSource_bilevel(first_v=60, second_v=900)
self.ready_plot_source(plotter, test_source1)
unique1, _ = numpy.unique(plot, return_counts=True)
self.assertTrue(numpy.alltrue(unique1 == [0]),
"Checking all pixels start as 0")
# Fill screen with first 200
for _ in range(200):
plotter.data_add((test_source1.data(),))
non_zero_rows1 = self.count_nz_rows(plot)
self.assertEqual(non_zero_rows1, list(range(0, 40 + 1)),
"From value 60 being plotted at 40 but also upward line at end")
# Rewrite screen with next 200 but these should force an internal
# rescaling of y axis
for _ in range(200):
plotter.data_add((test_source1.data(),))
self.assertEqual(plotter.y_range, (-108.0, 1000.0),
"Check rescaled y range")
non_zero_rows2 = self.count_nz_rows(plot)
self.assertEqual(non_zero_rows2, [18],
"Only pixels now should be from value 900 being plotted at 18")
plotter.display_off()
def test_rescale_zoom_in_minequalsmax(self):
"""Test y_range adjusts any attempt to set the effective range to 0."""
plotter = self.make_a_Plotter("lines", "wrap")
(tg, plot) = (Mock(), numpy.zeros((self._PLOT_WIDTH, self._PLOT_HEIGHT), numpy.uint8))
plotter.display_on(tg_and_plot=(tg, plot))
test_source1 = self.make_a_PlotSource_bilevel(first_v=20, second_v=20)
self.ready_plot_source(plotter, test_source1)
# Set y_range to a value which will cause a range of 0 with
# the potential dire consequence of divide by zero
plotter.y_range = (20, 20)
plotter.data_add((test_source1.data(),))
y_min, y_max = plotter.y_range
self.assertTrue(y_max - y_min > 0,
"Range is not zero and implicitly"
+ "ZeroDivisionError exception has not occurred.")
plotter.display_off()
def test_rescale_zoom_in_narrowrangedata(self):
"""Test y_range adjusts on data from a narrow range with unusual per pixel scaling mode."""
# There was a bug which was visually obvious in pixel scale_mode
# test this to ensure bug was squashed
# time.monotonic_ns.return_value = lambda: global_time_ns
local_time_ns = time.monotonic_ns()
with patch('time.monotonic_ns', create=True,
side_effect=lambda: local_time_ns) as _:
plotter = self.make_a_Plotter("lines", "wrap", scale_mode="pixel")
(tg, plot) = (Mock(), numpy.zeros((self._PLOT_WIDTH, self._PLOT_HEIGHT), numpy.uint8))
plotter.display_on(tg_and_plot=(tg, plot))
test_source1 = self.make_a_PlotSource_narrowrange()
self.ready_plot_source(plotter, test_source1)
# About 11 seconds worth - will have zoomed in during this time
for _ in range(300):
val = test_source1.data()
plotter.data_add((val,))
local_time_ns += round(1/27 * 1e9) # emulation of time.sleep(1/27)
y_min1, y_max1 = plotter.y_range
self.assertAlmostEqual(y_min1, 232.4)
self.assertAlmostEqual(y_max1, 264.6)
unique, counts = numpy.unique(plotter._data_y_pos[0],
return_counts=True)
self.assertEqual(min(unique), 29)
self.assertEqual(max(unique), 171)
self.assertEqual(len(unique), 24)
self.assertLessEqual(max(counts) - min(counts), 1)
# Another 14 seconds and now data is in narrow range so another zoom is due
# Why does this take so long?
for _ in range(400):
val = test_source1.data()
plotter.data_add((val,))
local_time_ns += round(1/27 * 1e9) # emulation of time.sleep(1/27)
y_min2, y_max2 = plotter.y_range
self.assertAlmostEqual(y_min2, 40.0)
self.assertAlmostEqual(y_max2, 460.0)
#unique2, counts2 = numpy.unique(plotter._data_y_pos[0],
# return_counts=True)
#self.assertEqual(list(unique2), [29, 100, 171])
#self.assertLessEqual(max(counts2) - min(counts2), 1)
if verbose >= 3:
self.aprint_plot(plot)
# Look for a specific bug which leaves some previous pixels
# set on screen at column 24
# Checking either side as this will be timing sensitive but the time
# functions are now precisely controlled in this test so should not vary
# with test execution duration vs wall clock
for offset in range(-15, 15 + 5, 5):
self.assertEqual(list(plot[24 + offset][136:172]), [0] * 36,
"Checking for erased pixels at various columns")
plotter.display_off()
if __name__ == '__main__':
unittest.main(verbosity=verbose)

View file

@ -59,18 +59,32 @@ def read_region(timeout=30):
while time.time() - start_time < timeout:
if cpx.touch_A1:
val = PAD_REGION['A1']
time.sleep(.3)
break
elif cpx.touch_A2:
val = PAD_REGION['A2']
time.sleep(.3)
break
elif cpx.touch_A3:
val = PAD_REGION['A3']
time.sleep(.3)
break
elif cpx.touch_A4:
val = PAD_REGION['A4']
time.sleep(.3)
break
elif cpx.touch_A5:
val = PAD_REGION['A5']
time.sleep(.3)
break
elif cpx.touch_A6:
val = PAD_REGION['A6']
time.sleep(.3)
break
elif cpx.touch_A7:
val = PAD_REGION['A7']
time.sleep(.3)
break
return val
def play_sequence(sequence):

View file

@ -1,10 +1,22 @@
import time
import array
import math
import audioio
import board
import digitalio
try:
from audiocore import RawSample
except ImportError:
from audioio import RawSample
try:
from audioio import AudioOut
except ImportError:
try:
from audiopwmio import PWMAudioOut as AudioOut
except ImportError:
pass # not always supported by every board!
button = digitalio.DigitalInOut(board.A1)
button.switch_to_input(pull=digitalio.Pull.UP)
@ -15,8 +27,8 @@ sine_wave = array.array("H", [0] * length)
for i in range(length):
sine_wave[i] = int((1 + math.sin(math.pi * 2 * i / length)) * tone_volume * (2 ** 15 - 1))
audio = audioio.AudioOut(board.A0)
sine_wave_sample = audioio.RawSample(sine_wave)
audio = AudioOut(board.A0)
sine_wave_sample = RawSample(sine_wave)
while True:
if not button.value:

View file

@ -1,14 +1,26 @@
import time
import audioio
import board
import digitalio
try:
from audiocore import WaveFile
except ImportError:
from audioio import WaveFile
try:
from audioio import AudioOut
except ImportError:
try:
from audiopwmio import PWMAudioOut as AudioOut
except ImportError:
pass # not always supported by every board!
button = digitalio.DigitalInOut(board.A1)
button.switch_to_input(pull=digitalio.Pull.UP)
wave_file = open("StreetChicken.wav", "rb")
wave = audioio.WaveFile(wave_file)
audio = audioio.AudioOut(board.A0)
wave = WaveFile(wave_file)
audio = AudioOut(board.A0)
while True:
audio.play(wave)

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View file

@ -0,0 +1,136 @@
import random
import time
import board
import displayio
import framebufferio
import rgbmatrix
displayio.release_displays()
matrix = rgbmatrix.RGBMatrix(
width=64, height=32, bit_depth=3,
rgb_pins=[board.D6, board.D5, board.D9, board.D11, board.D10, board.D12],
addr_pins=[board.A5, board.A4, board.A3, board.A2],
clock_pin=board.D13, latch_pin=board.D0, output_enable_pin=board.D1)
display = framebufferio.FramebufferDisplay(matrix, auto_refresh=False)
# This bitmap contains the emoji we're going to use. It is assumed
# to contain 20 icons, each 20x24 pixels. This fits nicely on the 64x32
# RGB matrix display.
bitmap_file = open("emoji.bmp", 'rb')
bitmap = displayio.OnDiskBitmap(bitmap_file)
# Each wheel can be in one of three states:
STOPPED, RUNNING, BRAKING = range(3)
# Return a duplicate of the input list in a random (shuffled) order.
def shuffled(seq):
return sorted(seq, key=lambda _: random.random())
# The Wheel class manages the state of one wheel. "pos" is a position in
# scaled integer coordinates, with one revolution being 7680 positions
# and 1 pixel being 16 positions. The wheel also has a velocity (in positions
# per tick) and a state (one of the above constants)
class Wheel(displayio.TileGrid):
def __init__(self):
# Portions of up to 3 tiles are visible.
super().__init__(bitmap=bitmap, pixel_shader=displayio.ColorConverter(),
width=1, height=3, tile_width=20)
self.order = shuffled(range(20))
self.state = STOPPED
self.pos = 0
self.vel = 0
self.y = 0
self.x = 0
self.stop_time = time.monotonic_ns()
def step(self):
# Update each wheel for one time step
if self.state == RUNNING:
# Slowly lose speed when running, but go at least speed 64
self.vel = max(self.vel * 9 // 10, 64)
if time.monotonic_ns() > self.stop_time:
self.state = BRAKING
elif self.state == BRAKING:
# More quickly lose speed when baking, down to speed 7
self.vel = max(self.vel * 85 // 100, 7)
# Advance the wheel according to the velocity, and wrap it around
# after 7680 positions
self.pos = (self.pos + self.vel) % 7680
# Compute the rounded Y coordinate
yy = round(self.pos / 16)
# Compute the offset of the tile (tiles are 24 pixels tall)
yyy = yy % 24
# Find out which tile is the top tile
off = yy // 24
# If we're braking and a tile is close to midscreen,
# then stop and make sure that tile is exactly centered
if self.state == BRAKING and self.vel == 7 and yyy < 4:
self.pos = off * 24 * 16
self.vel = 0
yy = 0
self.state = STOPPED
# Move the displayed tiles to the correct height and make sure the
# correct tiles are displayed.
self.y = yyy - 20
for i in range(3):
self[i] = self.order[(19 - i + off) % 20]
# Set the wheel running again, using a slight bit of randomness.
# The 'i' value makes sure the first wheel brakes first, the second
# brakes second, and the third brakes third.
def kick(self, i):
self.state = RUNNING
self.vel = random.randint(256, 320)
self.stop_time = time.monotonic_ns() + 3000000000 + i * 350000000
# Our fruit machine has 3 wheels, let's create them with a correct horizontal
# (x) offset and arbitrary vertical (y) offset.
g = displayio.Group(max_size=3)
wheels = []
for idx in range(3):
wheel = Wheel()
wheel.x = idx * 22
wheel.y = -20
g.append(wheel)
wheels.append(wheel)
display.show(g)
# Make a unique order of the emoji on each wheel
orders = [shuffled(range(20)), shuffled(range(20)), shuffled(range(20))]
# And put up some images to start with
for si, oi in zip(wheels, orders):
for idx in range(3):
si[idx] = oi[idx]
# We want a way to check if all the wheels are stopped
def all_stopped():
return all(si.state == STOPPED for si in wheels)
# To start with, though, they're all in motion
for idx, si in enumerate(wheels):
si.kick(idx)
# Here's the main loop
while True:
# Refresh the dislpay (doing this manually ensures the wheels move
# together, not at different times)
display.refresh(minimum_frames_per_second=0)
if all_stopped():
# Once everything comes to a stop, wait a little bit and then
# start everything over again. Maybe you want to check if the
# combination is a "winner" and add a light show or something.
for idx in range(100):
display.refresh(minimum_frames_per_second=0)
for idx, si in enumerate(wheels):
si.kick(idx)
# Otherwise, let the wheels keep spinning...
for idx, si in enumerate(wheels):
si.step()

View file

@ -0,0 +1,120 @@
import random
import time
import board
import displayio
import framebufferio
import rgbmatrix
displayio.release_displays()
# Conway's "Game of Life" is played on a grid with simple rules, based
# on the number of filled neighbors each cell has and whether the cell itself
# is filled.
# * If the cell is filled, and 2 or 3 neighbors are filled, the cell stays
# filled
# * If the cell is empty, and exactly 3 neighbors are filled, a new cell
# becomes filled
# * Otherwise, the cell becomes or remains empty
#
# The complicated way that the "m1" (minus 1) and "p1" (plus one) offsets are
# calculated is due to the way the grid "wraps around", with the left and right
# sides being connected, as well as the top and bottom sides being connected.
#
# This function has been somewhat optimized, so that when it indexes the bitmap
# a single number [x + width * y] is used instead of indexing with [x, y].
# This makes the animation run faster with some loss of clarity. More
# optimizations are probably possible.
def apply_life_rule(old, new):
width = old.width
height = old.height
for y in range(height):
yyy = y * width
ym1 = ((y + height - 1) % height) * width
yp1 = ((y + 1) % height) * width
xm1 = width - 1
for x in range(width):
xp1 = (x + 1) % width
neighbors = (
old[xm1 + ym1] + old[xm1 + yyy] + old[xm1 + yp1] +
old[x + ym1] + old[x + yp1] +
old[xp1 + ym1] + old[xp1 + yyy] + old[xp1 + yp1])
new[x+yyy] = neighbors == 3 or (neighbors == 2 and old[x+yyy])
xm1 = x
# Fill 'fraction' out of all the cells.
def randomize(output, fraction=0.33):
for i in range(output.height * output.width):
output[i] = random.random() < fraction
# Fill the grid with a tribute to John Conway
def conway(output):
# based on xkcd's tribute to John Conway (1937-2020) https://xkcd.com/2293/
conway_data = [
b' +++ ',
b' + + ',
b' + + ',
b' + ',
b'+ +++ ',
b' + + + ',
b' + + ',
b' + + ',
b' + + ',
]
for i in range(output.height * output.width):
output[i] = 0
for i, si in enumerate(conway_data):
y = output.height - len(conway_data) - 2 + i
for j, cj in enumerate(si):
output[(output.width - 8)//2 + j, y] = cj & 1
# bit_depth=1 is used here because we only use primary colors, and it makes
# the animation run a bit faster because RGBMatrix isn't taking over the CPU
# as often.
matrix = rgbmatrix.RGBMatrix(
width=64, height=32, bit_depth=1,
rgb_pins=[board.D6, board.D5, board.D9, board.D11, board.D10, board.D12],
addr_pins=[board.A5, board.A4, board.A3, board.A2],
clock_pin=board.D13, latch_pin=board.D0, output_enable_pin=board.D1)
display = framebufferio.FramebufferDisplay(matrix, auto_refresh=False)
SCALE = 1
b1 = displayio.Bitmap(display.width//SCALE, display.height//SCALE, 2)
b2 = displayio.Bitmap(display.width//SCALE, display.height//SCALE, 2)
palette = displayio.Palette(2)
tg1 = displayio.TileGrid(b1, pixel_shader=palette)
tg2 = displayio.TileGrid(b2, pixel_shader=palette)
g1 = displayio.Group(max_size=3, scale=SCALE)
g1.append(tg1)
display.show(g1)
g2 = displayio.Group(max_size=3, scale=SCALE)
g2.append(tg2)
# First time, show the Conway tribute
palette[1] = 0xffffff
conway(b1)
display.auto_refresh = True
time.sleep(3)
n = 40
while True:
# run 2*n generations.
# For the Conway tribute on 64x32, 80 frames is appropriate. For random
# values, 400 frames seems like a good number. Working in this way, with
# two bitmaps, reduces copying data and makes the animation a bit faster
for _ in range(n):
display.show(g1)
apply_life_rule(b1, b2)
display.show(g2)
apply_life_rule(b2, b1)
# After 2*n generations, fill the board with random values and
# start over with a new color.
randomize(b1)
# Pick a random color out of 6 primary colors or white.
palette[1] = (
(0x0000ff if random.random() > .33 else 0) |
(0x00ff00 if random.random() > .33 else 0) |
(0xff0000 if random.random() > .33 else 0)) or 0xffffff
n = 200

View file

@ -0,0 +1,109 @@
# This example implements a rainbow colored scroller, in which each letter
# has a different color. This is not possible with
# Adafruit_Circuitpython_Display_Text, where each letter in a label has the
# same color
#
# This demo also supports only ASCII characters and the built-in font.
# See the simple_scroller example for one that supports alternative fonts
# and characters, but only has a single color per label.
import array
from _pixelbuf import wheel
import board
import displayio
import framebufferio
import rgbmatrix
import terminalio
displayio.release_displays()
matrix = rgbmatrix.RGBMatrix(
width=64, height=32, bit_depth=3,
rgb_pins=[board.D6, board.D5, board.D9, board.D11, board.D10, board.D12],
addr_pins=[board.A5, board.A4, board.A3, board.A2],
clock_pin=board.D13, latch_pin=board.D0, output_enable_pin=board.D1)
display = framebufferio.FramebufferDisplay(matrix, auto_refresh=False)
# Create a tilegrid with a bunch of common settings
def tilegrid(palette):
return displayio.TileGrid(
bitmap=terminalio.FONT.bitmap, pixel_shader=palette,
width=1, height=1, tile_width=6, tile_height=14, default_tile=32)
g = displayio.Group(max_size=2)
# We only use the built in font which we treat as being 7x14 pixels
linelen = (64//7)+2
# prepare the main groups
l1 = displayio.Group(max_size=linelen)
l2 = displayio.Group(max_size=linelen)
g.append(l1)
g.append(l2)
display.show(g)
l1.y = 1
l2.y = 16
# Prepare the palettes and the individual characters' tiles
sh = [displayio.Palette(2) for _ in range(linelen)]
tg1 = [tilegrid(shi) for shi in sh]
tg2 = [tilegrid(shi) for shi in sh]
# Prepare a fast map from byte values to
charmap = array.array('b', [terminalio.FONT.get_glyph(32).tile_index]) * 256
for ch in range(256):
glyph = terminalio.FONT.get_glyph(ch)
if glyph is not None:
charmap[ch] = glyph.tile_index
# Set the X coordinates of each character in label 1, and add it to its group
for idx, gi in enumerate(tg1):
gi.x = 7 * idx
l1.append(gi)
# Set the X coordinates of each character in label 2, and add it to its group
for idx, gi in enumerate(tg2):
gi.x = 7 * idx
l2.append(gi)
# These pairs of lines should be the same length
lines = [
b"This scroller is brought to you by CircuitPython & PROTOMATTER",
b" .... . .-.. .-.. --- / .--. .-. --- - --- -- .- - - . .-.",
b"Greetz to ... @PaintYourDragon @v923z @adafruit ",
b" @danh @ladyada @kattni @tannewt all showers & tellers",
b"New York Strong Wash Your Hands ",
b" Flatten the curve Stronger Together",
]
even_lines = lines[0::2]
odd_lines = lines[1::2]
# Scroll a top text and a bottom text
def scroll(t, b):
# Add spaces to the start and end of each label so that it goes from
# the far right all the way off the left
sp = b' ' * linelen
t = sp + t + sp
b = sp + b + sp
maxlen = max(len(t), len(b))
# For each whole character position...
for i in range(maxlen-linelen):
# Set the letter displayed at each position, and its color
for j in range(linelen):
sh[j][1] = wheel(3 * (2*i+j))
tg1[j][0] = charmap[t[i+j]]
tg2[j][0] = charmap[b[i+j]]
# And then for each pixel position, move the two labels
# and then refresh the display.
for j in range(7):
l1.x = -j
l2.x = -j
display.refresh(minimum_frames_per_second=0)
#display.refresh(minimum_frames_per_second=0)
# Repeatedly scroll all the pairs of lines
while True:
for e, o in zip(even_lines, odd_lines):
scroll(e, o)

View file

@ -0,0 +1,84 @@
# This example implements a simple two line scroller using
# Adafruit_CircuitPython_Display_Text. Each line has its own color
# and it is possible to modify the example to use other fonts and non-standard
# characters.
import adafruit_display_text.label
import board
import displayio
import framebufferio
import rgbmatrix
import terminalio
# If there was a display before (protomatter, LCD, or E-paper), release it so
# we can create ours
displayio.release_displays()
# This next call creates the RGB Matrix object itself. It has the given width
# and height. bit_depth can range from 1 to 6; higher numbers allow more color
# shades to be displayed, but increase memory usage and slow down your Python
# code. If you just want to show primary colors plus black and white, use 1.
# Otherwise, try 3, 4 and 5 to see which effect you like best.
#
# These lines are for the Feather M4 Express. If you're using a different board,
# check the guide to find the pins and wiring diagrams for your board.
# If you have a matrix with a different width or height, change that too.
# If you have a 16x32 display, try with just a single line of text.
matrix = rgbmatrix.RGBMatrix(
width=64, height=32, bit_depth=1,
rgb_pins=[board.D6, board.D5, board.D9, board.D11, board.D10, board.D12],
addr_pins=[board.A5, board.A4, board.A3, board.A2],
clock_pin=board.D13, latch_pin=board.D0, output_enable_pin=board.D1)
# Associate the RGB matrix with a Display so that we can use displayio features
display = framebufferio.FramebufferDisplay(matrix, auto_refresh=False)
# Create two lines of text to scroll. Besides changing the text, you can also
# customize the color and font (using Adafruit_CircuitPython_Bitmap_Font).
# To keep this demo simple, we just used the built-in font.
# The Y coordinates of the two lines were chosen so that they looked good
# but if you change the font you might find that other values work better.
line1 = adafruit_display_text.label.Label(
terminalio.FONT,
color=0xff0000,
text="This scroller is brought to you by CircuitPython RGBMatrix")
line1.x = display.width
line1.y = 8
line2 = adafruit_display_text.label.Label(
terminalio.FONT,
color=0x0080ff,
text="Hello to all CircuitPython contributors worldwide <3")
line2.x = display.width
line2.y = 24
# Put each line of text into a Group, then show that group.
g = displayio.Group()
g.append(line1)
g.append(line2)
display.show(g)
# This function will scoot one label a pixel to the left and send it back to
# the far right if it's gone all the way off screen. This goes in a function
# because we'll do exactly the same thing with line1 and line2 below.
def scroll(line):
line.x = line.x - 1
line_width = line.bounding_box[2]
if line.x < -line_width:
line.x = display.width
# This function scrolls lines backwards. Try switching which function is
# called for line2 below!
def reverse_scroll(line):
line.x = line.x + 1
line_width = line.bounding_box[2]
if line.x >= display.width:
line.x = -line_width
# You can add more effects in this loop. For instance, maybe you want to set the
# color of each label to a different value.
while True:
scroll(line1)
scroll(line2)
#reverse_scroll(line2)
display.refresh(minimum_frames_per_second=0)

View file

@ -1,8 +1,10 @@
import time
import random
import audioio
import board
import busio
from digitalio import DigitalInOut
import digitalio
import neopixel
from adafruit_esp32spi import adafruit_esp32spi
from adafruit_esp32spi import adafruit_esp32spi_wifimanager
@ -10,9 +12,13 @@ import adafruit_fancyled.adafruit_fancyled as fancy
print("ESP32 Open Weather API demo")
# Use cityname, country code where countrycode is ISO3166 format.
# E.g. "New York, US" or "London, GB"
LOCATION = "Manhattan, US"
button = digitalio.DigitalInOut(board.A1)
button.switch_to_input(pull=digitalio.Pull.UP)
wave_file = open("sound/Rain.wav", "rb")
wave = audioio.WaveFile(wave_file)
audio = audioio.AudioOut(board.A0)
# Get wifi details and more from a secrets.py file
try:
@ -21,8 +27,12 @@ except ImportError:
print("WiFi secrets are kept in secrets.py, please add them there!")
raise
# Use cityname, country code where countrycode is ISO3166 format.
# E.g. "New York, US" or "London, GB"
LOCATION = secrets['timezone']
# Set up where we'll be fetching data from
DATA_SOURCE = "http://api.openweathermap.org/data/2.5/weather?q="+LOCATION
DATA_SOURCE = "http://api.openweathermap.org/data/2.5/weather?q="+secrets['timezone']
DATA_SOURCE += "&appid="+secrets['openweather_token']
# If you are using a board with pre-defined ESP32 Pins:
@ -32,64 +42,129 @@ esp32_reset = DigitalInOut(board.ESP_RESET)
spi = busio.SPI(board.SCK, board.MOSI, board.MISO)
esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset)
status_light = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) # Uncomment for Most Boards
status_light = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.2) # Uncomment for Most Boards
wifi = adafruit_esp32spi_wifimanager.ESPSPI_WiFiManager(esp, secrets, status_light)
pixels = neopixel.NeoPixel(board.D2, 16, brightness=1.0, auto_write=False)
pixels = neopixel.NeoPixel(board.D2, 150, brightness=1.0, auto_write=False)
pixels.fill(0x050505)
pixels.show()
# clouds palette
cloudy_palette = [fancy.CRGB(1.0, 1.0, 1.0), # White
fancy.CRGB(0.5, 0.5, 0.5), # gray
fancy.CRGB(0.5, 0.5, 1.0)] # blue-gray
cloudy_palette = [fancy.CRGB(1.0, 1.0, 1.0), # White
fancy.CRGB(0.5, 0.5, 0.5), # gray
fancy.CRGB(0.5, 0.5, 1.0)] # blue-gray
# sunny palette
sunny_palette = [fancy.CRGB(1.0, 1.0, 1.0), # White
fancy.CRGB(1.0, 1.0, 0.0),# Yellow
fancy.CRGB(1.0, 0.5, 0.0),] # Orange
sunny_palette = [fancy.CRGB(1.0, 1.0, 1.0), # White
fancy.CRGB(1.0, 1.0, 0.0), # Yellow
fancy.CRGB(1.0, 0.5, 0.0), ] # Orange
# thunderstorm palette
thunder_palette = [fancy.CRGB(0.0, 0.0, 1.0), # blue
fancy.CRGB(0.5, 0.5, 0.5), # gray
fancy.CRGB(0.5, 0.5, 1.0)] # blue-gray
thunder_palette = [fancy.CRGB(0.0, 0.0, 1.0), # blue
fancy.CRGB(0.5, 0.5, 0.5), # gray
fancy.CRGB(0.5, 0.5, 1.0)] # blue-gray
last_thunder_bolt = None
palette = None # current palette
pal_offset = 0 # Positional offset into color palette to get it to 'spin'
levels = (0.25, 0.3, 0.15) # Color balance / brightness for gamma function
raining = False
snowing = False
thundering = False
has_sound = False
weather_refresh = None
weather_type = None
button_mode = 4
button_select = False
cloud_on = True
while True:
if not button.value:
button_mode = button_mode + 1
print("Button Pressed")
if button_mode > 4:
button_mode = 0
print("Mode is:", button_mode)
pressed_time = time.monotonic()
button_select = True
weather_refresh = None
while not button.value: # Debounce
audio.stop()
if (time.monotonic() - pressed_time) > 4:
print("Turning OFF")
cloud_on = False
pixels.fill(0x000000) # bright white!
pixels.show()
while not cloud_on:
while not button.value: # Debounce
pass
if not button.value:
pressed_time = time.monotonic()
print("Button Pressed")
cloud_on = True
button_select = False
weather_refresh = None
if button_mode == 0:
weather_type = 'Sunny'
if button_mode == 1:
weather_type = 'Clouds'
if button_mode == 2:
weather_type = 'Rain'
if button_mode == 3:
weather_type = 'Thunderstorm'
if button_mode == 4:
weather_type = 'Snow'
# only query the weather every 10 minutes (and on first run)
if (not weather_refresh) or (time.monotonic() - weather_refresh) > 600:
try:
response = wifi.get(DATA_SOURCE).json()
print("Response is", response)
weather_type = response['weather'][0]['main']
weather_type = 'Thunderstorm' # manually adjust weather type for testing!
print(weather_type) # See https://openweathermap.org/weather-conditions
if not button_select:
response = wifi.get(DATA_SOURCE).json()
print("Response is", response)
weather_type = response['weather'][0]['main']
if weather_type == 'Clear':
weather_type = 'Sunny'
print(weather_type) # See https://openweathermap.org/weather-conditions
# default to no rain or thunder
raining = thundering = False
if weather_type == 'Clouds':
palette = cloudy_palette
if weather_type == 'Rain':
palette = cloudy_palette
raining = True
raining = snowing = thundering = has_sound = False
if weather_type == 'Sunny':
palette = sunny_palette
wave_file = open("sound/Clear.wav", "rb")
wave = audioio.WaveFile(wave_file)
has_sound = True
if weather_type == 'Clouds':
palette = cloudy_palette
wave_file = open("sound/Clouds.wav", "rb")
wave = audioio.WaveFile(wave_file)
has_sound = True
if weather_type == 'Rain':
palette = cloudy_palette
wave_file = open("sound/Rain.wav", "rb")
wave = audioio.WaveFile(wave_file)
raining = True
has_sound = True
if weather_type == 'Thunderstorm':
palette = thunder_palette
raining = thundering = True
has_sound = True
# pick next thunderbolt time now
next_bolt_time = time.monotonic() + random.randint(1, 5)
if weather_type == 'Snow':
palette = cloudy_palette
wave_file = open("sound/Snow.wav", "rb")
wave = audioio.WaveFile(wave_file)
snowing = True
has_sound = True
weather_refresh = time.monotonic()
except RuntimeError as e:
print("Some error occured, retrying! -", e)
time.sleep(5)
continue
if not audio.playing and has_sound:
if not thundering:
audio.play(wave)
if palette:
for i in range(len(pixels)):
color = fancy.palette_lookup(palette, pal_offset + i / len(pixels))
@ -100,20 +175,35 @@ while True:
if raining:
# don't have a droplet every time
if random.randint(0, 25) == 0: # 1 out of 25 times
for i in range(random.randint(1, 5)): # up to 3 times...
pixels[random.randint(0, len(pixels)-1)] = 0x0000FF # make a random pixel Blue
pixels.show()
if snowing:
# don't have a droplet every time
for i in range(random.randint(1, 5)): # up to 3 times...
pixels[random.randint(0, len(pixels)-1)] = 0xFFFFFF # make a random pixel white
pixels.show()
pixels.show()
# if its time for a thunderbolt
if thundering and (time.monotonic() > next_bolt_time):
print("Ka Bam!")
# fill pixels white, delay, a few times
for i in range(random.randint(1, 3)): # up to 3 times...
pixels.fill(0xFFFFFF) # bright white!
pixels.fill(0xFFFFFF) # bright white!
pixels.show()
time.sleep(random.uniform(0.01, 0.2)) # pause
pixels.fill(0x0F0F0F) # gray
time.sleep(random.uniform(0.01, 0.2)) # pause
pixels.fill(0x0F0F0F) # gray
pixels.show()
time.sleep(random.uniform(0.01, 0.3)) # pause
time.sleep(random.uniform(0.01, 0.3)) # pause
# pick next thunderbolt time now
next_bolt_time = time.monotonic() + random.randint(5, 15) # between 5 and 15 s
Thunder = random.randint(0, 2)
if Thunder == 0:
wave_file = open("sound/Thunderstorm0.wav", "rb")
elif Thunder == 1:
wave_file = open("sound/Thunderstorm1.wav", "rb")
elif Thunder == 2:
wave_file = open("sound/Thunderstorm2.wav", "rb")
wave = audioio.WaveFile(wave_file)
audio.play(wave)
next_bolt_time = time.monotonic() + random.randint(5, 15) # between 5 and 15 s

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -31,7 +31,7 @@ while True:
# Try and decode them
try:
# Attempt to convert received pulses into numbers
received_code = tuple(decoder.decode_bits(pulses, debug=False))
received_code = tuple(decoder.decode_bits(pulses))
except adafruit_irremote.IRNECRepeatException:
# We got an unusual short code, probably a 'repeat' signal
# print("NEC repeat!")

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

View file

@ -0,0 +1,172 @@
import time
import board
import displayio
from adafruit_clue import clue
from simpleio import map_range
from adafruit_bitmap_font import bitmap_font
from adafruit_lsm6ds import LSM6DS33, Rate, AccelRange
from adafruit_progressbar import ProgressBar
from adafruit_display_text.label import Label
# turns off onboard NeoPixel to conserve battery
clue.pixel.brightness = (0.0)
# accessing the Clue's accelerometer
sensor = LSM6DS33(board.I2C())
# step goal
step_goal = 10000
# onboard button states
a_state = False
b_state = False
# array to adjust screen brightness
bright_level = [0, 0.5, 1]
countdown = 0 # variable for the step goal progress bar
clock = 0 # variable used to keep track of time for the steps per hour counter
clock_count = 0 # holds the number of hours that the step counter has been running
clock_check = 0 # holds the result of the clock divided by 3600 seconds (1 hour)
last_step = 0 # state used to properly counter steps
mono = time.monotonic() # time.monotonic() device
mode = 1 # state used to track screen brightness
steps_log = 0 # holds total steps to check for steps per hour
steps_remaining = 0 # holds the remaining steps needed to reach the step goal
sph = 0 # holds steps per hour
# variables to hold file locations for background and fonts
clue_bgBMP = "/clue_bgBMP.bmp"
small_font = "/fonts/Roboto-Medium-16.bdf"
med_font = "/fonts/Roboto-Bold-24.bdf"
big_font = "/fonts/Roboto-Black-48.bdf"
# glyphs for fonts
glyphs = b'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-,.: '
# loading bitmap fonts
small_font = bitmap_font.load_font(small_font)
small_font.load_glyphs(glyphs)
med_font = bitmap_font.load_font(med_font)
med_font.load_glyphs(glyphs)
big_font = bitmap_font.load_font(big_font)
big_font.load_glyphs(glyphs)
# creating display and default brightness
clue_display = board.DISPLAY
clue_display.brightness = 0.5
# graphics group
clueGroup = displayio.Group(max_size=20)
# loading bitmap background
clue_bg = displayio.OnDiskBitmap(open(clue_bgBMP, "rb"))
clue_tilegrid = displayio.TileGrid(clue_bg, pixel_shader=displayio.ColorConverter())
clueGroup.append(clue_tilegrid)
# creating the ProgressBar object
bar_group = displayio.Group(max_size=20)
prog_bar = ProgressBar(1, 1, 239, 25, bar_color=0x652f8f)
bar_group.append(prog_bar)
clueGroup.append(bar_group)
# text for step goal
steps_countdown = Label(small_font, text='%d Steps Remaining' % step_goal, color=clue.WHITE)
steps_countdown.x = 55
steps_countdown.y = 12
# text for steps
text_steps = Label(big_font, text="0 ", color=0xe90e8b)
text_steps.x = 45
text_steps.y = 70
# text for steps per hour
text_sph = Label(med_font, text=" -- ", color=0x29abe2)
text_sph.x = 8
text_sph.y = 195
# adding all text to the display group
clueGroup.append(text_sph)
clueGroup.append(steps_countdown)
clueGroup.append(text_steps)
# sending display group to the display at startup
clue_display.show(clueGroup)
# setting up the accelerometer and pedometer
sensor.accelerometer_range = AccelRange.RANGE_2G
sensor.accelerometer_data_rate = Rate.RATE_26_HZ
sensor.gyro_data_rate = Rate.RATE_SHUTDOWN
sensor.pedometer_enable = True
while True:
# button debouncing
if not clue.button_a and not a_state:
a_state = True
if not clue.button_b and not b_state:
b_state = True
# setting up steps to hold step count
steps = sensor.pedometer_steps
# creating the data for the ProgressBar
countdown = map_range(steps, 0, step_goal, 0.0, 1.0)
# actual counting of the steps
# if a step is taken:
if abs(steps-last_step) > 1:
step_time = time.monotonic()
# updates last_step
last_step = steps
# updates the display
text_steps.text = '%d' % steps
clock = step_time - mono
# logging steps per hour
if clock > 3600:
# gets number of hours to add to total
clock_check = clock / 3600
# logs the step count as of that hour
steps_log = steps
# adds the hours to get a new hours total
clock_count += round(clock_check)
# divides steps by hours to get steps per hour
sph = steps_log / clock_count
# adds the sph to the display
text_sph.text = '%d' % sph
# resets clock to count to the next hour again
clock = 0
mono = time.monotonic()
# adjusting countdown to step goal
prog_bar.progress = float(countdown)
# displaying countdown to step goal
if step_goal - steps > 0:
steps_remaining = step_goal - steps
steps_countdown.text = '%d Steps Remaining' % steps_remaining
else:
steps_countdown.text = 'Steps Goal Met!'
# adjusting screen brightness, a button decreases brightness
if clue.button_a and a_state:
mode -= 1
a_state = False
if mode < 0:
mode = 0
clue_display.brightness = bright_level[mode]
else:
clue_display.brightness = bright_level[mode]
# b button increases brightness
if clue.button_b and b_state:
mode += 1
b_state = False
if mode > 2:
mode = 2
clue_display.brightness = bright_level[mode]
else:
clue_display.brightness = bright_level[mode]
time.sleep(0.001)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,126 @@
"""
Power Glove BLE MIDI with Feather Sense nRF52840
Sends MIDI CC values based on finger flex sensors and accelerometer
"""
import time
import board
import adafruit_ble
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
import adafruit_ble_midi
import adafruit_midi
from adafruit_midi.control_change import ControlChange
# from adafruit_midi.note_on import NoteOn
# from adafruit_midi.pitch_bend import PitchBend
import adafruit_lsm6ds # accelerometer
import simpleio
from analogio import AnalogIn
i2c = board.I2C()
sense_accel = adafruit_lsm6ds.LSM6DS33(i2c)
analog_in_thumb = AnalogIn(board.A3)
analog_in_index = AnalogIn(board.A2)
analog_in_middle = AnalogIn(board.A1)
analog_in_ring = AnalogIn(board.A0)
# Pick your MIDI CC numbers here
cc_x_num = 7 # volume
cc_y_num = 70 # unassigned
cc_thumb_num = 71 # unassigned
cc_index_num = 75 # unassigned
cc_middle_num = 76 # unassigned
cc_ring_num = 77 # unassigned
midi_channel = 1 # pick your midi out channel here
# Use default HID descriptor
midi_service = adafruit_ble_midi.MIDIService()
advertisement = ProvideServicesAdvertisement(midi_service)
ble = adafruit_ble.BLERadio()
if ble.connected:
for c in ble.connections:
c.disconnect()
midi = adafruit_midi.MIDI(midi_out=midi_service, out_channel=midi_channel - 1)
print("advertising")
ble.name="Power Glove MIDI"
ble.start_advertising(advertisement)
# reads an analog pin and returns value remapped to out range, e.g., 0-127
def get_flex_cc(sensor, low_in, high_in, min_out, max_out):
flex_raw = sensor.value
flex_cc = simpleio.map_range(flex_raw, low_in, high_in, min_out, max_out)
flex_cc = int(flex_cc)
return flex_cc
debug = False # set debug mode True to test raw values, set False to run BLE MIDI
while True:
if debug:
accel_data = sense_accel.acceleration # get accelerometer reading
accel_x = accel_data[0]
accel_y = accel_data[1]
accel_z = accel_data[2]
print(
"x:{} y:{} z:{} thumb:{} index:{} middle:{} ring:{}".format(
accel_x,
accel_y,
accel_x,
analog_in_thumb.value,
analog_in_index.value,
analog_in_middle.value,
analog_in_ring.value,
)
)
time.sleep(0.2)
else:
print("Waiting for connection")
while not ble.connected:
pass
print("Connected")
while ble.connected:
# Feather Sense accelerometer readings to CC
accel_data = sense_accel.acceleration # get accelerometer reading
accel_x = accel_data[0]
accel_y = accel_data[1]
# accel_z = accel_data[2]
# Remap analog readings to cc range
cc_x = int(simpleio.map_range(accel_x, 0, 9, 127, 0))
cc_y = int(simpleio.map_range(accel_y, 1, -9, 0, 127))
cc_thumb = get_flex_cc(analog_in_thumb, 49000, 35000, 127, 0)
cc_index = get_flex_cc(analog_in_index, 50000, 35000, 0, 127)
cc_middle = get_flex_cc(analog_in_middle, 55000, 40000, 0, 127)
cc_ring = get_flex_cc(analog_in_ring, 55000, 42000, 0, 127)
'''
print(
"CC_X:{} CC_Y:{} CC_Thumb:{} CC_Index:{} CC_Middle:{} CC_Ring:{}".format(
cc_x, cc_y, cc_thumb, cc_index, cc_middle, cc_ring
)
)'''
# send all the midi messages in a list
midi.send(
[
ControlChange(cc_x_num, cc_x),
ControlChange(cc_y_num, cc_y),
ControlChange(cc_thumb_num, cc_thumb),
ControlChange(cc_index_num, cc_index),
ControlChange(cc_middle_num, cc_middle),
ControlChange(cc_ring_num, cc_ring),
]
)
# If you want to send NoteOn or Pitch Bend, here are examples:
# midi.send(NoteOn(44, 120)) # G sharp 2nd octave
# a_pitch_bend = PitchBend(random.randint(0, 16383))
# midi.send(a_pitch_bend)
print("Disconnected")
print()
ble.start_advertising(advertisement)

View file

@ -0,0 +1,143 @@
"""
Read data from a BerryMed pulse oximeter, model BM1000C, BM1000E, etc.
Run this on Feather nRF52840
Log data to SD card on Autologger FeatherWing
"""
# Protocol defined here:
# https://github.com/zh2x/BCI_Protocol
# Thanks as well to:
# https://github.com/ehborisov/BerryMed-Pulse-Oximeter-tool
# https://github.com/ScheindorfHyenetics/berrymedBluetoothOxymeter
#
# The sensor updates the readings at 100Hz.
import time
import adafruit_sdcard
import board
import busio
import digitalio
import storage
import adafruit_pcf8523
import _bleio
import adafruit_ble
from adafruit_ble.advertising.standard import Advertisement
from adafruit_ble.services.standard.device_info import DeviceInfoService
from adafruit_ble_berrymed_pulse_oximeter import BerryMedPulseOximeterService
# Logging setup
SD_CS = board.D10
spi = busio.SPI(board.SCK, board.MOSI, board.MISO)
cs = digitalio.DigitalInOut(SD_CS)
sd_card = adafruit_sdcard.SDCard(spi, cs)
vfs = storage.VfsFat(sd_card)
storage.mount(vfs, "/sd_card")
log_interval = 2 # you can adjust this to log at a different rate
# RTC setup
I2C = busio.I2C(board.SCL, board.SDA)
rtc = adafruit_pcf8523.PCF8523(I2C)
days = ("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday")
set_time = False
if set_time: # change to True if you want to write the time!
# year, mon, date, hour, min, sec, wday, yday, isdst
t = time.struct_time((2020, 4, 21, 18, 13, 0, 2, -1, -1))
# you must set year, mon, date, hour, min, sec and weekday
# yearday not supported, isdst can be set but we don't use it at this time
print("Setting time to:", t) # uncomment for debugging
rtc.datetime = t
print()
# PyLint can't find BLERadio for some reason so special case it here.
ble = adafruit_ble.BLERadio() # pylint: disable=no-member
pulse_ox_connection = None
while True:
t = rtc.datetime
print("Scanning for Pulse Oximeter...")
for adv in ble.start_scan(Advertisement, timeout=5):
name = adv.complete_name
if not name:
continue
# "BerryMed" devices may have trailing nulls on their name.
if name.strip("\x00") == "BerryMed":
pulse_ox_connection = ble.connect(adv)
print("Connected")
break
# Stop scanning whether or not we are connected.
ble.stop_scan()
print("Stopped scan")
try:
if pulse_ox_connection and pulse_ox_connection.connected:
print("Fetch connection")
if DeviceInfoService in pulse_ox_connection:
dis = pulse_ox_connection[DeviceInfoService]
try:
manufacturer = dis.manufacturer
except AttributeError:
manufacturer = "(Manufacturer Not specified)"
try:
model_number = dis.model_number
except AttributeError:
model_number = "(Model number not specified)"
print("Device:", manufacturer, model_number)
else:
print("No device information")
pulse_ox_service = pulse_ox_connection[BerryMedPulseOximeterService]
while pulse_ox_connection.connected:
values = pulse_ox_service.values
if values is not None:
# unpack the message to 'values' list
valid, spo2, pulse_rate, pleth, finger = values
if not valid:
continue
if (
pulse_rate == 255
): # device sends 255 as pulse until it has a valid read
continue
print(
"SpO2: {}% | ".format(spo2),
"Pulse Rate: {} BPM | ".format(pulse_rate),
"Pleth: {}".format(pleth),
)
# print((pleth,)) # uncomment to see graph on Mu plotter
try: # logging to SD card
with open("/sd_card/log.txt", "a") as sdc:
t = rtc.datetime
sdc.write(
"{} {}/{}/{} {}:{}:{}, ".format(
days[t.tm_wday],
t.tm_mday,
t.tm_mon,
t.tm_year,
t.tm_hour,
t.tm_min,
t.tm_sec
)
)
sdc.write(
"{}, {}, {:.2f}\n".format(
spo2, pulse_rate, pleth
)
)
time.sleep(log_interval)
except OSError:
pass
except RuntimeError:
pass
except _bleio.ConnectionError:
try:
pulse_ox_connection.disconnect()
except _bleio.ConnectionError:
pass
pulse_ox_connection = None

View file

@ -15,7 +15,7 @@ import neopixel
from adafruit_esp32spi import adafruit_esp32spi
from adafruit_esp32spi import adafruit_esp32spi_wifimanager
import adafruit_esp32spi.adafruit_esp32spi_socket as socket
from adafruit_minimqtt import MQTT
import adafruit_minimqtt as MQTT
from adafruit_aws_iot import MQTT_CLIENT
from adafruit_seesaw.seesaw import Seesaw
import aws_gfx_helper
@ -82,6 +82,9 @@ print("Connecting to WiFi...")
wifi.connect()
print("Connected!")
# Initialize MQTT interface with the esp interface
MQTT.set_socket(socket, esp)
# Soil Sensor Setup
i2c_bus = busio.I2C(board.SCL, board.SDA)
ss = Seesaw(i2c_bus, addr=0x36)
@ -120,10 +123,8 @@ def message(client, topic, msg):
print("Message from {}: {}".format(topic, msg))
# Set up a new MiniMQTT Client
client = MQTT(socket,
broker = secrets['broker'],
client_id = secrets['client_id'],
network_manager = wifi)
client = MQTT.MQTT(broker = secrets['broker'],
client_id = secrets['client_id'])
# Initialize AWS IoT MQTT API Client
aws_iot = MQTT_CLIENT(client)

View file

@ -16,7 +16,7 @@ import adafruit_touchscreen
from adafruit_mcp9600 import MCP9600
TITLE = "EZ Make Oven Controller"
VERSION = "1.1.0"
VERSION = "1.2.0"
print(TITLE, "version ", VERSION)
time.sleep(2)
@ -25,15 +25,16 @@ display_group = displayio.Group(max_size=20)
board.DISPLAY.show(display_group)
PROFILE_SIZE = 2 # plot thickness
PROFILE_COLOR = 0x00FF55 # plot color
GRID_SIZE = 2
GRID_COLOR = 0x2020FF
GRID_STYLE = 3
TEMP_SIZE = 2
TEMP_COLOR = 0xFF0000
LABEL_COLOR = 0x8080FF
AXIS_SIZE = 2
AXIS_COLOR = 0xFFFF00
BLACK = 0x0
BLUE = 0x2020FF
GREEN = 0x00FF55
RED = 0xFF0000
YELLOW = 0xFFFF00
WIDTH = board.DISPLAY.width
HEIGHT = board.DISPLAY.height
@ -41,15 +42,27 @@ HEIGHT = board.DISPLAY.height
pyportal = PyPortal()
palette = displayio.Palette(5)
palette[0] = 0x0
palette[1] = PROFILE_COLOR
palette[2] = GRID_COLOR
palette[3] = TEMP_COLOR
palette[4] = AXIS_COLOR
palette[0] = BLACK
palette[1] = GREEN
palette[2] = BLUE
palette[3] = RED
palette[4] = YELLOW
palette.make_transparent(0)
plot = displayio.Bitmap(WIDTH, HEIGHT, 8)
pyportal.splash.append(displayio.TileGrid(plot, pixel_shader=palette))
BACKGROUND_COLOR = 0
PROFILE_COLOR = 1
GRID_COLOR = 2
TEMP_COLOR = 3
AXIS_COLOR = 2
GXSTART = 100
GYSTART = 80
GWIDTH = WIDTH - GXSTART
GHEIGHT = HEIGHT - GYSTART
plot = displayio.Bitmap(GWIDTH, GHEIGHT, 4)
pyportal.splash.append(displayio.TileGrid(plot, pixel_shader=palette, x=GXSTART, y=GYSTART))
ts = adafruit_touchscreen.Touchscreen(board.TOUCH_XL, board.TOUCH_XR,
board.TOUCH_YD, board.TOUCH_YU,
@ -253,8 +266,8 @@ class Graph(object):
self.ymax = 240
self.xstart = 0
self.ystart = 0
self.width = WIDTH
self.height = HEIGHT
self.width = GWIDTH
self.height = GHEIGHT
# pylint: disable=too-many-branches
def draw_line(self, x1, y1, x2, y2, size=PROFILE_SIZE, color=1, style=1):
@ -323,14 +336,14 @@ class Graph(object):
for yy in range(y-offset, y+offset+1):
if yy in range(self.ystart, self.ystart + self.height):
try:
yy = HEIGHT - yy
yy = GHEIGHT - yy
plot[xx, yy] = color
except IndexError:
pass
def draw_profile(graph, profile):
"""Update the display with current info."""
for i in range(WIDTH * HEIGHT):
for i in range(GWIDTH * GHEIGHT):
plot[i] = 0
# draw stage lines
@ -338,40 +351,40 @@ def draw_profile(graph, profile):
graph.draw_line(profile["stages"]["preheat"][0], profile["temp_range"][0],
profile["stages"]["preheat"][0], profile["temp_range"][1]
* 1.1,
GRID_SIZE, 2, GRID_STYLE)
GRID_SIZE, GRID_COLOR, GRID_STYLE)
graph.draw_line(profile["time_range"][0], profile["stages"]["preheat"][1],
profile["time_range"][1], profile["stages"]["preheat"][1],
GRID_SIZE, 2, GRID_STYLE)
GRID_SIZE, GRID_COLOR, GRID_STYLE)
# soak
graph.draw_line(profile["stages"]["soak"][0], profile["temp_range"][0],
profile["stages"]["soak"][0], profile["temp_range"][1]*1.1,
GRID_SIZE, 2, GRID_STYLE)
GRID_SIZE, GRID_COLOR, GRID_STYLE)
graph.draw_line(profile["time_range"][0], profile["stages"]["soak"][1],
profile["time_range"][1], profile["stages"]["soak"][1],
GRID_SIZE, 2, GRID_STYLE)
GRID_SIZE, GRID_COLOR, GRID_STYLE)
# reflow
graph.draw_line(profile["stages"]["reflow"][0], profile["temp_range"][0],
profile["stages"]["reflow"][0], profile["temp_range"][1]
* 1.1,
GRID_SIZE, 2, GRID_STYLE)
GRID_SIZE, GRID_COLOR, GRID_STYLE)
graph.draw_line(profile["time_range"][0], profile["stages"]["reflow"][1],
profile["time_range"][1], profile["stages"]["reflow"][1],
GRID_SIZE, 2, GRID_STYLE)
GRID_SIZE, GRID_COLOR, GRID_STYLE)
# cool
graph.draw_line(profile["stages"]["cool"][0], profile["temp_range"][0],
profile["stages"]["cool"][0], profile["temp_range"][1]*1.1,
GRID_SIZE, 2, GRID_STYLE)
GRID_SIZE, GRID_COLOR, GRID_STYLE)
graph.draw_line(profile["time_range"][0], profile["stages"]["cool"][1],
profile["time_range"][1], profile["stages"]["cool"][1],
GRID_SIZE, 2, GRID_STYLE)
GRID_SIZE, GRID_COLOR, GRID_STYLE)
# draw labels
x = profile["time_range"][0]
y = profile["stages"]["reflow"][1]
xp = (graph.xstart + graph.width * (x - graph.xmin)
// (graph.xmax - graph.xmin))
yp = (graph.ystart + int(graph.height * (y - graph.ymin)
/ (graph.ymax - graph.ymin)))
xp = int(GXSTART + graph.width * (x - graph.xmin)
// (graph.xmax - graph.xmin))
yp = int(GHEIGHT * (y - graph.ymin)
// (graph.ymax - graph.ymin))
label_reflow.x = xp + 10
label_reflow.y = HEIGHT - yp
@ -379,26 +392,36 @@ def draw_profile(graph, profile):
print("reflow temp:", str(profile["stages"]["reflow"][1]))
print("graph point: ", x, y, "->", xp, yp)
x = profile["stages"]["reflow"][0]
y = profile["stages"]["reflow"][1]
# draw time line (horizontal)
graph.draw_line(graph.xmin, graph.ymin, graph.xmax, graph.ymin, AXIS_SIZE, 4, 1)
graph.draw_line(graph.xmin, graph.ymax - AXIS_SIZE, graph.xmax, graph.ymax
- AXIS_SIZE, AXIS_SIZE, 4, 1)
graph.draw_line(graph.xmin, graph.ymin + 1, graph.xmax,
graph.ymin + 1, AXIS_SIZE, AXIS_COLOR, 1)
graph.draw_line(graph.xmin, graph.ymax, graph.xmax, graph.ymax,
AXIS_SIZE, AXIS_COLOR, 1)
# draw time ticks
tick = graph.xmin
while tick < (graph.xmax - graph.xmin):
graph.draw_line(tick, graph.ymin, tick, graph.ymin + 10, AXIS_SIZE, 4, 1)
graph.draw_line(tick, graph.ymax, tick, graph.ymax - 10 - AXIS_SIZE, AXIS_SIZE, 4, 1)
graph.draw_line(tick, graph.ymin, tick, graph.ymin + 10,
AXIS_SIZE, AXIS_COLOR, 1)
graph.draw_line(tick, graph.ymax, tick, graph.ymax - 10 - AXIS_SIZE,
AXIS_SIZE, AXIS_COLOR, 1)
tick += 60
# draw temperature line (vertical)
graph.draw_line(graph.xmin, graph.ymin, graph.xmin, graph.ymax, AXIS_SIZE, 4, 1)
graph.draw_line(graph.xmax - AXIS_SIZE, graph.ymin, graph.xmax - AXIS_SIZE,
graph.ymax, AXIS_SIZE, 4, 1)
graph.draw_line(graph.xmin, graph.ymin, graph.xmin,
graph.ymax, AXIS_SIZE, AXIS_COLOR, 1)
graph.draw_line(graph.xmax - AXIS_SIZE + 1, graph.ymin,
graph.xmax - AXIS_SIZE + 1,
graph.ymax, AXIS_SIZE, AXIS_COLOR, 1)
# draw temperature ticks
tick = graph.ymin
while tick < (graph.ymax - graph.ymin)*1.1:
graph.draw_line(graph.xmin, tick, graph.xmin + 10, tick, AXIS_SIZE, 4, 1)
graph.draw_line(graph.xmax, tick, graph.xmax - 10 - AXIS_SIZE, tick, AXIS_SIZE, 4, 1)
graph.draw_line(graph.xmin, tick, graph.xmin + 10, tick,
AXIS_SIZE, AXIS_COLOR, 1)
graph.draw_line(graph.xmax, tick, graph.xmax - 10 - AXIS_SIZE,
tick, AXIS_SIZE, AXIS_COLOR, 1)
tick += 50
# draw profile
@ -407,7 +430,7 @@ def draw_profile(graph, profile):
for point in profile["profile"]:
x2 = point[0]
y2 = point[1]
graph.draw_line(x1, y1, x2, y2, PROFILE_SIZE, 1, 1)
graph.draw_line(x1, y1, x2, y2, PROFILE_SIZE, PROFILE_COLOR, 1)
# print(point)
x1 = x2
y1 = y2
@ -429,7 +452,7 @@ font3.load_glyphs(b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567
label_reflow = label.Label(font1, text="", max_glyphs=10,
color=LABEL_COLOR, line_spacing=0)
color=0xFFFFFF, line_spacing=0)
label_reflow.x = 0
label_reflow.y = -20
pyportal.splash.append(label_reflow)
@ -445,7 +468,7 @@ message = label.Label(font2, text="Wait", max_glyphs=30)
message.x = 100
message.y = 40
pyportal.splash.append(message)
alloy_label = label.Label(font1, text="Alloy: ", color=0xAAAAAA)
alloy_label = label.Label(font1, text="Alloy:", color=0xAAAAAA)
alloy_label.x = 5
alloy_label.y = 40
pyportal.splash.append(alloy_label)
@ -482,10 +505,14 @@ pyportal.splash.append(circle)
sgraph = Graph()
sgraph.xstart = 100
sgraph.ystart = 4
sgraph.width = WIDTH - sgraph.xstart - 4 # 216 for standard PyPortal
sgraph.height = HEIGHT - 80 # 160 for standard PyPortal
#sgraph.xstart = 100
#sgraph.ystart = 4
sgraph.xstart = 0
sgraph.ystart = 0
#sgraph.width = WIDTH - sgraph.xstart - 4 # 216 for standard PyPortal
#sgraph.height = HEIGHT - 80 # 160 for standard PyPortal
sgraph.width = GWIDTH # 216 for standard PyPortal
sgraph.height = GHEIGHT # 160 for standard PyPortal
sgraph.xmin = oven.sprofile["time_range"][0]
sgraph.xmax = oven.sprofile["time_range"][1]
sgraph.ymin = oven.sprofile["temp_range"][0]
@ -535,7 +562,7 @@ while True:
last_status = ""
if p:
if p[0] >= 0 and p[0] <= 80 and p[1] >= 200 and p[1] <= 240:
if p[0] >= 0 and p[0] <= 80 and p[1] >= HEIGHT - 40 and p[1] <= HEIGHT:
print("touch!")
if oven.state == "ready":
button.label = "Stop"
@ -587,6 +614,6 @@ while True:
print(oven.state)
if oven_temp >= 50:
sgraph.draw_graph_point(int(timediff), oven_temp,
size=TEMP_SIZE, color=3)
size=TEMP_SIZE, color=TEMP_COLOR)
last_state = oven.state

View file

@ -15,7 +15,7 @@ import neopixel
from adafruit_esp32spi import adafruit_esp32spi, adafruit_esp32spi_wifimanager
import adafruit_esp32spi.adafruit_esp32spi_socket as socket
from adafruit_gc_iot_core import MQTT_API, Cloud_Core
from adafruit_minimqtt import MQTT
import adafruit_minimqtt as MQTT
from adafruit_seesaw.seesaw import Seesaw
import digitalio
@ -44,6 +44,9 @@ print("Connecting to WiFi...")
wifi.connect()
print("Connected!")
# Initialize MQTT interface with the esp interface
MQTT.set_socket(socket, esp)
# Soil Sensor Setup
i2c_bus = busio.I2C(board.SCL, board.SDA)
ss = Seesaw(i2c_bus, addr=0x36)
@ -138,12 +141,10 @@ jwt = google_iot.generate_jwt()
print("Your JWT is: ", jwt)
# Set up a new MiniMQTT Client
client = MQTT(socket,
broker=google_iot.broker,
username=google_iot.username,
password=jwt,
client_id=google_iot.cid,
network_manager=wifi)
client = MQTT.MQTT(broker=google_iot.broker,
username=google_iot.username,
password=jwt,
client_id=google_iot.cid)
# Initialize Google MQTT API Client
google_mqtt = MQTT_API(client)
@ -187,4 +188,5 @@ while True:
except (ValueError, RuntimeError) as e:
print("Failed to get data, retrying", e)
wifi.reset()
google_mqtt.reconnect()
continue

113
PyPortal_Quarantine_Clock/code.py Executable file
View file

@ -0,0 +1,113 @@
import time
import board
import busio
import digitalio
from adafruit_esp32spi import adafruit_esp32spi_socket as socket
from adafruit_esp32spi import adafruit_esp32spi
import adafruit_requests as requests
from adafruit_pyportal import PyPortal
from adafruit_bitmap_font import bitmap_font
from adafruit_display_text import label
try:
from secrets import secrets
except ImportError:
print("""WiFi settings are kept in secrets.py, please add them there!
the secrets dictionary must contain 'ssid' and 'password' at a minimum""")
raise
# Label colors
LABEL_DAY_COLOR = 0xFFFFFF
LABEL_TIME_COLOR = 0x2a8eba
# the current working directory (where this file is)
cwd = ("/"+__file__).rsplit('/', 1)[0]
background = None
# un-comment to set background image
# background = cwd+"/background.bmp"
# Descriptions of each hour
# https://github.com/mwfisher3/QuarantineClock/blob/master/today.html
time_names = ["midnight-ish", "late night", "late", "super late",
"super early","really early","dawn","morning",
"morning","mid-morning","mid-morning","late morning",
"noon-ish","afternoon","afternoon","mid-afternoon",
"late afternoon","early evening","early evening","dusk-ish",
"evening","evening","late evening","late evening"]
# Days of the week
week_days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
esp32_cs = digitalio.DigitalInOut(board.ESP_CS)
esp32_ready = digitalio.DigitalInOut(board.ESP_BUSY)
esp32_reset = digitalio.DigitalInOut(board.ESP_RESET)
spi = busio.SPI(board.SCK, board.MOSI, board.MISO)
esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset, debug=False)
requests.set_socket(socket, esp)
# initialize pyportal
pyportal = PyPortal(esp=esp,
external_spi=spi,
default_bg = None)
# set pyportal's backlight brightness
pyportal.set_backlight(0.7)
if esp.status == adafruit_esp32spi.WL_IDLE_STATUS:
print("ESP32 found and in idle mode")
print("Firmware vers.", esp.firmware_version)
print("MAC addr:", [hex(i) for i in esp.MAC_address])
print("Connecting to AP...")
while not esp.is_connected:
try:
esp.connect_AP(secrets['ssid'], secrets['password'])
except RuntimeError as e:
print("could not connect to AP, retrying: ", e)
continue
# Set the font and preload letters
font_large = bitmap_font.load_font("/fonts/Helvetica-Bold-44.bdf")
font_small = bitmap_font.load_font("/fonts/Helvetica-Bold-24.bdf")
font_large.load_glyphs(b'abcdefghjiklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890- ')
font_small.load_glyphs(b'abcdefghjiklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890- ()')
# Set up label for the day
label_day = label.Label(font_large, color=LABEL_DAY_COLOR, max_glyphs=200)
label_day.x = board.DISPLAY.width // 7
label_day.y = 80
pyportal.splash.append(label_day)
# Set up label for the time
label_time = label.Label(font_small, color=LABEL_TIME_COLOR, max_glyphs=200)
label_time.x = board.DISPLAY.width // 4
label_time.y = 150
pyportal.splash.append(label_time)
refresh_time = None
while True:
# only query the network time every hour
if (not refresh_time) or (time.monotonic() - refresh_time) > 3600:
try:
print("Getting new time from internet...")
pyportal.get_local_time(secrets['timezone'])
refresh_time = time.monotonic()
# set the_time
the_time = time.localtime()
except (ValueError, RuntimeError) as e:
print("Failed to get data, retrying\n", e)
esp.reset()
continue
# Convert tm_wday to name of day
weekday = week_days[the_time.tm_wday]
# set the day label's text
label_day.text = weekday
# set the time label's text
label_time.text = "({})".format(time_names[the_time.tm_hour])
# update every minute
time.sleep(60)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,138 @@
import time
import board
import busio
import digitalio
from adafruit_esp32spi import adafruit_esp32spi_socket as socket
from adafruit_esp32spi import adafruit_esp32spi
import adafruit_requests as requests
from adafruit_pyportal import PyPortal
from adafruit_bitmap_font import bitmap_font
from adafruit_display_text import label
try:
from secrets import secrets
except ImportError:
print("""WiFi settings are kept in secrets.py, please add them there!
the secrets dictionary must contain 'ssid' and 'password' at a minimum""")
raise
# Label colors
LABEL_DAY_COLOR = 0xFFFFFF
LABEL_TIME_COLOR = 0x2a8eba
# the current working directory (where this file is)
cwd = ("/"+__file__).rsplit('/', 1)[0]
background = None
# un-comment to set background image
# background = cwd+"/background.bmp"
# Descriptions of each hour
# https://github.com/mwfisher3/QuarantineClock/blob/master/today.html
time_names = ["midnight-ish", "late night", "late", "super late",
"super early","really early","dawn","morning",
"morning","mid-morning","mid-morning","late morning",
"noon-ish","afternoon","afternoon","mid-afternoon",
"late afternoon","early evening","early evening","dusk-ish",
"evening","evening","late evening","late evening"]
# Months of the year
months = ["January", "January", "February", "March", "April",
"May", "June", "July", "August",
"September", "October", "November", "December"]
# Dictionary of tm_mon and month name.
# note: tm_mon structure in CircuitPython ranges from [1,12]
months = {
1: "January",
2: "February",
3: "March",
4: "April",
5: "May",
6: "June",
7: "July",
8: "August",
9: "September",
10: "October",
11: "November",
12: "December"
}
esp32_cs = digitalio.DigitalInOut(board.ESP_CS)
esp32_ready = digitalio.DigitalInOut(board.ESP_BUSY)
esp32_reset = digitalio.DigitalInOut(board.ESP_RESET)
spi = busio.SPI(board.SCK, board.MOSI, board.MISO)
esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset, debug=False)
requests.set_socket(socket, esp)
# initialize pyportal
pyportal = PyPortal(esp=esp,
external_spi=spi,
default_bg = None)
# set pyportal's backlight brightness
pyportal.set_backlight(0.2)
if esp.status == adafruit_esp32spi.WL_IDLE_STATUS:
print("ESP32 found and in idle mode")
print("Firmware vers.", esp.firmware_version)
print("MAC addr:", [hex(i) for i in esp.MAC_address])
print("Connecting to AP...")
while not esp.is_connected:
try:
esp.connect_AP(secrets['ssid'], secrets['password'])
except RuntimeError as e:
print("could not connect to AP, retrying: ", e)
continue
# Set the font and preload letters
font_large = bitmap_font.load_font("/fonts/Helvetica-Bold-44.bdf")
font_small = bitmap_font.load_font("/fonts/Helvetica-Bold-24.bdf")
font_large.load_glyphs(b'abcdefghjiklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890- ')
font_small.load_glyphs(b'abcdefghjiklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890- ()')
# Set up label for the month
label_month = label.Label(font_large, color=LABEL_DAY_COLOR, max_glyphs=200)
label_month.x = board.DISPLAY.width // 10
label_month.y = 80
pyportal.splash.append(label_month)
# Set up label for the time
label_time = label.Label(font_small, color=LABEL_TIME_COLOR, max_glyphs=200)
label_time.x = board.DISPLAY.width // 3
label_time.y = 150
pyportal.splash.append(label_time)
refresh_time = None
while True:
# only query the network time every hour
if (not refresh_time) or (time.monotonic() - refresh_time) > 3600:
try:
print("Getting new time from internet...")
pyportal.get_local_time(secrets['timezone'])
refresh_time = time.monotonic()
# set the_time
the_time = time.localtime()
except (ValueError, RuntimeError) as e:
print("Failed to get data, retrying\n", e)
esp.reset()
continue
# convert tm_mon value to month name
month = months[the_time.tm_mon]
# determine and display how far we are in the month
if 1 <= the_time.tm_mday <= 14:
label_month.text = "Early %s-ish"%month
elif 15 <= the_time.tm_mday <= 24:
label_month.text = "Mid %s-ish"%month
else:
label_month.text = "Late %s-ish"%month
# set the time label's text
label_time.text = "({})".format(time_names[the_time.tm_hour])
# update every minute
time.sleep(60)

View file

@ -1,5 +1,6 @@
import time
import board
import microcontroller
import displayio
import busio
from analogio import AnalogIn
@ -12,10 +13,13 @@ import adafruit_touchscreen
from adafruit_pyportal import PyPortal
# ------------- Inputs and Outputs Setup ------------- #
# init. the temperature sensor
i2c_bus = busio.I2C(board.SCL, board.SDA)
adt = adafruit_adt7410.ADT7410(i2c_bus, address=0x48)
adt.high_resolution = True
try: # attempt to init. the temperature sensor
i2c_bus = busio.I2C(board.SCL, board.SDA)
adt = adafruit_adt7410.ADT7410(i2c_bus, address=0x48)
adt.high_resolution = True
except ValueError:
# Did not find ADT7410. Probably running on Titano or Pynt
adt = None
# init. the light sensor
light_sensor = AnalogIn(board.LIGHT)
@ -332,10 +336,14 @@ board.DISPLAY.show(splash)
while True:
touch = ts.touch_point
light = light_sensor.value
tempC = round(adt.temperature)
tempF = tempC * 1.8 + 32
sensor_data.text = 'Touch: {}\nLight: {}\n Temp: {}°F'.format(touch, light, tempF)
if adt: # Only if we have the temperature sensor
tempC = adt.temperature
else: # No temperature sensor
tempC = microcontroller.cpu.temperature
tempF = tempC * 1.8 + 32
sensor_data.text = 'Touch: {}\nLight: {}\n Temp: {:.0f}°F'.format(touch, light, tempF)
# ------------- Handle Button Press Detection ------------- #
if touch: # Only do this if the screen is touched

View file

@ -0,0 +1,332 @@
// ConvertBMPinspector01 - Read and enlarge a modified 32x24 24-bit gray BMP file,
// display an upscaled 256x192 BMP image in 256 colors.
// Ver. 1 - Fetch filenames and display BMPs in sequence.
// Add nav buttons and mouseover pixel temperatures
// This sketch does no checking for file compatibility.
// Only frm_____.bmp images from the thermal camera sketch will work.
// Any other files in the data folder will fail.
import java.util.Date;
byte b[], colorPal[]; // Buffers for input file bytes and for colors
int i, fileCount = 0, BGcolor = 48, colorMap = 1,
butnsX = 30, butnsY = 290,
offsetX = 153, offsetY = 6, // These value pairs control where the onscreen features appear
numbersX = 40, numbersY = 48,
probeX = 190, probeY = 210;
boolean celsiusFlag = false;
float fixedPoint[];
String[] filenames;
void setup() {
size(480, 360); // Size must be the first statement
background(BGcolor); // Clear the screen with a gray background
colorPal = new byte[1024]; // Prepare a 1K color table
loadColorTable(colorMap, 0); // Load color table, 1 == ironbow palette
fixedPoint = new float[5]; // A buffer for appended fixed point values
String path = sketchPath() + "/data"; // Read from the "/data" subdirectory
filenames = listFileNames(path);
fileCount = filenames.length;
i = 0;
if(fileCount < 1) {
println("No files found. Stopping.");
noLoop();
} else {
loadBMPscreen(i); // Read in the first frame for inspection
}
}
void draw() {
int sampleX, sampleY, pixelVal;
float sampleTemp;
sampleX = (mouseX - offsetX) >> 3; // Map mouse position to BMP pixel space
sampleY = 23 - ((mouseY - offsetY) >> 3);
noStroke();
smooth();
fill(BGcolor + 16);
rect(probeX, probeY, 180, 40); // Clear the interactive window space
if((sampleX >= 0) && (sampleX < 32) && (sampleY >= 0) && (sampleY < 24)) { // Mouse within BMP image bounds?
pixelVal = b[54 + (32 * sampleY + sampleX) * 3] & 0xff; // Read the 8-bit pixel value
fill(colorPal[4 * pixelVal + 2] & 0xFF, colorPal[4 * pixelVal + 1] & 0xFF, colorPal[4 * pixelVal + 0] & 0xFF);
rect(probeX, probeY, 180, 40);
fill(BGcolor);
rect(probeX + 10, probeY + 10, 160, 20); // Draw a colorized frame for the interactive temp readout
sampleTemp = (float(pixelVal) + 1.0) / 257.0 * (fixedPoint[3] - fixedPoint[1]) + fixedPoint[1];
if(!celsiusFlag)
sampleTemp = sampleTemp * 1.8 + 32.0;
fill(255); // Ready to display white interactive text
textSize(11);
text(sampleX, probeX + 154, probeY + 19); // Display X Y position
text(sampleY, probeX + 154, probeY + 29);
textSize(15);
text(sampleTemp, probeX + 60, probeY + 25); // Display temperature
if(pixelVal == 0 && fixedPoint[0] < fixedPoint[1]) // Pixel values clipped at bottom limit?
text("<", probeX + 40, probeY + 25); // Show out-of-range indicator
if(pixelVal == 255 && fixedPoint[4] > fixedPoint[3]) // Clipped at top?
text(">", probeX + 40, probeY + 25); // Same
}
noSmooth(); // Clear any highlighted buttons
stroke(0);
noFill();
for(sampleX = 0; sampleX < 8; ++sampleX)
rect(butnsX + sampleX * 52, butnsY, 51, 24);
sampleX = mouseX - butnsX;
sampleY = mouseY - butnsY;
if(sampleX >=0 && sampleX < 416 && sampleY >= 0 && sampleY < 24) { // Mouse over buttons?
sampleX = sampleX / 52; // Map mouse X to button X space
stroke(BGcolor + 64);
rect(butnsX + sampleX * 52, butnsY, 51, 24); // Highlight border around a button
}
}
void keyPressed() { // Load a different thermal BMP image based on keystroke
switch(key) {
case '.': // Next image
i = (i + 1) % fileCount;
break;
case ',': // Prev Image
i = (i + fileCount - 1) % fileCount;
break;
case '>': // 16 images forward
i = i + 16 < fileCount ? i + 16 : fileCount - 1;
break;
case '<': // 16 images back
i = i - 16 < 0 ? 0 : i - 16;
break;
case '/': // Last image
i = fileCount - 1;
break;
case 'm': // First image
i = 0;
break;
}
loadBMPscreen(i);
}
void mousePressed() {
int sampleX, sampleY;
sampleX = mouseX - butnsX;
sampleY = mouseY - butnsY;
if(sampleX >=0 && sampleX < 416 && sampleY >= 0 && sampleY < 24) { // Is mouse over button row?
sampleX = sampleX / 52; // Map mouse X to button X space
switch(sampleX) {
case 1: // First image
i = 0;
break;
case 2: // 16 images back
i = i - 16 < 0 ? 0 : i - 16;
break;
case 3: // Prev Image
i = (i + fileCount - 1) % fileCount;
break;
case 4: // Next image
i = (i + 1) % fileCount;
break;
case 5: // 16 images forward
i = i + 16 < fileCount ? i + 16 : fileCount - 1;
break;
case 6: // Last image
i = fileCount - 1;
break;
case 7: // Change color map
loadColorTable(colorMap = (colorMap + 1) % 5, 0); // Load color table
break;
default: // Toggle C/F
celsiusFlag = !celsiusFlag;
break;
}
loadBMPscreen(i);
}
}
void loadBMPscreen(int fileIndex) {
int x, y;
b = loadBytes(filenames[fileIndex]); // Open a file and read its 8-bit data
background(BGcolor); // Clear screen
enlarge8bitColor(); // Place colored enlarged image on screen
for(x = 0; x < 5; ++x) { // Rebuild 5 float values from next 4*n bytes in the file
fixedPoint[x] = expandFloat(b[2360 + (x * 4) + 0], b[2360 + (x * 4) + 1],
b[2360 + (x * 4) + 2], b[2360 + (x * 4) + 3]);
}
y = ((b[2387] & 0xff) << 24) + ((b[2386] & 0xff) << 16)
+ ((b[2385] & 0xff) << 8) + (b[2384] & 0xff); // Reassemble a milliseconds time stamp
textSize(10); // Print text labels for the frame stats
smooth();
fill(255);
text(filenames[fileIndex], numbersX + 5, numbersY + 40); // Show current filename
if(celsiusFlag)
text("Frame\n\n\nSeconds\n\nDegrees C", numbersX + 5, numbersY + 8);
else
text("Frame\n\n\nSeconds\n\nDegrees F", numbersX + 5, numbersY + 8);
text("Approximate temperatures based on 8-bit pixel values", probeX - 42, probeY + 52); // Show approximation disclaimer
textSize(15);
text(fileIndex, numbersX + 5, numbersY + 25); // Print frame number
text(float(y) * 0.001, numbersX, numbersY + 74); // Print time stamp in seconds
if(celsiusFlag) { // Show 3 temps in Celsius
fill(255, 128, 64);
text(fixedPoint[4], numbersX, numbersY + 108);
fill(255, 200, 64);
text(fixedPoint[2], numbersX, numbersY + 128);
fill(128, 128, 255);
text(fixedPoint[0], numbersX, numbersY + 148);
} else { // or show them in Farenheit
fill(255, 128, 64);
text(fixedPoint[4] * 1.8 + 32.0, numbersX, numbersY + 108);
fill(255, 200, 64);
text(fixedPoint[2] * 1.8 + 32.0, numbersX, numbersY + 128);
fill(128, 128, 255);
text(fixedPoint[0] * 1.8 + 32.0, numbersX, numbersY + 148);
}
noSmooth();
stroke(0);
fill(BGcolor + 24);
for(x = 0; x < 8; ++x) // Draw 8 button rectangles
rect(butnsX + x * 52, butnsY, 51, 24);
for(x = 0; x < 50; ++x) { // Paint a mini colormap gradient within last button
y = int(map(x, 0, 50, 0, 255));
stroke(colorPal[4 * y + 2] & 0xFF, colorPal[4 * y + 1] & 0xFF, colorPal[4 * y + 0] & 0xFF);
line(butnsX + 365 + x, butnsY + 1, butnsX + 365 + x, butnsY + 23);
}
smooth(); // Add text labels to buttons
fill(255);
textSize(15);
text("|< << < > >> >|", butnsX + 70, butnsY + 17);
if(celsiusFlag)
text("C", butnsX + 20, butnsY + 18);
else
text("F", butnsX + 20, butnsY + 18);
}
void enlarge8bitColor() { // Convert a small gray BMP array and plot an enlarged colormapped version
int x, y;
noStroke();
for(y = 0; y < 24; ++y) { // Count all source pixels
for(x = 0; x < 32; ++x) {
int pixMid = b[54 + ((32 * y + x) + 0) * 3] & 0xFF;
fill(colorPal[4 * pixMid + 2] & 0xFF, colorPal[4 * pixMid + 1] & 0xFF, colorPal[4 * pixMid + 0] & 0xFF); // Get color from table
rect(offsetX + 8 * x, offsetY + 8 * (23 - y), 8, 8); // Draw a square pixel, bottom up
}
}
}
void loadColorTable(int choiceNum, int offset) {
int i, x;
switch(choiceNum) {
case 1: // Load 8-bit BMP color table with computed ironbow curves
for(x = 0; x < 256; ++x) {
float fleX = (float)x / 255.0;
float fleG = 255.9 * (1.02 - (fleX - 0.72) * (fleX - 0.72) * 1.96);
fleG = (fleG > 255.0) || (fleX > 0.75) ? 255.0 : fleG; // Truncate curve
i = (int)fleG;
colorPal[offset + x * 4 + 2] = byte(i & 0xFF); // Red vals
fleG = fleX * fleX * 255.9;
i = (int)fleG;
colorPal[offset + x * 4 + 1] = byte(i & 0xFF); // Grn vals
fleG = 255.9 * (14.0 * (fleX * fleX * fleX) - 20.0 * (fleX * fleX) + 7.0 * fleX);
fleG = fleG < 0.0 ? 0.0 : fleG; // Truncate curve
i = (int)fleG;
colorPal[offset + x * 4 + 0] = byte(i & 0xFF); // Blu vals
}
break;
case 2: // Compute quadratic "firebow" palette
for(x = 0; x < 256; ++x) {
float fleX = (float)x / 255.0;
float fleG = 255.9 * (1.00 - (fleX - 1.0) * (fleX - 1.0));
i = (int)fleG;
colorPal[offset + x * 4 + 2] = byte(i & 0xFF); // Red vals
fleG = fleX < 0.25 ? 0.0 : (fleX - 0.25) * 1.3333 * 255.9;
i = (int)fleG;
colorPal[offset + x * 4 + 1] = byte(i & 0xFF); // Grn vals
fleG = fleX < 0.5 ? 0.0 : (fleX - 0.5) * (fleX - 0.5) * 1023.9;
i = (int)fleG;
colorPal[offset + x * 4 + 0] = byte(i & 0xFF); // Blu vals
}
break;
case 3: // Compute "alarm" palette
for(x = 0; x < 256; ++x) {
float fleX = (float)x / 255.0;
float fleG = 255.9 * (fleX < 0.875 ? 1.00 - (fleX * 1.1428) : 1.0);
i = (int)fleG;
colorPal[offset + x * 4 + 2] = byte(i & 0xFF); // Red vals
fleG = 255.9 * (fleX < 0.875 ? 1.00 - (fleX * 1.1428) : (fleX - 0.875) * 8.0);
i = (int)fleG;
colorPal[offset + x * 4 + 1] = byte(i & 0xFF); // Grn vals
fleG = 255.9 * (fleX < 0.875 ? 1.00 - (fleX * 1.1428) : 0.0);
i = (int)fleG;
colorPal[offset + x * 4 + 0] = byte(i & 0xFF); // Blu vals
}
break;
case 4: // Grayscale, black hot
for(x = 0; x < 256; ++x) {
colorPal[offset + x * 4 + 2] = byte(255 - x & 0xFF); // Red vals
colorPal[offset + x * 4 + 1] = byte(255 - x & 0xFF); // Grn vals
colorPal[offset + x * 4 + 0] = byte(255 - x & 0xFF); // Blu vals
}
break;
default: // Grayscale, white hot
for(x = 0; x < 256; ++x) {
colorPal[offset + x * 4 + 2] = byte(x & 0xFF); // Red vals
colorPal[offset + x * 4 + 1] = byte(x & 0xFF); // Grn vals
colorPal[offset + x * 4 + 0] = byte(x & 0xFF); // Blu vals
}
}
}
// Rebuild a float from a fixed point decimal value encoded in 4 bytes
float expandFloat(byte m1, byte m2, byte e1, byte e2) {
int fracPart;
float floatPart;
fracPart = ((e2 & 0xff) << 8) + (e1 & 0xff); // Reassemble 16-bit value
floatPart = (float)fracPart / 49152.0; // Convert into fractional portion of float
fracPart = ((m2 & 0xff) << 8) + (m1 & 0xff); // Reassemble 16-bit value
return ((float)fracPart + floatPart) - 1000.0; // Complete reconstructing original float
}
String[] listFileNames(String dir) { // Return the filenames from a directory as an array of Strings
File file = new File(dir);
if (file.isDirectory()) {
String names[] = file.list();
return names;
} else // It's not a directory
return null;
}

View file

@ -0,0 +1,220 @@
// ConvertBMPto8bit - Read and enlarge a modified 32x24 24-bit gray BMP file,
// write an upscaled 256x192 BMP image with a 256 color table.
// Ver. 2 - Fetch filenames and convert all suitable BMPs we find.
// Builds sequences suitable for online animated GIF converters
import java.util.Date;
// BMP File Header, little end first
int BmpPSPHead[] = {
0x42, 0x4D, // "BM" in hex
0x36, 0xC4, 0x00, 0x00, // File size, 50230
0x00, 0x00, // reserved for app data 1
0x00, 0x00, // reserved for app data 2
0x36, 0x04, 0x00, 0x00 // Offset of pixel 0, 1078
};
// BMP 8-bit DIB Header, little end first
int DIBHeadPSP1[] = {
0x28, 0x00, 0x00, 0x00, // Header size, 40
0x00, 0x01, 0x00, 0x00, // pixel width, 256
0xC0, 0x00, 0x00, 0x00, // pixel height, 192
0x01, 0x00, // color planes, 1
0x08, 0x00, // bits per pixel, 8
0x00, 0x00, 0x00, 0x00, // Compression method, 0==none
0x00, 0x00, 0x00, 0x00, // Raw bitmap data size, dummy 0
0x12, 0x0B, 0x00, 0x00, // Pixels per meter H, 2834
0x12, 0x0B, 0x00, 0x00, // Pixels per meter V, 2834
0x00, 0x00, 0x00, 0x00, // Colors in palette, 0==default 2^n
0x00, 0x00, 0x00, 0x00 // Number of important colors, 0
};
byte outBytes[], b[]; // Buffer for the input file bytes
PImage img; // Declare variable of type PImage
int fileCount = 0, imageIndex = 0;
String[] filenames;
// "paletteChoice" selects a false color palette:
// 0 == Grayscale, white hot
// 1 == Ironbow
// 2 == Firebow
// 3 == Hot alarm
// 4 == Grayscale, black hot
int paletteChoice = 1;
void setup() {
int i, j, x, y;
String nameHead, nameTail;
size(256, 192); // Size must be the first statement
// noStroke();
frameRate(5);
background(0); // Clear the screen with a black background
outBytes = new byte[50230]; // 54 header + 1K colors + 12K pixels
String path = sketchPath() + "/data"; // Read from the "/data" subdirectory
println("Listing filenames: ");
filenames = listFileNames(path);
println(filenames);
fileCount = filenames.length;
println(fileCount + " entries");
if(fileCount < 1) {
println("No images found. Stopping.");
} else { // Filenames exist in the directory
for(i = 0; i < fileCount; ++i) { // Test each name
nameHead = filenames[i].substring(0, 3);
nameTail = filenames[i].substring(8);
j = int(filenames[i].substring(3, 8));
if(nameHead.equals("frm") && nameTail.equals(".bmp") && j != 0) // Source "frm_____.bmp" found?
enlarge8bit(i); // Process and write an enlarged 8-bit version
}
}
noLoop();
}
void draw() {
int countX, countY;
noSmooth();
for(countY = 0; countY < 192; ++countY) {
for(countX = 0; countX < 256; ++countX) {
stroke(0xFF & outBytes[1078 + (countY * 256 + countX)]); // Color from BMP buffer
point(countX, 191 - countY); // Draw a pixel, bottom up
}
}
}
void enlarge8bit(int fileNumber) { // Read a small gray "frm" BMP image and write an enlarged colormapped "out" BMP
int i, x, y;
b = loadBytes(filenames[fileNumber]); // Open a file and read its 8-bit data
for(i = 0; i < 14; ++i)
outBytes[i] = byte(BmpPSPHead[i] & 0xFF); // Copy BMP header 1 into output buffer
for(i = 0; i < 40; ++i)
outBytes[i + 14] = byte(DIBHeadPSP1[i] & 0xFF); // Copy header 2
loadColorTable(paletteChoice, 54); // Load color table, 54 byte BMP header offset
for(y = 0; y < 23; ++y) { // Bilinear interpolation, count the source pixels less one
for(x = 0; x < 31; ++x) {
for(int yLirp = 0; yLirp < 9; ++yLirp) {
int corner0 = b[54 + ((32 * y + x) + 32) * 3] & 0xFF;
int corner1 = b[54 + ((32 * y + x) + 0) * 3] & 0xFF;
int pixLeft = (corner0 * yLirp + corner1 * (8 - yLirp)) >> 3; // Lirp 1 endpoint from 2 L pixels,
int corner2 = b[54 + ((32 * y + x) + 33) * 3] & 0xFF;
int corner3 = b[54 + ((32 * y + x) + 1) * 3] & 0xFF;
int pixRight = (corner2 * yLirp + corner3 * (8 - yLirp)) >> 3; // and the other from 2 R pixels
for(int xLirp = 0; xLirp < 9; ++xLirp) {
int pixMid = (pixRight * xLirp + pixLeft * (8 - xLirp)) >> 3; // Lirp between lirped endpoints, bilinear interp
outBytes[1078 + y * 2048 + x * 8 + yLirp * 256 + xLirp + 771] = byte(pixMid & 0xFF);
}
}
}
}
for(y = 0; y < 192; ++y) { // Pad out the empty side pixels
for(x = 0; x < 4; ++x) {
outBytes[1078 + (3 - x) + 256 * y] = outBytes[1082 + 256 * y];
outBytes[1330 + x + 256 * y] = outBytes[1329 + 256 * y];
}
}
for(x = 0; x < 256; ++x) { // Pad out the empty above/below pixels
for(y = 0; y < 4; ++y) {
outBytes[ 1078 + 256 * (3 - y) + x] = outBytes[ 2102 + x];
outBytes[49206 + 256 * y + x] = outBytes[48950 + x];
}
}
saveBytes("data/out" + filenames[fileNumber].substring(3), outBytes); // Save a recolored 8-bit BMP as "out_____.bmp"
}
void loadColorTable(int choiceNum, int offset) {
int i, x;
switch(choiceNum) {
case 1: // Load 8-bit BMP color table with computed ironbow curves
for(x = 0; x < 256; ++x) {
float fleX = (float)x / 255.0;
float fleG = 255.9 * (1.02 - (fleX - 0.72) * (fleX - 0.72) * 1.96);
fleG = (fleG > 255.0) || (fleX > 0.75) ? 255.0 : fleG; // Truncate curve
i = (int)fleG;
outBytes[offset + x * 4 + 2] = byte(i & 0xFF); // Red vals
fleG = fleX * fleX * 255.9;
i = (int)fleG;
outBytes[offset + x * 4 + 1] = byte(i & 0xFF); // Grn vals
fleG = 255.9 * (14.0 * (fleX * fleX * fleX) - 20.0 * (fleX * fleX) + 7.0 * fleX);
fleG = fleG < 0.0 ? 0.0 : fleG; // Truncate curve
i = (int)fleG;
outBytes[offset + x * 4 + 0] = byte(i & 0xFF); // Blu vals
}
break;
case 2: // Compute quadratic "firebow" palette
for(x = 0; x < 256; ++x) {
float fleX = (float)x / 255.0;
float fleG = 255.9 * (1.00 - (fleX - 1.0) * (fleX - 1.0));
i = (int)fleG;
outBytes[offset + x * 4 + 2] = byte(i & 0xFF); // Red vals
fleG = fleX < 0.25 ? 0.0 : (fleX - 0.25) * 1.3333 * 255.9;
i = (int)fleG;
outBytes[offset + x * 4 + 1] = byte(i & 0xFF); // Grn vals
fleG = fleX < 0.5 ? 0.0 : (fleX - 0.5) * (fleX - 0.5) * 1023.9;
i = (int)fleG;
outBytes[offset + x * 4 + 0] = byte(i & 0xFF); // Blu vals
}
break;
case 3: // Compute "alarm" palette
for(x = 0; x < 256; ++x) {
float fleX = (float)x / 255.0;
float fleG = 255.9 * (fleX < 0.875 ? 1.00 - (fleX * 1.1428) : 1.0);
i = (int)fleG;
outBytes[offset + x * 4 + 2] = byte(i & 0xFF); // Red vals
fleG = 255.9 * (fleX < 0.875 ? 1.00 - (fleX * 1.1428) : (fleX - 0.875) * 8.0);
i = (int)fleG;
outBytes[offset + x * 4 + 1] = byte(i & 0xFF); // Grn vals
fleG = 255.9 * (fleX < 0.875 ? 1.00 - (fleX * 1.1428) : 0.0);
i = (int)fleG;
outBytes[offset + x * 4 + 0] = byte(i & 0xFF); // Blu vals
}
break;
case 4: // Grayscale, black hot
for(x = 0; x < 256; ++x) {
outBytes[offset + x * 4 + 2] = byte(255 - x & 0xFF); // Red vals
outBytes[offset + x * 4 + 1] = byte(255 - x & 0xFF); // Grn vals
outBytes[offset + x * 4 + 0] = byte(255 - x & 0xFF); // Blu vals
}
break;
default: // Grayscale, white hot
for(x = 0; x < 256; ++x) {
outBytes[offset + x * 4 + 2] = byte(x & 0xFF); // Red vals
outBytes[offset + x * 4 + 1] = byte(x & 0xFF); // Grn vals
outBytes[offset + x * 4 + 0] = byte(x & 0xFF); // Blu vals
}
}
}
String[] listFileNames(String dir) { // Return the filenames from a directory as an array of Strings
File file = new File(dir);
if (file.isDirectory()) {
String names[] = file.list();
return names;
} else // It's not a directory
return null;
}

View file

@ -0,0 +1,448 @@
// ConvertBMPtoSeq01 - Read and enlarge a modified 32x24 24-bit gray BMP file,
// saving 256x192 BMP images in 256 colors for converting to MOV.
// Ver. 1 - Fetch filenames and scan all suitable BMPs we find for their time/temp data,
// to set the scale for graphing these numbers through the MOV.
import java.util.Date;
byte colorPal[], b[]; // Buffers for a color palette, and reading bytes from files
PImage img;
int i, fileCount = 0, frameTotal = 0, earlyFrame = 0, lastFrame = 0,
hotLowFrame, hotHighFrame, coldLowFrame, coldHighFrame, targLowFrame, targHighFrame,
framX1, framX2, coldY1, coldY2, targY1, targY2, hotY1, hotY2,
offsetX = 153, offsetY = 6, numbersX = 40, numbersY = 30, graphX = 8, graphY = 342,
histoX = 410, histoY = 342, histoH = 140, histoW = 64, BGcolor = 48;
float hottestLow, hottestHigh, coldestLow, coldestHigh, targetLow, targetHigh;
String[] filenames;
// Change the following values to customize the output images.
// "paletteChoice" selects a false color palette:
// 0 == Grayscale, white hot
// 1 == Ironbow
// 2 == Firebow
// 3 == Hot alarm
// 4 == Grayscale, black hot
int paletteChoice = 1;
boolean markersVisible = true, celsiusFlag = false, lirpSmoothing = true;
void setup() {
int x, y;
float fixedPoint[];
String nameHead, nameTail;
size(480, 360); // Size must be the first statement
background(BGcolor); // Clear the screen with a gray background
noSmooth();
colorPal = new byte[1024]; // Reserve a 1K color table
loadColorTable(paletteChoice, 0); // Load color table
fixedPoint = new float[5]; // Buffer for added fixed point values
String path = sketchPath() + "/data"; // Read from the "/data" subdirectory
// println("Listing filenames: ");
filenames = listFileNames(path);
// println(filenames);
fileCount = filenames.length;
// println(fileCount + " entries");
if(fileCount < 1) {
println("No images found. Stopping.");
} else { // Filenames exist in the directory, convert what we can
// First pass: Read the embedded times/temps and find maxes/mins for graphing
print("Counting through files: ");
for(i = 0; i < fileCount; ++i) { // Test each filename for conformity
if((i & 0x3F) == 0)
print(i + ", ");
nameHead = filenames[i].substring(0, 3);
nameTail = filenames[i].substring(8);
if(nameHead.equals("frm") && nameTail.equals(".bmp") && int(filenames[i].substring(3, 8)) != 0) { // Source "frm_____.bmp" found?
b = loadBytes(filenames[i]); // Open a file and read its 8-bit data
for(x = 0; x < 5; ++x) { // Rebuild float values from next 4*n bytes in the file
fixedPoint[x] = expandFloat(b[2360 + (x * 4) + 0], b[2360 + (x * 4) + 1],
b[2360 + (x * 4) + 2], b[2360 + (x * 4) + 3]); // 2360 == headers + pixels + 2
}
y = ((b[2387] & 0xff) << 24) + ((b[2386] & 0xff) << 16)
+ ((b[2385] & 0xff) << 8) + (b[2384] & 0xff); // Reassemble a uint32_t millis() stamp
if(++frameTotal == 1) { // First frame found so far?
coldestLow = coldestHigh = fixedPoint[0];
targetLow = targetHigh = fixedPoint[2]; // Initialize all values
hottestLow = hottestHigh = fixedPoint[4];
hotLowFrame = hotHighFrame = coldLowFrame = coldHighFrame = targLowFrame = targHighFrame = earlyFrame = lastFrame = y;
} else { // Compare everything, update where necessary
if(y < earlyFrame)
earlyFrame = y; // These will set the left and right bounds
else if(y > lastFrame) // of the temperature over time graphs
lastFrame = y;
if(fixedPoint[0] < coldestLow) { // These will define the high and low bounds
coldestLow = fixedPoint[0];
coldLowFrame = y;
} else if(fixedPoint[0] > coldestHigh) {
coldestHigh = fixedPoint[0];
coldHighFrame = y;
}
if(fixedPoint[2] < targetLow) {
targetLow = fixedPoint[2];
targLowFrame = y;
} else if(fixedPoint[2] > targetHigh) {
targetHigh = fixedPoint[2];
targHighFrame = y;
}
if(fixedPoint[4] < hottestLow) {
hottestLow = fixedPoint[4];
hotLowFrame = y;
} else if(fixedPoint[4] > hottestHigh) {
hottestHigh = fixedPoint[4];
hotHighFrame = y;
}
}
}
}
println(i + ", done.\n");
// The high and low points of three datasets are found, display them
println("Frame times " + earlyFrame + " to " + lastFrame + " totaling " + (lastFrame - earlyFrame));
println("Cold values " + coldestLow + " at " + coldLowFrame + " to " + coldestHigh + " at " + coldHighFrame);
println("Targ values " + targetLow + " at " + targLowFrame + " to " + targetHigh + " at " + targHighFrame);
println("Hot values " + hottestLow + " at " + hotLowFrame + " to " + hottestHigh + " at " + hotHighFrame);
stroke(BGcolor + 48);
for(y = 0; y <= 140; y += 35)
line(graphX, graphY - y, graphX + 400, graphY - y); // Draw a generic grid for the time graph
for(x = 0; x <= 400; x += 40)
line(graphX + x, graphY - 140, graphX + x, graphY);
noStroke(); // Text labels for the top & bottom temp values of the graph
textSize(10);
fill(255);
if(celsiusFlag) {
text(hottestHigh, graphX + 402, graphY - 142);
text(coldestLow, graphX + 402, graphY + 12);
} else {
text(hottestHigh * 1.8 + 32.0, graphX + 402, graphY - 142);
text(coldestLow * 1.8 + 32.0, graphX + 402, graphY + 12);
}
fill(BGcolor + 128); // Predraw 6 little high/low markers in the graph space
rect(graphX + 400 * (coldLowFrame - earlyFrame) / (lastFrame - earlyFrame) - 1,
graphY - int((coldestLow - coldestLow) / (coldestLow - hottestHigh) * 140.0) - 1, 3, 3);
rect(graphX + 400 * (coldHighFrame - earlyFrame) / (lastFrame - earlyFrame) - 1,
graphY - int((coldestLow - coldestHigh) / (coldestLow - hottestHigh) * 140.0) - 1, 3, 3);
rect(graphX + 400 * (targLowFrame - earlyFrame) / (lastFrame - earlyFrame) - 1,
graphY - int((coldestLow - targetLow) / (coldestLow - hottestHigh) * 140.0) - 1, 3, 3);
rect(graphX + 400 * (targHighFrame - earlyFrame) / (lastFrame - earlyFrame) - 1,
graphY - int((coldestLow - targetHigh) / (coldestLow - hottestHigh) * 140.0) - 1, 3, 3);
rect(graphX + 400 * (hotLowFrame - earlyFrame) / (lastFrame - earlyFrame) - 1,
graphY - int((coldestLow - hottestLow) / (coldestLow - hottestHigh) * 140.0) - 1, 3, 3);
rect(graphX + 400 * (hotHighFrame - earlyFrame) / (lastFrame - earlyFrame) - 1,
graphY - int((coldestLow - hottestHigh) / (coldestLow - hottestHigh) * 140.0) - 1, 3, 3);
}
i = 0;
}
// Second pass: Read each frame again, plot color mapped enlarged image, temperature values and graph, save each frame
void draw() {
int x, y, histogram[];
float tempY, fixedPoint[];
String nameHead, nameTail;
noSmooth();
fixedPoint = new float[5]; // Buffer for appended fixed point values
histogram = new int[256]; // Buffer for color histogram
for(x = 0; x < 256; ++x)
histogram[x] = 0; // Initialize histogram
if(i < fileCount) { // Test each filename for conformity
nameHead = filenames[i].substring(0, 3);
nameTail = filenames[i].substring(8);
if(nameHead.equals("frm") && nameTail.equals(".bmp") && int(filenames[i].substring(3, 8)) != 0) { // Source "frm_____.bmp" found?
b = loadBytes(filenames[i]); // Open a file and read its 8-bit data
// println(i + " " + filenames[i]);
enlarge8bitColor(); // Place colored enlarged image on screen
for(x = 0; x < 5; ++x) { // Rebuild float values from next 4*n bytes in the file
fixedPoint[x] = expandFloat(b[2360 + (x * 4) + 0], b[2360 + (x * 4) + 1],
b[2360 + (x * 4) + 2], b[2360 + (x * 4) + 3]);
}
y = ((b[2387] & 0xff) << 24) + ((b[2386] & 0xff) << 16)
+ ((b[2385] & 0xff) << 8) + (b[2384] & 0xff); // Reassemble a milliseconds time stamp
smooth();
framX2 = graphX + 400 * (y - earlyFrame) / (lastFrame - earlyFrame);
coldY2 = graphY - int((coldestLow - fixedPoint[0]) / (coldestLow - hottestHigh) * 140.0); // Map data values into graph space
targY2 = graphY - int((coldestLow - fixedPoint[2]) / (coldestLow - hottestHigh) * 140.0);
hotY2 = graphY - int((coldestLow - fixedPoint[4]) / (coldestLow - hottestHigh) * 140.0);
if(i == 0) {
framX1 = framX2; // Set starting points for 3 graphs
coldY1 = coldY2;
targY1 = targY2;
hotY1 = hotY2;
}
stroke(128, 128, 255);
line(framX1, coldY1, framX2, coldY2); // Graph cold data point
stroke(255, 200, 64);
line(framX1, targY1, framX2, targY2); // Graph center data point
stroke(255, 128, 64);
line(framX1, hotY1, framX2, hotY2); // Graph hot data point
framX1 = framX2; // Remember endpoints of graphed lines
coldY1 = coldY2;
targY1 = targY2;
hotY1 = hotY2;
noStroke(); // Print key values onscreen for current frame
fill(BGcolor);
rect(numbersX, numbersY, 82, 152); // Erase number region
fill(BGcolor + 32); // A color to highlight any extreme values
if(y == hotLowFrame || y == hotHighFrame)
rect(numbersX, numbersY + 95, 80, 16);
if(y == targLowFrame || y == targHighFrame)
rect(numbersX, numbersY + 115, 80, 16);
if(y == coldLowFrame || y == coldHighFrame)
rect(numbersX, numbersY + 135, 80, 16);
textSize(10);
fill(255);
text(filenames[i], numbersX + 5, numbersY + 40); // Show current filename
if(celsiusFlag)
text("Frame\n\n\nElapsed sec\n\nDegrees C", numbersX + 5, numbersY + 8);
else
text("Frame\n\n\nElapsed sec\n\nDegrees F", numbersX + 5, numbersY + 8);
textSize(15);
text(i, numbersX + 5, numbersY + 25); // Print frame number
text(float(y - earlyFrame) * 0.001, numbersX, numbersY + 74); // Print elapsed time
if(celsiusFlag) { // Print temps in Celsius
fill(255, 128, 64);
text(fixedPoint[4], numbersX, numbersY + 108);
fill(255, 200, 64);
text(fixedPoint[2], numbersX, numbersY + 128);
fill(128, 128, 255);
text(fixedPoint[0], numbersX, numbersY + 148);
} else { // or print them in Farenheit
fill(255, 128, 64);
text(fixedPoint[4] * 1.8 + 32.0, numbersX, numbersY + 108);
fill(255, 200, 64);
text(fixedPoint[2] * 1.8 + 32.0, numbersX, numbersY + 128);
fill(128, 128, 255);
text(fixedPoint[0] * 1.8 + 32.0, numbersX, numbersY + 148);
}
for(x = 0; x < 768; ++x)
++histogram[b[54 + 3 * x] & 0xFF]; // Count all colors
framX2 = histogram[0];
for(x = 1; x < 256; ++x) { // Find most numerous color
if(histogram[x] > framX2) {
framX2 = histogram[x];
targY2 = x;
}
}
fill(BGcolor);
rect(histoX, histoY - 140, histoW, histoH + 1); // Erase histogram region
for(y = 0; y < 256; ++y) {
if(histogram[y] > 0) {
tempY = float(y) * (fixedPoint[3] - fixedPoint[1]) / 255.0 + fixedPoint[1]; // Convert a 8-bit value to a temperature
tempY = float(histoH) * (coldestLow - tempY) / (coldestLow - hottestHigh); // Position it on the graph Y axis
stroke(colorPal[4 * y + 2] & 0xFF, colorPal[4 * y + 1] & 0xFF, colorPal[4 * y + 0] & 0xFF); // Color map the stroke
line(histoX, histoY - int(tempY), histoX + (histoW - 1) * histogram[y] / framX2, histoY - int(tempY)); // Draw a line proportional to the pixel count
}
noStroke();
noSmooth();
textSize(10);
if(targY2 < 0x80) // Histogram peak in the dark side?
fill(255); // Set contrasting test to white
else
fill(0);
tempY = float(targY2) * (fixedPoint[3] - fixedPoint[1]) / 255.0 + fixedPoint[1]; // Convert a 8-bit value to a temperature
if(celsiusFlag) // Print the Y-positioned float value in C?
text(tempY, histoX, histoY + 3 - int(float(histoH) * (coldestLow - tempY) / (coldestLow - hottestHigh)));
else
text(tempY * 1.8 + 32.0, histoX, histoY + 3 - int(float(histoH) * (coldestLow - tempY) / (coldestLow - hottestHigh)));
}
saveFrame("mov#####.jpg"); // Save the image into a sequence for Movie Maker
}
++i;
}
}
void enlarge8bitColor() { // Convert a small gray BMP array and plot an enlarged colormapped version
int x, y;
if(lirpSmoothing) { // Bilinear interpolation?
for(y = 0; y < 23; ++y) { // Count the source pixels less one
for(x = 0; x < 31; ++x) {
for(int yLirp = 0; yLirp < 9; ++yLirp) {
int corner0 = b[54 + ((32 * y + x) + 32) * 3] & 0xFF;
int corner1 = b[54 + ((32 * y + x) + 0) * 3] & 0xFF;
int pixLeft = (corner0 * yLirp + corner1 * (8 - yLirp)) >> 3; // Lirp 1 endpoint from 2 L pixels,
int corner2 = b[54 + ((32 * y + x) + 33) * 3] & 0xFF;
int corner3 = b[54 + ((32 * y + x) + 1) * 3] & 0xFF;
int pixRight = (corner2 * yLirp + corner3 * (8 - yLirp)) >> 3; // and the other from 2 R pixels
for(int xLirp = 0; xLirp < 9; ++xLirp) {
int pixMid = (pixRight * xLirp + pixLeft * (8 - xLirp)) >> 3; // Lirp between lirped endpoints, bilinear interp
stroke(colorPal[4 * pixMid + 2] & 0xFF, colorPal[4 * pixMid + 1] & 0xFF, colorPal[4 * pixMid + 0] & 0xFF);
point(offsetX + 4 + 8 * x + xLirp, offsetY + 188 - (8 * y + yLirp)); // Draw a pixel, bottom up
}
}
}
}
for(y = 0; y < 192; ++y) { // Pad out the empty side pixels
stroke(get(offsetX + 4, offsetY + y));
line(offsetX + 0, offsetY + y, offsetX + 3, offsetY + y);
stroke(get(offsetX + 252, offsetY + y));
line(offsetX + 253, offsetY + y, offsetX + 255, offsetY + y);
}
for(x = 0; x < 256; ++x) {
stroke(get(offsetX + x, offsetY + 4));
line(offsetX + x, offsetY + 0, offsetX + x, offsetY + 3);
stroke(get(offsetX + x, offsetY + 188));
line(offsetX + x, offsetY + 189, offsetX + x, offsetY + 191);
}
} else { // Plain square pixels
noStroke();
for(y = 0; y < 24; ++y) { // Count all source pixels
for(x = 0; x < 32; ++x) {
int pixMid = b[54 + ((32 * y + x) + 0) * 3] & 0xFF;
fill(colorPal[4 * pixMid + 2] & 0xFF, colorPal[4 * pixMid + 1] & 0xFF, colorPal[4 * pixMid + 0] & 0xFF); // Get color from table
rect(offsetX + 8 * x, offsetY + 8 * (23 - y), 8, 8); // Draw a pixel, bottom up
}
}
}
if(markersVisible) { // Show the green marker crosses?
stroke(0, 192, 0); // Deep green
y = ((b[2381] & 0xff) << 8) + (b[2380] & 0xff); // Reassemble 16-bit addresses of cold / hot pixels
line(offsetX + 8 * (y & 31) + 1, offsetY + 188 - 8 * (y >> 5), offsetX + 8 * (y & 31) + 7, offsetY + 188 - 8 * (y >> 5));
line(offsetX + 8 * (y & 31) + 4, offsetY + 185 - 8 * (y >> 5), offsetX + 8 * (y & 31) + 4, offsetY + 191 - 8 * (y >> 5));
y = ((b[2383] & 0xff) << 8) + (b[2382] & 0xff);
line(offsetX + 8 * (y & 31) + 1, offsetY + 188 - 8 * (y >> 5), offsetX + 8 * (y & 31) + 7, offsetY + 188 - 8 * (y >> 5));
line(offsetX + 8 * (y & 31) + 4, offsetY + 185 - 8 * (y >> 5), offsetX + 8 * (y & 31) + 4, offsetY + 191 - 8 * (y >> 5));
y = 400;
line(offsetX + 8 * (y & 31) + 1, offsetY + 188 - 8 * (y >> 5), offsetX + 8 * (y & 31) + 7, offsetY + 188 - 8 * (y >> 5));
line(offsetX + 8 * (y & 31) + 4, offsetY + 185 - 8 * (y >> 5), offsetX + 8 * (y & 31) + 4, offsetY + 191 - 8 * (y >> 5));
}
}
void loadColorTable(int choiceNum, int offset) {
int i, x;
switch(choiceNum) {
case 1: // Load 8-bit BMP color table with computed ironbow curves
for(x = 0; x < 256; ++x) {
float fleX = (float)x / 255.0;
float fleG = 255.9 * (1.02 - (fleX - 0.72) * (fleX - 0.72) * 1.96);
fleG = (fleG > 255.0) || (fleX > 0.75) ? 255.0 : fleG; // Truncate curve
i = (int)fleG;
colorPal[offset + x * 4 + 2] = byte(i & 0xFF); // Red vals
fleG = fleX * fleX * 255.9;
i = (int)fleG;
colorPal[offset + x * 4 + 1] = byte(i & 0xFF); // Grn vals
fleG = 255.9 * (14.0 * (fleX * fleX * fleX) - 20.0 * (fleX * fleX) + 7.0 * fleX);
fleG = fleG < 0.0 ? 0.0 : fleG; // Truncate curve
i = (int)fleG;
colorPal[offset + x * 4 + 0] = byte(i & 0xFF); // Blu vals
}
break;
case 2: // Compute quadratic "firebow" palette
for(x = 0; x < 256; ++x) {
float fleX = (float)x / 255.0;
float fleG = 255.9 * (1.00 - (fleX - 1.0) * (fleX - 1.0));
i = (int)fleG;
colorPal[offset + x * 4 + 2] = byte(i & 0xFF); // Red vals
fleG = fleX < 0.25 ? 0.0 : (fleX - 0.25) * 1.3333 * 255.9;
i = (int)fleG;
colorPal[offset + x * 4 + 1] = byte(i & 0xFF); // Grn vals
fleG = fleX < 0.5 ? 0.0 : (fleX - 0.5) * (fleX - 0.5) * 1023.9;
i = (int)fleG;
colorPal[offset + x * 4 + 0] = byte(i & 0xFF); // Blu vals
}
break;
case 3: // Compute "alarm" palette
for(x = 0; x < 256; ++x) {
float fleX = (float)x / 255.0;
float fleG = 255.9 * (fleX < 0.875 ? 1.00 - (fleX * 1.1428) : 1.0);
i = (int)fleG;
colorPal[offset + x * 4 + 2] = byte(i & 0xFF); // Red vals
fleG = 255.9 * (fleX < 0.875 ? 1.00 - (fleX * 1.1428) : (fleX - 0.875) * 8.0);
i = (int)fleG;
colorPal[offset + x * 4 + 1] = byte(i & 0xFF); // Grn vals
fleG = 255.9 * (fleX < 0.875 ? 1.00 - (fleX * 1.1428) : 0.0);
i = (int)fleG;
colorPal[offset + x * 4 + 0] = byte(i & 0xFF); // Blu vals
}
break;
case 4: // Grayscale, black hot
for(x = 0; x < 256; ++x) {
colorPal[offset + x * 4 + 2] = byte(255 - x & 0xFF); // Red vals
colorPal[offset + x * 4 + 1] = byte(255 - x & 0xFF); // Grn vals
colorPal[offset + x * 4 + 0] = byte(255 - x & 0xFF); // Blu vals
}
break;
default: // Grayscale, white hot
for(x = 0; x < 256; ++x) {
colorPal[offset + x * 4 + 2] = byte(x & 0xFF); // Red vals
colorPal[offset + x * 4 + 1] = byte(x & 0xFF); // Grn vals
colorPal[offset + x * 4 + 0] = byte(x & 0xFF); // Blu vals
}
}
}
// Rebuild a float from a fixed point decimal value encoded in 4 bytes
float expandFloat(byte m1, byte m2, byte e1, byte e2) {
int fracPart;
float floatPart;
fracPart = ((e2 & 0xff) << 8) + (e1 & 0xff); // Reassemble 16-bit value
floatPart = (float)fracPart / 49152.0; // Convert into fractional portion of float
fracPart = ((m2 & 0xff) << 8) + (m1 & 0xff); // Reassemble 16-bit value
return ((float)fracPart + floatPart) - 1000.0; // Complete reconstructing original float
}
String[] listFileNames(String dir) { // Return the filenames from a directory as an array of Strings
File file = new File(dir);
if (file.isDirectory()) {
String names[] = file.list();
return names;
} else // It's not a directory
return null;
}

View file

@ -0,0 +1,9 @@
## MLX9064- Thermal Image Recording Guide Code
See https://learn.adafruit.com/mlx90640-thermal-image-recording/ for this guide
The files consist of an Arduino sketch for an Adafruit Pybadge moard with attached MLX90640 thermal image sensor.
The Processing directory contains post processing for images.
Guide by Eduardo Blume for Adafruit Industries. MIT License

View file

@ -0,0 +1,834 @@
/*
ThermalImager_009b - Collect thermal image values from a MLX90640 sensor array,
display them as color-mapped pixels on a TFT screen,
include data capture to flash media, and a user configuration menu.
Written by Eduardo using code from these sources.
Arcada and MLX90640 libraries from adafruit.com
Ver. 1 - Read temps, auto-range extremes, display gray squares on TFT
Ver. 2 - Add Ironbow color palette, low+center+high markers
Ver. 3 - Add crude BMP image write to SD
Ver. 4 - Attach interrupts to improve button response
Ver. 5 - Store BMPs to SD in an orderly manner, in folders
Ver. 6 - Port to Teensy 3.2, where the libraries used are suited
Ver. 7 - Port to Adafruit PyBadge using Arcada library. Use simulated data while awaiting hardware release
Ver. 8 - Convert menu to scrolling style and add settings for emissivity and frame rate, more if feasible.
Ver. 9 - Bring in the current Adafruit library and read a real sensor.
*/
#include <Adafruit_MLX90640.h>
#include "Adafruit_Arcada.h"
Adafruit_MLX90640 mlx;
Adafruit_Arcada arcada;
#if !defined(USE_TINYUSB)
#warning "Compile with TinyUSB selected!"
#endif
File myFile;
float mlx90640To[768]; // Here we receive the float vals acquired from MLX90640
#define DE_BOUNCE 200
// Wait this many msec between button clicks
#define MENU_LEN 12
// Number of total available menu choices
#define MENU_ROWS 9
// Number of menu lines that can fit on screen
#define MENU_VPOS 6
#define GRAY_33 0x528A
#define BOTTOM_DIR "MLX90640"
#define DIR_FORMAT "/dir%05d"
#define BMP_FORMAT "/frm%05d.bmp"
#define CFG_FLNAME "/config.ini"
#define MAX_SERIAL 999
// BMP File Header, little end first, Photoshop ver.
const PROGMEM uint8_t BmpPSPHead[14] = {
0x42, 0x4D, // "BM" in hex
0x38, 0x09, 0x00, 0x00, // File size, 2360
0x00, 0x00, // reserved for app data 1
0x00, 0x00, // reserved for app data 2
0x36, 0x00, 0x00, 0x00 // Offset of first pixel, 54
};
// BMP 24-bit DIB Header, little end first, Photoshop ver.
const PROGMEM uint8_t DIBHeadPSP1[40] = {
0x28, 0x00, 0x00, 0x00, // Header size, 40
0x20, 0x00, 0x00, 0x00, // pixel width, 32
0x18, 0x00, 0x00, 0x00, // pixel height, 24
0x01, 0x00, // color planes, 1
0x18, 0x00, // bits per pixel, 24
0x00, 0x00, 0x00, 0x00, // Compression method, 0==none
0x00, 0x00, 0x00, 0x00, // Raw bitmap data size, dummy 0
0x12, 0x0B, 0x00, 0x00, // Pixels per meter H, 2834
0x12, 0x0B, 0x00, 0x00, // Pixels per meter V, 2834
0x00, 0x00, 0x00, 0x00, // Colors in palette, 0==default 2^n
0x00, 0x00, 0x00, 0x00 // Number of important colors, 0
};
// BMP file data, 2 byte padding
const PROGMEM uint8_t PSPpad[2] = {0x00, 0x00};
//Byte arrays of bitmapped icons, 16 x 12 px:
const PROGMEM uint8_t battIcon[] = {
0x0f, 0x00, 0x3f, 0xc0, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40,
0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x3f, 0xc0};
const PROGMEM uint8_t camIcon[] = {
0x01, 0xe0, 0x61, 0x20, 0xff, 0xf0, 0x80, 0x10, 0x86, 0x10, 0x89, 0x10,
0x90, 0x90, 0x90, 0x90, 0x89, 0x10, 0x86, 0x10, 0x80, 0x10, 0xff, 0xf0};
const PROGMEM uint8_t SDicon[] = {
0x0f, 0xe0, 0x1f, 0xe0, 0x3c, 0x60, 0x78, 0x60, 0x70, 0x60, 0x60, 0x60,
0x60, 0x60, 0x60, 0x60, 0x6f, 0x60, 0x60, 0x60, 0x7f, 0xe0, 0x7f, 0xe0};
const PROGMEM uint8_t snowIcon[] = {
0x15, 0x00, 0x4E, 0x40, 0xC4, 0x60, 0x75, 0xC0, 0x9F, 0x20, 0x0E, 0x00,
0x0E, 0x00, 0x9F, 0x20, 0x75, 0xC0, 0xC4, 0x60, 0x4E, 0x40, 0x15, 0x00};
uint8_t pixelArray[2304]; // BMP image body, 32 pixels * 24 rows * 3 bytes
// Some global values that several functions will use, including
// 5 floats to append to the BMP pixel data:
// coldest pixel, coldest color, center temp, hottest color, hottest pixel
float sneakFloats[5] = {3.1415926, 0.0, -11.7, 98.6, -12.34}; // Test values that get overwritten
uint16_t highAddr = 0, lowAddr = 0; // Append the pixel addresses, too
uint16_t backColor, lowPixel, highPixel, buttonRfunc = 1,
emissivity = 95, frameRate = 4,
thermRange = 0, paletteNum = 1, colorPal[256], // Array for color palettes
nextDirIndex = 0, nextBMPindex = 0, nextBMPsequence = 1; // These keep count of SD files and dirs, 0==error
uint32_t deBounce = 0, buttonBits = 0;
boolean mirrorFlag = false, celsiusFlag = false, markersOn = true,
screenDim = false, smoothing = false, showLastCap = false,
save1frame = false, recordingInProg = false, buttonActive = false;
float battAverage = 0.0, colorLow = 0.0, colorHigh = 100.0; // Values for managing color range
volatile boolean clickFlagMenu = false, clickFlagSelect = false; // Volatiles for timer callback handling
void setup()
{
if (!arcada.arcadaBegin()) { // Start TFT and fill with black
// Serial.print("Failed to begin");
while (1);
}
arcada.filesysBeginMSD(); // Set up SD or QSPI flash as an external USB drive
arcada.displayBegin(); // Activate TFT screen
arcada.display->setRotation(1); // wide orientation
arcada.display->setTextWrap(false);
arcada.setBacklight(255); // Turn on backlight
battAverage = arcada.readBatterySensor();
Serial.begin(115200);
// while(!Serial); // Wait for user to open terminal
Serial.println("MLX90640 IR Array Example");
if(arcada.filesysBegin()){ // Initialize flash storage, begin setting up indices for saving BMPs
if(!arcada.exists(BOTTOM_DIR)) { // Is base "MLX90640" directory absent?
if(arcada.mkdir(BOTTOM_DIR)) // Can it be added?
nextDirIndex = nextBMPindex = 1; // Success, prepare to store numbered files & dirs
} else { // "MLX90640" directory exists, can we add files | directories?
// Get the number of the next unused serial directory path
nextDirIndex = availableFileNumber(1, BOTTOM_DIR + String(DIR_FORMAT));
// and the next unused serial BMP name
nextBMPindex = availableFileNumber(1, BOTTOM_DIR + String(BMP_FORMAT));
}
} // By now each global index variable is either 0 (no nums available), or the next unclaimed serial num
if(!mlx.begin(MLX90640_I2CADDR_DEFAULT, &Wire)) {
Serial.println("MLX90640 not found!");
arcada.haltBox("MLX90640 not found!");
while(1)
delay(10); // Halt here
}
Serial.println("Found Adafruit MLX90640");
Serial.print("Serial number: ");
Serial.print(mlx.serialNumber[0], HEX);
Serial.print(mlx.serialNumber[1], HEX);
Serial.println(mlx.serialNumber[2], HEX);
//mlx.setMode(MLX90640_INTERLEAVED);
mlx.setMode(MLX90640_CHESS);
mlx.setResolution(MLX90640_ADC_18BIT);
switch(frameRate) {
case 0: mlx.setRefreshRate(MLX90640_0_5_HZ); break; // 6 frame rates, 0.5 to 16 FPS in powers of 2
case 1: mlx.setRefreshRate(MLX90640_1_HZ); break;
case 2: mlx.setRefreshRate(MLX90640_2_HZ); break;
case 3: mlx.setRefreshRate(MLX90640_4_HZ); break;
case 4: mlx.setRefreshRate(MLX90640_8_HZ); break;
default: mlx.setRefreshRate(MLX90640_16_HZ); break;
}
Wire.setClock(1000000); // max 1 MHz
for(int counter01 = 0; counter01 < 2304; ++counter01)
pixelArray[counter01] = counter01 / 9; // Initialize BMP pixel buffer with a gradient
loadPalette(paletteNum); // Load false color palette
backColor = GRAY_33; // 33% gray for BG
setBackdrop(backColor, buttonRfunc); // Current BG, current button labels
arcada.timerCallback(50, buttonCatcher); // Assign a 50Hz callback function to catch button presses
}
void loop()
{
static uint32_t frameCounter = 0;
float scaledPix, highPix, lowPix;
uint16_t markColor;
// Show the battery level indicator, 3.7V to 3.3V represented by a 7 segment bar
battAverage = battAverage * 0.95 + arcada.readBatterySensor() * 0.05; // *Gradually* track battery level
highPix = (int)constrain((battAverage - 3.3) * 15.0, 0.0, 6.0) + 1; // Scale it to a 7-segment bar
markColor = highPix > 2 ? 0x07E0 : 0xFFE0; // Is the battery level bar green or yellow?
markColor = highPix > 1 ? markColor : 0xF800; // ...or even red?
arcada.display->fillRect(146, 2, 12, 12, backColor); // Erase old battery icon
arcada.display->drawBitmap(146, 2, battIcon, 16, 12, 0xC618); // Redraw gray battery icon
arcada.display->fillRect(150, 12 - highPix, 4, highPix, markColor); // Add the level bar
// Fetch 768 fresh temperature values from the MLX90640
arcada.display->drawBitmap(146, 18, camIcon, 16, 12, 0xF400); // Show orange camera icon during I2C acquisition
if(mlx.getFrame(mlx90640To) != 0) {
Serial.println("Failed");
return;
}
arcada.display->fillRect(146, 18, 12, 12, backColor); // Acquisition done, erase camera icon
// First pass: Find hottest and coldest pixels
highAddr = lowAddr = 0;
highPix = lowPix = mlx90640To[highAddr];
for (int x = 1 ; x < 768 ; x++) { // Compare every pixel
if(mlx90640To[x] > highPix) { // Hotter pixel found?
highPix = mlx90640To[x]; // Record its values
highAddr = x;
}
if(mlx90640To[x] < lowPix) { // Colder pixel found?
lowPix = mlx90640To[x]; // Likewise
lowAddr = x;
}
}
if(thermRange == 0) { // Are the colors set to auto-range?
colorLow = lowPix; // Then high and low color values get updated
colorHigh = highPix;
}
sneakFloats[0] = lowPix; // Retain these five temperature values
sneakFloats[1] = colorLow; // to append to the BMP file, if any
sneakFloats[2] = mlx90640To[400];
sneakFloats[3] = colorHigh;
sneakFloats[4] = highPix;
// Second pass: Scale the float values down to 8-bit and plot colormapped pixels
if(mirrorFlag) { // Mirrored display (selfie mode)?
for(int y = 0; y < 24; ++y) { // Rows count from bottom up
for(int x = 0 ; x < 32 ; x++) {
scaledPix = constrain((mlx90640To[32 * y + x] - colorLow) / (colorHigh - colorLow) * 255.9, 0.0, 255.0);
pixelArray[3 * (32 * y + x)] = (uint8_t)scaledPix; // Store as a byte in BMP buffer
arcada.display->fillRect(140 - x * 4, 92 - y * 4, 4, 4, colorPal[(uint16_t)scaledPix]); // Filled rectangles, bottom up
}
}
} else { // Not mirrored
for(int y = 0; y < 24; ++y) {
for(int x = 0 ; x < 32 ; x++) {
scaledPix = constrain((mlx90640To[32 * y + x] - colorLow) / (colorHigh - colorLow) * 255.9, 0.0, 255.0);
pixelArray[3 * (32 * y + x)] = (uint8_t)scaledPix;
arcada.display->fillRect(16 + x * 4, 92 - y * 4, 4, 4, colorPal[(uint16_t)scaledPix]);
}
}
}
// Post pass: Screen print the lowest, center, and highest temperatures
arcada.display->fillRect( 0, 96, 53, 12, colorPal[0]); // Contrasting mini BGs for cold temp
arcada.display->fillRect(107, 96, 53, 12, colorPal[255]); // and for hot temperature texts
scaledPix = constrain((mlx90640To[400] - colorLow) / (colorHigh - colorLow) * 255.9, 0.0, 255.0);
arcada.display->fillRect(53, 96, 54, 12, colorPal[(uint16_t)scaledPix]); // Color coded mini BG for center temp
arcada.display->setTextSize(1);
arcada.display->setCursor(10, 99);
arcada.display->setTextColor(0xFFFF ^ colorPal[0]); // Contrasting text color for coldest value
arcada.display->print(celsiusFlag ? lowPix : lowPix * 1.8 + 32.0); // Print Celsius or Fahrenheit
arcada.display->setCursor(120, 99);
arcada.display->setTextColor(0xFFFF ^ colorPal[255]); // Contrast text for hottest value
arcada.display->print(celsiusFlag ? highPix : highPix * 1.8 + 32.0); // Print Celsius or Fahrenheit
arcada.display->setCursor(65, 99);
if((mlx90640To[400] < (colorLow + colorHigh) * 0.5) == (paletteNum < 3))
arcada.display->setTextColor(0xFFFF); // A contrasting text color for center temp
else
arcada.display->setTextColor(0x0000);
arcada.display->print(celsiusFlag ? mlx90640To[400] : mlx90640To[400] * 1.8 + 32.0); // Pixel 12 * 32 + 16
markColor = 0x0600; // Deep green color to draw onscreen cross markers
if(markersOn) { // Show markers?
if(mirrorFlag) { // ...over a mirrored display?
arcada.display->drawFastHLine(156 - (( lowAddr % 32) * 4 + 16), 93 - 4 * ( lowAddr / 32), 4, markColor); // Color crosses mark cold pixel,
arcada.display->drawFastVLine(159 - (( lowAddr % 32) * 4 + 17), 92 - 4 * ( lowAddr / 32), 4, markColor);
arcada.display->drawFastHLine(156 - ((highAddr % 32) * 4 + 16), 93 - 4 * (highAddr / 32), 4, markColor); // hot pixel,
arcada.display->drawFastVLine(159 - ((highAddr % 32) * 4 + 17), 92 - 4 * (highAddr / 32), 4, markColor);
arcada.display->drawFastHLine(76, 45, 4, markColor); // and center pixel
arcada.display->drawFastVLine(78, 44, 4, markColor);
} else { // Not mirrored
arcada.display->drawFastHLine(( lowAddr % 32) * 4 + 16, 93 - 4 * ( lowAddr / 32), 4, markColor); // Color crosses mark cold pixel,
arcada.display->drawFastVLine(( lowAddr % 32) * 4 + 17, 92 - 4 * ( lowAddr / 32), 4, markColor);
arcada.display->drawFastHLine((highAddr % 32) * 4 + 16, 93 - 4 * (highAddr / 32), 4, markColor); // hot pixel,
arcada.display->drawFastVLine((highAddr % 32) * 4 + 17, 92 - 4 * (highAddr / 32), 4, markColor);
arcada.display->drawFastHLine(80, 45, 4, markColor); // and center pixel
arcada.display->drawFastVLine(81, 44, 4, markColor);
}
}
// Print the frame count on the left sidebar
arcada.display->setRotation(0); // Vertical printing
arcada.display->setCursor(48, 4);
arcada.display->setTextColor(0xFFFF, backColor); // White text, current BG
arcada.display->print("FRM ");
arcada.display->print(++frameCounter);
arcada.display->setRotation(1); // Back to horizontal
// Handle any button presses
if(!buttonActive && clickFlagMenu) { // Was B:MENU button pressed?
buttonActive = true; // Set button flag
deBounce = millis() + DE_BOUNCE; // and start debounce timer
menuLoop(backColor); // Execute menu routine until finished
clickFlagSelect = recordingInProg = false; // Clear unneeded flags
nextBMPsequence = 1;
setBackdrop(backColor, buttonRfunc); // Repaint current BG & button labels
}
if(!buttonActive && clickFlagSelect) { // Was the A button pressed?
buttonActive = true; // Set button flag
deBounce = millis() + DE_BOUNCE; // and start debounce timer
if(buttonRfunc == 0) { // Freeze requested?
arcada.display->drawBitmap(146, 48, snowIcon, 16, 12, 0xC61F); // Freeze icon on
while(buttonBits & ARCADA_BUTTONMASK_A) // Naive freeze: loop until button released
delay(10); // Short pause
deBounce = millis() + DE_BOUNCE; // Restart debounce timer
arcada.display->fillRect(146, 48, 12, 12, backColor); // Freeze icon off
} else if(buttonRfunc == 1) { // Capture requested?
if((nextBMPindex = availableFileNumber(nextBMPindex, BOTTOM_DIR + String(BMP_FORMAT))) != 0) { // Serialized BMP filename available?
save1frame = true; // Set the flag to save a BMP
arcada.display->fillRect(0, 96, 160, 12, 0x0600); // Display a green strip
arcada.display->setTextColor(0xFFFF); // with white capture message text
arcada.display->setCursor(16, 99);
arcada.display->print("Saving frame ");
arcada.display->print(nextBMPindex);
}
} else { // Begin or halt recording a sequence of BMP files
if(!recordingInProg) { // "A:START RECORDING" was pressed
if((nextDirIndex = availableFileNumber(nextDirIndex, BOTTOM_DIR + String(DIR_FORMAT))) != 0) { // Serialized directory name available?
// Make the directory
if(newDirectory()) { // Success in making a new sequence directory?
recordingInProg = true; // Set the flag for saving BMP files
nextBMPsequence = 1; // ...numbered starting with 00001
setBackdrop(backColor, 3); // Show "A:STOP RECORDING" label
} else // Couldn't make the new directory, so
nextDirIndex = 0; // disable further sequences
}
} else { // "A:STOP RECORDING" was pressed
recordingInProg = false;
setBackdrop(backColor, 2); // Clear "A:STOP RECORDING" label
}
}
}
// Saving any BMP images to flash media happens here
if(save1frame || recordingInProg) { // Write a BMP file to SD?
arcada.display->drawBitmap(146, 32, SDicon, 16, 12, 0x07E0); // Flash storage activity icon on
prepForSave(); // Save to flash. Use global values for parameters
nextBMPsequence += recordingInProg ? 1 : 0; // If recording a series, increment frame count
save1frame = false; // If one frame saved, clear the flag afterwards
arcada.display->fillRect(146, 32, 12, 12, backColor); // Flash storage activity icon off
}
if(showLastCap) { // Redisplay the last BMP saved?
buttonActive = true; // Set button flag
deBounce = millis() + DE_BOUNCE; // and start debounce timer
recallLastBMP(backColor); // Redisplay last bitmap from buffer until finished
setBackdrop(backColor, buttonRfunc); // Repaint current BG & button labels
showLastCap = false;
}
// Here we protect against button bounces while the function loops
if(buttonActive && millis() > deBounce && (buttonBits
& (ARCADA_BUTTONMASK_B | ARCADA_BUTTONMASK_A)) == 0) // Has de-bounce wait expired & all buttons released?
buttonActive = false; // Clear flag to allow another button press
clickFlagMenu = clickFlagSelect = false; // End of the loop, clear all interrupt flags
}
// Compute and fill an array with 256 16-bit color values
void loadPalette(uint16_t palNumber) {
uint16_t x, y;
float fleX, fleK;
switch(palNumber) {
case 1: // Compute ironbow palette
for(x = 0; x < 256; ++x) {
fleX = (float)x / 255.0;
// fleK = 65535.9 * (1.02 - (fleX - 0.72) * (fleX - 0.72) * 1.96);
// fleK = (fleK > 65535.0) || (fleX > 0.75) ? 65535.0 : fleK; // Truncate red curve
fleK = 63487.0 * (1.02 - (fleX - 0.72) * (fleX - 0.72) * 1.96);
fleK = (fleK > 63487.0) || (fleX > 0.75) ? 63487.0 : fleK; // Truncate red curve
colorPal[x] = (uint16_t)fleK & 0xF800; // Top 5 bits define red
// fleK = fleX * fleX * 2047.9;
fleK = fleX * fleX * 2015.0;
colorPal[x] += (uint16_t)fleK & 0x07E0; // Middle 6 bits define green
// fleK = 31.9 * (14.0 * (fleX * fleX * fleX) - 20.0 * (fleX * fleX) + 7.0 * fleX);
fleK = 30.9 * (14.0 * (fleX * fleX * fleX) - 20.0 * (fleX * fleX) + 7.0 * fleX);
fleK = fleK < 0.0 ? 0.0 : fleK; // Truncate blue curve
colorPal[x] += (uint16_t)fleK & 0x001F; // Bottom 5 bits define blue
}
break;
case 2: // Compute quadratic "firebow" palette
for(x = 0; x < 256; ++x) {
fleX = (float)x / 255.0;
// fleK = 65535.9 * (1.00 - (fleX - 1.0) * (fleX - 1.0));
fleK = 63487.0 * (1.00 - (fleX - 1.0) * (fleX - 1.0));
colorPal[x] = (uint16_t)fleK & 0xF800; // Top 5 bits define red
// fleK = fleX < 0.25 ? 0.0 : (fleX - 0.25) * 1.3333 * 2047.9;
fleK = fleX < 0.25 ? 0.0 : (fleX - 0.25) * 1.3333 * 2015.0;
colorPal[x] += (uint16_t)fleK & 0x07E0; // Middle 6 bits define green
// fleK = fleX < 0.5 ? 0.0 : (fleX - 0.5) * (fleX - 0.5) * 127.9;
fleK = fleX < 0.5 ? 0.0 : (fleX - 0.5) * (fleX - 0.5) * 123.0;
colorPal[x] += (uint16_t)fleK & 0x001F; // Bottom 5 bits define blue
}
break;
case 3: // Compute "alarm" palette
for(x = 0; x < 256; ++x) {
fleX = (float)x / 255.0;
fleK = 65535.9 * (fleX < 0.875 ? 1.00 - (fleX * 1.1428) : 1.0);
colorPal[x] = (uint16_t)fleK & 0xF800; // Top 5 bits define red
fleK = 2047.9 * (fleX < 0.875 ? 1.00 - (fleX * 1.1428) : (fleX - 0.875) * 8.0);
colorPal[x] += (uint16_t)fleK & 0x07E0; // Middle 6 bits define green
fleK = 31.9 * (fleX < 0.875 ? 1.00 - (fleX * 1.1428) : 0.0);
colorPal[x] += (uint16_t)fleK & 0x001F; // Bottom 5 bits define blue
}
break;
case 4: // Compute negative gray palette, black hot
for(x = 0; x < 256; ++x)
colorPal[255 - x] = (((uint16_t)x << 8) & 0xF800) + (((uint16_t)x << 3) & 0x07E0) + (((uint16_t)x >> 3) & 0x001F);
break;
default: // Compute gray palette, white hot
for(x = 0; x < 256; ++x)
colorPal[x] = (((uint16_t)x << 8) & 0xF800) + (((uint16_t)x << 3) & 0x07E0) + (((uint16_t)x >> 3) & 0x001F);
break;
}
}
void setColorRange(int presetIndex) { // Set coldest/hottest values in color range
switch(presetIndex) {
case 1: // Standard range, from FLIR document: 50F to 90F
colorLow = 10.0;
colorHigh = 32.22;
break;
case 2: // Cool/warm range, for detecting mammals outdoors
colorLow = 5.0;
colorHigh = 32.0;
break;
case 3: // Warm/warmer range, for detecting mammals indoors
colorLow = 20.0;
colorHigh = 32.0;
break;
case 4: // Hot spots, is anything hotter than it ought to be?
colorLow = 20.0;
colorHigh = 50.0;
break;
case 5: // Fire & ice, extreme temperatures only!
colorLow = -10.0;
colorHigh = 200.0;
break;
default: // Default is autorange, so these values will change with every frame
colorLow = 0.0;
colorHigh = 100.0;
break;
}
}
// Draw the stationary screen elements behind the live camera window
void setBackdrop(uint16_t bgColor, uint16_t buttonFunc) {
arcada.display->fillScreen(bgColor);
for(int x = 0; x < 160; ++x) // Paint current palette across bottom
arcada.display->drawFastVLine(x, 110, 6, colorPal[map(x, 0, 159, 0, 255)]);
arcada.display->setCursor(16, 120);
arcada.display->setTextColor(0xFFFF, bgColor); // White text, current BG for button labels
switch(buttonFunc) {
case 0:
arcada.display->print("B:MENU A:FREEZE");
break;
case 1:
arcada.display->print("B:MENU ");
if(nextBMPindex == 0) // No room to store a BMP in flash media?
arcada.display->setTextColor(GRAY_33 >> 1); // Grayed button label
arcada.display->print("A:CAPTURE");
break;
case 2:
arcada.display->print("B:MENU ");
if(nextDirIndex == 0) // Has flash storage no room for a new directory?
arcada.display->setTextColor(GRAY_33 >> 1); // Grayed button label
arcada.display->print("A:START RECORD");
break;
case 3:
arcada.display->print("B:MENU ");
arcada.display->setTextColor(0xFFFF, 0xF800); // White text, red BG recording indicator
arcada.display->print("A:STOP RECORD");
break;
case 4:
arcada.display->print(" A:EXIT"); // Use for bitmap redisplay only
break;
}
}
void prepForSave() {
for(int x = 0; x < 768; ++x)
pixelArray[3 * x + 2] = pixelArray[3 * x + 1] = pixelArray[3 * x]; // Copy each blue byte into R & G for 256 grays in 24 bits
if(!writeBMP()) { // Did BMP write to flash fail?
arcada.display->fillRect(0, 96, 160, 12, 0xF800); // Red error signal
arcada.display->setTextColor(0xFFFF); // with white text
arcada.display->setCursor(20, 99);
arcada.display->print("Storage error!");
}
}
boolean newDirectory() { // Create a subdirectory, converting the name between char arrays and string objects
char fileArray[64];
String fullPath;
sprintf(fileArray, DIR_FORMAT, nextDirIndex); // Generate subdirectory name
fullPath = BOTTOM_DIR + String(fileArray); // Make a filepath out of it, then
return arcada.mkdir(fullPath.c_str()); // try to make a real subdirectory from it
}
// Here we write the actual bytes of a BMP file (plus extras) to flash media
boolean writeBMP() {
uint16_t counter1, shiftedFloats[14]; // A buffer for the appended floats and uint16_t's
uint32_t timeStamp;
float shiftAssist;
char fileArray[64];
String fullPath;
// First, figure out a name and path for our new BMP
fullPath = BOTTOM_DIR; // Build a filepath starting with the base subdirectory
if(buttonRfunc == 2) { // BMP sequence recording in progress?
sprintf(fileArray, DIR_FORMAT, nextDirIndex); // Generate subdirectory name
fullPath += String(fileArray); // Add it to the path
sprintf(fileArray, BMP_FORMAT, nextBMPsequence); // Generate a sequential filename
fullPath += String(fileArray); // Complete the filepath string
} else { // Not a sequence, solitary BMP file
sprintf(fileArray, BMP_FORMAT, nextBMPindex); // Generate a serial filename
fullPath += String(fileArray); // Complete the filepath string
}
myFile = arcada.open(fullPath.c_str(), FILE_WRITE); // Only one file can be open at a time
if(myFile) { // If the file opened okay, write to it:
myFile.write(BmpPSPHead, 14); // BMP header 1
myFile.write(DIBHeadPSP1, 40); // BMP header 2
myFile.write(pixelArray, 2304); // Array of 768 BGR byte triples
myFile.write(PSPpad, 2); // Pad with 2 zeros 'cause Photoshop does it.
// My BMP hack - append 5 fixed-point temperature values as 40 extra bytes
for(counter1 = 0; counter1 < 5; ++counter1) { // Shift 5 floats
shiftAssist = sneakFloats[counter1] + 1000.0; // Offset MLX90640 temps to positive
shiftedFloats[counter1 * 2] = (uint16_t)shiftAssist;
shiftAssist = (shiftAssist - (float)shiftedFloats[counter1 * 2]) * 49152.0; // Scale up fraction
shiftedFloats[counter1 * 2 + 1] = (uint16_t)shiftAssist;
}
shiftedFloats[10] = lowAddr; // Two more appended numbers, the 2 extreme pixel addresses
shiftedFloats[11] = highAddr;
timeStamp = millis(); // Recycle this variable to append a time stamp
lowAddr = timeStamp & 0xFFFF;
highAddr = timeStamp >> 16;
shiftedFloats[12] = lowAddr;
shiftedFloats[13] = highAddr;
myFile.write(shiftedFloats, 28); // Write appended uint16_t's
myFile.close();
return true;
} else { // The file didn't open, return error
return false;
}
}
void recallLastBMP(uint16_t bgColor) { // Display 8-bit values left in buffer from the last BMP save
int counter1, counter2;
boolean exitFlag = false;
setBackdrop(bgColor, 4); // Clear screen, just a color palette & "A:EXIT" in the BG
for(int counter1 = 0; counter1 < 24; ++counter1) { // Redraw using leftover red byte values, not yet overwritten
for(int counter2 = 0 ; counter2 < 32 ; ++counter2) {
arcada.display->fillRect(16 + counter2 * 4, 92 - counter1 * 4, 4, 4,
colorPal[(uint16_t)pixelArray[3 * (32 * counter1 + counter2) + 2]]);
}
}
while(!exitFlag) { // Loop here until exit button
if(!buttonActive && (buttonBits & ARCADA_BUTTONMASK_A)) { // "A:EXIT" button freshly pressed?
exitFlag = true;
buttonActive = true;
deBounce = millis() + DE_BOUNCE;
}
if(buttonActive && millis() > deBounce
&& (buttonBits & (ARCADA_BUTTONMASK_A | ARCADA_BUTTONMASK_B)) == 0) // Has de-bounce wait expired & all buttons released?
buttonActive = false; // Clear flag to allow another button press
}
}
uint16_t availableFileNumber(uint16_t startNumber, String formatBase) { // Find unclaimed serial number for file series
uint16_t counter1;
char nameArray[80];
for(counter1 = startNumber; counter1 % MAX_SERIAL != 0; ++counter1) { // Start counting
sprintf(nameArray, formatBase.c_str(), counter1); // Generate a serialized filename
if(!arcada.exists(nameArray)) // If it doesn't already exist
return counter1; // return the number as available
}
return 0; // Loop finished, no free number found, return fail
}
boolean menuLoop(uint16_t bgColor) { // Lay out a menu screen, interact to change values
int counter1 = 0, scrollPosition = 0;
boolean exitFlag = false, settingsChanged = false;
uint32_t menuButtons;
arcada.display->fillScreen(bgColor);
arcada.display->fillRect(0, 12 * (counter1 + scrollPosition) + MENU_VPOS - 2, 160, 12, 0x0000); // Black stripe cursor on menu
arcada.display->setTextColor(0xFFFF); // White text
arcada.display->setCursor(16, 120); // at screen bottom
arcada.display->print("B:ADVANCE A:CHANGE"); // for button labels
for(counter1 = 0; counter1 < MENU_ROWS; ++counter1) { // Display menu texts
menuLines(counter1, scrollPosition);
}
counter1 = 0;
while(!exitFlag) { // Loop until exit is activated
if(!buttonActive && (buttonBits & ARCADA_BUTTONMASK_B)) { // Fresh press of B:ADVANCE button?
buttonActive = true; // Set button flag
deBounce = millis() + DE_BOUNCE; // and start debounce timer.
arcada.display->fillRect(0, 12 * (counter1 - scrollPosition) + MENU_VPOS - 2, 160, 12, bgColor); // Erase cursor & text
menuLines(counter1, scrollPosition); // Refresh menu text line
counter1 = (counter1 + 1) % MENU_LEN; // Advance menu counter
if(counter1 == 0) { // Have we cycled around to the menu top?
scrollPosition = 0;
for(int counter2 = 0; counter2 < MENU_ROWS; ++counter2) { // Redisplay all menu texts
arcada.display->fillRect(0, 12 * counter2 + MENU_VPOS - 2, 160, 12, bgColor); // Erase old text
menuLines(counter2 + scrollPosition, scrollPosition); // Redraw each text line
}
} else if((counter1 + 1 < MENU_LEN) && (counter1 - scrollPosition == MENU_ROWS - 1)) { // Should we scroll down 1 menu line?
++scrollPosition;
for(int counter2 = 0; counter2 < MENU_ROWS; ++counter2) { // Redisplay all menu texts
arcada.display->fillRect(0, 12 * counter2 + MENU_VPOS - 2, 160, 12, bgColor); // Erase old text
menuLines(counter2 + scrollPosition, scrollPosition); // Redraw each text line
}
}
arcada.display->fillRect(0, 12 * (counter1 - scrollPosition) + MENU_VPOS - 2, 160, 12, 0x0000); // New black cursor
menuLines(counter1, scrollPosition); // Refresh text line
deBounce = millis() + DE_BOUNCE; // Restart debounce timer, just for safety
}
if(!buttonActive && (buttonBits & ARCADA_BUTTONMASK_A)) { // Fresh press of A:CHANGE button?
buttonActive = true; // Set button flag
deBounce = millis() + DE_BOUNCE; // and start debounce timer.
switch(counter1) { // Change whichever setting is currently hilighted
case 0:
showLastCap = true; // Set flag to display the last frame captured to SD
exitFlag = true; // and exit
break;
case 1:
celsiusFlag = !celsiusFlag; // Toggle Celsius/Fahrenheit
break;
case 2:
buttonRfunc = (buttonRfunc + 1) % 3; // Step through button functions
break;
case 3:
loadPalette(paletteNum = (paletteNum + 1) % 5); // Step through various color palettes
break;
case 4:
thermRange = (thermRange + 1) % 6; // Step through various temp range presets
break;
case 5:
markersOn = !markersOn; // Toggle hot/cold marker visibility
break;
case 6:
mirrorFlag = !mirrorFlag; // Toggle mirrored display
break;
case 7:
switch(frameRate = (frameRate + 1) % 6) { // 6 frame rates, 0.5 to 16 in powers of 2
case 0: mlx.setRefreshRate(MLX90640_0_5_HZ); break;
case 1: mlx.setRefreshRate(MLX90640_1_HZ); break;
case 2: mlx.setRefreshRate(MLX90640_2_HZ); break;
case 3: mlx.setRefreshRate(MLX90640_4_HZ); break;
case 4: mlx.setRefreshRate(MLX90640_8_HZ); break;
default: mlx.setRefreshRate(MLX90640_16_HZ); break;
}
break;
case 8:
emissivity = (emissivity + 90) % 100; // Step from 95% to 5% by -10%
break;
case 9:
smoothing = !smoothing; // Toggle pixel smoothing
break;
case 10:
arcada.setBacklight((screenDim = !screenDim) ? 64 : 255); // Change backlight LED
break;
default:
exitFlag = true;
break;
}
if((counter1 > 0) && (counter1 < MENU_LEN - 1)) // Was any setting just changed?
settingsChanged = true;
arcada.display->fillRect(0, 12 * (counter1 - scrollPosition) + MENU_VPOS - 2, 160, 12, 0x0000); // Erase hilit menu line
menuLines(counter1, scrollPosition); // Retype hilit menu line
}
if(buttonActive && millis() > deBounce
&& (buttonBits & (ARCADA_BUTTONMASK_A | ARCADA_BUTTONMASK_B)) == 0) // Has de-bounce wait expired & all buttons released?
buttonActive = false; // Clear flag to allow another button press
}
return(settingsChanged);
}
void menuLines(int lineNumber, int scrollPos) { // Screen print a single line in the settings menu
arcada.display->setTextColor(0xFFFF); // White text
arcada.display->setCursor(10, 12 * (lineNumber - scrollPos) + MENU_VPOS); // Menu lines 12 pixels apart
if(lineNumber - scrollPos == 0 && scrollPos > 0) { // Are any menu lines scrolled off screen top?
arcada.display->print(" ^"); // Print a small up arrow indicator
} else if(lineNumber - scrollPos == 8 && lineNumber + 1 < MENU_LEN) { // How about off the bottom?
arcada.display->print(" v"); // Print a small down arrow indicator... yeah, it's a v
} else {
switch(lineNumber) {
case 0:
arcada.display->print(" Display last capture");
break;
case 1:
arcada.display->print(" Scale - ");
arcada.display->print(celsiusFlag ? "CELSIUS" : "FAHRENHEIT");
break;
case 2:
arcada.display->print(" Rt button - ");
switch(buttonRfunc) {
case 1:
arcada.display->print("CAPTURE"); break;
case 2:
arcada.display->print("RECORD"); break;
default:
arcada.display->print("FREEZE"); break;
}
break;
case 3:
arcada.display->print(" Palette - ");
for(int xPos = 0; xPos < 72; ++xPos) // Display the current heat spectrum colors
arcada.display->drawFastVLine(xPos + 87, (lineNumber - scrollPos) * 12 + MENU_VPOS,
8, colorPal[map(xPos, 0, 71, 0, 255)]);
switch(paletteNum) {
case 1:
arcada.display->print("IRONBOW");
break;
case 2:
arcada.display->print("FIREBOW");
break;
case 3:
arcada.display->setTextColor(0x0000); // Black text for reverse contrast
arcada.display->print("ALARM");
break;
case 4:
arcada.display->setTextColor(0x0000); // Black text
arcada.display->print("BLACK HOT");
break;
default:
arcada.display->print("WHITE HOT");
break;
}
break;
case 4:
arcada.display->print("Temp range - ");
setColorRange(thermRange);
switch(thermRange) {
case 1:
arcada.display->print("STANDARD"); break;
case 2:
arcada.display->print("COOL/WARM"); break;
case 3:
arcada.display->print("WARM/WARMER"); break;
case 4:
arcada.display->print("HOT SPOTS"); break;
case 5:
arcada.display->print("FIRE & ICE"); break;
default:
arcada.display->print("AUTO-RANGE"); break;
}
break;
case 5:
arcada.display->print(" Markers - ");
arcada.display->print(markersOn ? "ON" : "OFF");
break;
case 6:
arcada.display->print(" Image - ");
arcada.display->print(mirrorFlag ? "MIRRORED" : "FORWARD");
break;
case 7:
arcada.display->print("Frame rate - ");
arcada.display->print((float)(1 << frameRate) * 0.5);
arcada.display->print(" FPS");
break;
case 8:
arcada.display->setTextColor(GRAY_33 << 1); // Grayed menu item
arcada.display->print("Emissivity - ");
arcada.display->print(emissivity);
arcada.display->print("%");
break;
case 9:
arcada.display->setTextColor(GRAY_33 << 1); // Grayed menu item
arcada.display->print(" Smoothing - ");
arcada.display->print(smoothing ? "ON" : "OFF");
break;
case 10:
arcada.display->print(" Backlight - ");
arcada.display->print(screenDim ? "DIM" : "FULL");
break;
case 11:
arcada.display->print(" Exit menu");
}
}
}
// This is the function that substitutes for GPIO external interrupts
// It will check for A and B button presses at 50Hz
void buttonCatcher(void) {
buttonBits = arcada.readButtons();
clickFlagMenu |= (buttonBits & ARCADA_BUTTONMASK_B) != 0;
clickFlagSelect |= (buttonBits & ARCADA_BUTTONMASK_A) != 0;
}

View file

@ -5,7 +5,11 @@ Display a "sinking" or "rising" graphic on the screen along with recent reading
Code by Erin St Blaine for Adafruit Industries
"""
<<<<<<< HEAD
import time
=======
>>>>>>> origin/master
import board
import neopixel
from adafruit_clue import clue
@ -50,8 +54,11 @@ reading3 = reading2
counter = 0
toggle = 1 # for on/off switch on button A
displayOn = 1 # to turn the display on and off with button B
<<<<<<< HEAD
button_b_pressed = False
button_a_pressed = False
=======
>>>>>>> origin/master
clue.display.brightness = 0.8
clue_display = displayio.Group(max_size=4)
@ -102,7 +109,11 @@ waterPalette = [0x00d9ff, 0x006f82, 0x43bfb9, 0x0066ff]
icePalette = [0x8080FF, 0x8080FF, 0x8080FF, 0x0000FF, 0xC88AFF]
sunPalette = [0xffaa00, 0xffdd00, 0x7d5b06, 0xfffca8]
firePalette = [0xff0000, 0xff5500, 0x8a3104, 0xffaa00 ]
<<<<<<< HEAD
forestPalette = [0x76DB00, 0x69f505, 0x05f551, 0x3B6D00]
=======
forestPalette = [0xccffa8, 0x69f505, 0x05f551, 0x2c8247]
>>>>>>> origin/master
# set up default initial palettes, just for startup
palette = forestPalette
@ -128,6 +139,7 @@ while True:
toggle = 1
pixels.brightness = 1.0
clue.display.brightness = 0.8
<<<<<<< HEAD
button_a_pressed = True # Set to True.
time.sleep(0.03) # Debounce.
if not clue.button_a and button_a_pressed: # On button release...
@ -136,6 +148,10 @@ while True:
if clue.button_b and not button_b_pressed: # If button B pressed...
print("Button B pressed.")
# Toggle only the display on and off
=======
if clue.button_b:
# Toggle only the display on and off
>>>>>>> origin/master
if displayOn == 0:
clue.display.brightness = 0.8
displayOn = 1
@ -240,7 +256,11 @@ while True:
reading3_label.y = 194
timer_label.y = 224
# if reading is falling, show sinking image and position text at the top
<<<<<<< HEAD
elif reading1 < reading2: #reading is falling
=======
elif reading2 < reading3: #reading is falling
>>>>>>> origin/master
sinking_sprite.x = 0
reading_label.y = 24
reading2_label.y = 54

View file

@ -0,0 +1,218 @@
/*********************************************************************
Learn Guide: BLE Temperature Monitoring Armband
Adafruit invests time and resources providing this open source code,
please support Adafruit and open-source hardware by purchasing
products from Adafruit!
MIT license, check LICENSE for more information
All text above, and the splash screen below must be included in
any redistribution
*********************************************************************/
#include <bluefruit.h>
#include <Adafruit_LittleFS.h>
#include <InternalFileSystem.h>
#include <Wire.h>
#include <Adafruit_NeoPixel.h>
#include "Adafruit_MCP9808.h"
// Read temperature in degrees Fahrenheit
#define TEMPERATURE_F
// uncomment the following line if you want to read temperature in degrees Celsius
//#define TEMPERATURE_C
// Feather NRF52840 Built-in NeoPixel
#define PIN 16
Adafruit_NeoPixel pixels(1, PIN, NEO_GRB + NEO_KHZ800);
// Maximum temperature value for armband's fever indicator
// NOTE: This is in degrees Fahrenheit
float fever_temp = 100.4;
// temperature calibration offset is +0.5 to +1.0 degree
// to make axillary temperature comparible to ear or temporal.
float temp_offset = 0.5;
// Sensor read delay, in minutes
int sensor_delay = 1;
// Measuring your armpit temperature for a minimum of 12 minutes
// is equivalent to measuring your core body temperature.
int calibration_time = 12;
// BLE transmit buffer
char temperature_buf [8];
// BLE Service
BLEDfu bledfu; // OTA DFU service
BLEDis bledis; // device information
BLEUart bleuart; // uart over ble
BLEBas blebas; // battery
// Create the MCP9808 temperature sensor object
Adafruit_MCP9808 tempsensor = Adafruit_MCP9808();
void setup() {
Serial.begin(115200);
Serial.println("Wearable BlueFruit Temperature Sensor");
Serial.println("-------------------------------------\n");
if (!tempsensor.begin(0x18)) {
Serial.println("Couldn't find MCP9808! Check your connections and verify the address is correct.");
while (1);
}
Serial.println("Found MCP9808!");
// Sets the resolution of reading
tempsensor.setResolution(3);
// Configure BLE
// Setup the BLE LED to be enabled on CONNECT
// Note: This is actually the default behaviour, but provided
// here in case you want to control this LED manually via PIN 19
Bluefruit.autoConnLed(true);
// Config the peripheral connection with maximum bandwidth
Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
Bluefruit.begin();
Bluefruit.setTxPower(4); // Check bluefruit.h for supported values
Bluefruit.setName("Bluefruit52");
Bluefruit.Periph.setConnectCallback(connect_callback);
Bluefruit.Periph.setDisconnectCallback(disconnect_callback);
// To be consistent OTA DFU should be added first if it exists
bledfu.begin();
// Configure and Start Device Information Service
bledis.setManufacturer("Adafruit Industries");
bledis.setModel("Bluefruit Feather52");
bledis.begin();
// Configure and Start BLE Uart Service
bleuart.begin();
// Start BLE Battery Service
blebas.begin();
blebas.write(100);
// Set up and start advertising
startAdv();
Serial.println("Please use Adafruit's Bluefruit LE app to connect in UART mode");
// initialize neopixel object
pixels.begin();
// set all pixel colors to 'off'
pixels.clear();
}
void loop() {
// wakes up MCP9808 - power consumption ~200 mikro Ampere
Serial.println("Wake up MCP9808");
tempsensor.wake();
// read and print the temperature
Serial.print("Temp: ");
#if defined(TEMPERATURE_F)
float temp = tempsensor.readTempF();
// add temperature offset
temp += temp_offset;
Serial.print(temp);
Serial.println("*F.");
#elif defined(TEMPERATURE_C)
float temp = tempsensor.readTempC();
// add temperature offset
temp += temp_offset;
Serial.print(temp);
Serial.println("*C.");
#else
#warning "Must define TEMPERATURE_C or TEMPERATURE_F!"
#endif
// set NeoPixels to RED if fever_temp
if (temp >= fever_temp) {
pixels.setPixelColor(1, pixels.Color(255, 0, 0));
pixels.show();
}
// float to buffer
snprintf(temperature_buf, sizeof(temperature_buf) - 1, "%0.*f", 1, temp);
if (calibration_time == 0) {
Serial.println("Writing to UART");
// write to UART
bleuart.write(temperature_buf);
}
else {
Serial.print("Calibration time:");
Serial.println(calibration_time);
calibration_time-=1;
}
// shutdown MSP9808 - power consumption ~0.1 mikro Ampere
Serial.println("Shutting down MCP9808");
tempsensor.shutdown_wake(1);
// sleep for sensor_delay minutes
// NOTE: NRF delay() puts mcu into a low-power sleep mode
delay(1000*60*sensor_delay);
}
void startAdv(void)
{
// Advertising packet
Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
Bluefruit.Advertising.addTxPower();
// Include bleuart 128-bit uuid
Bluefruit.Advertising.addService(bleuart);
// Secondary Scan Response packet (optional)
// Since there is no room for 'Name' in Advertising packet
Bluefruit.ScanResponse.addName();
/* Start Advertising
* - Enable auto advertising if disconnected
* - Interval: fast mode = 20 ms, slow mode = 152.5 ms
* - Timeout for fast mode is 30 seconds
* - Start(timeout) with timeout = 0 will advertise forever (until connected)
*
* For recommended advertising interval
* https://developer.apple.com/library/content/qa/qa1931/_index.html
*/
Bluefruit.Advertising.restartOnDisconnect(true);
Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms
Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode
Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds
}
// callback invoked when central connects
void connect_callback(uint16_t conn_handle)
{
// Get the reference to current connection
BLEConnection* connection = Bluefruit.Connection(conn_handle);
char central_name[32] = { 0 };
connection->getPeerName(central_name, sizeof(central_name));
Serial.print("Connected to ");
Serial.println(central_name);
}
/**
* Callback invoked when a connection is dropped
* @param conn_handle connection where this event happens
* @param reason is a BLE_HCI_STATUS_CODE which can be found in ble_hci.h
*/
void disconnect_callback(uint16_t conn_handle, uint8_t reason)
{
(void) conn_handle;
(void) reason;
Serial.println();
Serial.print("Disconnected, reason = 0x"); Serial.println(reason, HEX);
}

View file

@ -1,11 +1,14 @@
#!/bin/bash
PYLINT="`type -p pylint3 2>/dev/null || type -p pylint`"
function find_pyfiles() {
for f in $(find . -type f -iname '*.py'); do
if [ $# -eq 0 ]; then set -- .; fi
for f in $(find "$@" -type f -iname '*.py'); do
if [ ! -e "$(dirname $f)/.circuitpython.skip" ]; then
echo "$f"
fi
done
}
find_pyfiles | xargs pylint
find_pyfiles "$@" | xargs "$PYLINT"

View file

@ -11,7 +11,7 @@ import neopixel
from adafruit_bitmap_font import bitmap_font
from adafruit_display_text.label import Label
from adafruit_io.adafruit_io import IO_MQTT
from adafruit_minimqtt import MQTT
import adafruit_minimqtt as MQTT
from adafruit_pyportal import PyPortal
from adafruit_seesaw.seesaw import Seesaw
from simpleio import map_range
@ -181,14 +181,13 @@ while not esp.is_connected:
continue
print("Connected to WiFi!")
# Initialize a new MiniMQTT Client object
mqtt_client = MQTT(
socket=socket,
broker="io.adafruit.com",
username=secrets["aio_username"],
password=secrets["aio_key"],
network_manager=wifi
)
# Initialize MQTT interface with the esp interface
MQTT.set_socket(socket, esp)
# Initialize a new MQTT Client object
mqtt_client = MQTT.MQTT(broker="https://io.adafruit.com",
username=secrets["aio_user"],
password=secrets["aio_key"])
# Adafruit IO Callback Methods
# pylint: disable=unused-argument

View file

@ -8,7 +8,7 @@ import board
import audiobusio
import displayio
import ulab
import ulab.fft
import ulab.extras
import ulab.vector
display = board.DISPLAY
@ -76,7 +76,7 @@ def main():
while True:
mic.record(samples_bit, len(samples_bit))
samples = ulab.array(samples_bit[3:])
spectrogram1 = ulab.fft.spectrum(samples)
spectrogram1 = ulab.extras.spectrogram(samples)
# spectrum() is always nonnegative, but add a tiny value
# to change any zeros to nonzero numbers
spectrogram1 = ulab.vector.log(spectrogram1 + 1e-7)