On a previous tutorial, we explored how to control a Raspberry Pi using Jupyter Notebook: RPI Physical Computing Using Jupyter Notebook
It was a great experience, and once the project worked very well I thought, "how about I also test the Jupyter Notebook on an ESP8266 (or even on ESP32) using MicroPython?"
As we all know, the Jupyter Notebook is an open-source web application that allows you to create and share documents that contain live code, equations, visualizations and narrative text. It includes data cleaning and transformation, numerical simulation, statistical modeling, data visualization, machine learning, and much more. For "much more", we have also explored "Physical Computing".
So far on my projects, I have mostly explored IoT and physical computing projects using ESP8266-01, 8266-12E (NodeMCU) and ESP32 programmed by an Arduino IDE, using its C/C++ type language. But another great tool to be used on programming those devices is MicroPython:
MicroPython is a lean and efficient implementation of the Python 3 programming language that includes a small subset of the Python standard library and is optimised to run on microcontrollers and in constrained environments. It aims to be as compatible with normal Python as possible to allow you to transfer code with ease from the desktop to a microcontroller or embedded system.
Also, I think that using Jupyter Notebook to program an ESP device using MicroPython can be a great tool to teach Physical Computing to kids and also help scientists to quickly access real-world playing with sensors on acquiring data.
This is what we will try to accomplish in this tutorial:
- Output a digital signal to turn on/off an LED
- Read the digital input from a button
- Output a PWM signal to fade an LED
- Control a Servo Motor position using a PWM output
- Reading analog signal (Luminosity using LDR )
- Reading temperature via 1-Wire (DS18B20)
- Reading temperature and humidity (DHT22)
- Displaying data using an OLED via I2C bus.
The first thing to do with a fresh NodeMCU (or ESP32) is to erase wherever it is loaded in its memory and "flash" a new firmware, which in this case will be the MicroPython interpreter.
A. Getting the new FW
Go to the site: MicroPython downloads and download the appropriate FW for your device:
For example, for ESP8266 the latest version is:
esp8266-20180511-v1.9.4.bin (Latest 01Jun18)<br>
(You can find details on how to install the FW here.)
The ideal is to create a directory where you will work with MicroPython. For example, starting from your root directory:
cd Documents
mkdir MicroPython
cd MicroPython
Move the ESP8266 firmware to this recently created directory.
At this point, connect your NodeMCU or ESP32 on your PC using the serial USB cable.
Check where the serial port that is being used by your device is using the command:
ls /dev/tty.*
In my case, I got:
/dev/tty.SLAB_USBtoUART
Install esptool (tool used to flash/erase FW on devices)
pip install esptool
Erase the NodeMCU flash:
esptool.py --port /dev/tty.SLAB_USBtoUART erase_flash
Flash the new FW:
esptool.py --port /dev/tty.SLAB_USBtoUART --baud 460800 write_flash --flash_size=detect 0 esp8266-20180511-v1.9.4.bin
Once you have the Firmware installed, you can play with REPL* on Terminal using the command Screen Serial comm with ESP:
screen /dev/tty.SLAB_USBtoUART 115200
>>> print (‘hello ESP8266’)
>>> hello ESP8266
If you are at REPL: [Ctrl+C] to break a pgm [Ctrl+A] [K] [Y] to quit and return to the terminal.
* REPL stands for "Read Evaluate Print Loop", and is the name given to the interactive MicroPython prompt that you can access on the ESP8266. You can learn more about REPL here.
Step 2: BoM (Bill of Materials)- NodeMCU - US$ 8.39
- DS18B20 Waterproof Temperature Sensor - USD 5.95
- OLED Display SSD1366- USD 8.99
- Servo Motor - USD8.48
- LDR (1x)
- LEDs (1x)
- Push Button (1x)
- Resistor 4K7 ohm (2x)
- Resistor 10K ohm (1x)
- Resistor 220 ohm (1x)
To interact with a MicroPython ESP8266 or ESP32 over its serial REPL, we will need to install a specific Jupyter Kernel. This is necessary to be done only once.
From the Jupyter Documentation website, we can list all "Community-maintained kernels". From there we will be sent to: Jupyter MicroPython Kernel
Once we have Python 3 installed on our machine (in my case it is a Mac), clone the repository to a directory using the shell command (i.e. on a command line):
git clone <a href="https://github.com/goatchurchprime/jupyter_micropython_kernel.git"> https://github.com/goatchurchprime/jupyter_microp...</a>
Next, install the library (in editable mode) into Python3 using the shell command:
pip install -e jupyter_micropython_kernel
This creates a small file pointing to this directory in the python/../site-packages directory and makes it possible to "git update" the library later as it gets improved.
Things can go wrong here, and you might need "pip3" or "sudo pip" if you have numerous different versions of python installed.
Install the kernel into Jupyter itself using the shell command:
python -m jupyter_micropython_kernel.install
This creates the small file ".local/share/jupyter/kernels/micropython/kernel.json" that Jupyter uses to reference it's kernels.
To find out where your kernelspecs are stored, you can type:
jupyter kernelspec list
The Terminal PrintScreen shows you the list of kernels that I have installed on my machine. Note that, in my case, I installed the MicroPython kernel using PIP3 command, and so the Kernel is not in the same directory as the other ones (I got an error when trying to install my kernel using PIP).
Now run Jupyter notebooks:
jupyter notebook
In the notebook, click the New Notebook button in the upper right, you should see your MicroPython kernel display name listed.
On the first cell, you will need to define the port and baud rate that will be used (115200 works fine):
%serialconnect to --port=/dev/tty.SLAB_USBtoUART --baud=115200
As a response, the cell will return:
Connecting to --port=/dev/tty.SLAB_USBtoUART --baud=115200
Ready.
And that's it! When "Ready" appears, you should be able to execute MicroPython commands by running the cells.
Let's try:
print ('hello esp8266')
You should receive the response of your ESP8266 as an output on the cell:
hello esp8266
The available pins are: 0, 1, 2, 3, 4, 5, 12, 13, 14, 15 and 16, which correspond to the actual GPIO pin numbers of ESP8266 chip. Note that many end-user boards use their own adhoc pin numbering (marked e.g., D0, D1, …).
As usual, let's start our journey to Physical Computing "blinking an LED".
Install an LED on NodeMCU pin 0 (D3) and test it, turning it ON and OFF:
# import library to deal with pins:
from machine import Pin
# define pin 0 as output
led = Pin(0, Pin.OUT)
# define value of "led" as "1" or "True" to turn on the LED
led.value(1)
# define value of "led" as "0" or "False" to turn off the LED
led.value(0)
# also you can use .on() or .off methods to control the pin:
led.on()
led.off()
Now, let's import a time library and blink the LED:
from time import sleep
for i in range(5):
led.on()
sleep(0.5)
led.off()
sleep(0.5)
The simple sensor data that you can read on a NodeMCU is a push-button.
Let's install a push-button on a pin 13 (D7) as shown in the diagram.
Our push-button will be connected in such a way that pin 13's normal state will be "High" (so we will use an internal pull-up resistor to guarantee this state). When pressed, pin 13 will be "Low".
# define pin 13 as an input and activate an internal Pull-up resistor:
button = Pin(13, Pin.IN, Pin.PULL_UP)
# Read button state:
print(button.value())
When you run the above cell, the result will be:
1
Pressing the button, run the cell again:
# Read button state:
print(button.value())
The result is now:
0
Note that when you stop pressing the button, it does not return the "cell value" to "1". To see "1", you must run the cell again.
Let's now do a small program to turn on the LED only if the button is pressed:
print (button.value())
if button.value() == 0:
led.on()
else:
led.off()
PWM can be enabled on all pins except Pin(16). There is a single frequency for all channels, with a range between 1 and 1000 (measured in Hz). The duty cycle is between 0 and 1023 inclusive.
Start calling the appropriate library:
from machine import Pin, PWM
Several commands are available:
pwm0 = PWM(Pin(0)) # create PWM object from a pin
pwm0.freq() # get current frequency
pwm0.freq(1000) # set frequency
pwm0.duty() # get current duty cycle
pwm0.duty(200) # set duty cycle
pwm0.deinit() # turn off PWM on the pin
Or you can set to configure the pin at once:
pwm2 = PWM(Pin(2), freq=500, duty=512)
Let's dim the LED connected to Pin 0 from OFF to ON:
from machine import Pin, PWM
pwm0 = PWM(Pin(0), freq=1000, duty=0)
for i in range (0,1023,20):
pwm0.duty(i)
sleep(0.1)
pwm0.duty(0)
pwm0.deinit()
And how about to control a Servo Motor?
Let's install a small hobby servo on our NodeMCU as shown in the diagram. Note that I am connecting the Servo VCC to NodeMCU +3.3V. This is OK for this tutorial, but on real projects, you must connect the Servo VCC to an external +5V power supply (do not forget to connect the GNDs to NodeMCU GND).
The servo data pin will be connected to NodeMCU pin 14 (D5).
Servos usually work with frequency of 50Hz and then a duty cycle between about 40 and 115 will position them from 0 to 180 degrees respectively. A duty cycle of 77 will position the servo on its center value (90 degrees).
servo = PWM(Pin(14), freq=50)
Test the servo on different positions:
# Minimum position (angle 0)
servo.duty(40)
# Maximun position (angle 180)
servo.duty(40)
# center position (angle 90)
servo.duty(40)
You can also create a simple swapping program to test your servo:
# swipping servo
step = 2
for i in range (40, 115, step):
servo.duty(i)
sleep (0.1)
step = -1*step
for i in range (115, 40, step):
servo.duty(i)
sleep (0.1)
servo.duty(77)
Below is the result. I am not using the sonar here, so I will leave to you to develop a code to use it. It's simple already! Try it!
The ESP8266 has a single pin A0 which can be used to read analog voltages and convert them to a digital value. You can construct such an ADC pin object using:
from machine import ADC
adc = ADC(0)
Next, you can read the value of A0 pin using:
adc.read()
The analog pin could be used to read, for example, a variable value got from a potentiometer as a voltage divider. This can be translated as an output for dimming the LED or move the servo to a specific position. You can try it based on what we have learned so far.
Another useful example is to capture data from an analog sensor, as temperature (LM35), ultraviolet (UV) radiation (as discussed in this tutorial) or luminosity using an LDR ("Light Dependent Resistor).
An LDR decreases its resistance when the luminosity increase. So, you can create a voltage divider with an LDR and a resistor as shown in the diagram.
Reading the analog voltage directly over the resistor, we will get a signal directly proportional to luminosity. leave the sensor exposed to light and read the ADC value. Cover the sensor and get a lower value.
In my case:
- Maximum Light ==> adc value > 850
- Minimun Light ==> adc value < 40
The 1-wire bus is a serial bus that uses just a single wire for communication (in addition to wires for ground and power). The DS18B20 temperature sensor is a very popular 1-wire device, and here we show how to use the onewire module to read from such a device.
For the following code to work you need to have at least one DS18B20 temperature sensor with its data line connected to GPIO 2 (D4).
You must also power the sensors and connect a 4.7k Ohm resistor between the data pin and the power pin as shown in the diagram.
Import the libraries:
import onewire, ds18x20
Define to which pin the 1-wire device will be connected. In our case ==> pin 2 (D4)
dat = Pin(2)
Create the onewire object:
ds = ds18x20.DS18X20(onewire.OneWire(dat))
Scan for devices on the bus. Remember that you can have multiple devices connected to the same bus.
sensors = ds.scan()
print('found devices:', sensors)
"sensors" is an array with the address of all 1-wire sensors connected. We will use "arrays[0]" to point to our sensor.
Note that you must execute the convert_temp() function to initiate a temperature reading, then wait at least 750ms before reading the value (do not forget to import time library). To read the value use: ds.read_temp(sensors[0]):
ds.convert_temp()
time.sleep_ms(750)
print(ds.read_temp(sensors[0]))
DHT (Digital Humidity & Temperature) sensors are low-cost digital sensors with capacitive humidity sensors and thermistors to measure the surrounding air. They feature a chip that handles analog to digital conversion and provides a digital interface using only a single data wire. Newer sensors additionally provide an I2C interface.
The DHT11 (blue) and DHT22 (white) sensors provide the same digital interface, however, the DHT22 requires a separate object as it has a more complex calculation. DHT22 have 1 decimal place resolution for both humidity and temperature readings. DHT11 have whole number for both. A custom protocol is used to get the measurements from the sensor. The payload consists of a humidity value, a temperature value, and a checksum.
Connect the DHT22 as shown in the diagram. The data pin will be connected to NodeMCU pin 12 (D6).
To use the DHT interface, construct the objects referring to their data pin. Start calling the library:
from dht import DHT22
Define the appropriated pin and construct the object:
data = DHT22(Pin(12))
Get the Temperature and Humidity values:
data.measure()
temp = data.temperature()
hum = data.humidity()
print('Temp: {}oC'.format(temp))
print('Hum: {}%'.format(hum))
The DHT11 can be called no more than once per second and the DHT22 once every two seconds for most accurate results. Sensor accuracy will degrade over time. Each sensor supports a different operating range. Refer to the product datasheets for specifics.
DHT22 sensors are now sold under the name AM2302 and are otherwise identical.
I2C is a two-wire protocol for communicating between devices. At the physical level it consists of 2 wires:
- SCL and SDA, the clock and data lines respectively.
I2C objects are created attached to a specific bus. They can be initialized when created or initialized later on.
First, let's import the library:
from machine import I2C
Considering a device on Pin 4 (SDA) and Pin 5 (SCL), let's create an i2c object:
i2c = I2C(scl=Pin(5), sda=Pin(4))
Now, you should scan the I2C bus for eventual devices there. The function below will do this, returning the number of connected devices and its address:
def scanI2c():
print('Scan i2c bus...')
devices = i2c.scan()
if len(devices) == 0:
print("No i2c device !")
else:
print('i2c devices found:',len(devices))
for device in devices:
print("Decimal address: ",device," | Hexa address: ",hex(device))
Let's install an I2C OLED display on our NodeMCU as shown in the diagram. The display is the SSD 1306 (128 x 64).
Running the scan function:
scanI2c()
We will get as a result that 1 device was found at address 0x3c.
This address will be used for an OLED object creation as below:
import ssd1306
i2c = I2C(scl=Pin(5), sda=Pin(4))
oled = ssd1306.SSD1306_I2C(128, 64, i2c, 0x3c)
Some methods to manage the display:
- poweroff(), turns off the screen. Convenient for battery operation.
- contrast(), to adjust the contrast
- invert(), invert the colors of the screen (finally white and black!)
- show(), to refresh the view
- fill(), to fill the screen in black (1) or white (0)
- pixel(), to turn on a particular pixel
- scroll(), scroll the screen.
- text(), to display on text at the indicated x, y position
- Draw lines hline(), vline() or any line line()
- Draw a rect rect rectangle() or rectangle filled fill_rect()
Let's test our display:
oled.fill(0)
oled.text("Hello esp8266", 0, 0)
oled.show()
And now, let's display the DHT22 sensor data on put OLED:
data.measure()
temp = data.temperature()
hum = data.humidity()
oled.fill(0)
oled.text("Temp: " + str(temp) + "oC", 0, 0)
oled.text("Hum: " + str(hum) + "%",0, 16)
oled.show()
This tutorial delivered the bits and pieces for you to build a more robust project, using MicroPython as a programming language and Jupyter as a tool for quick development and as an analysis tool.
Of course, if you want to run a program written in MicroPython on a NodeMCU independent of your PC and Jupyter, you must save your code as a "main.py" file. For example, in any text editor and download it on your device using: Ampy, that is an utility developed by Adafruit, to interact with a MicroPython board over a serial connection.
Ampy is meant to be a simple command line tool to manipulate files and run code on a MicroPython board over its serial connection. With ampy, you can send files from your computer to a MicroPython board's file system, download files from a board to your computer, and even send a Python script to a board to be executed.
Installation:
sudo pip3 install adafruit-ampy
Ampy is made to talk to a MicroPython board over its serial connection. You will need your board connected and any drivers to access it serial port installed. Then, for example, to list the files on the board run a command like:
ampy --port /dev/tty.SLAB_USBtoUART ls
For convenience, you can set an AMPY_PORT environment variable which will be used if the port parameter is not specified. For example, on Linux or OSX:
export AMPY_PORT=/dev/tty.SLAB_USBtoUART
So, from now on, you can simplify commands:
List internal NodeMCU files:
ampy ls
You can read a file installed on your NodeMCU using:
ampy get boot.py
Once you create a file using your text editor (Nano, for example), you can install it on your NodeMCU using:
ampy put main.py
Now, when you press "Reset" button on your NodeMCU, the program that will run first is "main.py".
For Windows, please see the Adafruit instructions here.
Step 12: ConclusionAs always, I hope this project can help others find their way into the exciting world of electronics!
For details and final code, please visit my GitHub depository: Pyhon4DS/Micropython
For more projects, please visit my blog: MJRoBot.org
Saludos from the south of the world!
See you in my next tutorial!
Thank you,
Marcelo
Comments