Skip to content

Networking and Communications

the assignment for this week was to design, build, and connect wired or wireless node(s) with network or bus addresses and local input &/or output device(s)

What I want to do

My goals for this week are to connect my camera module with my stepper module. It will have to be wireless, and be high enough bandwidth to be able to transmit the camera data without compression. I also need a phone(or other device) to be able to join into this network, so that I can see the camera output and control the stepper motor.

I want the base stepper (the one not with the camera) to act as the main node in this, as 1. it will probably have less power consumption then the s3, and therefore will turn itself off after the s3, which will ensure no big bugs. 2. The camera is already dealing with enough. From the times I have run the esp32-s3 cam with a working camera and a wifi webserver, it struggled already with that. I think also coordinating the movements of two steppers, as well as managing which network it is connected to would put it over the edge.


I also am hoping, but not expecting it to work, to be able to use OTA updates with my boards. This is because the USB-C kind of just juts out the side, so im hoping to be able to remove it in in the final version, and have a fully portable camera that doesn't even need wires for coding.

What is networking?

networking can be most simply explained as the word to describe the ability for 2 computers to talk with eachother. For my week specifically, the network type im using is wifi, but there are others, like wired with i2c, wired with spi, wireless with ESP-NOW (or other protocols), or wireless with bluetooth. Each of these have their own Pros and Cons, but in general, the wired have the highest bandwidth(which means they can transmit the most data per second) and are much less prone to loosing data or loosing the connection entirely. Wireless is better because, its wireless. the reason that we don't have 1 wireless network to rule them all is because each have their own pros and cons. In general, wifi strikes a good balance of range and bandwith, bluetooth is good at not randomly dropping out, and other specific protocols like ESP-NOW usally have better range than wifi at the cost of less bandwidth/hard to implement. The main reason I decided on wifi above was that phones don't understand the other protocols (natively, maybe someones done something to make them), and bluetooth is simply harder to work with then wifi.

Putting it into practice

The esp32 series was made to be a microcontroller with wifi originally, and it excels at that. The specific chips im using are c3 and the s3 variant. the c3 has wifi and bluetooth, s3 is more limited with just wifi, but wifi was the best choice here so im not limited in my options.

MDNS

MDNS, or Multicast DNS, is a way to allow a wifi enabled device to discover other devices by names instead of raw ips. It allows for any supported device, including a esp32-c3, to broadcast itself not as 1.192.168.1 for example, but instead esp32.local. This is required for my purposes, as without this I would need some complicated script to allow the base stepper to discover the head stepper. Using MDNS is as simple as adding

  // Initialize MDNS
  if (!MDNS.begin(mdnsHostname)) {
    Serial.println("Error setting up MDNS!");
    while (1) {
      delay(1000);
    }
  }
MDNS.addService("http", "tcp", 80);

after turning on wifi. Other devices programmed with arduino can discover the ip of the device with

