Welcome to this Introduction to Micropython Workshop!¶
Note
This is a fork of Radomir Dopieralski’s Micropython workshop shared under an Attribution-ShareAlike license. The main adaptations were to reorganise the content around the specific hardware available in this workshop session and misc fixes/updates.
Contents:
Setup¶
Prerequisites¶
To participate in the workshop, you will need the following:
- A laptop with Linux, Mac OS or Windows and at least one free USB port.
- If it’s Windows or Mac OS, we may need to install drivers for the CH340 UBS2TTL chip.
- A micro-USB cable with data lines that fits your USB port.
- You will need Python, Pip and a library called pyserial. Instructions for installing these are included in this setup section.
- Please note that the workshop will be in English.
- In addition, at the workshop, you will receive:
- WeMos D1 Mini development board with ESP8266 on it,
- WeMos OLED shield,
- WeMos SHT30 shield,
- Blue LED,
- Push button,
- LDR light sensor (sharing is caring)
The firmware that is flashed on the boards is also available at https://github.com/MaximusV/d1workshop/blob/master/libs/firmware-combined.bin
Notes on Handling¶
The board can be disconnected and reconnected at any time, there is no power off or shut down command you need to issue first. This is typical of microcontrollers and embedded devices in general, they have to operate in conditions where power may be unreliable and need to be able to handle sudden restarts.
Always disconnect the board from power before adding or removing the shields or components. Be careful to align the pins correctly, use the RST pin on top left for reference. Gently rock the shield back and forth while pulling upwards to remove it, try not to bend the pins. Sometimes the pins won’t fully insert on some shields or boards so don’t try to force it flush.
Note that the pins are exposed on the bottom of the board so don’t let that touch any metal or water as it might short circuit the pins. When plugging in the components double check you’re connecting the right pins, if in doubt ask somebody to check for you. All that aside, don’t worry too much, these devices are fairly robust and worst case they are cheap replaceable components. Accidents happen and that’s ok!
Development Board¶
The board we are using is called “WeMos D1 Mini” and has an ESP8266 module on it, which we will be programming. It comes with the latest version of MicroPython already setup on it, together with all the drivers we are going to use.
Note
The D0, D1, D2, … numbers printed on the board are different from what Micropython uses – because originally those boards were made for a different software. Make sure to refer to the image below to determine which pins are which.

It has a micro-USB socket for connecting to the computer. On the side is a button for resetting the board. Along the sides of the board are two rows of pins, to which we will be connecting cables.
The symbols meaning is as follows:
3v3
- this is a fancy way to write 3.3V, which is the voltage that the board runs on internally. You can think about this pin like the plus side of a battery.gnd
,G
- this is the ground. Think about it like the minus side of the battery.gpioXX
- “gpio” stands for “general purpose input output”. Those are the pins we will be using for sending and receiving signals to and from various devices that we will connect to them. They can act as output – pretty much like a switch that you can connect to plus or to minus with your program. Or they can act as input, telling your program whether they are connected to plus or minus.a0
- this is the analog pin. It can measure the voltage that is applied to it, but it can only handle up to 3.3V.5V
- this pin is connected with the 5V from your computer. You can also use it to power your board with a battery when it’s not connected to the computer. The voltage applied here will be internally converted to the 3.3V that the board needs.rst
- this is a reset button (and a corresponding pin, to which you can connect external button).
Many of the gpio pins have an additional function, we will cover them separately.
Connecting¶
The board you got should already have MicroPython with all the needed libraries flashed on it. In order to access its console, you will need to connect it to your computer with the micro-USB cable, and access the serial interface that appears with a terminal program.
General¶
We are going to connect to the board using a Python library called Pyserial. This way we can use a consistent tool across all platforms with a similar installation process. The following sections will give step by step guides for each platform but let’s go over the steps at a high level now.
First, make sure you have Python installed (any version but Python3 is always recommended). Python is installed on Linux and Mac by default but Windows users may need to download the installer. For this workshop we’re going to use the Python interpreter as a cross platform way to run a terminal emulator which is how we will interact with MicroPython.
Next we’ll ensure you have pip, the Python package manager installed. This is the standard Python package installer used for installing libraries and packages that are not in the Python standard library. Pip can be included from the Python installer on Windows and installed through the usual tools on Mac and Linux e.g homebrew and apt. We will use Pip to install pyserial, a library that provides tools for accessing the serial port.
Finally, we may also need to install drivers for the specific serial to USB chip that the WeMos board uses (the CH340). On Linux this usually comes in the kernel, Windows will generally autodetect/install the drivers and MacOS seems to sometimes have it by default.
Linux¶
On Linux Python should be installed by default but you may need to install Pip if you haven’t before. Open a terminal to execute these steps:
sudo apt-get install python-pip
# If you're familiar with virtual envs, you may want to create one before this step.
sudo pip install pyserial
# Now to check that you've installed pyserial, run the following:
python -m serial.tools.list_ports
/dev/ttyS4
1 ports found
# Next connect the USB cable to the WeMos D1 and run it again:
python -m serial.tools.list_ports
/dev/ttyS4
/dev/ttyUSB0
2 ports found
# The new port that appeared is the one we should connect to.
python -m serial.tools.miniterm --raw /dev/ttyUSB0 115200
You should get see a blank terminal screen and if you press ‘enter’ you should see a line like ‘>>>’ which means you’re in the REPL. Skip to the Hello world! section.
If you don’t get to the REPL, try unplug the cable once and try again. Failing that, try another cable.
MacOS¶
On MacOS Python should be installed by default but you may need to install Pip if you haven’t before. homebrew is a useful tool for installing packages on MacOS, you should install that first if you don’t have it. Then open a terminal to execute these steps:
pip install pyserial
# output will look something like this:
Collecting pyserial
Downloading https://pypi.org/pyserial/3.4/pyserial-3.4-py2.py3-none-any.whl (193kB)
|████████████████████████████████| 194kB 225kB/s
Installing collected packages: pyserial
Successfully installed pyserial-3.4
# Now to check that you've installed pyserial, run the following to list
# available serial ports.
python -m serial.tools.list_ports
# Output will vary depending on your devices but should look similar to this.
# There might even be 0 devices, that's ok!
/dev/cu.Bluetooth-Incoming-Port
/dev/cu.MALS
/dev/cu.SOC
3 ports found
# Next connect the USB cable to the WeMos D1 and run it again:
python -m serial.tools.list_ports
/dev/cu.Bluetooth-Incoming-Port
/dev/cu.MALS
/dev/cu.SOC
/dev/cu.usbserial-14430
4 ports found
If a new port showed up then that’s the MicroPython board and you should be ready to go! If there is no change then it probably means the device driver was not detected and you have to install it. Follow the instructions for this Mac Driver on GitHub.
You may have to disable a security check for this driver and reboot your Mac to complete the install process. Sometimes we’ve seen an alarming message on the first boot that says something like ‘failed to install operating system’ but don’t worry, just reboot again, it’s just a really badly written error message. This security setting is because this is a low level device driver and MacOS doesn’t always recognise the manufacturers signature on the driver.
Once the driver is installed and you have been able to find the right port, you can use the miniterm from pyserial to connect to the device:
python -m serial.tools.miniterm --raw /dev/cu.usbserial-14430 115200
--- Miniterm on /dev/cu.usbserial-14430 115200,8,N,1 ---
--- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---
>>>
If you disconnect the cable while connected you might see an error like the following but don’t worry, that’s ok:
--- exit ---
Exception in thread rx:
Traceback (most recent call last):
File "/usr/local/Cellar/python@2/2.7.16_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/threading.py", line 801, in __bootstrap_inner
self.run()
File "/usr/local/Cellar/python@2/2.7.16_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/threading.py", line 754, in run
self.__target(*self.__args, **self.__kwargs)
File "/usr/local/lib/python2.7/site-packages/serial/tools/miniterm.py", line 445, in reader
data = self.serial.read(self.serial.in_waiting or 1)
File "/usr/local/lib/python2.7/site-packages/serial/serialposix.py", line 509, in read
raise SerialException('read failed: {}'.format(e))
SerialException: read failed: [Errno 6] Device not configured
This website has some good general troubleshooting instructions for mac serial drivers, just ignore any bits specific to their paid drivers https://www.mac-usb-serial.com/docs/support/troubleshooting.html.
Windows¶
Note
When I tested this recently I found that Windows 7 and 10 automatically installed the right drivers when connected to the internet so connect the board first and see if the autoinstaller pops up in the taskbar. Then follow the steps below to see if the device is detected after the autoinstaller completes. If the device doesn’t appear, then you may need to install the drivers manually as described later.

