empowering creative people

Pycom Expansion Board - Getting Started

Where do I start?

If you’re new to programming Python is a great place to start! I’ve seen Python taught to primary school age children and I have used Python in a Machine Intelligence course at university. It’s no wonder Pycom was set up to bring the power of Python to the Internet of Things!

While you can jump straight into pasting bits of code together to make what you want it wouldn’t hurt to get a little background learning in first. The Python Software Foundation website lists lots of resources to help when you’re just Getting Started. Try a tutorial. There are lots of them so play with a few to find one you like.

In this tutorial I’m going to assume you’ve got a Pycom microcontroller and an Expansion Board 2.0 set up with Atom as your integrated development environment (IDE). I go through this in some detail in the IoT From Scratch tutorial.

When programming a microcontroller device for a home or hobby application it’s easy to succumb to the idea that “sometimes the device locks up and needs to have the reset button pushed”. But that isn’t true. On rare occasions there might be something wrong with the device; but unless you can find an outstanding bug report that matches your usage very closely, it’s probably your code at fault. Aargh!

Putting it a little differently: only very rarely is the fault outside your control. This is good! It means that it’s always possible to solve whatever problem we’re having. It just takes persistence and a willingness to learn.

Sample Code

Here’s an example. While writing this Getting Started guide I went through the Pycom documentation looking for short cuts (code samples) to show me how things should work. My first stop was 6.2.1.11 SD, where I hoped to learn to use the SD card in just a few minutes. I started a new Atom project, created a new Python file and pasted in the Quick Example Usage code. Tada! It works! Job done, or so I thought.

After a while I had expanded my code to do many other things: connect to WiFi, set the internal clock (RTC) from a network time protocol (NTP) server online and more, but I kept finding the SD card wouldn’t open from time to time. The line sd = SD() got stuck. I commented out code until I was back at the Quick Example Usage code for the SD card. I understood that the first time the example code is run all is well. The second time, it’s not.

Atom IDE gives wrong line for error

TIP! When an error occurs your Pycom device will print a ‘traceback’ to the REPL console giving the line number(s) that failed. Sometimes it gives the line number for the line after the one that caused the error (see image above). You can chase your tail looking for an error in a line of code that hasn’t even run. When in doubt: double line space your code to find the line at fault.

This procedure recreates the issue I had with the SD card not starting:

  1. Open Atom IDE and connect to a Pycom device
  2. Use an SD card formatted with FAT16 or FAT32 and no bigger than 32GB
  3. Create a new code file and paste in the Quick Example Usage from the documentation (6.2.1.11 SD)
  4. Click the Run button. There’s no information printed out but also no error. Good.
    >>> Running \\mediapc\Shared (ce)\Content\Content2\Chris\Pycom\Pycom Expansion Board 2.0\Getting Started 2\sd-sample.py
    
    >>>
    >>>
    >
    MicroPython v1.8.6-849-d0dc708 on 2018-02-27; WiPy with ESP32
    Type "help()" for more information.
    >>>
    >>>
    
    
  5. Click Run again.
    >>> Running \\mediapc\Shared (ce)\Content\Content2\Chris\Pycom\Pycom Expansion Board 2.0\Getting Started 2\sd-sample.py
    
    >>>
    >>>
    Traceback (most recent call last):
      File "", line 6, in 
    OSError: the requested operation is not possible
    >
    MicroPython v1.8.6-849-d0dc708 on 2018-02-27; WiPy with ESP32
    Type "help()" for more information.
    >>>
    >>>
    
  6. What?!?!? Press the Reset button on the device. Run the code again. It works.
  7. Press Run again. It fails again!

We have an issue here. It’s a good lesson about microcontrollers in general, not just Pycom devices. It’s an easy mistake to make: to have a program that leaves something undone which causes the program not to start properly the next time it’s run. If the device you’re using is connecting to a Bluetooth or WiFi network, then someone who can push the reset button might be available. But an IoT device needs to be virtually bulletproof! It should recover on its own when something goes wrong.

Solving Problems

