An IoT story with a RaspberryPI, Redis, LEDs, and Mario
Introductions
James Alexander
Systems Engineer
Leaf Software Solutions
https://www.leafsoftwaresolutions.com/
- Large Scale ERP
- Rapid MVP Development
- Cloud DevOps
- Microsoft CRM
- Microsoft Dynamics
Motivation
Hardware
Other scales
That probably will work too
uint16_t scales[NSCALES][2] = {\
// Stamps.com Model 510 5LB Scale
{0x1446, 0x6a73},
// USPS (Elane) PS311 "XM Elane Elane UParcel 30lb"
{0x7b7c, 0x0100},
// Stamps.com Stainless Steel 5 lb. Digital Scale
{0x2474, 0x0550},
// Stamps.com Stainless Steel 35 lb. Digital Scale
{0x2474, 0x3550},
// Mettler Toledo
{0x0eb8, 0xf000},
// SANFORD Dymo 10 lb USB Postal Scale
{0x6096, 0x0158},
// Fairbanks Scales SCB-R9000
{0x0b67, 0x555e},
// Dymo-CoStar Corp. M25 Digital Postal Scale
{0x0922, 0x8004},
// DYMO 1772057 Digital Postal Scale
{0x0922, 0x8003}
};
https://github.com/erjiang/usbscale/blob/master/scales.h
Wiring
Wiring
Wiring
Rotating Box
Reset Wifi
TESTIP=10.1.1.1
ping -c4 ${TESTIP} > /dev/null
if [ $? != 0 ]
then
logger -t $0 "WiFi seems down, restarting"
sudo /sbin/ifdown --force wlan0
sleep 10
sudo /sbin/ifup wlan0
fi
Run cron every 5 minutes and call me in the morning
Software Diagram
Software - Read Scale
# Read 4 unsigned integers from USB device
fmt = "IIII"
bytes_to_read = struct.calcsize(fmt)
r = f.read(bytes_to_read)
usb_binary_read = struct.unpack(fmt, r)
Read Data From Scale
def getWeightInGrams(self, dev="/dev/usb/hiddev0"):
"""
This device normally appears on /dev/usb/hiddev0, assume
device still appears on this file handle.
"""
# If we cannot find the USB device, return -1
grams = -1
try:
with open(dev, 'r+b') as f:
# Read 4 unsigned integers from USB device
fmt = "IIII"
bytes_to_read = struct.calcsize(fmt)
r = f.read(bytes_to_read)
usb_binary_read = struct.unpack(fmt, r)
if len(usb_binary_read) == 4:
grams = usb_binary_read[3]
except OSError as e:
print("{0} - Failed to read from USB device".format(datetime.utcnow()))
return grams
Main Loop
def main(self):
self._currentWeight = self.getWeightInGrams()
signal.signal(signal.SIGALRM, self.handle_alarm)
while True:
try:
self._loopCount += 1
signal.alarm(5)
tmpWeight = self.getWeightInGrams()
# Log If Weight Changes
if self.shouldLogWeight(tmpWeight):
self._currentWeight = tmpWeight
self.postToLedRedis()
self.writeToDynamo()
# Post every 60 seconds
if self.shouldPostToLed():
self._loopCount = 0
self.postToLedRedis()
if self.potIsLifted():
self._mostRecentLiftedTime = datetime.now()
except Exception as e:
self._logger.error(e)
finally:
signal.alarm(0)
sleep(1)
def handle_alarm(self, signum,
frame):
raise Exception("signum: {0}
- frame: {1}"
.format(signum, frame))
postToLedRedis
def postToLedRedis(self):
displayJson = {}
animation, args = self.getLedMessage()
displayJson['moduleName'] = animation
displayJson['args'] = args
self._redis.publish(self.redisMessageQueue, json.dumps(displayJson))
def getLedMessage(self):
# return random animation if empty pot
# ...
args = "-t {0} mug{2}::{1}".format(self.getAvailableMugs(),
self._mostRecentLiftedTime.strftime("%H:%M"),
"" if available_mugs == 1 else "s")
return 'fixed-text.py', args
self._animations = ['mario.py', 'kit.py', 'scanning-pixel.py',
'rotating-block-generator.py', 'gol-acorn.py',
'gol-block-switch.py', 'gol-gosper-gun.py',
'gol-pent.py', 'gol-red-glider.py']
Coffee Graph
Software - pubsub.py
redis = redis.Redis()
self.pubsub = redis.pubsub(ignore_subscribe_messages = True)
self.pubsub.subscribe(['LED_QUEUE'])
def run(self):
for item in self.pubsub.listen():
if self._pid != None:
self.halt_process()
if item['data'].upper() == 'KILL':
self.pubsub.unsubscribe()
break
elif item['data'].upper() == 'STOP':
pass
else:
self.start_process(item)
def halt_process(self):
log.debug('send terminate')
Popen(['kill', str(self._pid)])
Software - pubsub.py
def start_process(self, item):
data = json.loads(item['data'])
args = None
path = os.path.abspath('animation/{0}'.format(data['moduleName']))
process = ['python', path, '--led-no-hardware-pulse', '1', '-r', '16',
'--led-pwm-lsb-nanoseconds', '300']
if 'args' in data.keys() and data['args']:
process.append(data['args'])
log.debug('Running {0}'.format(process))
p = Popen(process)
self._pid = p.pid
Software - samplebase.py
import argparse
import signal
from rgbmatrix import RGBMatrix, RGBMatrixOptions
class SampleBase(object):
def __init__(self, *args, **kwargs):
self.parser = argparse.ArgumentParser()
# ...
def exit_process(self, signum, frame):
sys.exit(0)
def process(self):
self.args = self.parser.parse_args()
options = RGBMatrixOptions()
# ...
self.matrix = RGBMatrix(options = options)
try:
signal.signal(signal.SIGTERM, self.exit_process)
self.run()
except KeyboardInterrupt:
print("Exiting\n")
sys.exit(0)
return True
Software - fixed-text.py
from samplebase import SampleBase
from rgbmatrix import graphics
class FixedText(SampleBase):
def run(self):
canvas = self.matrix
font = graphics.Font()
font.LoadFont("animation/fonts/5x7.bdf")
line1_color = [...] # Random RGB
line2_color = [...] # Random RGB
l1_color = graphics.Color(*tuple(line1_color))
l2_color = graphics.Color(*tuple(line2_color))
line1, line2 = self.args.text.strip().split('::')
graphics.DrawText(canvas, font, 0, 7, l1_color, line1)
graphics.DrawText(canvas, font, 0, 14, l2_color, line2)
while True:
time.sleep(2)
Displaying Text 9x7
Displaying Text 6x9
Displaying Text 5x7
Displaying Text 4x6
V1: Spark + (Tiny) OLED Fed by a Script Watching a Chat Room
Scrolling Marquee
Random Animations
# scanning-pixel.py
def run(self):
offset_canvas = self.matrix.CreateFrameCanvas()
x, y = 0, 0
min_x, max_x = 0, 32
min_y, max_y = 0, 16
direction = 1
while True:
self.usleep(50000)
for i in range(0, max_x):
for j in range(0, max_y):
if i == x and j == y:
offset_canvas.SetPixel(i, j, 150, 50, 0)
else:
offset_canvas.SetPixel(i, j, 0, 0, 0)
x = x + 1 * direction
if x > max_x or x < min_x:
direction = direction * -1
y = y + 1
if y > max_y:
y = 0
offset_canvas = self.matrix.SwapOnVSync(offset_canvas)
Scanning Pixel
kit.py
mario
mario.py
Conway's Game of Life
Let's See It Already!
Future Plans for Display
-
Display can be daisy-chained. Let’s do that.
-
Variable time for message display
-
Fixed-display statistics plus scrolling messages
-
HTTP 418 - "I am a teapot"
PyOhio Coffeebot 3000
By James Alexander
PyOhio Coffeebot 3000
Presentation of the Coffeebot 3000 for PyOhio 2017
- 2,762