Run the Python standard installer and be sure to check the box to install pip under the Customize Installation section.

Then open a Command Prompt (open the application bar and search for CMD) where we can execute Pip to install Pyserial.

Then we can run the pyserial list_ports tool to list available serial ports. On Windows these are usually named COM<X> where X is a number. Plug in the board and run list_ports again, if a new number pops up then that is the one we need to connect to using miniterm. Be sure to pass in the –raw argument like so:
python -m serial.tools.miniterm --raw COM3 115200

If there was no new port or there are no COM devices showing, you need to install the CH340 drivers first. It may be necessary to reboot to load the drivers properly.
Hello world!¶
Once you are connected, press “enter” and you should see the Micropython prompt, that looks like this:
>>>
Note
You may see some gibberish characters or an Error type message like ‘could not
find main.py’, that’s expected. As long as you can hit “enter” and see the
>>>
prompt then it’s working!
It’s traditional to start with a “Hello world!” program, so type this and press “enter”:
print("Hello world!")
If you see “Hello world!” displayed in the next line, then congratulations, you got it working.
Python Basics¶
If this is your first time ever using Python, this section will run over some of the main things to know for getting started. Remember, Micropython is just an implementation of the Python language interface so for basic behaviour everything is the same as regular Python here.
The REPL¶
We’re currently connected to the Python REPL (Read-Execute-Print-Loop) which is
a quick and easy way to play around with Python code. If you install and run
regular Python on your computer, you can also run the REPL. The MicroPython
REPL has some handy extra features; it will remember the last 8 lines of code, it will
auto-indent blocks for you, it has a special paste mode Ctrl+e
and it has
Tab completion, meaning it will offer suggestions for available methods on a
module or instance when you press the Tab key. Try to get used to these as we
go through the tutorial.
For actual complex Python programs running as services, the code is written into
a file with a .py
extension and then executed with the Python interpreter
(often referred to as Python scripts or modules). Later in the tutorial we
will look at putting files onto the devices over the WebREPL.
Note
During the first parts of the workshop, it’s a good idea to have some basic
text editor like NotePad open so that you can copy/paste in and modify the workshop
code easily using the Ctrl+e
paste mode in the MicroPython REPL. For each
example you can then try variations on the examples or test out your own ideas.
Variables¶
Python is a dynamically typed language which means you don’t have to declare the type of variables (unlike statically typed languages like C and Java):
x = 1
y = "string"
z = []
type(x)
type(y)
You can change the type of a variable at any time, you don’t have to stick to the original type:
x = 1
x = "x is now a string!"
type(x)
This may seem weird if you’re used to statically typed languages and it does
sometimes lead to subtle bugs but in general it is rarely a problem. The type
builtin function used to check the types here is just for illustation, it is very
rarely needed when writing Python in general.
Whitespace Delimited¶
Python is whitespace delimited which means that the whitespaces in the files
are used for flow control between blocks, loops, functions etc. In most other
popular languages, the curly brace chars {}
are used as delimiters but you
also generally indent codes by convention for ease of reading. Python chose to
remove the braces as they are redundant if you are indenting blocks anyway and
it makes for much cleaner code to read.
It is important that you use whitespace OR tabs for indentation but not both. For
this workshop you must stick with whitespaces. If you’re using an editor the
easiest thing is to set tabs to use whitespaces. The Micropython REPL handes
indentation automatically for you. As a rule, whereever you see the colon
character, :
, the next line must be indented. This is usually
applies to class and function definitions, conditional blocks (if/else) and loops:
def adder(x, y):
return x + y
result = adder(1, 3)
print("Result is {}".format(result))
test = False
if test is True:
print("yes")
else:
print("no")
When you’re typing an indented block and hit enter, the REPL will auto-indent to
the next line for you because it doesn’t know if you want to write more in that
block or not. The line will start with three dots ...
and then a suitable
amount of indented whitespace. You have to press “enter” three times to complete
the block and unindent, or manually delete the indented space to close that block.
This catches some people out at first so keep it in mind.
Loops¶
Loops in Python are fairly intuitive:
# lists can contain multiple types!
example_list = [0, 1, 3, "cat", "dog"]
for item in example_list:
print(item)
for i in range(0, 10):
print(i)
from time import sleep
while True:
# loop forever! ctrl-c to exit
print("Looping..")
sleep(1)
# Press enter three times to close the loop or delete the auto-indent!
Modules¶
Python comes with loads of useful standard libraries for all sorts of things,
math, web requests, logging, testing etc. The import
keyword is used to load
external libraries or modules into memory so we can call their methods etc. So
when you’re importing things, you’re calling functions defined elsewhere.
Let’s prove this by creating a script:
# use the open function to open a file in write mode 'w'
new_file = open("example.py", "w")
# note that we have to 'escape' the quote characters inside the string with
# backslash. Why is that do you think?
new_file.write("print(\"Test\")")
new_file.close()
# when importing we don't specify the .py, the 'module' is just the name
import example
This should print out ‘Test’ when you first import it. But what if you import it
again? Nothing happens! This is because the file is interpreted into machine code
when it is imported so simple statements like print
calls get executed.
Normally modules consist of classes and functions to be used multiple times and
it would be a waste of memory and CPU to interpret the file everytime it is imported.
Official Documentation and Support¶
The official documentation for this port of Micropython is available at
http://micropython.org/resources/docs/en/latest/esp8266/. There is a also a
forum on which you can ask questions and get help, located at
http://forum.micropython.org/. Finally, there are #esp8266
and
#micropython
channels on http://freenode.net IRC network, where people chat
in real time. Remember that all people there are just users like you, but
possibly more experienced, and not employees who get paid to help you.
Part 1¶
Blink¶
The traditional first program for hobby electronics is a blinking light. We will try to build that.
The boards you have actually have a light built-in, so we can use that. There
is a LED (light-emitting diode) near the antenna (the golden zig-zag). The plus
side of that LED is connected to the 3v3
pins internally, and the minus
side is connected to gpio2
. So we should be able to make that LED shine
with our program by making gpio2
behave like the gnd pins. We need to
“bring the gpio2
low”, or in other words, make it connected to gnd
.
Let’s try that:
from machine import Pin
led = Pin(2, Pin.OUT)
led(0)
The first line “imports” the “Pin” function from the “machine” module. In Python, to use any libraries, you first have to import them. The “machine” module contains most of the hardware-specific functions in Micropython.
Once we have the “Pin” function imported, we use it to create a pin object,
with the first parameter telling it to use gpio2
, and the second parameter
telling it to switch it into output mode. Once created, the pin is assigned to
the variable we called “led”.
Finally, we bring the pin low, by calling the “led” variable with value 0. At this point the LED should start shining. In fact, it may have started shining a line earlier, because once we switched the pin into output mode, its default state is “low”.
Now, how to make the LED stop shining? There are two ways. We could switch it back into “input” mode, where the pin is not connected to anything. Or we could bring it “high”. If we do that, both ends of the LED will be connected to “plus”, and the current won’t flow. We do that with:
led(1)
Now, how can we make the LED blink 10 times? We could of course type led(0)
and led(1)
ten times quickly, but that’s a lot of work and we have
computers to do that for us. We can repeat a command or a set of commands using
the “for” loop:
for i in range(10):
led(1)
led(0)
Note, that when you are typing this, it will look more like:
>>> for i in range(10):
... led(1)
... led(0)
...
...
>>>
That’s because the console automatically understands that when you indent a
line, you mean it to be a block of code inside the “for” loop. You have to
un-indent the last line (by removing the spaces with backspace) to finish this
command. You can avoid that by using “paste mode” – press ctrl+E
, paste
your code, and then press ctrl+D
to have it executed.
What happened? Nothing interesting, the LED just shines like it did. That’s because the program blinked that LED as fast as it could – so fast, that we didn’t even see it. We need to make it wait a little before the blinks, and for that we are going to use the “time” module. First we need to import it:
import time
And then we will repeat our program, but with the waiting included:
for i in range(10):
led(1)
time.sleep(0.5)
led(0)
time.sleep(0.5)
Now the LED should turn on and off every half second.
External Components¶
Now let’s try the same, but not with the build-in LED – let’s connect an external LED and try to use that. The connection should look like this:

