How to Manage an M5Stack Core2 for AWS. Part 2 – C/C++
The first article discussed M5Stack management based on the Micropython language. Now, we need to dive much deeper into a rabbit hole. Let’s try to use C and C++ only. The most important advantage of using C is the possibility of full, low-lever control of all controller aspects. The most important disadvantage of using C is the necessity of full, low-lever control of all controller aspects. Well… with great power comes great responsibility.
FreeRTOS
AWS FreeRTOS is a real-time operating system dedicated to AWS cloud and resource-constrained devices.
There is a lot of code to write this time, so we’ll use an example directly from AWS. There is no need to burn any firmware with the burning tool; however, we still need to pass the USB port to the WSL environment using usbip, as we’ve done in the “Micropython” section of the first chapter.
You can download the code we’re going to use from https://github.com/m5stack/Core2-for-AWS-IoT-EduKit.git . The only subdirectory we need is Blinky-Hello-World, but the repository is really small, so using a sparse checkout is pointless, and you can simply clone the entire repo.
Open VSCode and install a plugin called PlatformIO. There is a bug in PlatformIO, so you can’t see any files from your WSL environment using the PlatformIO browser (Windows WSL: I can’t open any files(projects) in the PIO open browser. · Issue #2316 · platformio/platformio-home · GitHub). To fix it, close VSCode, edit ~/.platformio/packages/contrib-piohome/main.*.min.js file in Windows, replace “\\”: “/” with “/”: “/”, and open VSCode again.
To verify the connection between PlatformIO and your controller, open PlatformIO from the very left menu and then pick “Devices” from the main left menu. You should see /dev/ttyUSB0 in the center part of the screen. Please remember to pass the USB device to WSL using usbip and to allow all users to use the port with chmod.
If everything looks good so far, you can open the Blinky-Hello-World directory (not the entire cloned repository) as a project from the PlatformIO home screen. Now you can follow the essential elements of the official instruction provided below.
You need to have AWS CLI v2 installed on your machine. If you don’t, you can install it using the official manual: Installing or updating the latest version of the AWS CLI – AWS Command Line Interface (amazon.com)
Now ensure you have a valid token, and you can interact with your AWS account using CLI (I propose listing some resources as the verification, e.g., aws s3 ls).
We will use the built-in script to create a Thing in AWS IoT. Just open a terminal using PlatformIO (standard bash terminal won’t work, so you need to open it from Miscellaneous -> New Terminal from the main PlatformIO menu in VSC), make sure you’re in Blinky-Hello-World directory, and run pio run -e core2foraws-device_reg -t register thing. The script will create the Thing and download the necessary certificate/key files. You can do it manually if you don’t trust such scripts; however, this one is created by the AWS team, so I believe it’s trustworthy.
In the AWS IoT console, go to Manage -> All devices -> Things and see the new Thing created by the script. The Thing name is autogenerated. In my case, it’s 0123FAA32AD40D8501.
OK, the next step is to allow the device to connect to the Internet. There is another script to help you with this task. Call pio run ‐‐environment core2foraws ‐‐target menuconfig. You’ll see a simple menu. Navigate to AWS IoT EduKit Configuration and set up WiFi SSID abd WiFi Password. Be aware that your network’s SSID and password will be stored as plaintext in a few files in your code now.
Let’s build the application. Just call pio run ‐‐environment core2foraws from the PlatformIO terminal and then pio run ‐‐environment core2foraws ‐‐target upload ‐‐target monitor to run it on your device and monitor logs.
Now you can use the MQTT test client from the AWS IoT console to send anything to <<thing name>>/blink topic. In my case, it’s 0123FAA32AD40D8501/blink. The message payload doesn’t matter for this example. Just send something to start blinking and send anything again to stop it.
As you can see, we have done a lot just to communicate between AWS Cloud and the controller. It was much simpler with Micropython and even more with UiFlow. However, C is much more powerful, and what’s most important here, we can extend it with libraries.
TensorFlow Lite for Microcontrollers
TensorFlow is an end-to-end open-source platform for machine learning. TensorFlow Lite is a library for deploying models on mobile, microcontrollers, and other edge devices.
TensorFlow Lite for Microcontrollers is just a lightweight version of TensorFlow Lite designed to run machine learning models on microcontrollers and other devices with only a few kilobytes of memory. The core runtime fits in 16 KB on an Arm Cortex M3 and can run many basic models. It doesn’t require operating system support, any standard C or C++ libraries, or dynamic memory allocation.
TensorFlow Lite is not designed to work on ESP32 processors, so the only one available for M5Stack is TensorFlow Lite for Microcontrollers. It has some limitations – it supports just a limited subset of TensorFlow operations and devices, it requires manual memory management in Low-level C++ API, and it doesn’t support on-device training. Therefore, to build a “learning at the edge” solution, you need a more powerful IoT Edge device, e.g. Raspberry Pi.
But you can still run ML models on the M5Stack controller.
Now, let’s try to modify our Blinky-Hello-World to add the TensorFlow Lite for the Microcontrollers library.
TensorFlow Lite for Microcontrollers in FreeRTOS
The first issue to solve is where to get the TensorFlow source code from. In the main TensorFlow repository, you can find information that it’s moved to a standalone one (https://github.com/tensorflow/tensorflow/tree/master/tensorflow/lite/micro) even if most of the documentation and examples still point there. The standalone repository used to contain a makefile for ESP32, but it seems to be deleted when moving examples to yet another repository (https://github.com/tensorflow/tflite-micro/commit/66cfa623cbe1c1ae3fcc8a4903e9fed1a345548a). Today, the best source seems to be this repository: https://github.com/espressif/tflite-micro-esp-examples/tree/master/components.
We’ll need tfline-lib, but it doesn’t work without esp-nn, so you should copy both to the components directory in your Blinky-Hello-World project.
Let’s modify our code, starting from including tensorflow headers at the beginning of the main.c file.
#include "tensorflow/lite/micro/all_ops_resolver.h" #include "tensorflow/lite/micro/micro_error_reporter.h" #include "tensorflow/lite/micro/micro_interpreter.h" #include "tensorflow/lite/schema/schema_generated.h"
Now we can try to use it. For example, just before the void app_main() function, let’s declare TF error reporter and use it in the function.
tflite::MicroErrorReporter micro_error_reporter; tflite::ErrorReporter* error_reporter = &micro_error_reporter; void app_main() { Core2ForAWS_Init(); Core2ForAWS_Display_SetBrightness(80); ui_init(); TF_LITE_REPORT_ERROR(error_reporter, "Hello TensorFlow" "This is just a test message/n" ); initialise_wifi(); xTaskCreatePinnedToCore(&aws_iot_task, "aws_iot_task", 4096 * 2, NULL, 5, NULL, 1); xTaskCreatePinnedToCore(&blink_task, "blink_task", 4096 * 1, NULL, 2, &xBlink, 1); } }
Obviously, it’s not an actual usage of TensorFlow, but it proves the library is linked and can be used whatever you need.
In the main directory, you must also add new libraries tflite-lib and esp-nn to the required components in CMakeLists.txt
set(COMPONENT_REQUIRES "nvs_flash" "esp-aws-iot" "esp-cryptoauthlib" "core2forAWS" "tflite-lib" "esp-nn")
It looks good, but it won’t work yet. During compilation using pio run –environment core2foraws, you’ll find out that the entire Blinky-Hello-World is made in pure C, and TensorFlow Lite for Microcontrollers library requires C++. The easiest way to convert it is as follows:
- Rename main.c to main.cc
- Change main.c to main.cc in the first line of main/CMakeList.txt
- Create extern “C” {} section for the entire main file code except for tensorflow imports.
It should look somehow like that:
#include "tensorflow/lite/micro/all_ops_resolver.h" #include "tensorflow/lite/micro/micro_error_reporter.h" #include "tensorflow/lite/micro/micro_interpreter.h" #include "tensorflow/lite/schema/schema_generated.h" extern "C" { ######original main.c content goes here###### tflite::MicroErrorReporter micro_error_reporter; tflite::ErrorReporter* error_reporter = &micro_error_reporter; void app_main() { #the main function code from the listing above } }
- In main.cc, delete TaskHandle_t xBlink; declaration because it’s already declared in another file
- In platform.ini, in [env:core2foraws] section add build_flags = -fpermissive to change permissive compilation errors into warnings
Now you can build the project again. When running it with the target –monitor, you’ll see the “Hello TensorFlow” message in logs, which means the TensorFlow library is included and working correctly.
Now, you can do whatever you want with an out-of-the-box machine learning library and AWS integration.
Arduino
As you can see, C is much more powerful but requires much more work. Let’s try to connect the same blocks (tensorflow, AWS IoT, and M5Stack library) but using a more user-friendly environment.
Arduino is an open-source electronic prototyping platform enabling users to create interactive electronic objects. Let’s try to combine the official M5Stack Core 2 for AWS with the Arduino IDE manual (https://docs.m5stack.com/en/quick_start/core2_for_aws/arduino) with TensorFlow Lite for Microcontrollers (https://github.com/tanakamasayuki/Arduino_TensorFlowLite_ESP32).
Hello world!
Firstly, install Arduino IDE from the official page https://www.arduino.cc/en/software. I assume you already have the CP210x driver installed, and the USB mode selected on your device.
Open the IDE, go to File -> Preferences, and add the boards’ management URL: https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/package_m5stack_index.json.
Then open the Boards manager from the left menu and install M5Stack-Core2. Now connect the controller to the computer and choose it from the top drop-down menu.
To use the M5Stack-specific library in the code, you need to open Sketch -> Include Libraries -> Library catalog and install M2Core2.
Now you can write the simple “Hello World!” code and run it with the green arrow in the IDE top menu.
#include <M5Core2.h> void setup(){ M5.begin(); M5.Lcd.print("Hello World"); } void loop() { }
Sometimes, Arduino cannot reset the controller via an RTS pin, so you need to reboot it manually after writing a new code to it.
So far, so good.
TensorFlow and AWS integration
The TensorFlow-official, Arduino_TensorFlowLite_ESP32 library is not designed to be used with M5Stack. Let’s adapt it. Clone the library and copy the Hello World example to another directory. You can open it from Arduino IDE now. It’s a fully working example of the usage of the TensorFlow model. Let’s adapt it to use the M5Core2 library. To hello_world.ino you need to add #include <M5Core2.h> at the beginning of the file and also M5.begin(); at the beginning of void setup() function. You can also add M5.Axp.SetLed(true); after this line to turn on the small green led and ensure the device is running.
Now, start the application. You can see TensorFlow output in the Serial Monitor tab. Just change the baud rate to 115200 to make it human-readable.
Can we mix it with AWS IoT integration? Yes, we can.
We will use the PubSubClient library by Nick O’Leary, so open the library catalog in Arduino IDE and install it, and then let’s connect to AWS IoT and MQTT.
Using Arduino IDE, create a new file secrets.h. We need a few declarations there:
#define AWS_IOT_PUBLISH_TOPIC " m5stack/pub" #define AWS_IOT_SUBSCRIBE_TOPIC " m5stack/sub" #define WIFI_SSID "ThisIsMyWiFiSSID" #define WIFI_PASSWORD "Don't use so easy passwords!" int8_t TIME_ZONE = 2; #define MQTT_HOST "xxxx.iot.eu-west-1.amazonaws.com" #define THINGNAME "UiFlow_test" static const char* ca_cert = R"KEY( -----BEGIN CERTIFICATE----- … -----END CERTIFICATE----- )KEY"; static const char* client_cert = R"KEY( -----BEGIN CERTIFICATE----- … -----END CERTIFICATE----- )KEY"; static const char* privkey = R"KEY( -----BEGIN RSA PRIVATE KEY----- … -----END RSA PRIVATE KEY----- )KEY";
AWS_IOT_PUBLISH_TOPIC and AWS_IOT_SUBSCRIBE_TOPIC are our test topics we’re going to use in this example. WIFI_SSID and WIFI_PASSWORD are our WiFi credentials. TIME_ZONE is the time zone offset. MQTT_HOST is the public AWS IoT endpoint (the same as in the first UiFlow example). THINGNAME is the name of Thing in AWS (I’ve used the same as in the UiFlow example). client_cert and privkey, you need to copy from the secrets generated when creating Thing for the UiFlow example. ca_cert is the public key of AWS certificate authority, so you can obtain it from the Thing creation wizard (certificate step) or from https://good.sca1a.amazontrust.com/).
Now it’s time to adapt the main hello_world.ino file.
We should add new imports (including our secret.h file).
#include <WiFiClientSecure.h> #include <PubSubClient.h> #include "secrets.h" #include <time.h>
Then we need a few new fields.
WiFiClientSecure net; PubSubClient client(net); time_t now; time_t nowish = 1510592825;
The field nowish is just some timestamp in the past.
In the setup() function, we need to open a WiFi connection with our local network and the Internet, set up the time to check certificates, install the certificates, set up the MQTT client, and open the AWS IoT connection.
delay(3000); WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); WiFi.waitForConnectResult(); while (WiFi.status() != WL_CONNECTED) { Serial.print("."); delay(1000); } M5.Lcd.println(String("Attempting to connect to SSID: ") + String(WIFI_SSID)); M5.Lcd.println(WiFi.localIP()); M5.Lcd.print("Setting time using SNTP"); configTime(TIME_ZONE * 3600, 0 * 3600, "pool.ntp.org", "time.nist.gov"); now = time(nullptr); while (now < nowish) { delay(500); Serial.print("."); now = time(nullptr); } M5.Lcd.println("done!"); struct tm timeinfo; gmtime_r(&now, &timeinfo); M5.Lcd.print("Current time: "); M5.Lcd.print(asctime(&timeinfo)); net.setCACert(ca_cert); net.setCertificate(client_cert); net.setPrivateKey(privkey); client.setServer(MQTT_HOST, 8883); client.setCallback(messageReceived); M5.Lcd.println("Connecting to AWS IOT"); while (!client.connect(THINGNAME)) { Serial.print("."); delay(1000); } if (!client.connected()) { M5.Lcd.println("AWS IoT Timeout!"); return; } client.subscribe(AWS_IOT_SUBSCRIBE_TOPIC); M5.Lcd.println("AWS IoT Connected!");
This is an entire code needed to set up the application, but I propose splitting it into multiple smaller and more readable functions. As you can see, I use Serial output for debugging.
To receive messages, we need a new function (the name matches the declaration in client.setCallback(messageReceived);)
void messageReceived(char *topic, byte *payload, unsigned int length) { M5.Lcd.print("Received ["); M5.Lcd.print(topic); M5.Lcd.print("]: "); for (int i = 0; i < length; i++) { M5.Lcd.print((char)payload[i]); } M5.Lcd.println(); }
The last thing to do is to loop the client with the entire application. To do that, just add a one-liner to the loop() function:
client.loop();
You need another one-liner to send something to AWS, but I’ve added two more to make it visible on the controller’s display.
M5.Lcd.println("Sending message"); client.publish(AWS_IOT_PUBLISH_TOPIC, "{\"message\": \"Hello from M5Stack\"}"); M5.Lcd.println("Sent");
The communication works both ways. You can subscribe to m5stack/pub using the MQTT Test client in the AWS console to read messages from the controller, and you can publish to m5stack/sub to send messages to the controller.
As you can see, using Arduino is easier than using FreeRTOS, but unfortunately, it’s a little bit babyish. Now we’ll try to avoid all IDE’s and use pure console only.
Espressif IoT Development Framework
Basically, there are three ways to burn software to the controller from a Linux console – Arduino, esptool.py, and ESP-IDF. When you create a new project using PlatformIO, you can pick Arduino or ESP-IDF. Now, let’s try to remove the IDE from the equation and use a pure bash.
First of all, you need to install a few prerequisites and then download and install the library.
sudo apt install git wget flex bison gperf python3 python3-venv cmake ninja-build ccache libffi-dev libssl-dev dfu-util libusb-1.0-0 mkdir -p ~/esp cd ~/esp git clone --recursive <a href="https://github.com/espressif/esp-idf.git">https://github.com/espressif/esp-idf.git</a> cd ~/esp/esp-idf ./install.sh esp32 ./export.sh
Please note you need to run install and export (last two commands) whenever you open a new WSL console. With the library, you also have some examples downloaded. Run one of them to check, does everything work.
cd examples/get-started/hello_world/ idf.py set-target esp32 set ESPPORT=/dev/ttyUSB0 idf.py build flash monitor
You should see the output like this one.
Hello world! This is an esp32 chip with 2 CPU core(s), WiFi/BT/BLE, silicon revision 300, 2MB external flash Minimum free heap size: 295868 bytes Restarting in 10 seconds... Restarting in 9 seconds... Restarting in 8 seconds... Restarting in 7 seconds... Restarting in 6 seconds... Restarting in 5 seconds... Restarting in 4 seconds... Restarting in 3 seconds... Restarting in 2 seconds... Restarting in 1 seconds... Restarting in 0 seconds... Restarting now.
To stop the serial port monitor, press CRTL + ]. Be aware that the application is still running on the controller. You need to power off the device by the hardware button on the side to stop it.
If you want to use TensorFlow Lite for Microcontrollers with ESP-IDF, you need to create a new project and add a proper library. You can use the command idf.py create-project <<project_name>> to create a project. My project name is hello_tf. The script creates a pure C project; we need to rename hello_tf.c file to hello_tf.cc. Then, we can copy tflite-micro and esp-nn libraries from FreeRTOS example and place them in the components directory. The main/CMakeList.txt content should be like that.
set(COMPONENT_SRCS "hello_tf.cc") set(COMPONENT_REQUIRES "tflite-lib" "esp-nn") register_component()
As you can see, the default components sources definition is changed, and new libraries are added.
Now, let’s see the main hello_tf.cc file content.
#include "tensorflow/lite/micro/all_ops_resolver.h" #include "tensorflow/lite/micro/micro_error_reporter.h" #include "tensorflow/lite/micro/micro_interpreter.h" #include "tensorflow/lite/schema/schema_generated.h" extern "C" { tflite::MicroErrorReporter micro_error_reporter; tflite::ErrorReporter* error_reporter = &micro_error_reporter; void app_main(void) { TF_LITE_REPORT_ERROR(error_reporter, "Hello from TensorFlow\n"); } }
As you can see, we had to use extern “C” block again because, by default, ESP-IDF runs the void app_main() function from C, not C++ context.
To run the application run idf.py build flash monitor.
In the same way, you can add other libraries needed, but without PlatformIO, dependency management is tricky, especially for the core2forAWS library with multiple dependencies. Alternatively, you can use https://github.com/m5stack/M5Stack-IDF as a library with M5Stack dependencies to control the I/O devices of the controller.
Summary
As I wrote at the beginning of this article, with C++, you can do much more; however, you are forced to manage the entire device by yourself. Yes, you can use AWS integration, M5Stack I/O interfaces, and TensorFlow (TensorFlow Lite for Microcontrollers version only) library together, but it requires a lot of code. Can we do anything to join the advantages of using Micropython and C together? Let’s try to do it in the last chapter.
Check related articles
Read our blog and stay informed about the industry's latest trends and solutions.
see all articles