Interface and Application Programming
Task
Group assignment:
- Compare as many tool options as possible
Individual assignment:
- Write an application that interfaces a user with an input &/or output device that you made.
Reflection
I decided that I wanted to use this week to try a bit of web development. It's a powerful tool to have in one's toolbox and I've wanted to try it for a long time.
During the local class I realized that most options require having a computer that runs the interface and is connected to the MCU, usually through a cable. I wanted a standalone solution in order to be able to later integrate it into Bonobo Lights. Cycling while carrying an open laptop wouldn't be fun!
Framework?
Flutter is Dart-based and compiled.
React Native is JS-based (you can use TypeScript) and interpreted.
What have other participants done?
I've checked a few assignments. They mostly use processing (specifically P5.js). Some write their apps in vanilla html + jss. One used streamlit! which is intriguing because it's super easy, but I don't think it can run in the ESP32 (or can it?). Another used QT.
Joe Wallace created a pretty cool interface with bare html, css, js and ESPAsyncWebServer.
Building a React app and serving it from an ESP32 web server
In the end I asked Tony for his opinion and I went for React. The task is then to 1) build a simple React app on my computer, 2) Set up a web server that can run on an ESP32, and 3) Integrate the React app into the Arduino project so that the web server serves the app and listens for input.
Searching "Arduino React" I found this pretty great Arduino Full Stack Tutorial, which I followed with only small variations.
Setup and creation of the basic React app
For this, I'm following instructions by ChatGPT until they break.
1. Install node.js
I'm installing it with conda in order not to mess with my system.
$> conda create -n react
$> conda activate react && conda install nodejs -c conda-forge
$> npm --version
10.5.0
2. Create React project
First, navigate to the mother folder and then create the project with:
$> npx create-react-app react-gui --template typescript
$> cd react-gui
3. Start the Development Server
npm start
4. Install additional modules
Recommended by ChatGPT
$> npm install react-router-dom
$> npm install @types/react-router-dom
$> npm install styled-components
$> npm install @types/styled-components
$> npm install axios
5. Set up develompment environment
I'm installing Sublime, as recommended by Tony.
- Download Sublime.
- Install Package Control (in the
Tools
section) - Install recommended (by ChatGPT) packages:
- Open Command Palette (command-shift-p)
- Package Control: Advanced Install Package
-
TypeScript, Babel, ESLint, Prettier, Emmet, SublimeLinter, SublimeLinter-contrib-eslint.
-
Open
react-gui/src/App.tsx
.
Done!
6. Play around, check it's working
I've run the default app by executing:
conda activate react && npm start
After playing with it and touching some things up, this is what I get:
Seems like I've got everything set up!
Plain text html served from the ESP32
My first step has been to basically copy the code from the networking class:
Basic web server. Click to show code.
#include "WiFi.h"
#include "ESPAsyncWebServer.h"
// Set your access point network credentials
const char* ssid = "bonobo-access-point";
const char* password = "123456789";
// Create AsyncWebServer object on port 80
AsyncWebServer server(80);
void setup(){
// Serial port for debugging purposes
Serial.begin(115200);
Serial.println();
// Setting the ESP as an access point
Serial.print("Setting AP (Access Point)…");
// Remove the password parameter, if you want the AP (Access Point) to be open
WiFi.softAP(ssid, password);
IPAddress IP = WiFi.softAPIP();
Serial.print("AP IP address: ");
Serial.println(IP);
server.on("/button", HTTP_GET, [](AsyncWebServerRequest *request){
request->send_P(200, "text/plain", "hey there");
});
// Start server
server.begin();
}
void loop() {
digitalWrite(LED_BUILTIN, HIGH);
delay(250);
digitalWrite(LED_BUILTIN, LOW);
delay(250);
}
It can be connected to from my phone!
I've removed a button that was there in the original example because React will allow me to grow the app farther.
Next step, deploying the React app!
Putting it all together: React on ESP32
I'm following the following Medium post: Arduino Full Stack Tutorial, by the author of aWOT, a "fast, unopinionated, minimalist web server library for Arduino.". Others that seem like they could be very interesting is React on the ESP32 with PlatformIO and Building a live data feed BLE application with React Native and ESP32, but I'm not following them for now.
To deploy the React app:
Create the .ino sketch
Run from the React project root folder:
$> npm install awot-scripts -g
$> npx awot-create BonoboServer
This creates a BonoboServer
folder inside with auto-generated code:
$> ll BonoboServer
total 120
-rw-r--r-- 1 dani staff 529B 14 jun 14:15 BonoboServer.ino
-rw-r--r-- 1 dani staff 421B 14 jun 14:15 StaticFiles.h
-rw-r--r-- 1 dani staff 37K 14 jun 14:15 aWOT.cpp
-rw-r--r-- 1 dani staff 10K 14 jun 14:15 aWOT.h
`StaticFiles.h` contains the html source and a description of the routes. Click to show code.
void static_index(Request &req, Response &res) {
P(index) =
"<html>\n"
"<head>\n"
"<title>Hello World!</title>\n"
"</head>\n"
"<body>\n"
"<h1>Greetings middle earth!</h1>\n"
"</body>\n"
"</html>";
res.set("Content-Type", "text/html");
res.printP(index);
}
Router staticFileRouter;
Router * staticFiles() {
staticFileRouter.get("/", &static_index);
return &staticFileRouter;
}
After uploading the code, accessing from my laptop the IP shown in the Serial monitor will show:
Then, it's a matter of porting the React app into that skeleton
Api endpoints
From "Back-end Development" in the Arduino Full Stack Tutorial: to create an api endpoint we need to include in the .ino
:
- A function that takes a
Request &req
and aResponse &res
. It can then read or write like this:req.read()
,res.write()
. - A line associating those to the app:
app.get("/led", &readLed); app.put("/led", &updateLed);
app.use(staticFiles());
My server will send a message through I2C to the spoke unit. That is, I need a nextPattern
API endpoint that will do that. For now, I'll simulate that with a toggleLED
function and a /toggle
endpoint:
void setup() {
pinMode(LED_PIN, OUTPUT);
Serial.begin(115200);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println(WiFi.localIP());
app.use(staticFiles());
app.get("/toggle", &toggleLED);
server.begin();
}
void toggleLED(Request &req, Response &res){
digitalWrite(LED_PIN, !digitalRead(LED_PIN));
Serial.println("Toggle");
}
So far, so good:
Done!
Front-end
- Dev frontend locally, route API calls to the board: change proxy for the API calls. In
package.json
, set "proxy" to the board IP:
`package.json`. Click to show code.
{
"name": "react-gui",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.98",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-scripts": "5.0.1",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"proxy": "http://172.16.21.147/"
}
I'm going to skip installing react-toggle-button
from the Arduino Full Stack Tutorial. It uses state, which I don't want ot to use until I am ready to fully understand it. I'll use a regular button.
-
App.js
:import React from 'react'; import logo from './logo.svg'; import './App.css'; function App() { return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Click on the button to change pattern. </p> <button onClick={toggleLED}>Click me if you dare man</button> </header> </div> ); } function toggleLED(){ fetch('/toggle') .then(response => console.log(response.text)) } export default App;
-
Run the app locally with
npm start
from the root folder - Profit!
This corresponds to this commit in the bonobo-lights repo.
Building and deploying on the board itself
Now I need to integrate the React app into the generated BonoboServer.ino
code, so that clicking the button will exercise the toggle
API endpoint and everything will be running in the board.
Execute npm run build
in the root folder of the React project:
$> npm run build
> react-gui@0.1.0 build
> react-scripts build
Creating an optimized production build...
Compiled successfully.
File sizes after gzip:
46.55 kB (+39 B) build/static/js/main.be0d81ad.js
1.78 kB build/static/js/453.bcf1b5ed.chunk.js
464 B build/static/css/main.0aed41c0.css
The project was built assuming it is hosted at /.
You can control this with the homepage field in your package.json.
The build folder is ready to be deployed.
You may serve it with a static server:
npm install -g serve
serve -s build
Arduino Full Stack Tutorial says: "To convert the files in the build directory to a Arduino compatible format we need first add some configurations to the package.json".
package.json
: add this to the "scripts" object
"dist": "awot-static"
and this to the root, after "proxy":
"awot-static": {
"sources": "./build",
"indexFile": "index.html",
"sketchDir": "./BonoboServer",
"exclude": [
"*.map",
"service-worker.js"]
}
BonoboServer
is the name of the folder I created earlier with npx awot-create BonoboServer
. It will be different for diferent projects.
Now, we're ready to do:
npm run dist
This integrates the React project into the StaticFiles.h
, so it's ready to be uploaded with the Arduino IDE and run.
IT WORKS!!!
Next step: integrate into bonobo-lights.ino
For my project, I'm going to implement an API endpoint that will change to the next pattern. The process will be:
- User clicks on button.
- Server running on hubunit sends "nextPattern" message through I2C to the spoke unit.
- Spoke unit executes its
nextPattern()
function.
References
- ESP32-S3: Which Pins Should I Use?
- Arduino Full Stack Tutorial
- aWOT
- React on the ESP32 with PlatformIO
- Using TypeScript with React
- Building a live data feed BLE application with React Native and ESP32
- Lopaka, a graphics and user interface editor for electronic projects created by Mikhail Ilin
- Example p5 sketch reading from Serial