Interface and Application Programming

Week of Nov 27, 2024

Write an application that interfaces a user with a self-made input/output device

For this week I wanted to find a way to remotely control the moving of a servo motor. Potential applications include being able to remotely trigger release of my convertible helmet based on sensor or user input from a different geographical location. Putting on my city planning hat, I wonder if the networking and interface application programming aspects taught in HTMAA could be applied to physical interventions at a city scale - perhaps have sensors installed throughout a large geographic area, that is connected to mechanisms serving a civic function during times of emergency?

This week's assignment thus required selecting a frontend interface to connect to my output device. As part of the "group assignment" this week of comparing tool options, I explored a few different user interface possibilies - React since it was mentioned in class and frequently pops up on UI forums; Render since my classmate Adi used it; and Shiny since I am learning how to use it this week for a data science in planning class.


Group Assignment: Compare Tool Options

React

Render

Shiny

Features Frontend JS library for web interfaces Full-stack platform for deploying apps; not a UI-builder Integrated backend (server logic) and frontend (UI) for web apps
Use Cases Highly interactive UIs Hosting of fully developed apps Integration with data visualization; quick prototyping
Limitations Need JS expertise and more complex configuration setups Low bandwidth limits and expensive tiered pricing Need R expertise; limited to small-scale deployment

Individual Assignment: Write an application that interfaces a user with a self-designed input/output device

Since I am learning to use RStudio for another class, I decided to try to connect RStudio's ShinyApp to my servo motor. This requires me to write a program in R for the ShinyApp (front-end web user interface), and code for uploading to my microcontroller.

Building upon last week's ESP32-S3 code and past week's servo movement code, trawling online forums for collective wisdom on ESP32-C3, and with the help of Mx ChatGPT, I wrote an Arduino sketch, but could not get the IP address to print. At this point I also decided that I do not want my application to have to constantly have to update the IP address in ShinyApp based on the wifi network my microcontroller was connecter to, and so searched online for solutions to streamline and automate this. I learnt about Firebase this way, and proceeded to set up a Firebase account.

Firebase console interface

Learning from last week's debugging, I tried troubleshooting steps that worked previously, and what worked time round was erasing the microcontroller's memory before each upload using Terminal. As it was a different microcontroller, the python code had to be adjusted to "esptool.py --port (insert port address e.g. "/dev/cu.usbmodem123") erase_flash" instead of last week's "esptool.py --chip ESP32S3 --port /dev/cu.usbmodem123".

Similar error as last week...

I also adjusted other settings under Tools within the Arduino IDE, but these were done before erasing the flash via Terminal, so it's inconclusive whether they helped. Either way, shown here are the modified settings in place at the point of my eventually successful code uploads (notably enabling the erasing of flash memory before upload). The full code/ Arduino sketch uploaded to the ESP32-C3 microcontroller is at the end of this page.

Settings within the Arduino IDE

Another issue I faced with the ESP32 code was whether to include "FIREBASE_AUTH" or not. Since I had set "Realtime Database > Rules" to be "true" to allow reading and writing, I initially thought that I could drop the "AUTH" lines in the ESP32 code since the Firebase console was not password protected. It turns out that there is a "secret key" function within Firebase that I could generate though (Firebase - Project Overview - Project Settings - Service Accounts), so I incorporated that back into my ESP32 code, and it worked.

Firebase project settings for generating a secret key

The last salient issue (out of numerous issues that I will not mention here, else I could write a novel) was with delay time - I found that by increasing the time specified in "delay()" functions in the ESP32 code, the resultant code was more forgiving of communication lag times between the microcontroller and the Firebase database, and allowed the application to actually work.

Firebase initialized! Success!

Next was to program in R to design a ShinyApp web interface. The main issues I ran into were with using functions within the Firebase library. After some trial and error and consultation with Mx ChatGPT and online forums, it became apparent that using httr and jsonlite libraries were a more foolproof way to communicate with Firebase. Another issue was the syntax of "firebase_url" - in the ESP32 Arduino IDE code I had to drop "https://" at the start in order for the code to run, while within RStudio I had to include it and also add ".json" at the end of the url in order for the ShinyApp to run. I also found that restarting R sessions before running the ShinyApp, and sometimes flat out closing and reopening the app, and restarting the laptop, worked when RStudio failed at compiling.

Another crucial troubleshooting step is to add in debugging lines within both the ESP32 and ShinyApp codes (serialprint and print respectively), so that Arduino IDE's serial monitor and RStudio's console + ShinyApp interface can produce text explanations identifying when something goes wrong. These debugging lines are annotated in my code for both programs below. The full code for my ShinyApp can also be found at the end of the page.

