Sunday, July 16, 2017

Arduino Loader Project

Hardware: Raspberry Pi 3 B, Adafruit Feather 32u4, 10,000 mAh extended cell battery, 3 button membrane

Software: Windows 10, Platformio 3.4.0, mpg321, Rasbian Jessie Lite, Python 2.7.9

I needed to create a method for "less-trained" staff to reload Arduino boards in the field.  I considered doing something with Android but immediately had several users ask "what about iPhones?!"  So I decided that I'd just build a unit to allow for a more controlled environment.

The result is a handheld Raspberry Pi that the techs can walk around with 



Overview

At a high level, this is how I approached it:


What isn't really laid out here is the minutia of "how" some of this gets done.  So let's break it down in the sections below.

Parts

I used the following hardware:
I built two of these.  On the first design I used x3 resistors (one on each GPIO #'d pin) and on the second I just used x1 resistor (one on the 3.3v supply).  The first didn't have any issues at all.  The second just occasionally seemed to mix up voltages across pins.  I didn't test too thoroughly and it could just have been quirk of my soldering vs. anything to do with the difference in resistors.

Total cost was probably around $47 US.

Hardware configuration

The membrane button connector is a 4-pin female that slides right onto the pins just fine.  Of course, if you do that then you're going to have some trouble w/ button bounce.  Hence, the resistors.  So it is not quite as simple as plugging something in.  

I normally do resistors inline on the wires, twist the resistor ends into circles/hooks, and hook/solder the wires as well to make them stronger.  However, since this is inside the case w/ no tension, I just soldered them straight inline for the wire (i.e. I got lazy).  I cut some male/female jumper wires in half, soldered the resistor in the middle, and slid the heatshrink over it.

The case I chose is just big enough for an RPi but trying to put a standard hookup wire connected to it makes it too tall to mount the case.  Hence the decision to use the Right angle PCB header, which worked very well.  

To wrap it up, I simply inserted the male side of the breadboard wire into the end of the membrane button standard connector and the female into the right-angle PCB header.  To keep everything in place, I added some hot glue.

While I did use pins 11, 13, 15 and 17 (GPIO 17, 22, 27 and a 3.3v supply), I think there's value in splitting these up to limit issues w/ interference and just make it easier on you working in such a tight space.

Folding the membrane wire back a bit on itself and removing the paper from the back let me easily stick the membrane buttons on the exterior of the case.

All in all, it was a pretty simple and bulletproof configuration.  

Using audio as the UI was me really just not wanting to mess with an LCD display or have to cut a case and mount it.  

Software Configuration

This is a bit of kludge laid on top of some really nice tools others have built.  I used:
  • Win32DiskImager
  • Raspbian Jessie Lite (2017-03-02)
  • pyserial
  • mpg321 (To play prompts)
  • platformio (To load software to the Arduino)
  • gTTs (Google TTS engine)
  • pip (because doing things differently is the Python way)
Now keep in mind, this was a short-term project to just get "something workable".  I definitely would have chopped this application and perhaps used a more interactive audio method (e.g. pygame) if I had more time and/or I was using this more broadly.  W/ the method I used I am really just doing a linear "play audio" that cannot be interrupted.  Users have to listen to the message and cannot bypass it.  As a quick & dirty method though, this works and can get you something functional quickly.

90% of my time was spent trying to get Platformio working in the way I wanted.  I actually was able to get it initially working in about an hour.  But then, I got trapped trying to utilize their methods for "pre/post" loading triggers.  In some ways, it was their documentation, in others, bad assumptions on my part.  However, in the end (and with some help from their online forum), I got it working as I expected.

To start, I installed the Raspbian Jessie image from my Windows PC using Win32DiskImager.  I did a bit of cleanup on the users that I won't go into here (created new admin user and removed default user) and running raspi-config to expand the disk and force audio out the headset port (which probably isn't necessary on a default install).