Remember the pin numbering does not match the numbers on the board, refer to the image in the setup page and in each section.
One leg of the LED is a little bit longer (the one the resistor is soldered to
but it was cut short before the workshop) and the other has a
flattening on the plastic of the LED next to it. The long leg should go to the
plus, and the short one to the minus. We are connecting the LED in opposite way
than the internal one is connected – between the pin and gnd
. That means
that it will shine when the pin is high, and be dark when it’s low.
Also note how we added a resistor in there. That is necessary to limit the amount of current that is going to flow through the LED, and with it, its brightness. Without the resistor, the LED would shine very bright for a short moment, until either it, or the board, would overheat and break. We don’t want that.
Now, let’s try the code:
from machine import Pin
import time
led = Pin(14, Pin.OUT)
for i in range(10):
led(1)
time.sleep_ms(500)
led(0)
time.sleep_ms(500)
Again, you should see the LED blink 10 times, half a second for each blink.
This time we used time.sleep_ms()
instead of time.sleep()
– it does
the same thing, but takes the number of milliseconds instead od seconds as the
parameter, so we don’t have to use fractions.
Pulse Width Modulation¶
Wouldn’t it be neat if instead of blinking, the LED slowly became brighter and then fade out again? Can we do this somehow?
The brightness of the LED depends on the voltage being supplied to it. Unfortunately, our GPIO pins only have a simple switch functionality – we can turn them on or off, but we can’t fluently change the voltage (there are pins that could do that, called DAC, for “digital to analog converter”, but our board doesn’t have those). But there is another way. Remember when we first tried to blink the LED without any delay, and it happened too fast to see?
Turns out we can blink the LED very fast, and by varying the time it is on and off change how bright it seems to be to the human eye. The longer it is on and the shorter it is off, the brighter it will seem.
Now, we could do that with a simple loop and some very small delays, but it would keep our board busy and prevent it from doing anything else, and also wouldn’t be very accurate or terribly fast. But the ESP8266 has special hardware dedicated just for blinking, and we can use that! This hardware is called PWM (for Pulse Width Modulation), and you can use it like this:
from machine import Pin, PWM
import time
pwm = PWM(Pin(14))
pwm.duty(896)
time.sleep(1)
pwm.duty(512)
time.sleep(1)
pwm.duty(0)
If you run this, you should see the external blue led on gpio14
change
brightness. The possible range is from 1023 (100% duty cycle, the LED is on full brightness)
to 0 (0% duty cycle, the LED is off).
You can also change the frequency of the blinking. Try this:
pwm.freq(1)
That should blink the LED with frequency of 1Hz, so once per second – we are basically back to our initial program, except the LED blinks “in the background” controlled by dedicated hardware, while your program can do other things!
Buttons¶
Disconnect the board and remove the SHT30 shield if connected (on the right). This frees up connections to add the button. ** Note that these are the same pins as on the left, i.g the labels are the same and they are physically connected.**
Connect the button to Pin 13
(a.k.a D7) and to ground on the right hand side.
Note
If you have the button with no wires, use D3 gpio0
instead.

Now we will write some code that will switch the LED on and off each time the button is pressed:
from machine import Pin
led = Pin(14, Pin.OUT)
button = Pin(13, Pin.IN, Pin.PULL_UP)
while True:
if not button():
led(not led())
while not button():
pass
We have used Pin.IN
because we want to use gpio13
as an input pin, on
which we will read the voltage. We also added Pin.PULL_UP
– that means
that there is a special internal resistor enabled between that pin and the
3V3
pins. The effect of this is that when the pin is not connected to
anything (we say it’s “floating”), it will return 1. If we didn’t do that, it
would return random values depending on its environment. Of course when you
connect the pin to GND
, it will return 0.
However, when you try this example, you will see that it doesn’t work reliably. The LED will blink, and sometimes stay off, sometimes switch on again, randomly. Why is that?
That’s because your hands are shaking. A mechanical switch has a spring inside that would shake and vibrate too. That means that each time you touch the wires (or close the switch), there are in reality multiple signals sent, not just one. This is called “bouncing”, because the signal bounces several times.
To fix this issue, we will do something that is called “de-bouncing”. There are several ways to do it, but the easiest is to just wait some time for the signal to stabilize:
import time
from machine import Pin
led = Pin(14, Pin.OUT)
button = Pin(13, Pin.IN, Pin.PULL_UP)
while True:
if not button.value():
led(not led())
time.sleep_ms(300)
while not button():
pass
Here we wait 3/10 of a second – too fast for a human to notice, but enough for the signal to stabilize. The exact time for this is usually determined experimentally, or by measuring the signal from the switch and analyzing it.
Analog to Digital Converter¶
Our board has only one “analog” pin, A0
. That pin is connected to an ADC,
or “analog to digital converter” – basically an electronic voltmeter, which
can tell you what voltage is on the pin. The one we have can only measure from
0 to 1V, and would be damaged if it got more than 1V, so we have to be careful.
We will connect a photo-resistor to it. It’s a special kind of a resistor that changes its resistance depending on how much light shines on it. But to make this work, we will need a second, fixed, resistor to make a “voltage divider”. This way the voltage will change depending on the resistance of our photo-resistor. Disconnect either the LED or button now to make room.