If you’re a beginner maker or IoT programmer, it’s understandable you feel lost at a point like this. Reach out! As should be done with all forums, search Pycom Forums to find if your question has already been asked and answered. If you strike out, find the right section and post a new question. Don’t forget to post what hardware you’re using, the code you’re running and links to anything you’re referring to in your question.

Back to the code. In trying to solve this problem I read the code to understand what it does. The line os.mount(sd, ‘/sd’) struck me as important. I know enough Linux to get myself into trouble and any time I’ve manually mounted a device I’ve had to unmount it. Would this work here? Where’s the information about unmounting the SD card? The one-page SD card sample in the documentation gives no clue. At the top of the documentation index there’s a Type to search box. I put “unmount” in there. This found one hit, on the 6.3.4 uos page. Scrolling down we see the sample code block:

os.mount(sd, '/sd')
uos.unmount(path)

There is and unmount command! I appended the line uos.unmount('/sd') to the end of my program. Now, another hard won lesson: if your code has thrown an error you’re not sure what state the hardware is in. A hard reset is the only way to get the device back to a “known state”. Use the reset button on your Pycom device or click into the REPL console and type Ctrl-F on your keyboard. We’re sure the hardware has been reset now and we can run our code without anything “hanging over” from the last run of the program.

In the REPL console, type Ctrl-Alt-R to run the code in the current editor. We get the error that name 'uos' is not defined.

>>>
>>>
Traceback (most recent call last):
  File "", line 19, in 
NameError: name 'uos' is not defined
>
MicroPython v1.8.6-849-d0dc708 on 2018-02-27; WiPy with ESP32
Type "help()" for more information.
>>>
>>>

Yes, it did look fishy that the code sample showed the SD card being mounted as os.mount(sd, '/sd') and unmounted with uos.unmount(path). There could be two ways to fix this. First, uos isn’t recognized because it was never imported, so we could try that. Second, we could modify the unmount line to os.unmount('/sd'). My testing of both options succeeded. But it would seem to me having both os and uos imported could cause problems. Mainly because I suspect the os class inherits from (is based on) the uos class. But also because importing both might effectively double the amount of memory used for this part of the program. I chose the option to add os.unmount('/sd') at the end of the program.

So did we properly fix it? To verify our modified code solves the original issue we need to:

  1. Reset the device
  2. Run the code, verify no errors occurred
  3. Run the code again, verify no errors occurred
  4. Repeat step 3 if you want to be doubly sure!

And yes, I can now run this code over and over without an error being thrown.

Leaping Forward

This is a real problem I've encountered. I build a device on the bench, even leave it powered up for days, and it works fine. I then deploy it somewhere that's inconvenient to get to, like the roof space of my house. For a while it works fine then something happens - it stops working! I begrudgingly reset it. The same thing happens again. How do I know whats happening? I wish there was a record of what's been happening so I can know for sure!

Now I’d like to give you a code file I’ve developed and extensively commented so you can run it on your Pycom device and Expansion Board 2.0. This code can:

  • Use the SD card to record a log file of the device’s activities
  • Give the log entries a timestamp to show when they occurred.

For this we’ll need to:

  • use the SD card socket with a micro SD card
  • use the in-built real-time clock (RTC)
  • set the RTC to the correct time automatically

We'll then be able to write event descriptions into the log file for any event we choose, say, when we push a button. Here's the code file:

# Pycom Expansion Board 2.0 Getting Started
# By Chris Murphy
# Core Electronics
# 2018-03-20
# URL: https://core-electronics.com.au/tutorials/pycom-expansion-board-2-0-getting-started.html

import os
import time
import machine
from machine import SD
from machine import RTC
from machine import Pin
from machine import Timer
from network import WLAN

# Settings variables

WIFI_SSID = "IoT"
WIFI_PASS = "not_our_actual_password"
WIFI_TIMEOUT = 30 # seconds

NTP_SERVER = "au.pool.ntp.org"

LOG_PATH = '/sd'
LOG_FILE = 'log.txt'
LOG_FULL = LOG_PATH + '/' + LOG_FILE
LOG_RESTART = True

BUTTON_EVENT_MAXIMUM = 10

# Global settings variables

is_sd_mounted = False
is_log_open = False
was_rtc_set_on_boot = False
button_event_count = 0

# Functions

