Skip to content

Lab 6: Deploying Code and Libraries

Time: ~40 minutes | Prerequisites: Lab 5 | Hardware: Pico 2, OLED, buttons

Let's cut the cord

Echo waving welcome So far your Pico has needed a laptop the way a puppet needs a hand. By the end of this lab it runs on its own from a phone charger. Time to transform!

What You'll Build

A program that inspects the board's own filesystem and import machinery — then a main.py that makes your Pico run standalone, no computer attached.

Learning Objectives

  • Explain how import finds a module, using sys.path
  • Describe what /lib is for and why drivers live there
  • List and inspect files on the device filesystem
  • Create a main.py that runs automatically at power-up
  • Recover a board whose main.py misbehaves

Concepts Introduced

ID Concept
249 File Transfer To Device
250 mpremote Tool
251 Library Directory
252 Import Path
253 Module Import
254 Autorun main.py
255 Standalone Operation
256 Code Organization

Background

How import actually works

When you write import config, MicroPython walks a list of places called sys.path, in order, looking for config.py. On your board that list is:

1
['', '.frozen', '/lib']
Entry Meaning
'' the device's root directory
.frozen modules baked into the firmware itself
/lib the conventional home for libraries

So import config finds /config.py, and import ssd1306 finds /lib/ssd1306.py. Nothing mysterious — just a search through three folders.

Two filesystems, easily confused

Thonny's Files panel shows your computer on top and the Pico underneath. A file open from your laptop doesn't exist on the chip at all. This is the single most common way to lose twenty minutes in this course.

How this course puts code on your board

Echo offering a tip All 35 labs' code is pre-loaded for you, using upload-code.sh in the kit directory. It uses a tool called mpremote — the command-line way to talk to a Pico. If you ever need to reset your board to a clean state, that script is how.

main.py: the autorun file

Two filenames are special to MicroPython:

File When it runs
boot.py first, at power-up — for low-level setup
main.py immediately after, every power-up

Put your program in main.py and the board becomes an appliance. Plug it into a USB charger and it just works.

Procedure

Step 1 — Inspect your board

Open 06-deploying-code.py and run it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# Lab 6: Deploying Code and Libraries
#
# Where do your files live, how does `import` find them, and how do you make
# a program run without a laptop attached?
#
# This program answers all three by inspecting the board it is running on.

import config
import os
import sys

print("=== Where Python looks for modules ===")
# sys.path is the search list. '' means the current directory (the root of
# the device), and '/lib' is the conventional home for libraries.
for entry in sys.path:
    print("  ", repr(entry))

print()
print("=== What is in the root directory ===")
root = sorted(os.listdir("/"))
for name in root:
    info = os.stat("/" + name)
    # info[0] is the mode. The 0x4000 bit means "this is a directory".
    # Asking a directory for its size (info[6]) returns nonsense, so check
    # the mode first -- this is the kind of detail that makes a listing
    # either trustworthy or quietly wrong.
    if info[0] & 0x4000:
        print("  %-28s <dir>" % name)
    else:
        print("  %-28s %6d bytes" % (name, info[6]))

print()
print("=== What is in /lib ===")
try:
    for name in sorted(os.listdir("/lib")):
        print("  ", name)
except OSError:
    print("   (no /lib directory yet)")

print()
print("=== Proving the import worked ===")
# `import config` searched sys.path, found /config.py, and ran it.
# Everything defined in that file is now available with a config. prefix.
print("config module :", config.__name__)
print("display size  : %dx%d" % (config.WIDTH, config.HEIGHT))
print("mic pins      : SCK=%d WS=%d SD=%d" % (
    config.SCK_PIN, config.WS_PIN, config.SD_PIN))

print()
print("=== Storage ===")
fs = os.statvfs("/")
total = fs[0] * fs[2]
free = fs[0] * fs[3]
print("flash total : %.0f KB" % (total / 1024))
print("flash free  : %.0f KB" % (free / 1024))
print("used        : %.0f KB" % ((total - free) / 1024))

print()
print("=== Making it standalone ===")
if "main.py" in root:
    print("main.py EXISTS -- this board runs it automatically on power-up.")
else:
    print("No main.py yet. Create one and the board will run it every time")
    print("it powers on, with no computer attached. See the lab for how.")

It prints sys.path, lists the root directory and /lib, proves import config worked, and reports your flash usage.

Step 2 — Trace an import yourself

In the REPL:

1
2
3
4
5
6
7
import sys
sys.path                    # where Python will look
import config
config.__name__             # 'config'
config.SCK_PIN              # 10 -- came from /config.py
import ssd1306
ssd1306.__name__            # found in /lib