My first successful Shinyapp interface generated in RStudio after lots of debugging:

ShinyApp - 1st iteration

Made further tweaks to the UI, and uploaded it onto my ShinyApps page so that anyone could access this interface and remotely control my servo: Shinyapp link for remote control of the servo

ShinyApp - 2nd iteration

And finally, after hours of debugging, I managed to make the servo move through a user interface designed with R! Some future explorations to consider: calibrating servo movement to more precise angles (currently the movements are jerky and do not seem to correlate with the angles selected); minimizing response time without causing the ESP32 program to crash.

After hours of debugging...finally success!

Update as of Dec 4, 2024

I realized that the easiest way to 'calibrate' the servo was to take the servo attachment off and reattach it to the angle specified. 0, 90, 180 degrees now correspond to servo attachment movements! I then tested it on my final project's prototype, to check if the microservo was strong enough to hold things now and if it could act as a release. Tried 2 different orientations of the servo attachment, like so. Both work!

Servo orientation option 1
Servo orientation option 1
Servo orientation option 2
Servo orientation option 2

Code for ESP32-C3 that was uploaded via Arduino IDE:

#include <WiFi.h> #include <FirebaseESP32.h> #include <ESP32Servo.h> const char* ssid = "********"; #replace with Wifi username const char* password = "********"; #replace with Wifi password // Firebase configuration #define FIREBASE_HOST "esp32servo-46add-default-rtdb.firebaseio.com" #define FIREBASE_AUTH "**********" #replace with secret key generated via Firebase - Project Overview - Project Settings - Service Accounts FirebaseConfig firebaseConfig; FirebaseAuth firebaseAuth; // Add FirebaseAuth object FirebaseData firebaseData; Servo servo; void setup() { Serial.begin(115200); // Connect to Wi-Fi WiFi.begin(ssid, password); unsigned long startAttemptTime = millis(); while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < 30000) { delay(1000); Serial.println("Connecting to WiFi..."); } if (WiFi.status() != WL_CONNECTED) { Serial.println("Failed to connect to WiFi"); while (true); // Halt execution } Serial.println("Connected to WiFi"); // Configure Firebase firebaseConfig.host = FIREBASE_HOST; firebaseConfig.signer.tokens.legacy_token = FIREBASE_AUTH; firebaseConfig.timeout.serverResponse = 10000; // Increase timeout for Firebase initialization // Initialize Firebase Firebase.begin(&firebaseConfig, &firebaseAuth); Firebase.reconnectWiFi(true); if (Firebase.ready()) { Serial.println("Firebase initialized successfully"); } else { Serial.println("Firebase initialization failed: " + firebaseData.errorReason()); } // Attach servo servo.attach(20); // GPIO20 for XIAO ESP32-C3 servo.write(90); // Default position } void loop() { if (Firebase.getInt(firebaseData, "servo_angle")) { int angle = firebaseData.intData(); Serial.println("Fetched angle: " + String(angle)); if (angle >= 0 && angle <= 180) { servo.write(angle); Serial.println("Servo moved to " + String(angle) + " degrees"); } else { Serial.println("Invalid angle: " + String(angle)); } } else { Serial.println("Failed to fetch data: " + firebaseData.errorReason()); } delay(1000); // Poll Firebase every 1 s }

Code for Shinyapp within RStudio:

library(shiny) library(httr) library(jsonlite) # Define UI ui <- fluidPage( titlePanel("Remote control"), sidebarLayout( sidebarPanel( sliderInput("angle", "Set Servo Angle:", min = 0, max = 180, value = 90), actionButton("set_servo", "Move it!") ), mainPanel( textOutput("status"), h5("Firebase Response:"), verbatimTextOutput("response") ) ) ) # Define Server server <- function(input, output, session) { # Replace with your Firebase database URL firebase_url <- "https://esp32servo-46add-default-rtdb.firebaseio.com/servo_angle.json" observeEvent(input$set_servo, { # Get the angle from the slider angle <- input$angle print(paste("Move it!", angle)) # Debugging line # Send the angle to Firebase response <- tryCatch({ PUT( url = firebase_url, body = toJSON(angle, auto_unbox = TRUE), encode = "json" ) }, error = function(e) { return(NULL) }) # Update UI based on response output$status <- renderText({ if (is.null(response)) { "Failed to communicate with Firebase. Please check your internet connection or database URL." } else if (status_code(response) == 200) { paste("Servo angle successfully set to:", angle) } else { paste("Failed to set servo angle. HTTP status code:", status_code(response)) } }) output$response <- renderPrint({ if (!is.null(response)) { content(response, "parsed", simplifyVector = TRUE) } else { "No response received." } }) }) } # Run the Shiny app shinyApp(ui, server)