# This function is just a convenience because the default print() always adds a line terminator
def print_no_wrap(string):
    print(string, end='')

# Formats Timer.Chrono as text
def time_string(time):
    return "[{:.6f}s]".format(time)

# Formats real-time clock as text
def rtc_string(time):
    return "{}-{:0>2d}-{:0>2d} {:0>2d}:{:0>2d}:{:0>2d}.{:0>6d} {}".format(time[0], time[1], time[2], time[3], time[4], time[5], time[6], "GMT" if time[7] is None else time[7])

# If the real-time clock (RTC) has been set, provide that. Otherwise give the chronometer (seconds elapsed since start)
def time_stamp():
    global rtc
    if rtc is not None and rtc.synced():
        return rtc_string(rtc.now())
    else:
        return time_string(chrono.read())

# Since an active WiFi connection is maintained automatically, except during reset, see if it's still connected and don't bother reconnecting if that's the case.
def is_wifi_connected():
    global wlan     # If 'global' was not used, wlan would be a new variable created inside this function. We want to use the one defined outside any functions.
    if wlan.isconnected():
        print("WiFi OK")
        return True
    print_no_wrap("Connecting WiFi to '" + WIFI_SSID + "' ") # Display progress during connection
    wlan = WLAN(mode=WLAN.STA)
    wlan.connect(WIFI_SSID, auth=(WLAN.WPA2, WIFI_PASS))
    for _ in range(WIFI_TIMEOUT):   # Each loop is 1 second (see time.sleep(1) below) The underscore means "whatever", it's variable without a name.
        if wlan.isconnected():      # If WiFi comes on, break out of the function (see 'return True' 3 lines down)
            print(" OK")
            log("WiFi connected to " + WIFI_SSID)
            return True
        else:                       # WiFi not yet started, wait another second
            print_no_wrap(".")
            time.sleep(1)
    print(" FAILED")                # At this point our self-imposed time limit is up. Proceed without WiFi.
    log("WiFi failed for " + WIFI_SSID)
    return False

# Uses any available Internet connection to set the internal clock (RTC) from a network time protocol (NTP) server.
def set_rtc():
    global rtc  # Use the rtc variable that was defined outside this function
    log("Sync RTC to NTP server: " + NTP_SERVER)
    rtc.ntp_sync(NTP_SERVER)
    print_no_wrap("Fetching current time from " + NTP_SERVER + " ... ")
    rtc.ntp_sync(NTP_SERVER)
    while not rtc.synced(): # Wait for RTC to synchronise with the server
        machine.idle()
    print("OK")

# This function writes the entire contents of the log file out to the REPL console
def log_dump():
    if not is_sd_mounted:
        print("ERROR: SD card not mounted. Can't dump log file.")
        return

    try:    # try: says we're expecting something could go wrong here. If it does we don't want the program to crash and stop, we want to keep going. (See 'except Exception:' below)
        print("BEGIN LOG FILE")
        print("--------------")
        with open(LOG_FULL) as f:   # Defaults to 'read' mode. This might not work.
            for line in f:
                print_no_wrap(line)
        print("--------------")
        print(" END LOG FILE")
    except Exception:
        print("Failed reading file: " + LOG_FULL)

# So we can use the micro SD card we have to 'mount' it. We basically give it a name '/sd' and refer to that when we want to access it.
def mount_sd():
    global sd
    global is_sd_mounted
    try:    # try: again, we're preparing for a crash. If something goes wrong in the try: section, we'll jump to 'except Exception:' and do that instead of crashing.
        os.mount(sd, LOG_PATH)
        print("SD mounted OK")
        is_sd_mounted = True
    except Exception:   # Failed to mount SD
        print("ERROR: Can't mount SD card")
        sd = None

# Convenience function to keep all log file entries formatted the same
def time_stamped_string(string):
    return time_stamp() + ' - ' + string