MDNS.queryHost("mdnsHostname)

Coding it

AI played a large portion in this, but was dumb, especially after switching to less mainstream code like the second camera library and MDNS in general.

when working with AI for coding something like this NEVER give it your wifi information or other personalized data. all AI nowadays is made to extract as much data for training as possible, and your ssids and passwords WILL be fed into the machine

Camera Issues

getting a simple signal between the two devices was really easy, but the camera was impossible. More specifically, it was impossible with the library I was using, which was the library officially by seeed studio, which I originally used following this guide. The problem with it is how it transmits the video from the camera to the website. I spent atleast 2 hours poking around at the code on the chip and on the webserver via my browsers (firefox) built in network inspector. The best I could get from it was one frame of the video, I couldn't get it better

Camera Fix

the fix was pretty simple, with just moving to another camera library. The problem with that fix is that there was not another camera library as good as seeed's. I found and used the espcam library, specifically the asynccam library and example sketch. This required a much more involved approach as I had to change the pin definitions. After getting it, I also found that it just ran worse than the seeed, but im guessing its user error and will be fixable by the time the final is due.

Base Code

This is the "main node" where all the communication and data flows into or comes out of. It needs to handle a camera and joystick, while also passing through that camera and communicating stepper movements. on top of all that, it also needs to drive a stepper of its own. It also is the website hoster, which adds even more drain on it. one optimization here is that it doesn't do anything with the camera stream but tell the connected phone the link to find it at. This means that it does not need to work with that, and therefore really is just sending a link once.


Heres a simplified version of the base code, that had comments added by AI

#include <WiFi.h>          // Include the WiFi library for network connectivity
#include <ESPAsyncWebServer.h> // Include the ESPAsyncWebServer library for creating a non-blocking web server
#include <ESPmDNS.h>         // Include the ESPmDNS library for Multicast DNS (hostname resolution)

// --- Pin Definitions ---
#define STEPPER_PHASE_1 2    // Define the GPIO pin connected to the first phase control of the stepper motor
#define STEPPER_ENABLE_1 3   // Define the GPIO pin connected to the enable/disable control of the first motor driver
#define STEPPER_PHASE_2 4    // Define the GPIO pin connected to the second phase control of the stepper motor
#define STEPPER_ENABLE_2 5   // Define the GPIO pin connected to the enable/disable control of the second motor driver

// --- WiFi Configuration ---
static const char* WIFI_SSID = "YourNetwork";     // Replace with the name (SSID) of your WiFi network
static const char* WIFI_PASS = "YourPassword"; // Replace with the password of your WiFi network

// --- mDNS Configuration ---
static const char* MDNS_HOSTNAME = "steppercontrol"; // Define the hostname that the ESP32 will use for mDNS
static const char* CAMERA_MDNS_HOSTNAME = "camerastream"; // Define the hostname for the camera stream (another device on the network)

// --- Global Variables ---
AsyncWebServer server(80); // Create an instance of the AsyncWebServer on port 80 (HTTP default)
bool motorEnabled = false; // A boolean variable to track whether the motor is currently enabled or disabled

// Define logic levels for enabling/disabling the motor driver
// These might need to be reversed depending on your specific motor driver board
#define MOTOR_ON HIGH   // Define HIGH logic level as the signal to turn the motor ON
#define MOTOR_OFF LOW    // Define LOW logic level as the signal to turn the motor OFF

void setup() {
  Serial.begin(115200); // Initialize serial communication at a baud rate of 115200 for debugging output

  // --- Connect to WiFi ---
  WiFi.begin(WIFI_SSID, WIFI_PASS); // Start the WiFi connection using the configured SSID and password
  Serial.print("Connecting to WiFi...");
  while (WiFi.status() != WL_CONNECTED) { // Loop until the ESP32 is successfully connected to the WiFi network
    delay(1000);
    Serial.print(".");
  }
  Serial.println("\nConnected!");               // Print a message indicating successful WiFi connection
  Serial.println(WiFi.localIP());           // Print the IP address that the ESP32 has obtained from the network

  // --- Setup Stepper Pins ---
  pinMode(STEPPER_PHASE_1, OUTPUT);  // Set the first phase control pin as an output pin
  pinMode(STEPPER_ENABLE_1, OUTPUT); // Set the first enable/disable control pin as an output pin
  pinMode(STEPPER_PHASE_2, OUTPUT);  // Set the second phase control pin as an output pin
  pinMode(STEPPER_ENABLE_2, OUTPUT); // Set the second enable/disable control pin as an output pin

  // --- Initialize Motor Driver ---
  digitalWrite(STEPPER_ENABLE_1, MOTOR_OFF); // Initially set the enable pin of the first driver to OFF (disable the motor)
  digitalWrite(STEPPER_ENABLE_2, MOTOR_OFF); // Initially set the enable pin of the second driver to OFF
  motorEnabled = false;                      // Initialize the motorEnabled state to false (disabled)
  Serial.println("Stepper driver initialized (disabled).");

  // --- Setup mDNS ---
  if (!MDNS.begin(MDNS_HOSTNAME)) { // Start the mDNS service with the defined hostname
    Serial.println("Error setting up mDNS!"); // Print an error message if mDNS fails to start
    while (1) {                       // Enter an infinite loop to stop further execution if mDNS fails (critical for network discovery)
      delay(1000);
    }
  }
  Serial.println("mDNS started: http://" + String(MDNS_HOSTNAME) + ".local"); // Print the mDNS address that can be used to access the ESP32
  MDNS.addService("http", "tcp", 80); // Advertise the HTTP service on TCP port 80 so other devices can find it

  // --- Define Web Server Routes ---
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { // Define a handler for HTTP GET requests to the root path ("/")
    String html = R"rawliteral(
      <!DOCTYPE html>
      <html>
      <head>
        <title>Control Interface</title>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <style> body { text-align: center; font-family: sans-serif; } button { padding: 10px 20px; font-size: 16px; margin: 10px; cursor: pointer; } </style>
      </head>
      <body>
        <h1>Device Control</h1>
        <div>
          <button onclick="fetch('/enable')">Enable Motor</button>   <button onclick="fetch('/disable')">Disable Motor</button> </div>
        <div>
          <img src="http://)rawliteral" + String(CAMERA_MDNS_HOSTNAME) + R"rawliteral(.local/stream" width="320" height="240" alt="Camera Feed">
          </div>
        <script>
          async function fetch(url) { // Asynchronous JavaScript function to make HTTP requests
            const response = await window.fetch(url); // Send a GET request to the specified URL
            const text = await response.text();       // Get the response text
            console.log(text);                       // Log the response text to the browser's console
          }
        </script>
      </body>
      </html>
    )rawliteral"; // Raw string literal for easier multiline HTML definition
    request->send(200, "text/html", html); // Send the HTML content as a response with HTTP status 200 (OK) and content type "text/html"
  });

  server.on("/enable", HTTP_GET, [](AsyncWebServerRequest *request) { // Define a handler for HTTP GET requests to the "/enable" path
    Serial.println("Motor enabled");              // Print a message to the serial monitor
    digitalWrite(STEPPER_ENABLE_1, MOTOR_ON);  // Set the enable pin of the first driver to ON
    digitalWrite(STEPPER_ENABLE_2, MOTOR_ON);  // Set the enable pin of the second driver to ON
    motorEnabled = true;                       // Update the motorEnabled state to true
    request->send(200, "text/plain", "Motor Enabled"); // Send a plain text response indicating success
  });

  server.on("/disable", HTTP_GET, [](AsyncWebServerRequest *request) { // Define a handler for HTTP GET requests to the "/disable" path
    Serial.println("Motor disabled");             // Print a message to the serial monitor
    digitalWrite(STEPPER_ENABLE_1, MOTOR_OFF); // Set the enable pin of the first driver to OFF
    digitalWrite(STEPPER_ENABLE_2, MOTOR_OFF); // Set the enable pin of the second driver to OFF
    motorEnabled = false;                      // Update the motorEnabled state to false
    request->send(200, "text/plain", "Motor Disabled"); // Send a plain text response indicating success
  });

  // --- Start Web Server ---
  server.begin(); // Start the AsyncWebServer, making it listen for incoming HTTP requests
  Serial.println("HTTP server started"); // Print a message indicating that the web server has started
}

