TouchOSC XY Controller
Table
| Function | ESP32-S3 pin | Notes |
|---|---|---|
| Joystick X | GPIO1 / ADC1_CH0 | 1 kΩ + 100 nF RC filter |
| Joystick Y | GPIO2 / ADC1_CH1 | 1 kΩ + 100 nF RC filter |
| Joystick press | GPIO4 | Active-low, external 10 kΩ pull-up |
| Function button 1 | GPIO5 | Active-low, external 10 kΩ pull-up |
| Function button 2 | GPIO6 | Active-low, external 10 kΩ pull-up |
| Status/WLAN LED | GPIO7 | Active-high through 1 kΩ |
| Native USB D− | GPIO19 | USB 2.0 differential pair |
| Native USB D+ | GPIO20 | USB 2.0 differential pair |
| BOOT | GPIO0 | Dedicated 10 kΩ pull-up and button to GND |
| RESET | EN | 10 kΩ pull-up, 1 µF to GND, button to GND |
esp32-s3-devkitc-1, 8 MB flash and USB CDC at boot.platformio.iniIni
[env:esp32-s3-wroom-1-n8r2] platform = espressif32 board = esp32-s3-devkitc-1 framework = arduino monitor_speed = 115200 upload_speed = 921600 board_upload.flash_size = 8MB board_build.partitions = default_8MB.csv build_flags = -DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=1 -DBOARD_HAS_PSRAM lib_deps = hideakitai/ArduinoOSC
src/main.cppCpp
#include <Arduino.h> #include <WiFi.h> #include <ArduinoOSCWiFi.h> // ---------- User configuration ---------- static const char* WIFI_SSID = "CHANGE_ME"; static const char* WIFI_PASSWORD = "CHANGE_ME"; static const char* OSC_TARGET_IP = "192.168.1.100"; static constexpr uint16_t OSC_TARGET_PORT = 9000; // ADC calibration: measure and replace these values on the assembled controller. static constexpr int X_MIN = 0; static constexpr int X_CENTER = 2048; static constexpr int X_MAX = 4095; static constexpr int Y_MIN = 0; static constexpr int Y_CENTER = 2048; static constexpr int Y_MAX = 4095; static constexpr float DEADZONE = 0.035f; static constexpr uint8_t PIN_JOY_X = 1; static constexpr uint8_t PIN_JOY_Y = 2; static constexpr uint8_t PIN_JOY_SW = 4; static constexpr uint8_t PIN_BUTTON_1 = 5; static constexpr uint8_t PIN_BUTTON_2 = 6; static constexpr uint8_t PIN_STATUS_LED = 7; static constexpr uint32_t SAMPLE_INTERVAL_MS = 10; static constexpr uint32_t OSC_REFRESH_MS = 50; static constexpr uint32_t DEBOUNCE_MS = 25; static constexpr uint32_t WIFI_RETRY_MS = 5000; struct DebouncedButton { uint8_t pin; bool stablePressed = false; bool rawPressed = false; uint32_t changedAt = 0; void begin() { pinMode(pin, INPUT_PULLUP); rawPressed = digitalRead(pin) == LOW; stablePressed = rawPressed; changedAt = millis(); } bool update() { const bool nowPressed = digitalRead(pin) == LOW; if (nowPressed != rawPressed) { rawPressed = nowPressed; changedAt = millis(); } if (stablePressed != rawPressed && millis() - changedAt >= DEBOUNCE_MS) { stablePressed = rawPressed; return true; } return false; } }; DebouncedButton joyButton{PIN_JOY_SW}; DebouncedButton button1{PIN_BUTTON_1}; DebouncedButton button2{PIN_BUTTON_2}; uint32_t lastSampleAt = 0; uint32_t lastOscAt = 0; uint32_t lastWifiAttemptAt = 0; float lastX = -1.0f; float lastY = -1.0f; float calibrateAxis(int raw, int minimum, int center, int maximum) { float value; if (raw >= center) { const int span = max(1, maximum - center); value = static_cast<float>(raw - center) / static_cast<float>(span); } else { const int span = max(1, center - minimum); value = static_cast<float>(raw - center) / static_cast<float>(span); } value = constrain(value, -1.0f, 1.0f); const float magnitude = fabsf(value); if (magnitude <= DEADZONE) return 0.0f; const float rescaled = (magnitude - DEADZONE) / (1.0f - DEADZONE); return copysignf(constrain(rescaled, 0.0f, 1.0f), value); } void setStatusLed() { if (WiFi.status() == WL_CONNECTED) { digitalWrite(PIN_STATUS_LED, HIGH); } else { digitalWrite(PIN_STATUS_LED, (millis() / 250U) & 1U); } } void startWifi() { WiFi.mode(WIFI_STA); WiFi.setAutoReconnect(true); WiFi.persistent(false); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); lastWifiAttemptAt = millis(); } void maintainWifi() { if (WiFi.status() == WL_CONNECTED) return; if (millis() - lastWifiAttemptAt >= WIFI_RETRY_MS) { WiFi.disconnect(); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); lastWifiAttemptAt = millis(); } } void sendButton(const char* address, bool pressed) { if (WiFi.status() == WL_CONNECTED) { OscWiFi.send(OSC_TARGET_IP, OSC_TARGET_PORT, address, pressed ? 1 : 0); } } void setup() { Serial.begin(115200); delay(200); pinMode(PIN_STATUS_LED, OUTPUT); digitalWrite(PIN_STATUS_LED, LOW); joyButton.begin(); button1.begin(); button2.begin(); analogReadResolution(12); analogSetPinAttenuation(PIN_JOY_X, ADC_11db); analogSetPinAttenuation(PIN_JOY_Y, ADC_11db); startWifi(); } void loop() { maintainWifi(); setStatusLed(); if (joyButton.update()) sendButton("/joystick/press", joyButton.stablePressed); if (button1.update()) sendButton("/button/1", button1.stablePressed); if (button2.update()) sendButton("/button/2", button2.stablePressed); const uint32_t now = millis(); if (now - lastSampleAt >= SAMPLE_INTERVAL_MS) { lastSampleAt = now; // Average four readings to reduce residual ADC noise after the hardware RC filter. uint32_t sumX = 0; uint32_t sumY = 0; for (int i = 0; i < 4; ++i) { sumX += analogRead(PIN_JOY_X); sumY += analogRead(PIN_JOY_Y); } const float x = calibrateAxis(sumX / 4, X_MIN, X_CENTER, X_MAX); const float y = calibrateAxis(sumY / 4, Y_MIN, Y_CENTER, Y_MAX); const bool changed = fabsf(x - lastX) >= 0.002f || fabsf(y - lastY) >= 0.002f; const bool refreshDue = now - lastOscAt >= OSC_REFRESH_MS; if (WiFi.status() == WL_CONNECTED && (changed || refreshDue)) { OscWiFi.send(OSC_TARGET_IP, OSC_TARGET_PORT, "/xy/x", x); OscWiFi.send(OSC_TARGET_IP, OSC_TARGET_PORT, "/xy/y", y); lastX = x; lastY = y; lastOscAt = now; } } delay(1); }
/xy/x float, range −1.0 to +1.0/xy/y float, range −1.0 to +1.0/button/1 integer 0/1/button/2 integer 0/1/joystick/press integer 0/1X_MIN, X_CENTER, X_MAX, Y_MIN, Y_CENTER, and Y_MAX; then tune DEADZONE if the center still moves.Pin Mapping
Toolchain
`platformio.ini`
`src/main.cpp`
Startup Sequence
TouchOSC Addresses
Calibration