An IoT story with a RaspberryPI, Redis, LEDs, and Mario

Introductions

James Alexander

Systems Engineer 

https://jamesralexander.com

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

  • HTCPCP

  • HTTP 418 - "I am a teapot"

PyOhio Coffeebot 3000

By James Alexander

PyOhio Coffeebot 3000

Presentation of the Coffeebot 3000 for PyOhio 2017

  • 2,572