{
  delay(100); // always good practice to have a delay even if its not stopping anything
}

and heres a simplified version of my head/camera code, also having comments added by AI

#include "AsyncCam.hpp"   // Handles asynchronous camera capture and streaming.
#include <WiFi.h>         // Provides Wi-Fi functionality.
#include <ESPmDNS.h>      // Enables .local hostname resolution on local networks.

// Wi-Fi credentials, intentionally left blank — these would normally be configured for local access.
static const char* WIFI_SSID = "";
static const char* WIFI_PASS = "";

// This defines the device's human-readable network name for mDNS access.
static const char* MDNS_HOSTNAME = "maestrocamera";

// Holds the resolution at which the camera will begin streaming.
esp32cam::Resolution initialResolution;

// The server that will deliver live camera images over HTTP.
AsyncWebServer server(80);

void setup() {
  Serial.begin(115200);  // Begin serial output for runtime status messages.
  Serial.println();
  esp32cam::setLogger(Serial);  // Directs log output from the camera system to the serial port.
  delay(1000);  // Slight pause to ensure stable startup.

  // Connects the ESP32 to the local Wi-Fi network in station mode.
  WiFi.persistent(false);        // Prevents Wi-Fi credentials from being saved to flash memory.
  WiFi.mode(WIFI_STA);           // Sets the device to act only as a client on the network.
  WiFi.begin(WIFI_SSID, WIFI_PASS);  // Begins Wi-Fi connection attempt.

  // If the device fails to connect, it waits briefly and restarts itself.
  if (WiFi.waitForConnectResult() != WL_CONNECTED) {
    Serial.printf("WiFi failure %d\n", WiFi.status());
    delay(5000);
    ESP.restart();
  }

  Serial.println("WiFi connected");
  delay(1000);  // Allow time for the connection to stabilize.

  // Starts mDNS service so the device can be reached via a local hostname (e.g., http://maestrocamera.local).
  if (!MDNS.begin(MDNS_HOSTNAME)) {
    Serial.println("Error setting up MDNS responder!");
    while (1) {
      delay(1000);  // Infinite loop to halt progress if mDNS fails.
    }
  }
  Serial.println("mDNS responder started");

  // Advertises the HTTP server using mDNS so browsers and other clients can discover it.
  MDNS.addService("http", "tcp", 80);

  // Camera configuration and startup logic.
  {
    using namespace esp32cam;

    // Finds a supported resolution close to 400x300 — a balance between quality and speed.
    initialResolution = Resolution::find(400, 300);

    Config cfg;
    cfg.setPins(pins::XiaoSense);       // Predefined GPIO mapping for Seeed XIAO ESP32-S3 Sense.
    cfg.setResolution(initialResolution); // Applies the chosen resolution.
    cfg.setBufferCount(10);             // Allocates 10 frame buffers to manage live stream throughput.
    cfg.setJpeg(50);                    // Sets JPEG compression level; 50 is a medium-quality setting.

    // Attempts to initialize the camera with the specified configuration.
    bool ok = Camera.begin(cfg);
    if (!ok) {
      Serial.println("camera initialize failure");
      delay(5000);
      ESP.restart();  // Reboot the device if the camera fails to initialize.
    }

    Serial.println("camera initialize success");
  }

  // Prints out the mDNS access URL, making the stream easily discoverable by human users.
  Serial.println("camera starting");
  Serial.print("http://");
  Serial.print(MDNS_HOSTNAME);
  Serial.println(".local");

  addRequestHandlers();  // Sets up the web server's response logic for camera streaming.
  server.begin();        // Starts the HTTP server.
}

void loop() {
  // A minimal delay ensures that FreeRTOS (the underlying task manager) can manage memory cleanly.
  // Without this, the system can crash under async load due to unreleased heap memory.
  delay(1);
}

Results

With that code, I was able to see a camera stream through a webserver only hosted on the stepper microcontroller, along with a button on that website that controlled the stepper. The advantages of doing this is that the computer only needs to connect to one microcontroller, which then forwards all the required setup from the other. In general, this will lead to a more stable connection and easier debugging later on. It also allows for me to do much more advanced stuff, as one of the limits of the camera on the esp is he amount of processing power to just understand a frame and send it off is already reaching the limits of the S3, and hosting a webserver would go above that.

Hero Shot

Group Work

As part of the group work, me and Cooper connected our 2 devices by connecting to the same network and using MDNS for easy discoverability. A link to our work can be found here