How to Create a DIP for FreeRTOS
Introduction
FreeRTOS is an open source, embedded, realtime operating system that can be custom tailored for very specific applications on all sorts of hardware. This makes it challenging to create a single Labscale DIP to cover all bases. In this document we will cover how to build a DIP for and RTOS based device, specifically for an Espressif ESP32 chipset.
The purpose of the Labscale DIP is to monitor the device's health and to potentially pull metrics. The ESP32 chipset provides Wifi and a serial USB UART. One or both of these cam be leveraged for monitoring, though serial is the better of the two interfaces because it is more or less always highly available. In this example, we will focus on monitoring the device via the serial UART.
Monitoring via the serial interface
There are various ways this can be done. The minimal and easiest method is to
simply rely on the existence of the serial device in the /dev
directory on the
host machine. If /dev/ttyUSBx
exists, then we know something is plugged in.
However, this isn't the most ideal method because there is no indication of the
device's actual health.
A second option has the device write something to the serial port periodically. This signal, or heartbeat, is read by the DIP and translated into an online state. The drawback of this method is that the signal will have to be sent often enough for the DIP to catch when it is briefly connected to the serial device (the DIP only polls the serial port and will not stay connected). In addition to that, if the device is offline, the DIP will have to stay connected waiting for a heartbeat signal until a timeout occurs which is not terribly efficient. Also, you may not want the device constantly sending data to the serial port if you plan to use the serial port for other things, like logging or monitoring.
The final option is to provide a simple, interactive, command line interface
where the DIP can send a request and the device immediately sends back a
response. This is ideal because it allows the DIP to connect for just long
enough to send a ping
and read an ACK
over the serial port. It also keeps
the serial channel clean of unnecessary data.
In this tutorial we are going to implement a very simple, interactive command line interface for the ESP32 using the ESP-IDF framework.
Create the CLI
Prerequisites
Before you begin, it is recommended that you first familiarize yourself with creating DIPs with our How to Create a DIP guide. You will also need to install the build tools required by the Espressif IoT Development Framework, or ESP-IDF for short.
- For Ubuntu, execute the following command in a terminal:
sudo apt-get install git wget flex bison gperf python3 python3-pip python3-venv cmake ninja-build ccache libffi-dev libssl-dev dfu-util libusb-1.0-0
- For access to the serial port, you will need to add your user to the
dialout group:
sudo adduser <your username> dialout
- For access to the serial port, you will need to add your user to the
dialout group:
- For Mac OS:
brew install cmake ninja dfu-util
- For M1 Macs, also install Rosetta2:
/usr/sbin/softwareupdate --install-rosetta --agree-to-license
- For M1 Macs, also install Rosetta2:
Install the ESP-IDF
The ESP-IDF repository is found on GitHub and can be cloned to the local file system; however, it is best to put it into a folder with other ESP related projects. Open a terminal and type the following commands:
mkdir -p ~/esp
cd ~/esp
git clone --recursive https://github.com/espressif/esp-idf.git
Once the repo is downloaded, you will now want to set up the tools for your specific chipset. To do this run the following:
cd ~/esp/esp-idf
./install.sh esp32
Create an RTOS application
Setup
Now, before working on any ESP-IDF projects, you will have to setup up the environment. To do this, run the following commands in a terminal:
cd ~/esp/esp-idf
. ./export.sh
You will have to run the previous command any time you open a new terminal and work on an ESP project.
Create the project folder
want to create a new project for your ESP32 application. Simply type the following into a terminal:
cd ~/esp
mkdir -p ping
cd ping
Create ping.c
Create the main.c file in its own subdirectory:
mkdir -p main
And then copy the following code and save it to main/ping.c
:
#include <stdio.h>
#include "esp_console.h"
#ifndef CONFIG_CONSOLE_MAX_COMMAND_LINE_LENGTH
#define CONFIG_CONSOLE_MAX_COMMAND_LINE_LENGTH 80
#endif
#define PROMPT_STR CONFIG_IDF_TARGET
static int ping(int argc, char **argv)
{
printf("ACK\n");
return 0;
}
void register_labscale_ping(void)
{
const esp_console_cmd_t ping_cmd = {
.command = "ping",
.help = "Labscale ping",
.hint = NULL,
.func = &ping,
};
ESP_ERROR_CHECK(esp_console_cmd_register(&ping_cmd));
}
void app_main(void)
{
esp_console_repl_t *repl = NULL;
esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT();
// Define the prompt and length of the command line, e.g. '[esp32]>' & 80 chars.∂
repl_config.prompt = PROMPT_STR ">";
repl_config.max_cmdline_length = CONFIG_CONSOLE_MAX_COMMAND_LINE_LENGTH;
// Register the Labscale 'ping' command.
register_labscale_ping();
esp_console_dev_uart_config_t hw_config = ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_console_new_repl_uart(&hw_config, &repl_config, &repl));
// Start the console's main loop
ESP_ERROR_CHECK(esp_console_start_repl(repl));
}
This is essentially all there is to creating the interactive console that
accepts a "ping" command and responds with "ACK". For simplicity we keep the
console in the main thread, but this could just easily be put into another
thread with xTaskCreate()
.
Create CMakeLists.txt
Now copy the following text to ~/esp/ping/main/CMakeLists.txt
:
idf_component_register(SRCS "ping.c"
INCLUDE_DIRS "."
REQUIRES console)
And the following to CMakeLists.txt
in the project folder (i.e.
~/esp/ping/CMakeLists.txt
):
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(main)
Build the image
To build, open a terminal and do the following:
cd ~/esp/esp-idf
. ./exports.sh
cd ~/esp/ping
idf.py build
If everything is installed properly, it should build without any errors.
Flash the image
Once built, you can test it by connecting the ESP32 to your host via USB, flashing this application to the device:
cd ~/esp/ping
idf.py -p /dev/ttyXXX flash
Connecting......
Chip is ESP32-D0WD-V3 (revision v3.1)
Features: WiFi, BT, Dual Core, 240MHz, VRef calibration in efuse, Coding Scheme None
Crystal is 40MHz
MAC: 48:e7:29:b7:ae:68
Uploading stub...
Running stub...
Stub running...
Changing baud rate to 460800
Changed.
Configuring flash size...
Flash will be erased from 0x00001000 to 0x00007fff...
Flash will be erased from 0x00010000 to 0x0004bfff...
Flash will be erased from 0x00008000 to 0x00008fff...
Compressed 26368 bytes to 16443...
Writing at 0x00001000... (50 %)
Writing at 0x000076b1... (100 %)
Wrote 26368 bytes (16443 compressed) at 0x00001000 in 0.8 seconds (effective 260.5 kbit/s)...
Hash of data verified.
Compressed 243776 bytes to 133976...
Writing at 0x00010000... (11 %)
Writing at 0x0001cd56... (22 %)
Writing at 0x00022743... (33 %)
Writing at 0x000280d3... (44 %)
Writing at 0x0002dc77... (55 %)
Writing at 0x000371c1... (66 %)
Writing at 0x0003d67b... (77 %)
Writing at 0x00044a41... (88 %)
Writing at 0x0004a57d... (100 %)
Wrote 243776 bytes (133976 compressed) at 0x00010000 in 3.4 seconds (effective 569.0 kbit/s)...
Hash of data verified.
Compressed 3072 bytes to 103...
Writing at 0x00008000... (100 %)
Wrote 3072 bytes (103 compressed) at 0x00008000 in 0.1 seconds (effective 346.0 kbit/s)...
Hash of data verified.
Leaving...
Hard resetting via RTS pin...
[100%] Built target flash
Done
Test the image
Once the image has been flashed to the device, you can connect to serial and test the ping function:
idf.py -p /dev/ttyXXX monitor
--- idf_monitor on /dev/ttyXXX 115200 ---
--- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---
ets Jul 29 2019 12:21:46
rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:2
load:0x3fff0030,len:6940
ho 0 tail 12 room 4
load:0x40078000,len:15500
load:0x40080400,len:3844
0x40080400: _init at ??:?
I (0) cpu_start: App cpu up.
I (226) cpu_start: Pro cpu start user code
I (226) cpu_start: cpu freq: 160000000 Hz
I (226) cpu_start: Application information:
I (231) cpu_start: Project name: main
I (235) cpu_start: App version: 1
I (240) cpu_start: Compile time: Nov 3 2023 12:16:23
I (246) cpu_start: ELF file SHA256: 0c1a04b17bac6457...
I (252) cpu_start: ESP-IDF: v5.0.2
I (257) cpu_start: Min chip rev: v0.0
I (262) cpu_start: Max chip rev: v3.99
I (266) cpu_start: Chip rev: v3.1
I (271) heap_init: Initializing. RAM available for dynamic allocation:
I (278) heap_init: At 3FFAE6E0 len 00001920 (6 KiB): DRAM
I (284) heap_init: At 3FFB2810 len 0002D7F0 (181 KiB): DRAM
I (291) heap_init: At 3FFE0440 len 00003AE0 (14 KiB): D/IRAM
I (297) heap_init: At 3FFE4350 len 0001BCB0 (111 KiB): D/IRAM
I (303) heap_init: At 4008C9C4 len 0001363C (77 KiB): IRAM
I (311) spi_flash: detected chip: generic
I (314) spi_flash: flash io: dio
W (318) spi_flash: Detected size(4096k) larger than the size in the binary image header(2048k). Using the size in the binary image header.
I (332) cpu_start: Starting scheduler on PRO CPU.
I (0) cpu_start: Starting scheduler on APP CPU.
Type 'help' to get the list of commands.
Use UP/DOWN arrows to navigate through command history.
Press TAB when typing command name to auto-complete.
esp32>
Now type ping...
esp32> ping
ACK
esp32>
To escape from the monitor, type CNTL+]
Create the RTOS DIP
Now that we have an RTOS device that can provide useful feedback, we can write a DIP to takes advantage of it. If you have not already read How to Create a DIP, you might want to read that first to acquaint yourself with terms and concepts related to DIPs. In this example we will be writing the DIP in Python.
Create the DIP folder
First you will want to create a project folder:
mdkir -p ~/rtos_dip
Next create the install script to install dependencies. In this demo, we will be installing dependencies into the Python virtual environment. We use virtualenv to isolate this DIP from other DIPs that may have different dependencies, and also to make it more simple for deploying setting up on new hosts.
#!/bin/bash -ex
EXEC_DIR="$(dirname "${0}")"
python3 -c "import virtualenv" 2>/dev/null || python3 -m pip install virtualenv
python3 -m virtualenv "${EXEC_DIR}"
source "${EXEC_DIR}/bin/activate"
python3 -m pip install pip --upgrade
python3 -m pip install filelock
python3 -m pip install pexpect-serial
Save this to rtos_dip/install.sh
, and be sure to
chmod +x rtos_dip/install.sh
. Notice that we will be installing Python Pexpect
and Filelock. Python Pexpect Serial makes it simple to connect over serial
and monitor for specifc output, and Filelock is used to coordinate access
to the serial device between the DIP and Jobs. You can test the script:
cd ~/rtos_dip
./install.sh
The script should create a virtual environment and install filelock and pexpect-serial into it. To activate the virtual environment, type:
cd ~/rtos_dip
. ./bin/activate
Create the dip.py
Now we have to write some code to open the serial port, send a ping, and wait for an ACK
import json
import os
from filelock import FileLock, Timeout
from contextlib import contextmanager
from typing import Union
import serial
from pexpect_serial import SerialSpawn
# Prequisites:
# - ESP32 board connected to USB
#
# For more information visit:
#
PORT = os.environ["LS_DIP_serial_port"]
devname = os.path.basename(PORT)
lockpath = os.path.join("/tmp/labscale_agent/run", f"{devname}.lock")
def cleanstr(output:Union[bytes, None]) -> str:
if output is None:
return ""
return output.replace(b"\x04", b"").decode('utf-8').strip()
@contextmanager
def Serial(port:str, baud:int=115200, timeout:int=0) -> serial.Serial:
with FileLock(lockpath):
s = serial.Serial(port, baudrate=baud, timeout=timeout)
ss = SerialSpawn(s)
try:
yield ss
finally:
s.close()
def ping_board(chip=None) -> bool:
if not os.path.exists(PORT):
return False
# Acquire the shared lockfile used by the
# test suites and the DIP. If the lockfile
# is locked, then it must be a test suite
# holding the lock. In this case we assume
# the device is online.
lock = FileLock(lockpath)
try:
lock.acquire(timeout=0.25)
except Timeout:
# Device is in use, assume it is online.
return True
finally:
lock.release()
# Ping the board
try:
with Serial(PORT, baud=BAUD, timeout=1.0) as s:
s.sendline('')
s.expect('.*>', timeout=0.5)
s.sendline('ping')
s.expect('ACK', timeout=0.5)
return True
except:
return False
def get_status() -> dict:
status = "online" if ping_board() else "offline"
return dict(status=status)
def get_state() -> dict:
out = get_status()
# The following info can either be pulled
# from the hardware or read from metadata
# provided by files produced by the the
# build system.
out.update({
"serialNumber": "<add data here>",
"softwareVersion": "<add data here>",
"state": {
"model": "<add data here>",
"baseSystemVersion": "<add data here>",
}
})
return out
if __name__ == "__main__":
from argparse import ArgumentParser
commands = {
"get_state": get_state,
"get_status": get_status,
}
parser = ArgumentParser()
parser.add_argument("command", nargs=1, choices=commands.keys())
args = parser.parse_args()
command = args.command[0]
print(json.dumps(commands[command]()))
Save this to rtos_dip/dip.py
. To test it, connect the ESP32 (with the RTOS
application flashed to the board) to USB, then run the following command:
LS_DIP_serial_port=/dev/ttyUSBxxx python3 dip.py get_status
You should see:
{status: online}
Create the exec.sh
Because we using the Python virtual environment, we will want to activate it on any calls to the DIP by first calling a wrapper script.
#!/bin/bash -e
EXEC_DIR="$(dirname "${0}")"
source "${EXEC_DIR}/bin/activate"
exec $@
Save this to ~/rtos_dip/exec.sh
and then execute
chmod +x ~/rtos_dip/exec.sh
.
Create the dip.yaml configuration file
Then create a configuration file used by the agent to call functions in the DIP.
name: rtos_dip
version: 1.0.0
commands:
dip_install: "${DIP_ROOT}/install.sh"
get_status: "${DIP_ROOT}/exec.sh python3 ${DIP_ROOT}/dip.py get_status"
get_state: "${DIP_ROOT}/exec.sh python3 ${DIP_ROOT}/dip.py get_state"
And save it to ~/rtos_dip/dip.yaml
. With this, we should have a working DIP
for our RTOS device.
Create the DIP package
Now that we have a working dip, it needs to packaged for use by Labscale. To do
this, use tar
:
tar czf rtos_dip.tgz -C ~/rtos_dip
Create the RTOS Device Type
- In the LabScale home screen, select the Labs & Devices menu item in the navbar on the left side of the page.
- Once in the Labs & Devices page, select the Device Types menu item in the nav bar.
- Click on the ADD DEVICE TYPE button in the top right corner.
- Enter a Name of the device type and the name of the DIP, in this case RTOS.
- Click on the SUBMIT button.
Update the Device Type with an environment specification
Then we must configure a DIP environment specification. This environment specification defines the form that is provided to the user during device creation where they can input the values of the environment varaibles.
- In the LabScale home screen, select the Labs & Devices menu item in the navbar on the left side of the page.
- Once in the Labs & Devices page, select the Device Types menu item in the nav bar.
- On the Device Types page, click on the name of the device type you created above.
- In the Details page, click on the CONFIGURE DIP ENV SPEC.
- Once the modal dialog panel appears, click on the ADD FIELD button.
- Enter the name of variable, for this example we will use serial_port.
- Next the type of value this field will accept, select String.
- Click on the Submit button.
Upload the DIP package
Now that we have a DIP, we need bind it to a device type.
- In the LabScale home screen, select the Labs & Devices menu item in the navbar on the left side of the page.
- Once in the Labs & Devices page, select the Device Types menu item in the nav bar.
- Again on the Device Types page, click on the name of the device type you created above.
- In the Details page, click on the UPLOAD DIP.
- Click on the Upload box and select the rtos_dip.tgz package you created from the previous examples, or drag and drop it onto the box.
- Click on the Submit button.
Create a device
Once we have a device type, we can now create a device of the RTOS type.
- In the LabScale home screen, select the Labs & Devices menu item in the navbar on the left side of the page.
- Once in the Labs & Devices page, select the Devices menu item in the nav bar.
- Click on the ADD DEVICE button.
- Enter the Name of the device, e.g. esp32-rtos
- Select which agent will be monitoring and executing tests on the device.
- Select a Pool if one is available, this optional for this example.
- Select the example device type.
- You should also notice that in the Environment Configuration on the modal dialog, there is a field labeled serial_Port, enter the serial port device that the ESP32 board is connected to, e.g. /dev/ttyUSBxxx
- Click on the SUBMIT button.
After this, you should see your esp32-rtos device appear as online
in the
Labscale devices page.