You've just followed the search path by hand.

Step 3 — Go standalone

Look at main.py.example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# main.py -- runs automatically every time the Pico powers on.
#
# Rename this file to main.py on the device to make your board standalone:
# plug it into any USB charger and it runs with no computer attached.
#
# IMPORTANT: always give yourself an escape hatch. A main.py that starts a
# tight infinite loop immediately can make the board hard to reconnect to.
# The delay below leaves a window where Ctrl-C still works.

import config
import time

# --- escape hatch ---------------------------------------------------------
print("main.py starting in 3 seconds. Press Ctrl-C now to cancel.")
try:
    time.sleep(3)
except KeyboardInterrupt:
    print("Cancelled. Board is yours.")
    raise SystemExit

# --- your program ---------------------------------------------------------
oled = config.init_display()
button_a, button_b = config.init_buttons()

count = 0
try:
    while True:
        oled.fill(config.BLACK)
        oled.text("Standalone!", 16, 12, config.WHITE)
        oled.text("No laptop", 24, 28, config.WHITE)
        oled.text("uptime %ds" % count, 16, 46, config.WHITE)
        oled.show()
        time.sleep(1)
        count += 1

except KeyboardInterrupt:
    oled.fill(config.BLACK)
    oled.text("Stopped.", 32, 28, config.WHITE)
    oled.show()

Copy it to your Pico and rename it to main.py. Then unplug the board from your computer and plug it into a phone charger.

It runs. No laptop. That's an embedded product.

Always leave yourself an escape hatch

Echo warning Notice the three-second delay at the top of main.py before anything else happens. A main.py that jumps straight into a tight loop can make the board hard to interrupt — it's busy running your program before you can get a word in. That delay is your window to press Ctrl-C. Put one in every main.py you write.

Step 4 — Rescue a board

If a main.py ever locks you out:

  1. Connect in Thonny and press Ctrl-C repeatedly while it boots.
  2. If that fails, delete the file over the REPL:
    1
    2
    import os
    os.remove('main.py')
    
  3. Last resort: hold BOOTSEL while plugging in and re-flash MicroPython. This erases everything — you'd re-run upload-code.sh afterwards.

Knowing step 3 exists is what lets you experiment fearlessly.

Expected Output

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
=== Where Python looks for modules ===
   ''
   '.frozen'
   '/lib'

=== What is in the root directory ===
  01-hello-world.py               552 bytes
  ...
  config.py                      1656 bytes
  lib                          <dir>

=== What is in /lib ===
   ssd1306.py

=== Proving the import worked ===
config module : config
display size  : 128x64
mic pins      : SCK=10 WS=11 SD=12

=== Storage ===
flash total : 3072 KB
flash free  : 2584 KB
used        : 488 KB

=== Making it standalone ===
No main.py yet. Create one and the board will run it every time
it powers on, with no computer attached. See the lab for how.

Troubleshooting

Symptom Likely cause Fix
ImportError: no module named 'config' File is on your laptop, not the Pico Check the Raspberry Pi Pico half of the Files panel
ImportError: no module named 'ssd1306' Driver missing from /lib Re-run upload-code.sh
main.py won't stop No escape hatch Ctrl-C during boot, or os.remove('main.py')
Board seems bricked Bad main.py BOOTSEL + re-flash, then re-upload
Edits keep disappearing Editing the laptop copy Reopen from the Pico side
Changed a module but behaviour is stale Module already imported Soft-reset (Ctrl-D) — imports are cached

Challenges

  1. Make your own library. Write mymath.py with a function, put it in /lib, and import it from a separate program. You've just built a reusable module.
  2. Boot counter. Have main.py read a count from a file, add one, display it, and write it back. Now your board remembers across power cycles.
  3. Button-selected startup. Have main.py check whether button A is held at boot, and run a different program if it is. That's a genuinely useful pattern for a shipped device.

Check Your Understanding

  1. What does sys.path contain, and in what order is it searched?
  2. Why do drivers go in /lib rather than the root?
  3. What's the difference between boot.py and main.py?
  4. Why should every main.py start with a delay?
  5. Your edits vanish each time you unplug. What's the most likely cause?

Setup complete — now the fun starts

Echo celebrating Screen, buttons, standalone operation, and a chip that knows its own name. That's the entire foundation. Next lab we plug in a microphone — and everything after that is about turning sound into insight. Now that's a superpower.


Next: Lab 7: Your First Sound Capture | Previous: Lab 5