# This opens our log file. Either to 'w' (create a new emtpy file) or 'a' append to the existing file.
def create_log():
    global sd           # We're referring to the variables defined outside this function
    global is_log_open
    try:    # And this part might fail. If it does, jump straight to 'except Exception:'. Succeed or fail, do the finally: part as well.
        if not is_sd_mounted:
            print("ERROR: Can't create log, SD not open")
            return
        if LOG_RESTART:
            f = open(LOG_FULL, 'w')
            f.write(time_stamped_string("Log file recreated\n"))
        else:
            f = open(LOG_FULL, 'a')
            f.write(time_stamped_string("Log file appending\n"))
    except Exception:   # Failed to append or write file
        print("ERROR: failed to recreate file")
    finally:
        f.close()

# Takes any text, appends the time_stamp to the front of it and writes it into the log file on the SD card. Also outputs to the REPL console whether it succeeded or failed.
def log(line):
    stamped_line = time_stamped_string(line)

    if not is_sd_mounted:
        print_no_wrap("NOT LOGGED " + line)
        return

    try:    # This could fail! Jump to 'except Exception:' if it does.
        with open(LOG_FULL, 'a') as f:
            f.write(stamped_line + '\r\n')
            print("LOGGED: " + stamped_line)
    except Exception:
        print("NOT LOGGED: " + stamped_line)

# This function is called every time the button is pressed. It just logs that it happened.
def pin_handler(arg):
    global button_event_count
    button_event_count += 1
    log("Button event at %s" % (arg.id()))

# Wires up an 'interrupt'. Whenever the button is pressed down or let go the function pin_handler() will be called. Whatever else the device was doing will be interrupted momentarily.
def pin_setup():
    p_in = Pin('P10', mode=Pin.IN, pull=Pin.PULL_UP)
    p_in.callback(Pin.IRQ_FALLING | Pin.IRQ_RISING, pin_handler)

#========================================================
# PROGRAM STARTS HERE
# Functions are listed below after keyword 'def'
#========================================================

rtc = RTC() # A variable to represent the on-board real time clock (RTC)
sd = SD()   # A variable to represent the micro SD memory card

chrono = Timer.Chrono() # Create a stopwatch
chrono.start()          # Start the stopwatch

wlan = WLAN(mode=WLAN.STA)  # A variable to represent the WiFi network hardware

print("{} Program start".format(time_stamp())) # Just shows us that the programm has started.

# Call these three functions to get the log file and interrupt set up.
mount_sd()
create_log()
pin_setup()

if rtc is not None and rtc.synced():    # Was the clock already set?
    was_rtc_set_on_boot = True          # Remember that it was!
    print("RTC already set")
else:
    print("RTC not set")

if is_wifi_connected():     # Call this function to see if WiFi is already set up and, if not, set it up now
    set_rtc()    # Set the time (RTC) over the Internet via Network Time Protocol (NTP)

if not was_rtc_set_on_boot and rtc.synced():    # If the clock wasn't set when the program started, but it is now, log both elapsed time and clock time to the log file.
    elapsed = chrono.read()
    now = rtc.now()
    log("{} elapsed at {}".format(time_string(elapsed), rtc_string(now)))

print("Logging the next 10 button events")
while True:
    if button_event_count >= BUTTON_EVENT_MAXIMUM:
        break;
    machine.idle()

# Now we're starting to shut everything down.
log("Program ended normally")

# Turn off the WiFi properly!
wlan.disconnect()
wlan = None

# Show the whole log file on the screen.
log_dump()

# If the SD card was being used close that down too.
if is_sd_mounted:
    try:
        os.unmount(LOG_PATH)
        print("SD unmounted")
    except Exception:
        print("Failed to unmount {}".format(LOG_PATH))
else:
    print("Unmount SD not required")

#========================================================

When I run this program my output looks like this:

