14. Embedded Networking and Communications¶
This week I learned how to use different wireless and wired networking techniques and used a milled board to demonstrate types of networking
Considerations¶
Networking Types¶
Because I’m already familiar with I2C, I decided that for wired networking I’m going to focus on wireless networking via wifi and bluetooth.
Understanding Wifi¶
Wi-Fi, or Wireless Fidelity, is a technology that enables devices to communicate wirelessly using radio waves. It operates on specific frequency bands, typically 2.4 GHz and 5 GHz, to transmit data between devices. It converts the information into radio signals and broadcasts them via an antenna. Another Wi-Fi-enabled device within range receives these signals, decodes them, and processes the data accordingly.
Understanding bluetooth¶
Bluetooth is a wireless communication technology that allows devices to exchange data over short distances. It works by creating a personal area network (PAN) between paired devices, enabling them to share information seamlessly. Bluetooth uses radio waves to establish connections and can support both simple one-to-one links and more complex networks involving multiple devices. When two Bluetooth devices connect, they go through a process to recognize each other and set up a secure communication channel.
Designing my group work Board¶
I originally intended to use UART and SPI, but I revisited this week after my final project, which I documented instead. Because of this, my board includes methods for UART and SPI.
Here is my complete board, the right side of the schematic has 2 functions, the first is connecting power, ground, and networking pins between the 2 microcontrollers. The 2nd is adding pinouts to add input or output devices. On the left side of the board is a button, to be used as an input device.
I might be using SPI later on in my project so I wanted to make a board that could do both UART and SPI networking. The Problem is that one of the SPI pins is also a UART pin. So, I added a switch so you can switch the pin between being used for UART and for SPI.
Here’s what this looked like on my board, this shows the connections to the seeed and the pinouts
Here is the input button
Here is the switch between UART and SPI
This shows a closer view of most of the connections between the Pico W and the Seeed.
File Download¶
Building and Programming¶
To solder my board, I tried to do a different process than the one we normally use. I found a thermal oven in our lab, which goes through a specific heating and cooling cycle to attatch components via solder paste. I decided to only test this with a few components on the board, just in case it would ruin the components.
The solder pasted components
The heating oven displays the heating curve to melt and harden the paste
Unfortunatly, the oven burn the board, and didn’t completely harden the paste. Because of this, I reheated all of the parts I put in the oven, and then traditionally soldered the rest of the board.
### Group work
In my group work, I worked with Stuart Christilf to network our 2 boards to communicate wirelessly
### Networking my final project
As I am revisiting this week, I will be documenting this week to explain bluetooth control with my final project. I will be using my final project board which I have the files for here.
### Bluetooth Considerations
At first, I didn’t know what type of interface to use to control the wagon. I considered using processing to create my own game pad, or use a mobile app that is capable of communicating with microcontrollers. However, I discovered the Bluepad32 Library. This Library makes it easy to connect to a HID controller (Like a game pad), and then recieve inputs via bluetooth. Because the ESP32-s3 has built in bluetooth, this is the option I decided to use.
### Testing a bluetooth connection
The first thing I did was test using the example code if I could get a connection between the gamepad and the seeed. I first had to install more custom board packages to add a list of bluepad32 supported boards. I did this by going to File>Preferances>Custom Board URLs
. Then, I connected my seeed and put it into bootloader mode. I made sure to select the Seeed XiaoESP32s3 (bluepad32), which indicates it’s the board version compatible for the library. Then, I uploaded the following example code:
#include <Bluepad32.h>
ControllerPtr myControllers[BP32_MAX_GAMEPADS];
// This callback gets called any time a new gamepad is connected.
// Up to 4 gamepads can be connected at the same time.
void onConnectedController(ControllerPtr ctl) {
bool foundEmptySlot = false;
for (int i = 0; i < BP32_MAX_GAMEPADS; i++) {
if (myControllers[i] == nullptr) {
Serial.printf("CALLBACK: Controller is connected, index=%d\n", i);
// Additionally, you can get certain gamepad properties like:
// Model, VID, PID, BTAddr, flags, etc.
ControllerProperties properties = ctl->getProperties();
Serial.printf("Controller model: %s, VID=0x%04x, PID=0x%04x\n", ctl->getModelName().c_str(), properties.vendor_id,
properties.product_id);
myControllers[i] = ctl;
foundEmptySlot = true;
break;
}
}
if (!foundEmptySlot) {
Serial.println("CALLBACK: Controller connected, but could not found empty slot");
}
}
void onDisconnectedController(ControllerPtr ctl) {
bool foundController = false;
for (int i = 0; i < BP32_MAX_GAMEPADS; i++) {
if (myControllers[i] == ctl) {
Serial.printf("CALLBACK: Controller disconnected from index=%d\n", i);
myControllers[i] = nullptr;
foundController = true;
break;
}
}
if (!foundController) {
Serial.println("CALLBACK: Controller disconnected, but not found in myControllers");
}
}
void dumpGamepad(ControllerPtr ctl) {
Serial.printf(
"idx=%d, dpad: 0x%02x, buttons: 0x%04x, axis L: %4d, %4d, axis R: %4d, %4d, brake: %4d, throttle: %4d, "
"misc: 0x%02x, gyro x:%6d y:%6d z:%6d, accel x:%6d y:%6d z:%6d\n",
ctl->index(), // Controller Index
ctl->dpad(), // D-pad
ctl->buttons(), // bitmask of pressed buttons
ctl->axisX(), // (-511 - 512) left X Axis
ctl->axisY(), // (-511 - 512) left Y axis
ctl->axisRX(), // (-511 - 512) right X axis
ctl->axisRY(), // (-511 - 512) right Y axis
ctl->brake(), // (0 - 1023): brake button
ctl->throttle(), // (0 - 1023): throttle (AKA gas) button
ctl->miscButtons(), // bitmask of pressed "misc" buttons
ctl->gyroX(), // Gyro X
ctl->gyroY(), // Gyro Y
ctl->gyroZ(), // Gyro Z
ctl->accelX(), // Accelerometer X
ctl->accelY(), // Accelerometer Y
ctl->accelZ() // Accelerometer Z
);
}
void dumpMouse(ControllerPtr ctl) {
Serial.printf("idx=%d, buttons: 0x%04x, scrollWheel=0x%04x, delta X: %4d, delta Y: %4d\n",
ctl->index(), // Controller Index
ctl->buttons(), // bitmask of pressed buttons
ctl->scrollWheel(), // Scroll Wheel
ctl->deltaX(), // (-511 - 512) left X Axis
ctl->deltaY() // (-511 - 512) left Y axis
);
}
void dumpKeyboard(ControllerPtr ctl) {
static const char* key_names[] = {
// clang-format off
// To avoid having too much noise in this file, only a few keys are mapped to strings.
// Starts with "A", which is offset 4.
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V",
"W", "X", "Y", "Z", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0",
// Special keys
"Enter", "Escape", "Backspace", "Tab", "Spacebar", "Underscore", "Equal", "OpenBracket", "CloseBracket",
"Backslash", "Tilde", "SemiColon", "Quote", "GraveAccent", "Comma", "Dot", "Slash", "CapsLock",
// Function keys
"F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12",
// Cursors and others
"PrintScreen", "ScrollLock", "Pause", "Insert", "Home", "PageUp", "Delete", "End", "PageDown",
"RightArrow", "LeftArrow", "DownArrow", "UpArrow",
// clang-format on
};
static const char* modifier_names[] = {
// clang-format off
// From 0xe0 to 0xe7
"Left Control", "Left Shift", "Left Alt", "Left Meta",
"Right Control", "Right Shift", "Right Alt", "Right Meta",
// clang-format on
};
Serial.printf("idx=%d, Pressed keys: ", ctl->index());
for (int key = Keyboard_A; key <= Keyboard_UpArrow; key++) {
if (ctl->isKeyPressed(static_cast<KeyboardKey>(key))) {
const char* keyName = key_names[key-4];
Serial.printf("%s,", keyName);
}
}
for (int key = Keyboard_LeftControl; key <= Keyboard_RightMeta; key++) {
if (ctl->isKeyPressed(static_cast<KeyboardKey>(key))) {
const char* keyName = modifier_names[key-0xe0];
Serial.printf("%s,", keyName);
}
}
Console.printf("\n");
}
void dumpBalanceBoard(ControllerPtr ctl) {
Serial.printf("idx=%d, TL=%u, TR=%u, BL=%u, BR=%u, temperature=%d\n",
ctl->index(), // Controller Index
ctl->topLeft(), // top-left scale
ctl->topRight(), // top-right scale
ctl->bottomLeft(), // bottom-left scale
ctl->bottomRight(), // bottom-right scale
ctl->temperature() // temperature: used to adjust the scale value's precision
);
}
void processGamepad(ControllerPtr ctl) {
// There are different ways to query whether a button is pressed.
// By query each button individually:
// a(), b(), x(), y(), l1(), etc...
if (ctl->a()) {
static int colorIdx = 0;
// Some gamepads like DS4 and DualSense support changing the color LED.
// It is possible to change it by calling:
switch (colorIdx % 3) {
case 0:
// Red
ctl->setColorLED(255, 0, 0);
break;
case 1:
// Green
ctl->setColorLED(0, 255, 0);
break;
case 2:
// Blue
ctl->setColorLED(0, 0, 255);
break;
}
colorIdx++;
}
if (ctl->b()) {
// Turn on the 4 LED. Each bit represents one LED.
static int led = 0;
led++;
// Some gamepads like the DS3, DualSense, Nintendo Wii, Nintendo Switch
// support changing the "Player LEDs": those 4 LEDs that usually indicate
// the "gamepad seat".
// It is possible to change them by calling:
ctl->setPlayerLEDs(led & 0x0f);
}
if (ctl->x()) {
// Some gamepads like DS3, DS4, DualSense, Switch, Xbox One S, Stadia support rumble.
// It is possible to set it by calling:
// Some controllers have two motors: "strong motor", "weak motor".
// It is possible to control them independently.
ctl->playDualRumble(0 /* delayedStartMs */, 250 /* durationMs */, 0x80 /* weakMagnitude */,
0x40 /* strongMagnitude */);
}
// Another way to query controller data is by getting the buttons() function.
// See how the different "dump*" functions dump the Controller info.
dumpGamepad(ctl);
}
void processMouse(ControllerPtr ctl) {
// This is just an example.
if (ctl->scrollWheel() > 0) {
// Do Something
} else if (ctl->scrollWheel() < 0) {
// Do something else
}
// See "dumpMouse" for possible things to query.
dumpMouse(ctl);
}
void processKeyboard(ControllerPtr ctl) {
if (!ctl->isAnyKeyPressed())
return;
// This is just an example.
if (ctl->isKeyPressed(Keyboard_A)) {
// Do Something
Serial.println("Key 'A' pressed");
}
// Don't do "else" here.
// Multiple keys can be pressed at the same time.
if (ctl->isKeyPressed(Keyboard_LeftShift)) {
// Do something else
Serial.println("Key 'LEFT SHIFT' pressed");
}
// Don't do "else" here.
// Multiple keys can be pressed at the same time.
if (ctl->isKeyPressed(Keyboard_LeftArrow)) {
// Do something else
Serial.println("Key 'Left Arrow' pressed");
}
// See "dumpKeyboard" for possible things to query.
dumpKeyboard(ctl);
}
void processBalanceBoard(ControllerPtr ctl) {
// This is just an example.
if (ctl->topLeft() > 10000) {
// Do Something
}
// See "dumpBalanceBoard" for possible things to query.
dumpBalanceBoard(ctl);
}
void processControllers() {
for (auto myController : myControllers) {
if (myController && myController->isConnected() && myController->hasData()) {
if (myController->isGamepad()) {
processGamepad(myController);
} else if (myController->isMouse()) {
processMouse(myController);
} else if (myController->isKeyboard()) {
processKeyboard(myController);
} else if (myController->isBalanceBoard()) {
processBalanceBoard(myController);
} else {
Serial.println("Unsupported controller");
}
}
}
}
// Arduino setup function. Runs in CPU 1
void setup() {
Serial.begin(115200);
Serial.printf("Firmware: %s\n", BP32.firmwareVersion());
const uint8_t* addr = BP32.localBdAddress();
Serial.printf("BD Addr: %2X:%2X:%2X:%2X:%2X:%2X\n", addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
// Setup the Bluepad32 callbacks
BP32.setup(&onConnectedController, &onDisconnectedController);
// "forgetBluetoothKeys()" should be called when the user performs
// a "device factory reset", or similar.
// Calling "forgetBluetoothKeys" in setup() just as an example.
// Forgetting Bluetooth keys prevents "paired" gamepads to reconnect.
// But it might also fix some connection / re-connection issues.
BP32.forgetBluetoothKeys();
// Enables mouse / touchpad support for gamepads that support them.
// When enabled, controllers like DualSense and DualShock4 generate two connected devices:
// - First one: the gamepad
// - Second one, which is a "virtual device", is a mouse.
// By default, it is disabled.
BP32.enableVirtualDevice(false);
}
// Arduino loop function. Runs in CPU 1.
void loop() {
// This call fetches all the controllers' data.
// Call this function in your main loop.
bool dataUpdated = BP32.update();
if (dataUpdated)
processControllers();
// The main loop must have some kind of "yield to lower priority task" event.
// Otherwise, the watchdog will get triggered.
// If your main loop doesn't have one, just add a simple `vTaskDelay(1)`.
// Detailed info here:
// https://stackoverflow.com/questions/66278271/task-watchdog-got-triggered-the-tasks-did-not-reset-the-watchdog-in-time
// vTaskDelay(1);
delay(150);
}
![alt text](<../images/Networking/unnamed (5)2.jpg>)
When I moved the sticks, pressed buttons, and pressed triggers, it would show the signal that each input gives off, and then printed it into the serial monitor.
Applying to my final project¶
Now that I was able to get the inputs from the controller, I gave ChatGPT the following instructions My goal is to build a three wheeled wagon that has 2 motorized wheels in the back, and a uncontrollable caster wheel in the front. I am using a Seeed Xiao ESP32S3, a L298N motor driver, an adafruit BNO05 and 2 DC motors .Write code in c++ compatible with the arduino IDE to drive the wagon forward for a certain time, make a 90 degree turn, stop, and then continue forward for another amount of time. I am using the Bluepad 32 library to connect the ESP32 to an xbox controller. Using the following example code that prints all inputs from the controller into the serial monitor, create a code so that I can control the speed and steer the wagon from a game controller.
. I then gave it the example code that I had proven to work. ChatGPT adapted the code my mapping the controller input values to motor outputs, it this by setting the value to correlate to a certain output action, and then restrained them within the bounds of the output. For example, the right trigger controls going forwards and backwards. So, it constrained the values of the trigger to be between the motor outputs of 0-255 in one direction, and then mapped the value to driving both motors in one direction. I had to change the bounds of the constraints, as the turning was constrained between -63 and 63, which I then changed to -255 and 255. After troubleshooting, I got the following code:
#include <Bluepad32.h>
// Pin definitions
const int motor1Pin1 = 9; // IN1 for Motor 1
const int motor1Pin2 = 8; // IN2 for Motor 1
const int motor2Pin1 = 7; // IN3 for Motor 2
const int motor2Pin2 = 44; // IN4 for Motor 2
const int enablePin1 = 2; // ENA for Motor 1
const int enablePin2 = 3; // ENB for Motor 2
ControllerPtr myControllers[BP32_MAX_GAMEPADS];
// This callback gets called any time a new gamepad is connected.
// Up to 4 gamepads can be connected at the same time.
void onConnectedController(ControllerPtr ctl) {
bool foundEmptySlot = false;
for (int i = 0; i < BP32_MAX_GAMEPADS; i++) {
if (myControllers[i] == nullptr) {
Serial.printf("CALLBACK: Controller is connected, index=%d\n", i);
// Additionally, you can get certain gamepad properties like:
// Model, VID, PID, BTAddr, flags, etc.
ControllerProperties properties = ctl->getProperties();
Serial.printf("Controller model: %s, VID=0x%04x, PID=0x%04x\n", ctl->getModelName().c_str(), properties.vendor_id, properties.product_id);
myControllers[i] = ctl;
foundEmptySlot = true;
break;
}
}
if (!foundEmptySlot) {
Serial.println("CALLBACK: Controller connected, but could not found empty slot");
}
}
void onDisconnectedController(ControllerPtr ctl) {
bool foundController = false;
for (int i = 0; i < BP32_MAX_GAMEPADS; i++) {
if (myControllers[i] == ctl) {
Serial.printf("CALLBACK: Controller disconnected from index=%d\n", i);
myControllers[i] = nullptr;
foundController = true;
break;
}
}
if (!foundController) {
Serial.println("CALLBACK: Controller disconnected, but not found in myControllers");
}
}
void dumpGamepad(ControllerPtr ctl) {
Serial.printf(
"idx=%d, dpad: 0x%02x, buttons: 0x%04x, axis L: %4d, %4d, axis R: %4d, %4d, brake: %4d, throttle: %4d, "
"misc: 0x%02x, gyro x:%6d y:%6d z:%6d, accel x:%6d y:%6d z:%6d\n",
ctl->index(), // Controller Index
ctl->dpad(), // D-pad
ctl->buttons(), // bitmask of pressed buttons
ctl->axisX(), // (-511 - 512) left X Axis
ctl->axisY(), // (-511 - 512) left Y axis
ctl->axisRX(), // (-511 - 512) right X axis
ctl->axisRY(), // (-511 - 512) right Y axis
ctl->brake(), // (0 - 1023): brake button
ctl->throttle(), // (0 - 1023): throttle (AKA gas) button
ctl->miscButtons(), // bitmask of pressed "misc" buttons
ctl->gyroX(), // Gyro X
ctl->gyroY(), // Gyro Y
ctl->gyroZ(), // Gyro Z
ctl->accelX(), // Accelerometer X
ctl->accelY(), // Accelerometer Y
ctl->accelZ() // Accelerometer Z
);
}
void processGamepad(ControllerPtr ctl) {
// Print out raw values for debugging
Serial.printf("Raw throttle: %d, Raw brake: %d\n", ctl->throttle(), ctl->brake());
Serial.printf("Raw axisRX: %d, Raw axisRY: %d\n", ctl->axisRX(), ctl->axisRY());
// Normalize throttle and brake values (0-255)
int throttle = ctl->throttle() / 4;
int brake = ctl->brake() / 4;
Serial.printf("Normalized Throttle: %d, Normalized Brake: %d\n", throttle, brake);
// Calculate speed based on throttle and brake
int speed = throttle - brake;
// Normalize direction value (-255 to 255)
int direction = map(ctl->axisRX(), -512, 512, -255, 255);
Serial.printf("Calculated Speed: %d, Calculated Direction: %d\n", speed, direction);
// Calculate motor speeds
int motorSpeed1 = speed - direction; // removed division by 4
int motorSpeed2 = speed + direction; // removed division by 4
// Ensure motor speeds are within range
motorSpeed1 = constrain(motorSpeed1, -255, 255);
motorSpeed2 = constrain(motorSpeed2, -255, 255);
Serial.printf("MotorSpeed1: %d, MotorSpeed2: %d\n", motorSpeed1, motorSpeed2);
// Set motor directions and speeds
if (motorSpeed1 > 0) {
digitalWrite(motor1Pin1, HIGH);
digitalWrite(motor1Pin2, LOW);
analogWrite(enablePin1, motorSpeed1);
} else {
digitalWrite(motor1Pin1, LOW);
digitalWrite(motor1Pin2, HIGH);
analogWrite(enablePin1, -motorSpeed1);
}
if (motorSpeed2 > 0) {
digitalWrite(motor2Pin1, HIGH);
digitalWrite(motor2Pin2, LOW);
analogWrite(enablePin2, motorSpeed2);
} else {
digitalWrite(motor2Pin1, LOW);
digitalWrite(motor2Pin2, HIGH);
analogWrite(enablePin2, -motorSpeed2);
}
dumpGamepad(ctl);
}
void processControllers() {
for (auto myController : myControllers) {
if (myController && myController->isConnected() && myController->hasData()) {
if (myController->isGamepad()) {
processGamepad(myController);
}
}
}
}
// Arduino setup function. Runs in CPU 1
void setup() {
Serial.begin(115200);
Serial.printf("Firmware: %s\n", BP32.firmwareVersion());
const uint8_t* addr = BP32.localBdAddress();
Serial.printf("BD Addr: %2X:%2X:%2X:%2X:%2X:%2X\n", addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
// Motor driver pin setup
pinMode(motor1Pin1, OUTPUT);
pinMode(motor1Pin2, OUTPUT);
pinMode(motor2Pin1, OUTPUT);
pinMode(motor2Pin2, OUTPUT);
pinMode(enablePin1, OUTPUT);
pinMode(enablePin2, OUTPUT);
// Setup the Bluepad32 callbacks
BP32.setup(&onConnectedController, &onDisconnectedController);
// "forgetBluetoothKeys()" should be called when the user performs
// a "device factory reset", or similar.
// Calling "forgetBluetoothKeys" in setup() just as an example.
// Forgetting Bluetooth keys prevents "paired" gamepads to reconnect.
// But it might also fix some connection / re-connection issues.
BP32.forgetBluetoothKeys();
// Enables mouse / touchpad support for gamepads that support them.
// When enabled, controllers like DualSense and DualShock4 generate two connected devices:
// - First one: the gamepad
// - Second one, which is a "virtual device", is a mouse.
// By default, it is disabled.
BP32.enableVirtualDevice(false);
}
// Arduino loop function. Runs in CPU 1.
void loop() {
// This call fetches all the controllers' data.
// Call this function in your main loop.
bool dataUpdated = BP32.update();
if (dataUpdated)
processControllers();
// The main loop must have some kind of "yield to lower priority task" event.
// Otherwise, the watchdog will get triggered.
// If your main loop doesn't have one, just add a simple `vTaskDelay(1)`.
// Detailed info here:
// https://stackoverflow.com/questions/66278271/task-watchdog-got-triggered-the-tasks-did-not-reset-the-watchdog-in-time
// vTaskDelay(1);
delay(150);
}
I connected microcontroller pins to the motor driver, a powersupply to the motor driver, and the motors to the driver, ran the code, and got the following result.
What I learned¶
This week, I learned 3 different communication protocols, UART and Wifi in my group, and bluetooth in my individual assignment. I also found an interface to control my final project and a stable control method