A Phone as a Gamepad: TouchOSC over Wi-Fi to a Microcontroller That Plays a USB Gamepad

Series: alt.ctrl activities; builds on Game controllers

A game takes a gamepad; TouchOSC turns a phone or tablet into a surface of controls you design yourself (its own Gamepad connection works the other way: it reads game controllers into TouchOSC). This recipe joins them through a small Wi-Fi microcontroller: the phone sends OSC over Wi-Fi, and the microcontroller, plugged into the computer by USB, presents itself as a USB gamepad, a HID game controller. A game that reads such gamepads needs nothing new (step 6 tries yours), and the controls can be laid out, sized and shaped for the player and the game.

It uses the CNMAT OSC library, Oscuino (CNMAT/OSC), whose release 4.0 made its Wi-Fi examples one sketch across the current Arduino Wi-Fi libraries, since each presents the same WiFiUDP object; at the release they compiled for all of them but the ESP8266, and had not yet run on hardware. After that release, the library's GitHub repository gained a WiFiProvision example, which brings a board up as its own network, with a settings page a phone is sent to, so no network password is written into the code.

This recipe has not been built as a whole. What has run on hardware: the OSC library over USB serial on both boards (the XIAO in its Sense version), and WiFiProvision on another ESP32-S3 board (step 3). The bridge itself, OSC over Wi-Fi in and a USB gamepad out, is not tried here.

A drawing: a phone showing TouchOSC, with two XY pads, four round buttons and two faders, sends OSC over Wi-Fi to a XIAO ESP32S3 box, which releases all if the heartbeat stops and connects by USB as a HID gamepad, with a serial log, to a computer that sees a USB gamepad.
The path: TouchOSC's controls, OSC over Wi-Fi to the ESP32-S3, and a USB gamepad to the computer. A drawing, not to scale.

Materials