I connected it up to my network where it used DHCP (the default) to get an IP address and access to the internet.  Then, from the root of the command line (CLI) I ran:
  • sudo apt-get update
  • sudo apt-get upgrade
  • wget https://bootstrap.pypa.io/get-pip.py 
  • sudo python get-pip.py  
    • because apparently you can't load most things python-related easily w/o pip
  • sudo pip install pyserial
  • sudo pip install -U pyserial
  • sudo pip install gTTS
  • sudo easy_install --upgrade pip  
    • just because I was troubleshooting some issues - might not be needed
  • sudo pip install -U requests  
    • you MUST DO THIS on the current version of Raspbian to get gTTs to work
  • sudo apt-get install mpg321 
    • there is also a precursor project to this called mpg123 and you'll kick yourself if you go all dyslexic here
That sets up the basic environment that allows for sending/receiving events from the Arduino board, reading GPIO events (included by default w/ Raspbian Jessie Lite), generating prompts using Text-to-Speech, playing audio from the CLI, and loading software onto the Arduino board.

At this point you can even test the audio and Text-To-Speech(TTS) at this point by starting a python session by typing in the Linux CLI:

python

Then copy/paste in the following:

from gtts import gTTS
import os
tts = gTTS(text='Testing.  One, two, three.', lang='en')
tts.save("test.mp3")
os.system("mpg321 test.mp3")

This will:
  • connect to Google's TTS service and send it text to turn into audio
  • save that audio to a file (e.g. test.mp3)
  • play that audio file out the headphone jack
So now everything works, right?  Not even close buddy.  You have some work to do on configuring Platformio.

Configure Platformio

Before I say another thing here I want to state clearly that I LOVE what this group is doing w/ Platformio.  I am changing over to Atom on my Windows/Arduino development as well based around the benefits provided.  I even paid them for a year of support/service I love them that much.

That being said, the documentation is...thorough.  And yet, a little off at times.  It isn't for lack of trying, it is simply that they are some pretty smart folks.  Smart folks tend to make things for other smart folks.  That's why I try to put on my stupid hat before I make things for anyone.  At least that's my excuse for churning out bad solutions.  ;)

What this means is that their documentation can be very tedious for a beginner who is trying to jump past spending 2 weeks learning everything about platformio before you do anything beyond the basics.

But let me again state that their basic setup was fairly straightforward.  I got things working the day I installed it and was able to push to a board via the Linux CLI.  That meant I was certain I could call it from Python (you can) and that I'd be able to get this working quickly (just not as quickly as I'd hoped).  

So while I totally suggest you go to their site and follow their walkthroughs on configuring your board for their software, I will give you the highlights here as well as the minor gotchas so you can avoid them.

.../libs directory

The base installation of platformio creates a couple of sub-directories for each project.  One of them is the .../libs directory.  This is where you'll drop any libraries you are using for your board.  So, in my case I was using Felix Rusu's RFM69 library (which is awesome and you should consider using his Moteinos where applicable).  You install the libraries just like you do on the standard Arduino IDE platform in that you put them into a subdirectory (e.g. .../libs/RFM69).

Beyond that, if you have done this on the Windows Arduino IDE before, then just know it is pretty much the same.  Download the library .zip from Github, unzip it, remove the "-master" at the end of the name and move the folder into the .../libs directory.

What you should be left with is a ../libs/(nameOfYourLibrary) subdirectory and the files associated with that library all under there.

.../src directory

Same story for the .../src directory except this is where your standard Arduino .ino file (or .c even) goes.  HOWEVER, you can only have one source file in here.  If you try to put two .ino files in then the project will not compile properly.  So take the .ino you previously did in the Arduino IDE and just save/copy it over to this subdirectory and you're set.

platformio.ini

There is no way I can/will cover everything to do with this file.  There is simply too much info.  My original understanding of the file in fact appeared completely wrong and I had to re-think much of how I assumed it functions.  So while I will give some tips later on per usage on this, just know that if "something" isn't working, then it is probably that you screwed up this file.  I recommend making some copies of this as you make changes.

Testing Platformio

Again, if you followed their tutorial then you should be able to load a blink application onto your board AT A MINIMUM.  If you cannot, then hit up their forum first to figure it out.  Then, you can try out your own application & libraries.

pre/post configuration

This was a major "gotcha" for me.

My application needed to play an audio cue for a user right at the point where they needed to do something else.  In my case, I needed them to press the reset button on the Feather 32u4 when the software had been compiled and the platformio application was trying to push the application out to "something".  For the board I was using (which was fully asleep and the USB port unpowered), if you don't time this properly, then it won't load.  So timing of the prompt playing and ensuring that it happened reliably was crucial.