Now, we will just read the values in our program, and print them in a loop:
from machine import ADC
adc = ADC(0)
while True:
print(adc.read())
You should see a column of numbers changing depending on how much light the photo-resistor has. Try to cover it or point it toward a window or lamp. The values are from 0 for 0V, to 1024 for 1V. Ours will be somewhere in between.
Part 2¶
Communication Protocols¶
So far all devices we connected to the board were relatively simple and only required a single pin. More sophisticated devices are controlled with multiple pins, and often have very elaborate ways in which you have to change the pins to make them do something, like sending a character to them, or retrieving a value. Those ways are often standardized, and already implemented for you, so that you don’t have to care about all the gory details – you just call high-level commands, and the libraries and/or hardware in your board handles it all for you.
Among the most popular protocols are UART, I²C and SPI. We are going to look at examples of I²C in particular, but we are not going to get into details of how it work internally. It’s enough to know that they let you send bytes to the device, and receive bytes in response.
Temperature and Humidity¶
The SHT30 sensor shield provides an accurate temperature and humidity sensor which communicates over the I²C protocol, the same as the OLED shield. This only needs two pins aside from ground and power; a clock pin (SCL) and a data pin (SDA). Multiple devices can use the same pins by having a different address on the bus (more on this later). A library for controlling the SHT30 has been built into the firmware already:
from sht30 import SHT30
sensor = SHT30()
temperature, humidity = sensor.measure()
Note that another cheaper and less accurate sensor is often used for this purpose as well, the DHT11/22. These are described in the ‘extra’ section for reference.
OLED¶
A small, 64×48 monochrome display. It uses pins gpio4
and gpio5
to talk
with the board with the I²C protocol. It will conflict with any other shield
that uses those pins, but doesn’t use I²C, like the neopixel shield or the
relay shield. It can coexist with other shields that use I²C, like the SHT30
shield.
Up to two such displays can be connected at the same time, provided they have different addresses set using the jumper on the back.
You can control the display using the ssd1306
library:
import ssd1306
from machine import I2C, Pin
i2c = I2C(-1, Pin(5), Pin(4))
display = ssd1306.SSD1306_I2C(64, 48, i2c)
display.fill(0)
display.text("Hello", 0, 0)
display.text("world!", 0, 8)
display.pixel(20, 20, 1)
# You have to call show to actually display your changes.
display.show()
The display driver “implements” the Framebuffer interface so you can use the methods documented on the linked page. Framebuf provides a common interface for display drivers so that you can use the same drawing code with multiple different hardware screens. This is a common concept used in programming.
Network¶
The ESP8266 has wireless networking support. It can act as a WiFi access point to which you can connect, and it can also connect to the Internet.
First let’s try set it up with the standard interface and connect to an existing network. We’ll try the Access Point in the WebREPL section later. To scan for available networks (and also get additional information about their signal strength and details), use:
import network
# STA_IF stands for Standard Interface
sta = network.WLAN(network.STA_IF)
sta.active(True)
print(sta.scan())
To connect to an existing network, use:
import network
sta = network.WLAN(network.STA_IF)
sta.active(True)
sta.connect("micropi", "pyladies")
Once the board connects to a network, it will remember it and reconnect after every restart. To get details about connection, use:
sta.ifconfig()
sta.status()
sta.isconnected()
HTTP Requests¶
Once you are connected to a network, you can talk to servers and interact with web services. The easiest example is to do a HTTP request to a simple webserver. After the last section, you should be connected to a network, probably the ‘micropi’ network hosted for the workshop. Make sure the AP network is disabled now because it will conflict with the ‘micropi’ network:
import network
ap = network.WLAN(network.AP_IF)
ap.active(False)
urequests Library¶
You might be familiar with the popular requests python library for making HTTP requests, it defines a much simpler interface than the builtin standard libraries for HTTP and is pretty much the de facto standard. There is a micropython version that implements the basic interface which is very nice for simple requests. This is included in the build on the board so let’s try that:
# we can use this import alias so that the code
# could be portable with standard python
import urequests as requests
# This is the IP address of the Pi serving the 'micropi' network
resp = requests.get("http://192.168.4.1")
resp.status_code
resp.text
HTTP status-codes tell the client whether the request was successful or some
kind of error was encountered. As you’ve just seen, 200 means success. Read more
about error codes from the link provided. The server provides a /user
endpoint
for creating, updating or viewing a score value for a user. If we try to query a user that
doesn’t exist, we should get a 404:
import urequests as requests
resp = requests.get("http://192.168.4.1/user/abcd")
resp.status_code
HTTP verbs like ‘GET’, ‘POST’, ‘DELETE’ are used to distinguish between requests that are purely informational e.g GET and requests that expect the server to make a change like saving some form data e.g POST. By convention, a GET request is expected to be ‘safe’ in that it won’t change or delete data. Let’s try PUT some data to the example server to create a score entry for a user:
import urequests as requests
import json
data = json.dumps({"score": 10})
# come up with a username yourself to create and put it in the path
name = ""
resp = requests.put("http://192.168.4.1/user/" + name, data=data)
resp.status_code
resp.text
# What happens if you make the same request again?
Now let’s say our user got a new high score and we want to update their entry. We should use the POST method for this, as the PUT method doesn’t allow us to change existing users:
import urequests as requests
import json
data = json.dumps({"score": 25})
name = "" # same as your username from the last example.
resp = requests.post("http://192.168.4.1/user/" + name, data=data)
resp.status_code
resp.text
Now you should have an idea of how HTTP web applications work and see how online game services could be implemented! The server code might be interesting to read through but it is just a quick example and may not make a lot of sense.
WebREPL¶
The command console in which you are typing all the code is called “REPL” – an acronym of “read-evaluate-print-loop”. It works over a serial connection over USB. However, once you have your board connected to network, you can use the command console in your browser, over network. That is called WebREPL.
First, you will need to download the web page for the WebREPL to your computer.
Get the file from https://github.com/micropython/webrepl/archive/master.zip and
unpack it somewhere on your computer, then click on the webrepl.html
file
to open it in the browser.
Note
We should make sure to disable the other interface, since it is configured with a similar IP and may cause weird conflicts with the AP network:
import network
sta = network.WLAN(network.STA_IF)
sta.active(False)
In order to connect to your board, you have to know its address. If the board works in access point mode, it uses the default address. To configure it as an access point, run code like this (use your own name and password):
import network
# AP_IF stands for Access Point Interface
ap = network.WLAN(network.AP_IF)
ap.active(True)
ap.config(essid="network-name", authmode=network.AUTH_WPA_WPA2_PSK, password="abcdabcdabcd")
print(ap.ifconfig())
For either interface you can check the connection details with the ifconfig()
function. You will see a number like XXX.XXX.XXX.XXX
– that’s the IP address
(probably 192.168.4.1 which is a standard address for Access Point networks).
Enter this in the WebREPL’s address box at the top like this
ws://XXX.XXX.XXX.XXX:8266/
.
To connect to your board, you first have to setup the webrepl. You do this by running the following code and following the instructions. Please use ‘pyladies’ as the password for consistency
import webrepl_setup
You have to turn off and on the board to get the webREPL running after first setup despite what it says about rebooting itself. Now you can go back to the browser and click “connect”.
Filesystem¶
Writing in the console is all fine for experimenting, but when you actually build something, you want the code to stay on the board, so that you don’t have to connect to it and type the code every time. For that purpose, there is a file storage on your board, where you can put your code and store data.
You can see the list of files in that storage with this code:
import os
print(os.listdir())
You should see something like []
or ['example.py']
– that’s a list with
just one file name in it, the example we created in the Setup section.
Note that boot.py
and later main.py
are two special filenames that
are executed automatically when the board starts. boot.py
is for configuration,
and you can put your own app code in main.py
.
You can create, write to and read from files like you would with normal Python:
with open("myfile.txt", "w") as f:
f.write("Hello world!")
print(os.listdir())
with open("myfile.txt", "r") as f:
print(f.read())
Please note that since the board doesn’t have much memory, you can’t put large files on it.
Uploading Files¶
You can use the WebREPL to upload files to the board from your computer. Either with the web interface or else with the Command Line tool provided. To do that, you need to open a terminal in the directory where you unpacked the WebREPL files, and run the command:
python webrepl_cli.py yourfile.xxx XXX.XXX.XXX.XXX:
Where yourfile.xxx
is the file you want to send, and XXX.XXX.XXX.XXX
is
the address of your board.
Note
You have to have Python installed on your computer for this to work.
This requires you to setup a network connection on your board first. However, you can also upload files to your board using the same serial connection that you use for the interactive console. You just need to install a small utility program:
pip install adafruit-ampy
And then you can use it to copy files to your board:
ampy --port=/dev/ttyUSB0 put yourfile.xxx
Warning
The serial connection can be only used by a single program at a time. Make sure that your console is discobbected while you use ampy, otherwise you may get a cryptic error about it not having the access rights.
OLED Shield Buttons¶
The OLED shield has two buttons at the bottom which we can use to interact with the screen to create menus etc. These buttons are controlled over I2C (for version 2.1.0 of the shield, version 2.0.0 just has simple pins) which means the shield only needs 2 pins to control both. However, this means that you need a driver to interact with the buttons.
Let’s upload the driver as a file through the WebREPL. Copy the contents of the file from https://github.com/MaximusV/d1workshop/raw/master/libs/i2c_button.py into a file locally and save it. Upload the file through the WebREPL as described earlier. Then you should be able to use the driver like so:
from time import sleep
from machine import Pin, I2C
from i2c_button import I2C_BUTTON
i2c = I2C(-1, Pin(5), Pin(4))
buttons = I2C_BUTTON(i2c)
buttons.get()
while True:
sleep(0.5)
buttons.get()
print("A:" + buttons.key[buttons.BUTTON_A])
print("B:" + buttons.key[buttons.BUTTON_B])
That’s all, folks!¶
You’ve reached the end of the content of the workshop for now! If there is time left then just play around with things, set yourself a task for example:
Can you get the screen to display the temperature and humidity, updating every 30 seconds?
Part 3¶
Game Programming¶
In this section we’re going to look at some basic games programming concepts, using the OLED shield and its buttons to implement a basic game. You should have setup the WebREPL in part2 as we will need to be able to upload our game file multiple times throughout this example.
Getting Started¶
On your laptop, open a text editor and start a new file, named ‘game.py’ or similar (you’ll just have to import your specific name in the examples later). Copy the code examples into this file in your editor and save it. When you want to test your code, you’ll have to upload this file to your device using the WebREPL or ampy and then import the class and instantiate it. Note you’ll need to soft reset MicroPython with Ctrl+d for every subsequent upload. Why do you think this is? Hint: we discussed the reason back in Modules section in Python Basics.
Game Structure¶
First we’ll have to import the libraries we need and setup some basic framework for the game, constants and so on:
from time import sleep
from machine import I2C, Pin, Timer, disable_irq, enable_irq
from micropython import const, alloc_emergency_exception_buf
import ssd1306
from i2c_button import I2C_BUTTON
alloc_emergency_exception_buf(100)
B_WIDTH = const(7)
B_HEIGHT = const(3)
B_STEP = const(2)
BUT_LEFT = 0
BUT_RIGHT = 2
UPDATE_PERIOD = 50
class Block():
def __init__(self, x, y, display, width=B_WIDTH, height=B_HEIGHT):
self.x = x
self.y = y
self.display = display
self.width = width
self.height = height
self.draw()
def draw(self):
self.display.fill_rect(self.x, self.y, self.width, self.height, 1)
class Game():
def __init__(self):
self.dirty = 0
i2c = I2C(sda=Pin(4), scl=Pin(5))
self.display = ssd1306.SSD1306_I2C(64, 48, i2c)
self.display.poweron()
self.buttons = I2C_BUTTON(i2c)
self.blocks = [Block((x*9)+2, (y * 5)+2, self.display)
for x in range(0, 8)
for y in range(0, 3)
]
self.draw()
self.tim = Timer(-1)
self.tim.init(period=UPDATE_PERIOD, mode=Timer.PERIODIC,
callback=self.set_dirty)
self.game_loop()
def set_dirty(self):
self.dirty = 1
def game_loop(self):
while True:
if self.dirty:
self.update()
self.draw()
# critical section
state = disable_irq()
self.dirty = 0
enable_irq(state)
def draw(self):
self.display.fill(0)
for block in self.blocks:
block.draw()
self.display.show()
def update(self):
pass
Ok, that’s a lot of code! Take some time to read through it and understand as much as you can. Don’t worry if some of it doesn’t make sense now, there are some MicroPython interrupt specific and so on that may not make sense.
The Block class defines a basic rectangle component that we can use for various bits of the game. It basically just contains some coordinates and a reference to a display where it can ‘draw’ itself.
The Game class is responsible for containing all of the game components and logic. The __init__ instantiates the display and buttons and does an initial draw. A classic game structure is to have an update function that handles updating the game state and a draw function that renders the interface for the player. These get called in a loop, often at the rate of the maximum refresh that the display device can handle (the frame rate e.g 60 Frames Per Second (FPS)). It is also possible to update the game logic more often than it is drawn if necessary.
In this example, the game uses a Timer to control the update rate, mostly for the sake of demonstrating the use of interrupts (and because that is how I wrote it at the time to be honest). You’ll notice the game loop is an infinite while loop which is often how game loops are implemented. You could not bother with a timer and just update/draw as fast as the main loop can run but I wanted to control the framerate more specifically. The update function should generally be called before the draw function for good practise, can you guess why this might be?
The timer just sets a dirty flag which the next game loop iteration will detect and run update/draw before resetting the flag in a ‘critical section’. This just means that we disable the interrupt while we make this change so that the Timer doesn’t fire and try to access/update the flag at the same time.
Let’s test the code, upload the file to your board with the WebREPL and run:
from game import Game
g = Game()
You should see the blocks appear on the screen. Nothing else is happening though which is a bit boring. Let’s add a ball!
The Ball¶
Let’s add a Ball class. You’ll notice it is very similar to the Block class and could be a good place to use inheritance (if your familiar with OO concepts, if not don’t worry about it). Inheritance behaviour was a little broken in MicroPython when I wrote the game so for now let’s just duplicate code :(
class Ball():
def __init__(self, x, y, display, width=B_WIDTH, height=B_HEIGHT):
self.x = x
self.y = y
self.display = display
self.width = width
self.height = height
self.v_x = 1
self.v_y = 2
self.draw()
def update(self):
if self.x == 64 or self.x == 0:
self.v_x *= -1
if self.y == 48 or self.y == 0:
self.v_y *= -1
self.y += self.v_y
def draw(self):
self.display.fill_rect(self.x, self.y, self.width, self.height, 1)
In the init of the Game class we need to instantiate the Ball now like so:
self.ball = Ball(32, 24, self.display, 2, 2)
In the previously empty update function replace the ‘pass’ with a call to update the ball:
self.ball.update()
The ball’s update function updates the ball’s x,y coordinates by v_x and v_y every update. The ‘v’ stands for velocity here. It also enforces the screen bounds so the ball doesn’t wrap around through the screen edges, instead it reverses the appropriate velocity value to give the impression of bouncing.
Now let’s test again, uploading the new version of the game file and instantiating the Game class in the REPL. You should see a ball boucing around now. Uh oh, looks like there is a bug (well there are several actually), the ball is only bouncing up and down. Can you see what is missing from the ball’s update function to make the ball move in the X-axis? How would you increase the ball’s speed if you had to?
The Paddle¶
So now we have something that looks kind of like a game but really it’s more like a screensaver at this point, the player can’t actually interact with it at all. Let’s add the paddle and allow some user input. We’ll need to instantiate the paddle and store it as a variable in the Game class and detect button presses in the update function:
# in the Game init
self.paddle = Block(26, 44, self.display)
# in the Game draw function
self.paddle.draw()
# in the Game update, before the ball update() call
self.buttons.get()
if self.buttons.BUTTON_A > 0:
self.paddle.move_left()
if self.buttons.BUTTON_B > 0:
self.paddle.move_right()
Ok, time to test again. Does this work as you expect, can you think of any improvements? The paddle moves a bit slowly maybe? The ball is still not interacting with the blocks or the paddle though, so let’s add that.
Collision Detection¶
We need to be able to tell when the ball hits against another game object like the paddle or the blocks. This bit involves a bit of basic coordinate maths to figure out if the rectangles intersect or not. We need a function that takes two objects and checks if they collide:
def collision(self, rect1, rect2):
# note this function doesn't use the self parameter so it could be static
# or defined outside the Game class if we wanted.
return (rect1.x < rect2.x + rect2.width and
rect1.x + rect1.width > rect2.x and
rect1.y < rect2.y + rect2.height and
rect1.y + rect1.height > rect2.y)
Now we need to call this fucntion on the ball and other objects on every update. This could be a bit expensive to calculate all the time so we should ideally only call it when necessary. For example, no point checking collisions when the Ball is in the empty space in the middle which we can check with a simple Y value check. For now, let’s not worry about it. We need to add a function to the ball class to make it react to a collision with the paddle:
def hit_paddle(self):
self.v_y *= -1
Then in the Game update function we can check for the collision and make the Ball react:
if self.collision(self.ball, self.paddle):
self.ball.hit_paddle()
Draw the rest of the Owl¶
That is as much of the Game example that I’ve written, I’ll leave the rest as an exercise for the reader! We still need to add a score tracking system, collision with the blocks and a Game Over for when the ball hits the bottom too many times. If you have time then try to implement these features!
That’s all, folks!¶
You’ve reached the end of the content of the workshop for now! If there is time left then just play around with things!
Shields¶
There is a number of ready to use “shields” – add-on boards – for the WeMos D1 Mini, containing useful components together with all the necessary connections and possible additional components. All you have to do is plug such a “shield” on top or bottom of the WeMos D1 Mini board, and load the right code to use the components on it.
Warning
These shields are not provided as part of this workshop but are included here for reference.
Button¶
This is a very basic shield that contains a single pushbutton. The button is
connected to pin gpio0
and to gnd
, so you can read its state with this
code:
from machine import Pin
button = Pin(0)
if button.value():
print("The button is not pressed.")
else:
print("The button is pressed.")
Of course everything we learned about buttons and debouncing applies here as well.
DHT and DHT Pro¶
Those two shield have temperature and humidity sensors on them. The first one, DHT, has DHT11 sensor, the second one, DHT Pro, has DHT22, which is more accurate and has better precision.
In both cases the sensors are available on the pin gpio2
, and you can
access them with code like this:
from machine import Pin
import dht
sensor = dht.DHT11(Pin(2))
sensor.measure()
print(sensor.temperature())
print(sensor.humidity())
(Use DHT22
class for the DHT Pro shield.)
It is recommended to use this shield with the “dual base”, so that the temperature sensor is not right above or below the ESP8266 module, which tends to become warm during work and can affect temperature measurements.
Neopixel¶
That shield has a single addressable RGB LED on it, connected to pin gpio4
.
Unfortunately, that means that this shield conflicts with any other shield that
uses the I²C protocol, such as the OLED shield or the motor shield. You can use
it with code lik this:
from machine import Pin
import neopixel
pixels = neopixel.NeoPixel(Pin(4, Pin.OUT), 1)
pixels[0] = (0xff, 0x00, 0x22)
pixels.write()
Relay¶
This shield contains a relay switch, together with a transistor and a couple of
other components required to reliably connect it to the board. It uses pin
gpio5
, which unfortunately makes it incompatible with any other shields
using the I²C protocol, such as the OLED shield or the motor shield. You can
control the relay with the following code:
from machine import Pin
relay = Pin(5, Pin.OUT)
relay.low() # Switch off
relay.high() # Switch on
Motor¶
The motor shield contains a H-bridge) and a PWM chip, and it’s able to drive up
to two small DC motors. You can control it using I²C on pins gpio4
and
gpio5
. It will conflict with any shields that use those pins but don’t use
I²C, such as the relay shield and the neopixel shield. It will work well
together with other shields using I²C.
Up to four such shields can be connected at the same time, provided they have different addresses selected using the jumpers at their backs.
In order to use this shield, use the d1motor
library:
import d1motor
from machine import I2C, Pin
i2c = I2C(-1, Pin(5), Pin(4), freq=10000)
m0 = d1motor.Motor(0, i2c)
m1 = d1motor.Motor(1, i2c)
m0.speed(5000)
Micro SD¶
This shield lets you connect a micro SD card to your board. It connects to pins
gpio12
, gpio13
, gpio14
and gpio15
and uses SPI protocol. It can
be used together with other devices using the SPI protocol, as long as they
don’t use pin gpio15
as CS.
You can mount an SD card in place of the internal filesystem using the following code:
import os
from machine import SPI, Pin
import sdcard
sd = sdcard.SDCard(SPI(1), Pin(15))
os.umount()
os.VfsFat(sd, "")
Afterwards you can use os.listdir()
, open()
and all other normal file
functions to manipulate the files on the SD card. In order to mount the
internal filesystem back, use the following code:
import flashbdev
os.umount()
os.VfsFat(flashbdev.bdev, "")
Battery¶
This shield lets you power your board from a single-cell LiPo battery. It
connects to the 5V
pin, and doesn’t require any communication from your
board to work. You can simply plug it in and use it.
Servo (Custom)¶
There is an experimental 18-channel servo shield. It uses the I²C protocol on
pins gpio4
and gpio5
and is compatible with other I²C shields.
In order to power the servos, you need to either provide external power to the
pin marked with +
next to the 5V
pin, or connect it with the 5V
pin
to make the servos share power with the board.
You can set the servo positions using the following code:
from servo import Servos
from machine import I2C, Pin
i2c = I2C(-1, Pin(5), Pin(4))
servos = Servos(i2c)
servos.position(0, degrees=45)
TFT Screen (Custom)¶
There is an experimental breakout board for the ST7735 TFT screen. It uses the
SPI interface on pins gpio12
, gpio13
, gpio14
, and gpio15
.
You can use it with the following example code:
from machine import Pin, SPI
import st7735
display = st7735.ST7735(SPI(1), dc=Pin(12), cs=None, rst=Pin(15))
display.fill(0x7521)
display.pixel(64, 64, 0)
If you have a display with a red tab, you need to use a different initialization:
display = st7735.ST7735R(SPI(1, baudrate=40000000), dc=Pin(12), cs=None, rst=Pin(15))
Extra¶
Warning
The following content is out of scope for this workshop but is included for reference. It’s worth having a read over to understand how other hardware can be used.
Servomechanisms¶
Time to actually physically move something. If you plan on building a robot, there are three main ways of moving things from the microcontroller:
- a servomechanism (servo for short),
- an H-bridge and a DC motor,
- a stepper or brushless motor with a driver.
We are going to focus on the servo first, because I think this is the easiest and cheapest way. We are going to use a cheap “hobby” servo, the kind that is used in toys – it’s not particularly strong, but it’s enough for most use cases.
Warning
Don’t try to force the movement of the servo arms with your hand, you are risking breaking the delicate plastic gears inside.
A hobby servo has three wires: brown or black gnd
, red or orange vcc
,
and white or yellow signal
. The gnd
should of course be connected to
the gnd
of our board. The vcc
is the power source for the servo, and
we are going to connect it to the vin
pin of our board – this way it is
connected directly to the USB port, and not powered through the board.

