Touch Designer Wireless Controller: Encoder & Button With XIAO ESP32

by gokux in Circuits > Microcontrollers

31 Views, 1 Favorites, 0 Comments

Touch Designer Wireless Controller: Encoder & Button With XIAO ESP32

thumnail.jpg

Welcome, everyone! This project serves as an introduction to interfacing data from TouchDesigner using the Xiao. If you're not familiar with TouchDesigner, it is a creative tool used to create real-time visuals and interactive art. It's popular among artists and designers because it enables the creation of impressive projects, such as light shows, music visuals, and digital installations, using simple building blocks. You can easily drag, drop, and connect different elements to bring your ideas to life—no extensive coding required!

For this demo project, I created a wireless encoder knob with a button. This setup allows you to send encoder data wirelessly to TouchDesigner using ESP-NOW. In TouchDesigner, I mapped the encoder data to control the radius of a circle. I also programmed the button press to switch between white and red colours.

This project is designed to help you create interactive art in TouchDesigner easily, without worrying about wiring management.

Supplies

DSC04445.JPG
  1. 2x Seeed Studio XIAO ESP32C3
  2. Encoder module
  3. 1000mah battery
  4. Slide power switch
  5. M2X3MM screws
  6. B7000 glue
  7. Connecting wires

Design and 3D Printing

Screenshot 2025-07-27 162939.png
DSC04446.JPG

I used Fusion 360 for designing this project. and 3d printed all the parts in my 3D printer with white PLA filament. You can find all STL and STEP files for this project below

Code

Screenshot 2025-07-27 164326.png
// Include necessary libraries for WiFi and ESP-NOW communication
#include <WiFi.h>
#include <esp_now.h>

// Define pin connections for encoder and button
#define ENCODER_A D0 // Encoder pin A
#define ENCODER_B D1 // Encoder pin B
#define BUTTON_PIN D2 // Pushbutton pin

// Create a structure to organize our data for transmission
typedef struct {
int16_t encoderValue; // Stores encoder position (0-100)
bool buttonState; // Stores button state (0 or 1)
} EncoderData;

// Broadcast address for ESP-NOW (send to all devices)
uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
EncoderData myData; // Create instance of our data structure

// Encoder tracking variables
volatile int encoderPos = 0; // Current encoder position
bool buttonToggleState = false; // Current toggle state of button
bool lastButtonReading = HIGH; // Previous reading of button
bool buttonPressed = false; // Flag for button press detection
unsigned long lastDebounceTime = 0; // Timer for debouncing
const unsigned long debounceDelay = 50; // Debounce delay in milliseconds

// Interrupt Service Routine for encoder updates
void IRAM_ATTR updateEncoder() {
static uint8_t oldAB = 0; // Stores previous encoder states
// Combine current A and B states with previous states
oldAB = ((oldAB << 2) | (digitalRead(ENCODER_A) << 1) | digitalRead(ENCODER_B)) & 0x0F;
// Lookup table for quadrature decoding
const int8_t encoderTable[16] = {0,-1,1,0,1,0,0,-1,-1,0,0,1,0,1,-1,0};
// Update encoder position based on state change
encoderPos += encoderTable[oldAB];
// Keep encoder value within 0-100 range
encoderPos = constrain(encoderPos, 0, 100);
}

void setup() {
// Configure encoder pins with pullup resistors
pinMode(ENCODER_A, INPUT_PULLUP);
pinMode(ENCODER_B, INPUT_PULLUP);
// Configure button pin with pullup resistor
pinMode(BUTTON_PIN, INPUT_PULLUP);
// Attach interrupts to encoder pins
attachInterrupt(digitalPinToInterrupt(ENCODER_A), updateEncoder, CHANGE);
attachInterrupt(digitalPinToInterrupt(ENCODER_B), updateEncoder, CHANGE);

// Initialize WiFi in Station mode
WiFi.mode(WIFI_STA);
// Initialize ESP-NOW protocol
esp_now_init();
// Configure peer information for broadcasting
esp_now_peer_info_t peerInfo;
memset(&peerInfo, 0, sizeof(peerInfo)); // Clear peer info structure
memcpy(peerInfo.peer_addr, broadcastAddress, 6); // Set broadcast address
// Add peer to ESP-NOW
esp_now_add_peer(&peerInfo);

// Initialize data values
myData.encoderValue = 0;
myData.buttonState = false;
}

void loop() {
// Read current button state (LOW when pressed due to pullup)
bool currentReading = digitalRead(BUTTON_PIN);

// Check for button state change for debouncing
if (currentReading != lastButtonReading) {
lastDebounceTime = millis(); // Reset debounce timer
lastButtonReading = currentReading; // Update last reading
}

// If button state has been stable for debounce period
if ((millis() - lastDebounceTime) > debounceDelay) {
// Detect button press (transition to LOW)
if (currentReading == LOW && !buttonPressed) {
buttonPressed = true; // Mark button as pressed
buttonToggleState = !buttonToggleState; // Toggle the state
}
// Detect button release (transition to HIGH)
else if (currentReading == HIGH && buttonPressed) {
buttonPressed = false; // Mark button as released
}
}

// Update data structure with current values
myData.encoderValue = encoderPos;
myData.buttonState = buttonToggleState;
// Send data via ESP-NOW
esp_now_send(broadcastAddress, (uint8_t *)&myData, sizeof(myData));
delay(10); // Small delay to prevent flooding
}

RX code

// Include necessary libraries for WiFi and ESP-NOW
#include <WiFi.h>
#include <esp_now.h>

// Match the data structure used in transmitter
typedef struct {
int16_t encoderValue; // Encoder position (0-100)
bool buttonState; // Button state (0 or 1)
} EncoderData;

// ESP-NOW receive callback function
void OnDataRecv(const esp_now_recv_info_t *info, const uint8_t *incomingData, int len) {
EncoderData receivedData; // Create local variable to store received data
// Copy received data into our structure
memcpy(&receivedData, incomingData, sizeof(receivedData));
// Print encoder value on first line
Serial.println(receivedData.encoderValue);
// Print button state on second line
Serial.println(receivedData.buttonState);
}

void setup() {
// Initialize serial communication at 115200 baud
Serial.begin(115200);
// Set WiFi to station mode
WiFi.mode(WIFI_STA);
// Initialize ESP-NOW
if (esp_now_init() != ESP_OK) {
return; // Exit if initialization fails
}
// Register callback function for received data
esp_now_register_recv_cb(OnDataRecv);
}

// Main loop does nothing - all work happens in callback
void loop() {}

Making the Encoder Knob

bitmap.jpg

I started by glueing the battery to the main body

Glue the switch and solder the battery wires to the switch

Connect the switch output and the battery -ve to the Xiao battery input

Glue the XIAO

Connect the antenna, stick it into the main body

Place the encoder module and square it into a 3d print using small M2 screws

Connect all the encoder wires to the Xiao according to the wiring diagram

Place the encoder cap 3d print

Configuring Touch Designer

Screenshot 2025-07-27 165818.png

We are finished with the build. Power on the TX and connect the RX to the USB. Determine which port is used to connect the RX; this can be found through the device manager.

You can find how to set up tuch deinser through this video