While this may not be required for your particular application, it was for mine.  Supposedly, using the pre/post method via the platformio.ini file is the correct way to approach this with a minimum of hassle (ha!).

But this exercise also showed me that I didn't fully grok the platformio.ini configuration.  Starting by using their example here and a few more elsewhere, I attempted to configure their application to run some python code both before and after the board was being loaded.  

Here's essentially my own mistake.  After following their tutorial, I wound up w/ a platformio.ini file that worked with my board.  Their example, however, shows a new INCOMPLETE .ini section as follows:

[env:pre_and_post_hooks]
extra_script = extra_script.py

Essentially this is a tag that tells platformio to run the extra_script.py "python" code (and those quotes are there for a reason - keep reading) whenever platformio is run for that project.  Since this section doesn't include anything relating to the board/platform/etc., I made the INVALID assumption that you'd just drop this after the currently 100% functional platformio.ini entries you created during their setup/tutorial.  WRONG!  IDIOT!

You essentially have to copy the working entries from your old section into this one to get everything functional.  In the end, my own file looked like this:

[env:pre_and_post_hooks]

platform = atmelavr
board = feather32u4
framework = arduino
upload_port = /dev/ttyAM0
extra_script = extra_script.py

Then delete the original section.  If you are totally stupid and don't do this (like me) then oddly enough the Platformio app attempts to load twice.  

The explanation of the settings above breaks down like this:
  • [env:pre_and_post_hooks]
    • used to indicate you're going to do "something" before and after certain points in the platformio upload process.
  • platform = atmelavr
    • must reflect the chipset of the thing you're pushing information TO vs. FROM.  
    • I screwed up here and had chosen linux_arm for the RPi.  That is not correct.
  • board = feather32u4
    • must cut/paste this from your working section to here
    • should reflect whatever board you are using
  • framework = arduino
    • assuming you're using Arduino then yes this must be there
  • upload_port = /dev/ttyAM0
    • optional but it will speed things up.  This is the default port where a single Arduino appears on Raspbian.  If you had >1 then you'd probably need to change things up a bit.
  • extra_script = extra_script.py
    • The script you're going to run
Also, remember how above I wrote "python", well, this also annoyed me.  You see their sample application (extra_script.py) is as follows:

Import("env")
#
# Upload actions
#
def before_upload(source, target, env):
    print "before_upload"
    # do some actions
def after_upload(source, target, env):
    print "after_upload"
    # do some actions
print "Current build targets", map(str, BUILD_TARGETS)
env.AddPreAction("upload", before_upload)
env.AddPostAction("upload", after_upload)
#
# Custom actions when building program/firmware
#
env.AddPreAction("buildprog", callback...)
env.AddPostAction("buildprog", callback...)
#
# Custom actions for specific files/objects
#
env.AddPreAction("$BUILD_DIR/firmware.elf", [callback1, callback2,...])
env.AddPostAction("$BUILD_DIR/firmware.hex", callback...)
# custom action before building SPIFFS image. For example, compress HTML, etc.
env.AddPreAction("$BUILD_DIR/spiffs.bin", callback...)
# custom action for project's main.cpp
env.AddPostAction("$BUILD_DIR/src/main.cpp.o", callback...)

Now if you look at that VERY first line and are a stickler for precision you might notice that this just plain won't work in python 2.7.9.  There is no such function as (again...note the exact word here): Import.  The lower case "import" is a thing, but "Import" is not.  So like a good coder who is used to fixing other people's online garbage I "fixed it".  Aaaaaand actually broke it.

You see, this is being read in by their own interpreter within platformio and is reading that uppercase Import and it works just fine within there.  Why?  Got me.  Don't know and I only minimally care now that I understand that the upper case I in Input it isn't a typo.

Their code here works AS IS without modification.  So, copying/pasting the above information into a new file named extra_script.py (e.g. sudo nano extra_script.py ) and running platformio to upload to your board from the root of your project (e.g. pio run -t upload  ), will work.  Nothing special happens except you can see that the lines "before_upload" and "after_upload" get printed out at different points during the process.  