Caution
Servos and motors usually require a lot of current, more then your board
can supply, and often even more than than you can get from USB. Don’t
connect them to the 3v3
pins of your board, and if you need two or
more, power them from a battery (preferably rechargeable).
The third wire, signal
tells the servo what position it should move to,
using a 50Hz PWM signal. The center is at around 77, and the exact range varies
with the servo model, but should be somewhere between 30 and 122, which
corresponds to about 180° of movement. Note that if you send the servo a signal
that is outside of the range, it will still obediently try to move there –
hitting a mechanical stop and buzzing loudly. If you leave it like this for
longer, you can damage your servo, your board or your battery, so please be
careful.
So now we are ready to try and move it to the center position:
from machine import Pin, PWM
servo = PWM(Pin(14), freq=50, duty=77)
Then we can see where the limits of its movement are:
servo.duty(30)
servo.duty(122)
There also exist “continuous rotation” servos, which don’t move to the specified position, but instead rotate with specified speed. Those are suitable for building simple wheeled robots. It’s possible to modify a normal servo into a continuous rotation servo.
Beepers¶
When I wrote that PWM has a frequency, did you immediately think about sound? Yes, electric signals can be similar to sound, and we can turn them into sound by using speakers. Or small piezoelectric beepers, like in our case.

The piezoelectric speaker doesn’t use any external source of power – it will be powered directly from the GPIO pin – that’s why it can be pretty quiet. Still, let’s try it:
from machine import Pin, PWM
import time
beeper = PWM(Pin(14), freq=440, duty=512)
time.sleep(0.5)
beeper.deinit()
We can even play melodies! For instance, here’s the musical scale:
from machine import Pin, PWM
import time
tempo = 5
tones = {
'c': 262,
'd': 294,
'e': 330,
'f': 349,
'g': 392,
'a': 440,
'b': 494,
'C': 523,
' ': 0,
}
beeper = PWM(Pin(14, Pin.OUT), freq=440, duty=512)
melody = 'cdefgabC'
rhythm = [8, 8, 8, 8, 8, 8, 8, 8]
for tone, length in zip(melody, rhythm):
beeper.freq(tones[tone])
time.sleep(tempo/length)
beeper.deinit()
Unfortunately, the maximum frequency of PWM is currently 1000Hz, so you can’t play any notes higher than that.
It’s possible to make the sounds louder by using a better speaker and possibly an audio amplifier.
Schematics¶
The pretty colorful pictures that we have been using so far are not very useful in practical projects. You can’t really draw them by hand, different components may look very similar, and it’s hard to see what is going on when there are a lot of connections. That’s why engineers prefer to use more symbolic representation of connection, a schematic.
A schematic doesn’t care how the parts actually look like, or how their pins are arranged. Instead they use simple symbols. For instance, here’s a schematic of our experiment with the external LED:

