Notification when the dryer finishes



  • Remote Dryer Alarm

    I live in a multifamily house with a washer and dryer in the basement. Which means that if you are on the third floor and want to see if your clothes are dry yet, you might have a very long hike!

    So I used an Onion Omega and an accelerometer (a cheap but functional MPU-6050) to make something to help out with this:

    0_1477343274105_IMG_20161014_143439.jpg

    (The accelerometer is slightly out of view, but it's attached with a magnet to the back of the dryer just below the Onion display.)

    It detects when the dryer is started or stopped and sends a message to a remote system (using MQTT). It also allows people to push a button to select who should be notified and how, and displays the status and notification selection on the OLED display.

    Parts I used:

    • An Onion Omega (version 1)
    • An Onion dock (to provide power and make it easier to break out the GPIO pins)
    • An Onion display
    • A USB number pad keyboard
    • A USB hub (to make the keypad show up properly)
    • A 40-pin ribbon cable (to hook up the display)
    • Wire, proto boards, etc.

    How it works:

    There are three Python scripts running on boot. (See below for why I ended up with three.)

    • A sensor script - polls the MPU-6050 for acceleration on each axis every 200ms
    • An input script - polls the keyboard (directly accessing /dev/input) and translates keycodes into who to notify and how
    • A main script that picks up the values from the other two, updates the display, and sends out MQTT messages

    The actual notification is running on a separate system which receives the MQTT messages and acts on them. (I use Amazon SNS to do the actual notifications as the free tier allows 100 SMS or many thousand emails per month and we just don't do that much laundry.)

    What went well:

    Initially I wrote everything in one single script, and it was really quite easy to get the data from the accelerometer using the Omega I2C Python libraries and write everything to the display.

    Sadly, that's about where the 'easy' parts ended.

    What went poorly:

    Lots of things combined to make this project way harder than it should have been.

    One weird one is that the USB keypad wouldn't show up in /dev/input unless I connected it through a USB hub. That doesn't make a ton of sense to me but I had a hub lying around so I didn't investigate much beyond that, but it did cost me a couple of hours of head-scratching.

    The biggest pain is that the packages for the Omega are all over the place. There seems to be a bug in the Python2 packages that provide select(), meaning I couldn't poll the keypad properly from my script. Quick tests showed that Python3 worked properly, but there just aren't Onion I2C libraries for Python3. I ended up having to split my simple script in two and run one in Python2 (for the accelerometer) and one in Python3 (for the keypad) and pass data between them with a socket. This dramatically increased the complexity of the project. :(

    Similarly, the MQTT packages for the Omega don't have Python support. So the code I developed on my laptop didn't work. This is why I ended up with a third script: I had to have something sending/receiving MQTT using the command-line tools rather than doing it inside my scripts. Again, this was a big increase in complexity.

    Finally, the 2.0mm pin pitch on the Onion means I couldn't use standard 2.54mm breadboards or proto boards to build my project. I wanted to have everything on one board but it would cost more to buy a

    My feelings at the end:

    I learned a ton about I2C here (as it was something I really hadn't done before) and up until running into Python2 bugs, this was pretty easy to do. But unfortunately it repeats a pattern I keep running into: the packages I want to use on the Omega are frequently broken. Usually it's that they simply won't install due to kernel header differences but this time it was missing a lot of support I needed (MQTT python bindings, Python3 bindings for the Onion I2C libraries). I'll probably stick with using the Omega for very simple projects where I'm not interfacing with anything more complex than a webserver or a single USB device.

    I really hope some of the package issues are resolved (especially the kernel header mismatches as that should just be a matter of throwing some compilers at the repository and waiting) -- I want to use the Omega for many more things but it's just a little too painful right now. I'll check in after the version 2 shipping wraps up and see how things are going then. :)



  • @fader Really cool - well done, and well described :-)
    Might be helpful to others if you made your code/scripts available.

    I really, really like the Omega ( :-) ), but your post does raise 2 general issues that have long bugged me (pretty well since the Omega was first available - the best part of a year now :-():

    1. Issues with the packages - you mention Python 2 vs Python 3 differences and the kernel mismatch issue.
      I really hope that when Omega2 is out that the Omega guys can put some effort into the software to address these things

    2. The 2mm spacing of the pins on the Omega - while 2mm headers are available, they can be hard to come by and, as you say, don't match standard breadboards/prototype boards making connectivity harder. I would really like it if the were to produce some form of adapter board the provided the more standard 2.54mm/0.1in spacing - e.g. it shouldn't be that hard to produce a version of the mini-dock that had 2.54mm/0.1in spacing headers along the sides that would expose all the Omega pins both on the top and bottom so that it could be plugged in to or accept plugs from standard breadboards/prototype boards - such a board would not be much bigger than the standard mini-dock (and smaller than the expansion dock)



  • Very nice! You made it!

    And as for your development/programming obstacles, this is so typical of Linux/Unix environments and systems, too many languages, too many options, hard to use, documentation spread all over and a lack of vision. I'd like to contribute to a new development environment, plenty of ideas about it, but no time to make some serious progress.



  • @Kit-Bishop They already did a breadboard adapter as part of the Omega2 campaign, it's a giveaway for Omega2 backers who pledged $64 or more. There's a photo in the kickstarter page, about halfway down (search for "Omega2 Breadboard Dock" within the page).



  • @luz Thanks for the pointer :-) I had somehow overlooked this.

    Definitely a move in the right directions, though:

    1. I would prefer it if the 2.54mm/0.1in connectors were along the sides of the Omega rather than sticking out the end - this would make it more compact
    2. I would like stacking headers (like what you get with Arduino stuff) - this would make it easier to stack custom hardware and make direct connections more conveniently to the Omega
    3. It appears that it exposes just the same pins as the standard Expansion Dock - it could be useful if it were to expose all the Omega pins


  • My code for this is really hacky and not something I'd want to be judged on or tell other people to use :) But I can throw in the bulk of it as a starting point other people can use to make something better.

    I wrote a shell script to install all the packages and put the scripts into /etc/rc.local (to start at boot). The "KEYPAD" listed there is the USB ID of the keypad I used, which wasn't recognized as a USB keyboard natively.

    #!/bin/sh
    
    AUTOSTART="python `pwd`/run.sh"
    KEYPAD="hid-generic vendor=0x05a4 product=0x9759 maxSize=2048"
    opkg update
    opkg install python-light python3-light pyOnionI2C libmosquitto mosquitto-client python3-codecs
    
    # Set up the keypad
    grep -q "$KEYPAD" /etc/modules.d/hid-generic || sed -i \$i"$KEYPAD" /etc/modules.d/hid-generic
    
    # Cause the script to start at boot:
    grep -q "$AUTOSTART" /etc/rc.local || sed -i \$i"$AUTOSTART" /etc/rc.local
    

    That will get you all the packages needed to run things as well.

    The most useful part is probably the library for working with the accelerometer. There was Raspberry Pi-specific code for working with it on GitHub, which I forked and modified slightly to work with the Onion: https://github.com/mccollam/mpu6050

    I built a little service to poll the sensor and make the data available over a socket, which is here:

    #!/usr/bin/env python
    
    from mpu6050 import mpu6050
    import socket
    
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('localhost', 6000))
    sock.listen(5)
    
    mpu = mpu6050(0x68)
    # Prime readings so we have something to compare against
    
    accel = mpu.get_accel_data()
    temp = mpu.get_temp()
    
    while True:
      connection, client_address = sock.accept()
    
      try:
        while True:
          data = connection.recv(1024)
          if data != '':
            # TODO: Might eventually want to actually look at the incoming request
            # but for now just return everything
            accel = mpu.get_accel_data()
            temp = mpu.get_temp()
            # Looks like the json libs for python2 are busticated on the Onion :(
            result = '{ "temp": ' + str(temp) + ', "x": ' + format(accel['x'], '.12f') + ', "y": ' + format(accel['y'], '.12f') + ', "z": ' + format(accel['z'], '.12f') + '}'
            connection.sendall(result)
          else:
            break
      
      finally:
        connection.close()
    

    (Note that I don't do a lot of error or bounds checking here, so it would be pretty easy to cause this script to crash by feeding it weird inputs over the socket it opens. So don't do that. :) )

    I also ended up having to write my own JSON encoder as I was having issues with the built-in one on the Onion. By the time I got there I was getting a bit frustrated so I just generated it myself rather than digging into the errors too far.

    Next up is the code that polls the keypad. I had written something really elegant that polled without blocking (using select) and was lightweight and just generally great. It worked fine on my laptop but not on the Onion, so I ended up doing a huge hack and having a separate script listen for keyboard events and write the results to a file that gets read by the main program. I'm not proud of this:

    import os
    from subprocess import call
    
    
    devicefile = '/dev/input/event0'
    scratchfile = '/tmp/notify_0'
    notifyfile = '/tmp/notify'
    
    # MQTT path:
    mqttPath = 'XXXXX/basement/dryer/'
    # The location of the MQTT server:
    mqttServer = 'mqtt.XXXXX.com'
    # The MQTT server port (default 1883):
    mqttPort = 1883
    
    byte = []
    i = open(devicefile, 'rb')
    notifyPerson = ''
    notifyMethod = ''
    
    def mqttPub(path, value):
      call(["mosquitto_pub", \
        "-r", \
        "-h", str(mqttServer), \
        "-p", str(mqttPort), \
        "-t", str(mqttPath) + str(path), "-m", str(value)])
    
    
    while True:
      for bit in i.read(1):
        byte.append(bit)
        if len(byte) == 8:
          if byte[1] == 1 and byte[7] == 1:
            if byte[3] == 55:
                notifyPerson = 'Alpha'
                notifyMethod = 'email'
            elif byte[3] == 98:
                notifyPerson = 'Alpha'
                notifyMethod = 'sms'
            elif byte[3] == 73:
                notifyPerson = 'Bravo'
                notifyMethod = 'email'
            elif byte[3] == 72:
                notifyPerson = 'Bravo'
                notifyMethod = 'sms'
            elif byte[3] == 77:
                notifyPerson = 'Charlie'
                notifyMethod = 'email'
            elif byte[3] == 76:
                notifyPerson = 'Charlie'
                notifyMethod = 'sms'
            elif byte[3] == 81:
                notifyPerson = 'Delta'
                notifyMethod = 'email'
            elif byte[3] == 80:
                notifyPerson = 'Delta'
                notifyMethod = 'sms'
            elif byte[3] == 83:
                notifyPerson = 'Echo'
                notifyMethod = 'email'
            elif byte[3] == 82:
                notifyPerson = 'Echo'
                notifyMethod = 'sms'
            elif byte[3] == 28:
                notifyPerson = ''
                notifyMethod = ''
            o = open('/tmp/notify_0', 'w+')
            o.write('{"notifyPerson": "' + notifyPerson + '", "notifyMethod": "' + notifyMethod + '"}')
            o.close()
            os.rename(scratchfile, notifyfile)
            mqttPub('notifyPerson', notifyPerson)
            mqttPub('notifyMethod', notifyMethod)
          byte = []
    

    The main chunk of code that pulls everything together and updates the UI is here:

    #!/usr/bin/env python
    
    # This script would be way shorter and cleaner if this worked:
    #import mosquitto.publish as publish
    
    import time
    from subprocess import call
    import select
    import socket, json
    
    # Name of this sensor (the MQTT path):
    mqttPath = 'XXXXX/basement/dryer/'
    # Whether or not to attempt to use an attached OLED display:
    enableOLED = True
    # The location of the MQTT server:
    mqttServer = 'mqtt.XXXXX.com'
    # The MQTT server port (default 1883):
    mqttPort = 1883
    # Frequency of MQTT messages (in seconds):
    mqttFreq = 5
    # Measurement frequency (in ms):
    measureFreq = 100
    # Absolute change of accelerometer readings to count as 'running':
    jitter = 0.6
    # Percentage of 'running' readings to change status to 'running':
    runChange = .45
    # Percentage of 'stopped' readings to change status to 'stopped':
    stoppedChange = .65
    # File to find the person to notify
    notifyFile = '/tmp/notify'
    
    #####
    
    def mqttPub(path, value):
      # If the MQTT libs worked, this would be a single line:
      #publish.single(mqttPath + "path", value, hostname=mqttServer, port=mqttPort)
      call(["mosquitto_pub", \
        #"-r", \ # enable to retain old messages
        "-h", str(mqttServer), \
        "-p", str(mqttPort), \
        "-t", str(mqttPath) + str(path), "-m", str(value)])
    
    def update_display():
      if notifyPerson != "":
        notify = notifyPerson + " (via " + notifyMethod + ")"
      else:
        notify = "[no one]"
    
      # By getting the string written to the display to be exactly the right length, we can
      # update the display without clearing it first (which is slow and a bit ugly when doing
      # it frequently).  That's the reason for assembling it as one string with a bunch of
      # carriage returns and blasting it to the display all at once.
      dtempC = "%.1f" % temp
      dtempF = "%.1f" % (9.0/5.0 * temp + 32)
      display = "XXXXX Dryer\\n\\n" \
        + "Temp: " + dtempC + "'C (" + dtempF + "'F)" \
        + "\\nStatus: " + status \
        + "\\n\\nNotify:\\n  " + notify + "\\n"
      call(["oled-exp", "-q", "write", display])
    
    #####
    
    if enableOLED:
      # Initialize and clear display
      call(["oled-exp", "-q", "-i"])
    
    cycles = 0
    running = 0
    stopped = 0
    lastX = 0
    lastY = 0
    lastZ = 0
    status = "stopped"
    notifyPerson = ""
    lastNotifyPerson = ""
    notifyMethod = ""
    lastNotifyMethod = ""
    accel = {}
    sleeptime = float(measureFreq) / 1000
    
    while True:
      accel['x'] = 0.0
      accel['y'] = 0.0
      accel['z'] = 0.0
      sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
      try:
        sock.connect(('localhost', 6000))
        message = 'feed me'
        sock.sendall(message.encode())
        data = sock.recv(1024)
        parsed = json.loads(data.decode())
        accel['x'] = parsed['x']
        accel['y'] = parsed['y']
        accel['z'] = parsed['z']
        temp = parsed['temp']
      finally:
        sock.close()
    
      # Measure change since last reading on each axis
      # (a more accurate way to do this would be to get a differential but
      # honestly that level of accuracy is not needed here and is more
      # expensive to calculate)
    
      dX = abs(accel['x'] - lastX)
      dY = abs(accel['y'] - lastY)
      dZ = abs(accel['z'] - lastZ)
    
      if dX > jitter or dY > jitter or dZ > jitter:
        running = running + 1
      else:
        stopped = stopped + 1
    
      if (cycles >= mqttFreq):
        if status == "stopped":
          if float(running) / (float(running) + float(stopped)) > runChange:
            status = "running"
        else:
          if float(stopped) / (float(running) + float(stopped)) > stoppedChange:
            status = "stopped"
            # FIXME: OMG THIS IS AWFUL
            # HUGE HACK: This should actually receive an MQTT message for updating
            # the person and method for notification.  It doesn't currently because
            # of the lack of support for Python MQTT in OpenWRT :(
            notifyPerson = ""
            notifyMethod = ""
            o = open('/tmp/notify', 'w+')
            o.write('{"notifyPerson": "", "notifyMethod": ""}')
            o.close()
            #mqttPub("notifyPerson", "")
            #mqttPub("notifyMethod", "")
    
    
        mqttPub("status", status)
        mqttPub("temperature", temp)
    
        cycles = 0
        running = 0
        stopped = 0
    
        if enableOLED:
          update_display()
    
      lastX = accel['x']
      lastY = accel['y']
      lastZ = accel['z']
    
      f = open(notifyFile, 'r')
      notify = json.load(f)
      notifyPerson = notify['notifyPerson']
      notifyMethod = notify['notifyMethod']
      if notifyPerson != lastNotifyPerson:
        update_display()
        lastNotifyPerson = notifyPerson
        lastNotifyMethod = notifyMethod
      f.close()
    
      cycles = cycles + sleeptime
      time.sleep(sleeptime)
    

    Finally, the shell script that runs everything is:

    #/bin/sh
    
    # Yet another Onion bug, local DNS doesn't work by default :(
    sed -i s/127.0.0.1/10.1.10.1/ /etc/resolv.conf
    
    while true
    do
      echo '{"notifyPerson": "", "notifyMethod": ""}' > /tmp/notify
      python /root/dryer/sensor.py &
      SENSOR=$!
      python3 /root/dryer/keyboard.py &
      KEYBOARD=$!
      sleep 3
      python3 /root/dryer/ui.py
      sleep 1
      kill $SENSOR $KEYBOARD
    done
    

    (The kill statement in there is just to make everything restart fresh if the mail script crashes. It was useful during debugging.)

    I hope this is helpful to people! Like I said, probably the only really good bit is the library for the accelerometer so hit that up on GitHub.


Log in to reply
 

Looks like your connection to Onion Community was lost, please wait while we try to reconnect.