The phone, the board and a cable
ItemAmazonAdafruitSeeed Studio
A phone or tablet running TouchOSC (hexler's app, for iOS and Android, and for Windows, macOS and Linux): TouchOSC
Seeed Studio XIAO ESP32S3: 21 × 17.8 mm; USB-C; Wi-Fi (fit the antenna that comes with it) and a native USB portXIAO ESP32S3
Or an Adafruit QT Py ESP32-S3: 21.7 × 17.8 × 5.7 mm; native USB (product 5426; 5700 adds 2 MB of PSRAM)5426
A USB-C cable to the computer
A 2.4 GHz Wi-Fi network both can join, or the board's own setup network (step 4)

Tools

  • The Arduino IDE with the esp32 core
  • TouchOSC's editor, on the phone or a computer
  • A coding assistant, given the Coding Prompt Build Block below
  • The computer's game-controller settings, or a gamepad tester in a browser

Skills

  • Designing a touch layout
  • Wi-Fi and OSC
  • USB HID devices
  • Programming with a coding assistant

Instructions

  1. Choose a microcontroller with Wi-Fi and a native USB port that can be a USB device: an ESP32-S3, such as Seeed's XIAO ESP32S3 or Adafruit's QT Py ESP32-S3, both of which (the XIAO in its Sense version) passed the OSC library's 4.0 echo and integer-width tests on hardware, over USB serial. The esp32 core's USB gamepad class is built only for chips with USB OTG, so an ESP32 with Wi-Fi but without it cannot be the gamepad.
  2. Set the board up in the Arduino IDE with the esp32 core, and in Tools choose USB Mode: USB-OTG (TinyUSB), which the core's Gamepad example warns that it needs (the XIAO's menu lists Hardware CDC and JTAG first); set USB CDC On Boot: Enabled, so that the serial log stays on the USB port beside the gamepad. Run that example first: pressing BOOT should press button 1 and the hat in the computer's game-controller settings, and after a longer press the next presses move the sticks and triggers too. If the IDE then loses the board's port, hold BOOT while plugging it in, and upload again.
  3. Bring the board onto the network with the WiFiProvision example, added to the OSC library's GitHub repository after release 4.0.0: if your installed copy of the library lacks it, take it from there. It makes the board its own open network at first, named OSCMCU- and part of its MAC: join it from the phone, which is sent to a settings page, type in your network's name and password, choose Join the network above, and Save and restart: the board restarts on that network. It answers /enq with its address, and has a USB serial console too. It has run on one ESP32-S3 board, M5Stack's Capsule (esp32 core 3.3.11), not yet on the XIAO or the QT Py, nor beside a USB gamepad. The setup network is open: while it is up, at first and again whenever the board cannot join, anyone in range can join it and change the settings, and the password you type crosses it once unencrypted (the example's README, section 8). Once joined, the board serves its settings page to anyone on that network, and it keeps the password unencrypted in its flash, where anyone with the board and a USB cable can read it. Give the board a network you are willing to have on a microcontroller, such as a guest network whose devices can reach one another.
  4. Design the layout in TouchOSC's editor: an XY pad for each stick, buttons for A, B, X, Y and the others, four buttons for the D-pad, and faders for the triggers. Give each control an OSC message, and check the range of values it sends, which the message settings can scale. Set each XY pad's x and y Default to the centre and their Default Pull high, so that the stick springs back when you let go, as a real one does, and each trigger's Default to its rest, with Default Pull too, so that it springs back like a real trigger. Then add an OSC connection of type UDP, with the board's address as Host and its OSC port, 8000 unless changed, as Send Port. The board prints its address on its USB serial log, and where mDNS works its name, such as OSCMCU-4DF8.local, serves too. Browse fills both in for a receiver that advertises itself on the network; WiFiProvision advertises its OSC port, though hexler's manual does not say which advertisements Browse looks for. With no network to join, the phone can stay on the board's open setup network and send to 192.168.4.1, where Browse will not find it.
  5. Program the board as the Coding Prompt Build Block below describes: receive the OSC, keep the gamepad's state, and send a gamepad report whenever it changes. TouchOSC sends a control's message only when its value changes, so a held button or stick is silent: to release everything when the link drops, time out on a heartbeat from the phone, not on the controls. Add one to the layout (not tried here): a short script at its root whose update function, which TouchOSC calls every frame, uses getMillis and sendOSC to send a message every half second, the period of WiFiProvision's own stream. After a dropout, a control still held on the phone stays released until it moves, since TouchOSC sends nothing new until then. If Wi-Fi stays down for 30 s, WiFiProvision restarts the board, and the gamepad leaves the computer and comes back. The board takes OSC from anyone who can reach it, with no password: have it accept gamepad messages only from the phone, and play on a network you trust.
  6. Test it: the computer's game-controller settings, or a gamepad tester in a browser, should show the sticks and buttons move as you touch the phone; then try the game itself. Then measure the board's share of the delay, and its spread: log when each OSC message arrives and when its report goes out. The time from touch to arrival (the phone, TouchOSC and Wi-Fi), and from report to game, needs a clock outside the board and is not covered here. Keep the settings page closed while measuring: the board serves it in the same loop.
  7. MIDI is TouchOSC's other way out: it sends MIDI to the phone's MIDI ports, among them Bluetooth MIDI devices and TouchOSC Bridge connections. Bluetooth MIDI has a recipe of its own, on a board built on Nordic's nRF52840; this one stays with OSC over Wi-Fi.
  8. Play: map the layout to a game, or make one layout per game.

Variations

  • Two players: two phones and two boards, one gamepad each.
  • Feedback to the phone: the board sends OSC back, so TouchOSC shows whether the computer is connected, or the gamepad's state.
  • A Raspberry Pi Pico W instead of the ESP32-S3: Arduino-Pico gives it Wi-Fi, and Adafruit TinyUSB its gamepad, as in the USB Nunchuck (not tried; WiFiProvision compiles for the Pico W but has not run on one).

Related resources

Coding Prompt Build Block

A prompt to give a coding assistant, to start the code for this activity. Copy the box, answer its questions about your board and pins, and test what comes back on the bench before you rely on it.