The resistor is symbolized by a zig-zag. The LED is marked by a diode symbol (a triangle with a bar), with additional two arrows showing that it’s a light emitting diode. The board itself doesn’t have a special symbol – instead it’s symbolized by a rectangle with the board’s name written in it.
There is also a symbol for “ground” – the three horizontal lines. Since a lot of components need to be usually connected to the ground, instead of drawing all those wires, it’s easier to simply use that symbol.
Here are some more symbols:

It’s important to learn to read and draw electric schematics, because anything more advanced is going to use them, and you will also need them when asking for help on the Internet.
Neopixels¶
Those are actually WS2812B addressable RGB LEDs, but they are commonly known as “neopixels”. You can control individually the brightness and color of each of the LEDs in a string (or matrix, or ring). The connection is simple:

And the code for driving them is not very complex either, because the library for generating the signal is included in Micropython:
from machine import Pin
import neopixel
pixels = neopixel.NeoPixel(Pin(14, Pin.OUT), 8)
pixels[0] = (0xff, 0x00, 0x00)
pixels.write()
Where 8
is the number of LEDs in a chain. You can create all sorts of
animations, rainbows and pretty effects with those.
Temperature and Humidity¶
The DHT11 and DHT22 sensors are quite popular for all sorts of weather stations. They use a single-wire protocol for communication. MicroPython on ESP8266 has that covered:
from machine import Pin
import dht
sensor = dht.DHT11(Pin(14))
sensor.measure()
print(sensor.temperature())
print(sensor.humidity())
The connections are simple:

LED Matrix and 7-segment Displays¶
Adafruit sells a lot of “backpacks” with 7- or 14-segment displays or LED matrices, that we can control easily over I²C. They use a HT16K33 chip, so that we don’t have to switch on and off the individual LEDs – we just tell the chip what to do, and it takes care of the rest.
The schematic for connecting any I²C device will be almost always the same:

Note
The two resistors on the schematic are needed for the protocol to work reliably with longer wires. For our experiments, it’s enough to rely on the pull-up resistors that are built into the board we are using.
The communication with the backpack is relatively simple, but I wrote two libraries for making it more convenient. For the matrix:
from machine import I2C, Pin
from ht16k33_matrix import Matrix8x8
i2c = I2C(sda=Pin(4), scl=Pin(5))
display = Matrix8x8(i2c)
display.brightness(8)
display.blink_rate(2)
display.fill(True)
display.pixel(0, 0, False)
display.pixel(7, 0, False)
display.pixel(0, 7, False)
display.pixel(7, 7, False)
display.show()
and for the 7- and 14-segment displays:
from machine import I2C, Pin
from ht16k33_seg import Seg7x4
i2c = I2C(sda=Pin(4), scl=Pin(5))
display = Seg7x4(i2c)
display.push("8.0:0.8")
display.show()
TFT LCD Display¶
The I²C protocol is nice and simple, but not very fast, so it’s only good when you have a few pixels to switch. With larger displays, it’s much better to use SPI, which can be much faster.
Here is an example on how to connect an ILI9340 display:

And here is a simple library that lets you draw on that display:
from machine import Pin, SPI
import ili9341
spi = SPI(miso=Pin(12), mosi=Pin(13), sck=Pin(14))
display = ili9341.ILI9341(spi, cs=Pin(2), dc=Pin(4), rst=Pin(5))
display.fill(ili9341.color565(0xff, 0x11, 0x22))
display.pixel(120, 160, 0)
As you can see, the display is still quite slow – there are a lot of bytes to send, and we are using software SPI implementation here. The speed will greatly improve when Micropython adds hardware SPI support.
HTTP Requests¶
Once you are connected to network, you can talk to servers and interact with web services. The easiest way is to just do a HTTP request – what your web browser does to get the content of web pages:
import urequests
r = urequests.get("http://harsh-enough.com")
print(r)
You can use that to get information from websites, such as weather forecasts:
import json
import urequests
r = urequests.get("http://api.openweathermap.org/data/2.5/weather?q=Limerick&appid=XXX").json()
print(r["weather"][0]["description"])
print(r["main"]["temp"] - 273.15)
It’s also possible to make more advanced requests, adding special headers to
them, changing the HTTP method and so on. However, keep in mind that our board
has very little memory for storing the answer, and you can easily get a
MemoryError
.
Low Level HTTP request¶
Let’s define a convenient function for making a HTTP request. This function is intentionally quite low level, there are of course libraries that provide a more simple inteface but this nicely demonstrates what a HTTP request is. When you open a website in your browser, the same sequence of calls in made within the browser engine.:
def http_req(host, path, verb="GET", json_data=""):
# this call resolves the DNS name into an IP address
addr = socket.getaddrinfo(host, 80)[0][-1]
# this instantiates a socket to use.
s = socket.socket()
s.connect(addr)
if verb == "GET":
req = '{} /{} HTTP/1.0\r\nHost: {}\r\n\r\n'
# send the formatted HTTP 1.0 request
s.send(bytes(req.format(verb, path, host), 'utf8'))
else:
req = '{} /{} HTTP/1.0\r\nHost: {}\r\nContent-Type:application/json\r\n{}\r\n'
s.send(bytes(req.format(verb, path, host, json_data), 'utf8'))
# read the response data from the socket and print it out.
while True:
data = s.recv(100)
if data:
print(str(data, 'utf8'), end='')
else:
break
s.close()
Now to make a request:
# This is the IP address of the Raspberry Pi server.
http_req("192.168.4.1", "")
It’s also possible to make more advanced requests, adding special headers to
them etc. However, keep in mind that our board has very little memory for
storing the answer, and you can easily get a MemoryError
.
Misc¶
This is just a dumping ground of old material that may still be useful and I wanted to keep just in case.
Mac¶
MacOS should have the device driver installed as well but we have seen varying levels of success at previous workshop sessions. Normally connecting with ‘screen’ should look similar to the Linux example but the device name will vary depending on the driver:
screen /dev/tty.SLAB_USBtoUART 115200
To check if the device is being detected and the driver is working, do ls /dev/tty*
to list tty devices on the filesystem with the device disconnected first. Reconnect
the board and do the `ls /dev/tty*
again to spot the difference.
This website has some good general troubleshooting instructions for mac serial drivers, just ignore any bits specific to their paid drivers https://www.mac-usb-serial.com/docs/support/troubleshooting.html. If the default driver doesn’t work, then try to follow the instructions here to uninstall that and install a new one: https://github.com/MPParsley/ch340g-ch34g-ch34x-mac-os-x-driver
Once the driver is working and you connect with a terminal emulator like screen, you should get a blank screen and if you hit enter a few times, you should see the usual python REPL prompt ‘>>>’. You might see some gibberish characters or get a SyntaxError when you first connect, that is just the initial serial connection. To exit screen just disconnect the cable. Skip to the Hello world! section.
Windows¶
COM port¶
To figure out what COM port the device is on, either open a CMD window and run the
mode
command or open settings and look under Devices and Printers. The
mode
command lists all controllable attributes of the console (CON) and more
importantly, the available COM devices. Run it once with the board disconnected
and then again having connected it to find the device that appeared. If there
was no change or there are no COM devices showing, you need to install the driver
first.
CH340 drivers¶
For the serial interface to appear in your system, you may need to install the drivers_ for CH340. It may be necessary to reboot to load the drivers properly. Once you have that, you can use either Hyper Terminal or PuTTy to connect to it.
PuTTy¶
I’d recommend using Putty which is described in detail here. Run the PuTTy exe or app from the start menu. You should see a screen similar to the image below.

Now select the Serial mode radio button because we want to make a serial type
connection over USB to the device. Set the Serial Line field to the COM port
number you got from the mode
command e.g COM3. Set the Speed field to 115200
(the unit is bits per second). This is the Baud Rate i.e the connection speed,
you can read more about Serial Communications online if you’re interested.
Note
This image is just for reference, make sure to set the Serial line to the COM port number you found earlier!

You might want to save this connection profile for convenience, enter a name like ‘micro’ into the Saved Sessions field and click the Save button. Next time you connect you can just double-click ‘micro’ in the list and PuTTy will load the connection settings. If you have the right COM port and the drivers are working a black console type window should pop up, it will be blank initially. If not, double check the steps above regarding COM ports and the drivers.