Merge remote-tracking branch 'origin/master'
# Conflicts: # Vertical_Garden_Barometer/code.py
This commit is contained in:
commit
eb84830c4b
68 changed files with 173745 additions and 122 deletions
50
Activity_Generator/code.py
Executable file
50
Activity_Generator/code.py
Executable 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
57
Activity_Generator/things.py
Executable 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",
|
||||
]
|
||||
|
|
@ -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
209
Baudot_TTY/baudot_tty.py
Normal 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)
|
||||
283
Baudot_TTY/baudot_tty_GUI.py
Normal file
283
Baudot_TTY/baudot_tty_GUI.py
Normal 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
|
||||
230
Baudot_TTY/baudot_tty_ble.py
Normal file
230
Baudot_TTY/baudot_tty_ble.py
Normal 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")
|
||||
46
Bonsai_Buckaroo/Bonsai_Buckaroo_CircuitPython_Code.py
Executable file
46
Bonsai_Buckaroo/Bonsai_Buckaroo_CircuitPython_Code.py
Executable 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)
|
||||
99
Buckaroo_Plant_Care_Bot/buckaroo_plant_care_bot.py
Normal file
99
Buckaroo_Plant_Care_Bot/buckaroo_plant_care_bot.py
Normal 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
|
||||
BIN
Buckaroo_Plant_Care_Bot/dry.bmp
Normal file
BIN
Buckaroo_Plant_Care_Bot/dry.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 113 KiB |
BIN
Buckaroo_Plant_Care_Bot/happy.bmp
Normal file
BIN
Buckaroo_Plant_Care_Bot/happy.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 113 KiB |
150
CLUE_BBQ/clue_bbq.py
Normal file
150
CLUE_BBQ/clue_bbq.py
Normal 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
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
9236
CLUE_BBQ/font/GothamBlack-50.bdf
Executable file
File diff suppressed because it is too large
Load diff
88
CLUE_Hand_Wash_Timer/clue_hand_wash_timer.py
Normal file
88
CLUE_Hand_Wash_Timer/clue_hand_wash_timer.py
Normal 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)
|
||||
12172
CLUE_Hand_Wash_Timer/font/RacingSansOne-Regular-29.bdf
Executable file
12172
CLUE_Hand_Wash_Timer/font/RacingSansOne-Regular-29.bdf
Executable file
File diff suppressed because it is too large
Load diff
14753
CLUE_Hand_Wash_Timer/font/RacingSansOne-Regular-38.bdf
Executable file
14753
CLUE_Hand_Wash_Timer/font/RacingSansOne-Regular-38.bdf
Executable file
File diff suppressed because it is too large
Load diff
BIN
CLUE_Hand_Wash_Timer/wash_off.bmp
Executable file
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
BIN
CLUE_Hand_Wash_Timer/wash_on.bmp
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 113 KiB |
1619
CLUE_I_Ching/christopher_done_24.bdf
Executable file
1619
CLUE_I_Ching/christopher_done_24.bdf
Executable file
File diff suppressed because it is too large
Load diff
130
CLUE_I_Ching/clue_iching.py
Executable file
130
CLUE_I_Ching/clue_iching.py
Executable 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
|
||||
BIN
CLUE_Sensor_Plotter/adafruit-spinning-logo-plot-2chan.wav
Normal file
BIN
CLUE_Sensor_Plotter/adafruit-spinning-logo-plot-2chan.wav
Normal file
Binary file not shown.
263
CLUE_Sensor_Plotter/clue-plotter.py
Normal file
263
CLUE_Sensor_Plotter/clue-plotter.py
Normal 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()
|
||||
378
CLUE_Sensor_Plotter/plot_source.py
Normal file
378
CLUE_Sensor_Plotter/plot_source.py
Normal 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
|
||||
871
CLUE_Sensor_Plotter/plotter.py
Normal file
871
CLUE_Sensor_Plotter/plotter.py
Normal 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
|
||||
111
CLUE_Sensor_Plotter/test_PlotSource.py
Normal file
111
CLUE_Sensor_Plotter/test_PlotSource.py
Normal 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)
|
||||
555
CLUE_Sensor_Plotter/test_Plotter.py
Normal file
555
CLUE_Sensor_Plotter/test_Plotter.py
Normal 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)
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
BIN
CircuitPython_RGBMatrix/emoji.bmp
Normal file
BIN
CircuitPython_RGBMatrix/emoji.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
136
CircuitPython_RGBMatrix/fruit.py
Normal file
136
CircuitPython_RGBMatrix/fruit.py
Normal 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()
|
||||
120
CircuitPython_RGBMatrix/life.py
Normal file
120
CircuitPython_RGBMatrix/life.py
Normal 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
|
||||
109
CircuitPython_RGBMatrix/scroller.py
Normal file
109
CircuitPython_RGBMatrix/scroller.py
Normal 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)
|
||||
84
CircuitPython_RGBMatrix/simple_scroller.py
Normal file
84
CircuitPython_RGBMatrix/simple_scroller.py
Normal 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)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
BIN
CircuitPython_WeatherCloud/sound/Clear.wav
Normal file
BIN
CircuitPython_WeatherCloud/sound/Clear.wav
Normal file
Binary file not shown.
BIN
CircuitPython_WeatherCloud/sound/Clouds.wav
Normal file
BIN
CircuitPython_WeatherCloud/sound/Clouds.wav
Normal file
Binary file not shown.
BIN
CircuitPython_WeatherCloud/sound/Rain.wav
Normal file
BIN
CircuitPython_WeatherCloud/sound/Rain.wav
Normal file
Binary file not shown.
BIN
CircuitPython_WeatherCloud/sound/Snow.wav
Normal file
BIN
CircuitPython_WeatherCloud/sound/Snow.wav
Normal file
Binary file not shown.
BIN
CircuitPython_WeatherCloud/sound/Thunderstorm0.wav
Normal file
BIN
CircuitPython_WeatherCloud/sound/Thunderstorm0.wav
Normal file
Binary file not shown.
BIN
CircuitPython_WeatherCloud/sound/Thunderstorm1.wav
Normal file
BIN
CircuitPython_WeatherCloud/sound/Thunderstorm1.wav
Normal file
Binary file not shown.
BIN
CircuitPython_WeatherCloud/sound/Thunderstorm2.wav
Normal file
BIN
CircuitPython_WeatherCloud/sound/Thunderstorm2.wav
Normal file
Binary file not shown.
|
|
@ -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!")
|
||||
|
|
|
|||
BIN
Clue_Step_Counter/clue_bgBMP.bmp
Normal file
BIN
Clue_Step_Counter/clue_bgBMP.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 166 KiB |
172
Clue_Step_Counter/clue_step_counter.py
Normal file
172
Clue_Step_Counter/clue_step_counter.py
Normal 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)
|
||||
54420
Clue_Step_Counter/fonts/Roboto-Black-48.bdf
Normal file
54420
Clue_Step_Counter/fonts/Roboto-Black-48.bdf
Normal file
File diff suppressed because it is too large
Load diff
31545
Clue_Step_Counter/fonts/Roboto-Bold-24.bdf
Normal file
31545
Clue_Step_Counter/fonts/Roboto-Bold-24.bdf
Normal file
File diff suppressed because it is too large
Load diff
24362
Clue_Step_Counter/fonts/Roboto-Medium-16.bdf
Normal file
24362
Clue_Step_Counter/fonts/Roboto-Medium-16.bdf
Normal file
File diff suppressed because it is too large
Load diff
126
PowerGlove_BLE_MIDI/powerglove_ble_midi.py
Normal file
126
PowerGlove_BLE_MIDI/powerglove_ble_midi.py
Normal 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)
|
||||
143
Pulse_Oximeter_Logger/pulse_oximeter_logger.py
Normal file
143
Pulse_Oximeter_Logger/pulse_oximeter_logger.py
Normal 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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
113
PyPortal_Quarantine_Clock/code.py
Executable 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)
|
||||
5355
PyPortal_Quarantine_Clock/fonts/Helvetica-Bold-24.bdf
Executable file
5355
PyPortal_Quarantine_Clock/fonts/Helvetica-Bold-24.bdf
Executable file
File diff suppressed because it is too large
Load diff
7827
PyPortal_Quarantine_Clock/fonts/Helvetica-Bold-44.bdf
Executable file
7827
PyPortal_Quarantine_Clock/fonts/Helvetica-Bold-44.bdf
Executable file
File diff suppressed because it is too large
Load diff
138
PyPortal_Quarantine_Clock/month_clock.py
Normal file
138
PyPortal_Quarantine_Clock/month_clock.py
Normal 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)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
220
Pybadge_Thermal_Image_Recording/Processing/ConvertBMPto8bit.pde
Normal file
220
Pybadge_Thermal_Image_Recording/Processing/ConvertBMPto8bit.pde
Normal 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;
|
||||
}
|
||||
448
Pybadge_Thermal_Image_Recording/Processing/ConvertBMPtoSeq01.pde
Normal file
448
Pybadge_Thermal_Image_Recording/Processing/ConvertBMPtoSeq01.pde
Normal 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;
|
||||
}
|
||||
9
Pybadge_Thermal_Image_Recording/README.md
Normal file
9
Pybadge_Thermal_Image_Recording/README.md
Normal 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
|
||||
834
Pybadge_Thermal_Image_Recording/thermalimager.ino
Normal file
834
Pybadge_Thermal_Image_Recording/thermalimager.ino
Normal 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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
218
Wearable_BLE_Temperature_Monitor/ble_temp_monitor.ino
Normal file
218
Wearable_BLE_Temperature_Monitor/ble_temp_monitor.ino
Normal 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);
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue