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.

_images/board.png

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.

_images/win_python_4.png

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

_images/win_python_2.png

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

_images/win_python_3.png

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
_images/win_python_5.png

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

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:

_images/led_external.png

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.

_images/button_led.png

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.

_images/LDR.png

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.

_images/servo.png

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.

_images/beeper.png

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:

_images/blink_schem.png

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:

_images/schematic.png

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:

_images/neopixel.png

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:

_images/dht11.png

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:

_images/matrix.png

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:

_images/tft.png

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.

_images/putty1.png

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!

_images/putty_3.png

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.

Indices and tables