I am building "A Phone as a Gamepad: TouchOSC over Wi-Fi to a Microcontroller That Plays a USB Gamepad", the activity at https://adrianfreed.com/touchosc-wifi-gamepad.html.

Help me make a Seeed XIAO ESP32S3 (or an Adafruit QT Py ESP32-S3) into a bridge: it receives OSC over Wi-Fi from TouchOSC on a phone and presents the controls to a computer as a USB gamepad. Use the Arduino esp32 core with Tools > USB Mode set to USB-OTG (TinyUSB) and USB CDC On Boot enabled, its USB and USBHIDGamepad classes, and the CNMAT OSC library (OSCMessage, OSCBundle) over WiFiUDP. Keep the USB serial port for a log, and declare the gamepad as a global object, as the core's Gamepad example does: with USB CDC On Boot enabled, the core starts USB before setup() and refuses interfaces added later.

Networking: start from the WiFiProvision example in the CNMAT OSC repository's master branch (it was added after release 4.0.0), so that the board is its own network until it is told which to join, with no credentials in the code. It already listens for OSC on one UDP port I can change, prints the board's address over USB serial and advertises _osc._udp with mDNS: add the gamepad's addresses to its routes on that port, keep its radio awake, and check that TouchOSC's Browse lists the board. Accept gamepad messages only from the phone's address (the first sender, or one I set).

Mapping: accept messages for the left and right sticks (x and y), the two triggers, the four D-pad buttons and the other buttons by number, pressed or released, as TouchOSC's XY pads, faders and buttons send them; ask me the range each control sends, and which way its y runs, rather than assuming them. Scale the axes to -127..127, the gamepad descriptor's range, and ask me where a trigger should rest; combine the four D-pad buttons into the hat's eight directions (centred when none, or two opposite ones, are pressed) unless I want them as buttons. Keep the gamepad's state and send one report whenever anything changes. Keep the whole address map in one table I can edit, with addresses of my choosing: ADDRESSES.md has no gamepad capability, and its /btn and /joy are readings a board sends about its own hardware, so do not announce them in /enq; keep WiFiProvision's /enq answer as it is.

Safety: TouchOSC sends a control only when it changes, so a held button sends nothing: do not time out on control messages. The layout sends a heartbeat message every half second (ask me its address); release every button, centre the sticks and the hat, and return each trigger to its rest if the heartbeat stops for a time I set, a few heartbeat periods long, so that a dropped Wi-Fi link cannot leave a button held.

Log, with timestamps, each OSC message's arrival and each gamepad report sent, so I can measure the time from arrival to report on the board, and its spread.

Libraries to explore:

  • OSC for Arduino (CNMAT): encodes and decodes OSC messages, and sends them over USB or hardware serial (framed with SLIP) or over Ethernet and Wi-Fi UDP; its Applications folder has Max/MSP and Pd examples that receive them (https://github.com/CNMAT/OSC)
  • CNMAT OSC address contract (ADDRESSES.md): one OSC namespace for every board, named by capability, not by board: a program asks /enq, reads back the capabilities present, and receives each stream as a bundle with /state (https://github.com/CNMAT/OSC/blob/master/ADDRESSES.md)
  • Arduino-ESP32 USB library: the esp32 core's USB device classes; USBHIDGamepad presents two sticks, two triggers, a hat and 32 buttons, and is built only for chips with USB OTG; its Gamepad example wants Tools > USB Mode set to USB-OTG (TinyUSB) (https://github.com/espressif/arduino-esp32/tree/master/libraries/USB)

Before writing anything, ask me what I am using: the board and its pins, or the software (such as Max, Pd or Python), and check its documentation for what this needs. Put the pin numbers, ranges and other settings in one block at the top, each with a comment. Say which of the libraries above you use, and why. Start with a test that shows the raw readings, so that I can check the wiring and the ranges before the rest.

This activity by Adrian Freed is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0): you may share and adapt it with attribution, for non-commercial purposes such as personal projects and teaching, and you must share adaptations under the same licence. Product names and links belong to their suppliers.