Well, COVID19 is taking a hit on things at SUNY Brockport. The administration told students:

• Classes are in session next week
• If you get a negative COVID test, go home early and the faculty will deal with it

I’m dealing with it by taking some personal time and playing with blinky lights.

One of my long-term goals is to develop Internet-of-Things (IoT) based sensors that can be used for remote chemical education. Furthermore, I want my research students to be able to do this. Few of our chemistry majors have programming experience, so it is hard to decide whether to introduce them to microcontroller programming via C++ (aka standard Arduino programming) or Python via CircuitPython. After spending some time with the toys in Adabox016, I’m thinking Python might be a good approach.

So here’s the latest. The above may just look like a simple radar screen with a red ping where your enemy is, but the question is: where does the enemy’s location come from? In this example, I have a Raspberry Pi running a python script that randomly walks the enemy around a grid the same size as the RGB matrix. It then sends that positioning information to Adafruit.IO, which is polled by the RGB Matrix. The pause in image above is real; it’s the RGB matrix asking Adafruit.IO what the current location of the enemy is.

What’s this have to do with chemical education and remote sensors? Everything. The method I’m using to allow the Raspberry Pi to communicate with the RGB matrix is the same method that can be used for sensors. The point of using these two devices is to demonstrate that the method is applicable to a wide array of tools (Linux computers, microcontrollers, etc).

The code that I’m using is below, in case you are interested. More likely, it’s here for when one of my systems crashes and I need to remember what I wrote.

The Matrix Portal code

import board # Required
import time
import displayio
import random
# import terminalio
from math import *
from secrets import *

matrixportal = MatrixPortal(status_neopixel=board.NEOPIXEL, bit_depth=6, debug=True)
display = matrixportal.display

# Create some constants
CX = floor(display.width / 2)
CY = floor(display.height / 2)
R = ceil(sqrt(CX*CX + CY*CY))

def radar_line(theta = 0, color = 0x00FF00):
"""
Creates a line starting from the center of the matrix and directed
outwards at the desired angle.
"""
return Line(CX, CY, CX + floor(R*cos(radians(theta))),

def light_color(index = 31, rgb = 0):
"""
Create one of the primary colors with varying intensity.
"""
colors =  (8, 2048, 524288)
return colors[(rgb % 3)]*(index % 32)

def trail(th):
"""
Creates multiple radar lines with decreasing intensity to give a
ghosting effect to the radar line.  Be sure to account for these lines in
the total number of items in the displayio group
"""
delta = 2
lines = []
intensity = (31,8,4,1)
for i in range(0,4):
return [l for l in reversed(lines) ]

def point(x, y, color):
"""
Simple routine to create a point
"""
#return Line(x,y,x,y,color)
return Circle(x,y,1,fill=color)

def find_angle(location):
"""
Clunky method to get the angle of vector from Matrix location {CX, CY} to
<location>.  Have to check for points on the axes and play some tricks
with signs of the angles
"""
loc_x = location[0]-32
loc_y = 16 - location[1]
if (loc_x==0):
theta = 90
if (loc_y < 0):
theta *= -1
else:
theta = degrees(atan(loc_y/loc_x))

# Fix points on the horizontal axis
if (loc_y == 0):
if (loc_x > 0):
return 0
else:
return 180

# Transform coordinate system
if (theta < 0):
if (loc_y > 0):
theta = 180 - theta
else:
theta = -theta
else:
if (loc_y > 0):
theta = 360 - theta
else:
theta = 180 - theta

return theta

def close_to(val, ref, tol):
"""
Alternative to equal, returns True if value is close enough (within <tol>)
"""
return (ref - tol < val < ref + tol)

def constrain(val, min_val, max_val):

if val < min_val: return min_val
if val > max_val: return max_val
return val

def update_location():
"""
Grabs location of walker from the position feed at Adafruit.io
"""

"X-AIO-Key": secrets["aio_key"]
}
t1 = time.monotonic_ns()/1000000
response = requests.get(url)
t2 = time.monotonic_ns()/1000000
loc_string = response.text.split(",")[0]
loc_string = loc_string.split("#")

print(t2-t1)
return (int(loc_string[0]), int(loc_string[1]))

# Set up networking by checking the time
matrixportal.get_local_time()

th = 0 # Set the initial angle
blip = False # Disable the blip until overlap with the radar

location = update_location() # location of blip

tolerance = 3 # change in theta per cycle
print(location)

while True:
g = displayio.Group(max_size=6)
for l in trail(th):
g.append(l)
# Check if blip falls on line of radar trail
if (close_to(find_angle(location),th,tolerance)) and (blip == False):
blip = True
counter = 8
# if blip activated, show the blip and get it to fade
if (blip):
g.append(point(location[0],location[1],light_color(counter,2)))
counter -= 1
if (counter == 0):
blip = False
location = update_location()
display.show(g)
# Move radar to new location
th  = (th + tolerance) % 360
time.sleep(.05)


The Raspberry Pi code

import time
import requests
from secrets import *
import random

# Initialize location
location = [random.randint(0,63), random.randint(0,31)]

def constrain(value, min_val, max_val):
if value < min_val: return min_val
if value > max_val: return max_val
return value

def walk(location):
walk_probability = 1 # Right now, always walk
temp_location = location
if (random.random() < walk_probability):
# axis? 0 for x, 1 for y
axis  = random.randint(0,1)
# direction? 0 for negative and 1 for positive
direction = random.randint(0,1)
if (direction == 0):
temp_location[axis] -= 1
else:
temp_location[axis] += 1
# make sure we don't walk off the map
return [constrain(temp_location[0],0,63), constrain(temp_location[1],0,31)]

while True:
location = walk(location)
print(location)
# Send location to the cloud