IoT Coffeebot 4000

Introductions

James Alexander

Andrew Kaczorek

.NET Developer

Python Fanboy

Part Time Coffee Drinker

Linux Administrator

Cloud Architect

DIY Enthusiast

Open Source Developer

Cloud Architect

Automate All The Things Specialist

Chris Chalfant

Code & Slides

Motivation

http://keirawong.com/blog/wp-content/uploads/2015/05/62110937.jpg?6bec58

The One Scale

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

Do I really need to Know C?

http://www.raywenderlich.com/wp-content/uploads/2014/01/pic2.png

Python to the Rescue

dev = "/dev/usb/hiddev0"
fd = os.open(dev, os.O_RDONLY)
Integer Value Name File stream
0 Standard Input stdin
1 Standard Output stdout
2 Standard Error stderr
with open("someFile.txt", "r") as f:
    line = f.readline()

!=

fd is a File Descriptor

f is a File Object

File Descriptor Integers

Python to the Rescue

from os import * # DON'T DO THIS

# Above import causes a name collision on "open"

with open("file.txt", "r") as f: 
    content = f.read()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: an integer is required

C - Inspired Python

# Read 4 unsigned integers from USB device
hiddev_event_fmt = "IIII"
bytes_to_read = struct.calcsize(hiddev_event_fmt)
usb_binary_read = struct.unpack(hiddev_event_fmt, os.read(fd, bytes_to_read))

All Together

def getWeightInGrams(self, dev="/dev/usb/hiddev0"):
    grams = -1
    try:
        fd = os.open(dev, os.O_RDONLY)

        # Read 4 unsigned integers from USB device
        hiddev_event_fmt = "IIII"
        bytes_to_read = struct.calcsize(hiddev_event_fmt)
        usb_binary_read = struct.unpack(hiddev_event_fmt, os.read(fd, bytes_to_read))
        grams = usb_binary_read[3]
        os.close(fd)
    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()

   while True:
       try:
           self._loopCount += 1
           tmpWeight = self.getWeightInGrams()

           if self.shouldLogWeight(tmpWeight):
               self._currentWeight = tmpWeight
               self.logToInitialState()
               self.writeToDynamo()

           if self.shouldPostToLed():
               self._loopCount = 0 
               self.postToLed()

           if self.potIsLifted():
               self._mostRecentLiftedTime = datetime.now()

       except Exception as e:
           self._logger.error(e)

       sleep(1)

Logging To Initial State

def logToInitialState(self):
   utcnow = datetime.utcnow()
   bucketKey = "{0} - coffee_scale_data".format(self.environment)

   streamer = Streamer(bucket_name="{0} - Coffee Scale Data".format(self.environment), 
           bucket_key=bucketKey, access_key=self.initialStateKey)

   if self.potIsLifted():
       streamer.log("Coffee Pot Lifted", True)
   streamer.log("Coffee Weight", self._currentWeight)
   streamer.close()

Initial State Dashboard

    Simple RESTful APIs Made Easy

  • Combines AWS API Gateway with AWS Lambda (python, java, or nodejs)

  • Approaching 1.0

  • Hides complexity and implements deployment workflow

  • Makes simple APIs very easy and inexpensive

Caveats

  • Rich error handling is still difficult

  • Framework is still a bit unstable as they head toward 1.0

  • Code with compiled libraries can be tricky to build/deploy

  • Enable API Gateway authentication!

Code

https://github.com/chalfant/serverless-coffee-scale

 

Python code using boto3 library to read/write records to DynamoDb

Curl To Get Entries

Coffee Graph

Display Hardware DIY

Andrew Kaczorek

It Begins With Hipchat

  • The move to a room based chat system added a new social outlet
  • Several Iterations of Chatbot
  • How could we bridge our chat system into the real world?

    V1: Spark + (Tiny) OLED Fed by a Script Watching a Chat Room

Tiny Display Becomes Tiresome

  • I2C implementation in Spark was Unreliable
  • Integration with Spark Cloud required non-standard outgoing port

Time for Something Bigger

    Also, a Raspberry Pi Model B

  • Allowed for running code other than Arduino-Compatible

  • Multiple developers could easily contribute  

Wiring was Pretty Easy

Purchased ribbon cables for each end and patched the pinouts with breadboard wires

    Linux Wifi on the Pi Was Flaky

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 via cron every 5 minutes and call me in the morning

def render(mytext)
  draw=Magick::Draw.new {
    self.font_family = 'Comic Sans MS'
    self.fill="#6495ED"
    self.pointsize = 16
    self.font_weight = 600
    self.gravity = Magick::SouthWestGravity
    }
  metrics=draw.get_type_metrics(mytext)
  image=Magick::Image.new(metrics['width']+30,16) { 
    self.background_color = "black" 
    self.format = "PPM"
    self.depth = 8
    }
  draw.annotate(image,0,0,0,0,mytext) {
    self.font_family = 'Comic Sans MS'
    self.fill="#6495ED"
    self.pointsize = 16
    self.font_weight = 600
    self.gravity = Magick::SouthWestGravity
    }

How We Drive the Display

Ruby ->

C++ ->

    Adding the Ability for Multiple Data Feeds to the LED Panel

class Display
  extend Resque::Plugins::Logger

  @queue = :leddisplay

  def self.perform(localpath)
    logger.info "displaying #{localpath}"
    system("/root/ledsvc/led-matrix","-r16","-D1","-t30","#{localpath}")
    File.delete(localpath)
  rescue Resque::TermException
    logger.error "display #{localpath} failed"
    Resque.enqueue(self, localpath)
  end

end

Ruby Resque to the Rescue: https://github.com/resque/resque

Let's Monitor Just in Case

Process 'ledsvc'
  status                            Running
  monitoring status                 Monitored
  pid                               2247
  parent pid                        1
  uptime                            23d 0h 17m 
  children                          0
  memory kilobytes                  32776
  memory kilobytes total            32776
  memory percent                    7.3%
  memory percent total              7.3%
  cpu percent                       1.6%
  cpu percent total                 1.6%
  data collected                    Fri, 24 Jun 2016 13:23:31

    The Scale Project and LCD Matrix Project Come Together

  • Initially, these ran on separate Pis
  • Have been coexisting for the past ~14mo

    Future Plans for Display

  • Display can be daisy-chained. Let’s do that.

  • Variable time for message display

  • Fixed-display statistics plus scrolling messages