>>>
>>>
[0.014071s] Program start
SD mounted OK
RTC not set
Connecting WiFi to 'IoT' ..... OK
LOGGED: [5.096512s] - WiFi connected to IoT
LOGGED: [5.114625s] - Sync RTC to NTP server: au.pool.ntp.org
Fetching current time from au.pool.ntp.org ... OK
LOGGED: 2018-03-19 22:58:57.322459 GMT - [5.197895s] elapsed at 2018-03-19 22:58:57.319423 GMT
Logging the next 10 button events
LOGGED: 2018-03-19 22:59:04.538100 GMT - Button event at P10
LOGGED: 2018-03-19 22:59:05.396623 GMT - Button event at P10
LOGGED: 2018-03-19 22:59:05.414408 GMT - Button event at P10
LOGGED: 2018-03-19 22:59:05.430884 GMT - Button event at P10
LOGGED: 2018-03-19 22:59:05.446657 GMT - Button event at P10
LOGGED: 2018-03-19 22:59:05.463024 GMT - Button event at P10
LOGGED: 2018-03-19 22:59:05.484141 GMT - Button event at P10
LOGGED: 2018-03-19 22:59:05.500654 GMT - Button event at P10
LOGGED: 2018-03-19 22:59:05.516704 GMT - Button event at P10
LOGGED: 2018-03-19 22:59:05.532761 GMT - Button event at P10
LOGGED: 2018-03-19 22:59:05.548135 GMT - Program ended normally
BEGIN LOG FILE
--------------
[0.049177s] - Log file recreated
[5.096512s] - WiFi connected to IoT
[5.114625s] - Sync RTC to NTP server: au.pool.ntp.org
2018-03-19 22:58:57.322459 GMT - [5.197895s] elapsed at 2018-03-19 22:58:57.319423 GMT
2018-03-19 22:59:04.538100 GMT - Button event at P10
2018-03-19 22:59:05.396623 GMT - Button event at P10
2018-03-19 22:59:05.414408 GMT - Button event at P10
2018-03-19 22:59:05.430884 GMT - Button event at P10
2018-03-19 22:59:05.446657 GMT - Button event at P10
2018-03-19 22:59:05.463024 GMT - Button event at P10
2018-03-19 22:59:05.484141 GMT - Button event at P10
2018-03-19 22:59:05.500654 GMT - Button event at P10
2018-03-19 22:59:05.516704 GMT - Button event at P10
2018-03-19 22:59:05.532761 GMT - Button event at P10
2018-03-19 22:59:05.548135 GMT - Program ended normally
--------------
 END LOG FILE
SD unmounted
>
MicroPython v1.8.6-849-d0dc708 on 2018-02-27; WiPy with ESP32
Type "help()" for more information.
>>>
>>> NOT LOGGED: 2018-03-19 22:59:24.790215 GMT - Button event at P10
NOT LOGGED: 2018-03-19 22:59:24.795346 GMT - Button event at P10
NOT LOGGED: 2018-03-19 22:59:24.920084 GMT - Button event at P10
NOT LOGGED: 2018-03-19 22:59:24.925154 GMT - Button event at P10
NOT LOGGED: 2018-03-19 22:59:24.930205 GMT - Button event at P10
NOT LOGGED: 2018-03-19 22:59:24.935248 GMT - Button event at P10
NOT LOGGED: 2018-03-19 22:59:24.940303 GMT - Button event at P10
NOT LOGGED: 2018-03-19 22:59:24.945343 GMT - Button event at P10
NOT LOGGED: 2018-03-19 22:59:25.586121 GMT - Button event at P10
NOT LOGGED: 2018-03-19 22:59:25.712119 GMT - Button event at P10
NOT LOGGED: 2018-03-19 22:59:25.717195 GMT - Button event at P10
NOT LOGGED: 2018-03-19 22:59:25.722237 GMT - Button event at P10
NOT LOGGED: 2018-03-19 22:59:25.727276 GMT - Button event at P10
NOT LOGGED: 2018-03-19 22:59:25.732313 GMT - Button event at P10
NOT LOGGED: 2018-03-19 22:59:25.737356 GMT - Button event at P10
NOT LOGGED: 2018-03-19 22:59:25.742396 GMT - Button event at P10
NOT LOGGED: 2018-03-19 22:59:25.747435 GMT - Button event at P10

It's not a mistake that the after the log file has closed pushing the button still causes the interrupt to fire! If you want that function to stop you need to explicitly do that.

Conclusion

So there you have it! The Expansion Board 2.0 is a great way to get started in Pycom development not least because you can use an SD card to give HUGE amounts of storage to your application. I hope this inspires you with your Internet of Things projects!

Where do I start? If you’re new to programming Python is a great place to start! I’ve seen Python taught to prim...

Have a question? Ask the Author of this guide today!