Worth noting is that if your board didn't load properly the "after_upload" does not print.  This means that you can catch the success of your upload here.

In my own case, I simply altered the above code to be:

Import("env")
#
# Upload actions
#
def before_upload(source, target, env):
   (os.system("mpg321 /opt/reloader/prompts/tone.mp3")
def after_upload(source, target, env):
   (os.system("mpg321 /opt/reloader/prompts/success.mp3")
print "Current build targets", map(str, BUILD_TARGETS)
env.AddPreAction("upload", before_upload)
env.AddPostAction("upload", after_upload)
#
# Custom actions when building program/firmware
#
env.AddPreAction("buildprog", callback...)
env.AddPostAction("buildprog", callback...)
#
# Custom actions for specific files/objects
#
env.AddPreAction("$BUILD_DIR/firmware.elf", [callback1, callback2,...])
env.AddPostAction("$BUILD_DIR/firmware.hex", callback...)
# custom action before building SPIFFS image. For example, compress HTML, etc.
env.AddPreAction("$BUILD_DIR/spiffs.bin", callback...)
# custom action for project's main.cpp
env.AddPostAction("$BUILD_DIR/src/main.cpp.o", callback...)

This means that it was going to play a couple of .mp3 files (that I created using the little gTTS script above and Goldwave to generate a DTMF # sign tone) from the /opt/reloader/prompts/ directory (which is where I put the .mp3 files for my own application...YMMV).  So now whenever I ran platformio, it would compile the source, then play the tone, then attempt to load the unit, then play the "success" message if it was successful.

Now this is again where I could have approached this differently.  I could have written out a code within the def after_upload section to indicate success.  I didn't do that and instead redirected the platformio output to a the /tmp directory on Raspbian and scanned that to verify success.  In hindsight, yeah, I'd have written out a simple 1/0 binary success value to a file and read it back in.  I'd already written a bit of code to scan for the success and didn't want to mess with it since I was short on time.  Sometimes you do things that are stupid but functional.

Final code

Keep in mind that yes, I wrote this very quickly.  I made some choices here that are suboptimal per "good coding" practices.  Also, I tend to chop things up in my own code to make it "more efficient" for reading and for future editing vs. doing the "most efficient" code CPU/memory-wise.

Also, my code is assuming that the prompts already exist.  I used the gTTS code above to generate prompts beforehand, not to do it real-time.  You could do that of course, but since I'm playing the same values over and over (and I'm not an @sshole...well...most days) I decided to not ask Google to keep generating them.

The code below is used essentially to change the device ID for a particular device's radio.  There are a number of devices at a location and occasionally need updates.  To void any security risks of OTA updates, we do this manually and locally.  The problem is that while the code is identical for all devices, one setting much be changed within the source code (e.g. the Node ID for the 915/868mhz radio).

So my application allows the user to navigate via a menu to choose which "Button" device they are reloading (10, 11, 12...etc.).  Once they press the "select" button, then the unit:
  1. Changes the source code to reflect the new device ID
  2. Runs platformio
  3. Checks to see if platformio completed successfully and if not play an error
  4. Return to main menu
The source code change is essentially scanning my Arduino source code for a tag I put in place (e.g. [NODEIDLINE] ).  It then replaces it with the correct line: #define NODEID        ' + str(button).  The value of "button" is what is changing based on the up/down arrow on the device.  You could easily do this to instead be selecting what kind of device you're reloading and selecting the platformio project to load vs. changing source code.  All really depends on what you're after.  But hopefully this is a good starting point for others.


import sys
import os
import time
import RPi.GPIO as GPIO

#Constants
UP_BUTTON = 27
DOWN_BUTTON = 22
SELECT_BUTTON = 17
BUTTON_MIN = 10
BUTTON_MAX = 49
RUN_TUTORIAL = True

#Setup
GPIO.setmode(GPIO.BCM)
GPIO.setup(UP_BUTTON, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.setup(DOWN_BUTTON, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.setup(SELECT_BUTTON, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
button = 10           #What button are we loading?

def main():
    global button
    buttonState = 0
    success = False    #did we upload to a board properly?
    
    filename = "mpg321 /opt/st_reload/prompts/" + str(button) + ".mp3"
    os.system(filename)

    GPIO.add_event_detect(UP_BUTTON, GPIO.RISING, bouncetime=100)
    GPIO.add_event_detect(DOWN_BUTTON, GPIO.RISING, bouncetime=100)
    GPIO.add_event_detect(SELECT_BUTTON, GPIO.RISING, bouncetime=100)
    while (buttonState == 0):
        if GPIO.event_detected(UP_BUTTON):
            buttonState = 1
        if GPIO.event_detected(DOWN_BUTTON):
            buttonState = 2
        if GPIO.event_detected(SELECT_BUTTON):
            buttonState = 3
    GPIO.remove_event_detect(UP_BUTTON)
    GPIO.remove_event_detect(DOWN_BUTTON)
    GPIO.remove_event_detect(SELECT_BUTTON)

    if (buttonState == 1):
        button += 1
    elif(buttonState == 2):
        button -= 1
    else:
        filename = "mpg321 /opt/st_reload/prompts/oneMoment.mp3"
        os.system(filename)
        #Update the source code with the selected button ID
        with open('/opt/st_reload/src/main.ino', 'r') as file :
            filedata = file.read()
        filedata = filedata.replace('[NODEIDLINE]', '#define NODEID        ' + str(button))
        with open('/opt/platformio/src/main.ino', 'w') as file:
            file.write(filedata)
        filename = "mpg321 /opt/st_reload/prompts/compiling.mp3"
        os.system(filename)
        filename = "pio run -d /opt/platformio"
        os.system(filename)
        filename = "pio run -d /opt/platformio -t upload > /tmp/st_reload_results.txt"
        os.system(filename)
        if "========================= [SUCCESS]" in open('/tmp/st_reload_results.txt').read():
            success = True
        if not success:
            filename = "mpg321 /opt/st_reload/prompts/errorLoading.mp3"
            os.system(filename)

    if (button < BUTTON_MIN):
        button = BUTTON_MAX
    elif (button > BUTTON_MAX):
        button = BUTTON_MIN

if (RUN_TUTORIAL):
    os.system("mpg321 /opt/st_reload/prompts/start.mp3")
    os.system("mpg321 /opt/st_reload/prompts/start1.mp3")
    os.system("mpg321 /opt/st_reload/prompts/start2.mp3")
    os.system("mpg321 /opt/st_reload/prompts/start3.mp3")
    os.system("mpg321 /opt/st_reload/prompts/start4.mp3")
    os.system("mpg321 /opt/st_reload/prompts/start5.mp3")
    os.system("mpg321 /opt/st_reload/prompts/start6.mp3")
    os.system("mpg321 /opt/st_reload/prompts/start7.mp3")
    os.system("mpg321 /opt/st_reload/prompts/start8.mp3")
    os.system("mpg321 /opt/st_reload/prompts/start9.mp3")
    os.system("mpg321 /opt/st_reload/prompts/start10.mp3")
    os.system("mpg321 /opt/st_reload/prompts/start11.mp3")
    os.system("mpg321 /opt/st_reload/prompts/start12.mp3")
    os.system("mpg321 /opt/st_reload/prompts/start13.mp3")

while True:
    try:
        main()
    except:
        GPIO.cleanup()
        try:
            sys.exit(0)
        except SystemExit:
            os._exit(0)

Hindsight

While my approach worked reasonably well, I definitely would have done the "error checking" on platformio's success/failure differently.  I really just ran out of time before I needed this live and in end-users' hands.

The use of mpg321 is a complete punt.  I originally intended to use pygame and if I have time will go back and use that to have a more interactive response vs. something so scripted/forced.

My menu is brutal at best.  I am mainly telling them how to do everything up front and then just looping through device IDs.  Don't take my approach as the way to instruct your users on how to use your device.  I'd originally intended to do a bit of a "scaled" approach and to play more instruction at the beginning and then to scale back as new devices were uploaded.  But again...no more time.

I also did a version of this w/ a single button membrane and used short/long presses to do certain things.  While the membrane button itself was ok, the implementation stunk per UI.  Fortunately these 3-button membranes arrived in time.

In the end, for a quick turnaround device to reload Arduino units in the field, I think this works out fairly well w/ a minimal investment.

No comments:

Post a Comment