Skip to content

guyni/test-usb

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

VEX V5 USB / Controller Tether POC

Proof-of-concept for bidirectional communication between a Mac laptop and a VEX V5 brain, over either:

  1. Brain USB direct — laptop → brain user port
  2. Controller USB tether — laptop → controller → VEXnet → brain

The brain runs one PROS program for both paths. The host CLI uses different serial protocols depending on which USB device is connected.

Brain USB vs controller USB

VEX exposes different USB serial interfaces depending on what you plug in. This is the main reason the host CLI has two code paths.

Brain plugged in directly (2 USB serial devices)

A V5 brain presents two CDC serial ports over one USB cable:

Port role Purpose Use with this POC?
System port PROS upload, V5 protocol, file transfer No — use for pros upload only
User port Program stdin/stdout (debug terminal) Yesusb_test.py brain-direct mode

On macOS, port names are usually /dev/cu.usbmodemNNN. The user port often ends in 3 (e.g. …103); the system port is the other number (e.g. …101). Linux may show /dev/ttyACM0 and /dev/ttyACM1 instead — check descriptions with --list-ports.

Example with only the brain connected over USB:

$ python usb_test.py --list-ports
Available serial ports:
  /dev/cu.usbmodem101 [brain_system]
    description : VEX V5 Brain
    manufacturer: VEX Robotics
    product     : VEX V5 Brain
  /dev/cu.usbmodem103 [brain_user]
    description : VEX V5 Brain
    manufacturer: VEX Robotics
    product     : VEX V5 Brain

Run the client on the user port:

python usb_test.py --port /dev/cu.usbmodem103

Data flow:

Host ──USB──► Brain user port ──► program stdin/stdout (COBS, sout topic)
              (fast, stable)

The system port (…101) is for PROS tooling (pros upload, system commands). This POC rejects it if you pass it by mistake.


Controller plugged in (1 USB serial device)

A V5 controller presents one CDC serial port — the system port only. There is no separate user/debug port on the controller.

The controller must be paired to the brain over VEXnet (or tethered). Program I/O does not stay on the controller; it is forwarded to the brain over the radio link using UserFifo packets (CDC2 ext command 0x27).

Example with only the controller connected over USB (brain linked via VEXnet, not USB):

$ python usb_test.py --list-ports
Available serial ports:
  /dev/cu.usbmodem102 [controller]
    description : VEX V5 Controller
    manufacturer: VEX Robotics
    product     : VEX V5 Controller

Run the client on that port:

python usb_test.py --port /dev/cu.usbmodem102

Data flow:

Host ──USB──► Controller system port ──VEXnet──► Brain ──► program stdin/stdout
              (slower; UserFifo protocol; download radio channel)

Side-by-side comparison

Brain USB direct Controller USB tether
USB devices shown 2 (system + user) 1 (system only)
Port to use User port (often …103) Controller port (e.g. …102)
Path to brain program Direct Controller → VEXnet → brain
Host protocol PROS stream on user port V5 system port + UserFifo 0x27
Speed / stability Best Slower; sensitive to VEXnet link
Brain USB required? Yes No (brain can be wireless)
Controller paired? Optional (for KEY: lines) Required

Both connected at once

If the brain and controller are both on USB, --list-ports may show all three:

Available serial ports:
  /dev/cu.usbmodem101 [brain_system]
    description : VEX V5 Brain
    ...
  /dev/cu.usbmodem102 [controller]
    description : VEX V5 Controller
    ...
  /dev/cu.usbmodem103 [brain_user]
    description : VEX V5 Brain
    ...

Pick one path explicitly with --port:

  • Brain direct: --port /dev/cu.usbmodem103
  • Controller tether: --port /dev/cu.usbmodem102

Only one process can open a given port. Do not run usb_test.py and PROS terminal on the same port at the same time.

Note: Port numbers (101, 102, 103) are examples from test hardware. Yours may differ — always use --list-ports and the [brain_user] / [controller] labels to choose.

Goals

  • Send text from the laptop to the brain (stdin)
  • Read text from the brain (stdout): echo, controller key presses, link status
  • Understand the V5 UserFifo protocol well enough for reliable controller-tether I/O
  • Document protocol pitfalls for VEX / PROS engineers

Project layout

test-usb/
├── src/main.cpp          # Brain program (PROS 4.2.1)
├── include/main.h
├── usb_test_cli/
│   ├── usb_test.py       # Host-side test client
│   └── requirements.txt
└── README.md

Brain program

Minimal PROS project (no drivetrain):

  • serial_task: reads stdin via getchar(), echoes to stdout, shows received line on LCD line 2 (RX: …)
  • opcontrol: polls master controller; prints KEY:<name> on button press and CTRL:connected|disconnected on link changes
  • Uses default PROS stream multiplexing (COBS + sout topic on stdout)

LCD lines:

Line Content
0 USB Echo POC
1 Controller link status
2 Last received message (RX: …)
4 Last key pressed

Optional stdin guards in main.cpp (for controller tether artifacts):

  • Ignore @ on stdin (see Protocol findings)
  • Reset RX buffer when H follows junk containing " or digits

Host CLI

usb_test_cli/usb_test.py auto-detects the port type (see Brain USB vs controller USB) and picks a code path.

Port (macOS example) [kind] label Handler
/dev/cu.usbmodem103 brain_user PROS V5UserDevice on user port
/dev/cu.usbmodem102 controller Custom UserFifo (0x27) over download radio channel
/dev/cu.usbmodem101 brain_system Rejected — use user port for this POC

Setup

# Brain firmware
pros upload

# Host CLI
cd usb_test_cli
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

PROS 4.2.1 requires GCC 14 (arm-gcc-bin@14 on Homebrew). Ensure pros --version and arm-none-eabi-g++ --version work before building.

Usage

cd usb_test_cli
source .venv/bin/activate

# List ports
python usb_test.py --list-ports

# Brain USB direct (recommended for development)
python usb_test.py --port /dev/cu.usbmodem103

# Controller tether (VEXnet)
python usb_test.py --port /dev/cu.usbmodem102

# Return controller to pit radio channel on exit (may drop VEXnet link)
python usb_test.py --port /dev/cu.usbmodem102 --restore-pit

On start the client sends Hello World\n and then prints brain stdout until Ctrl+C.

Expected output examples:

Hello World
KEY:A
CTRL:connected

Modes

--mode Description
auto (default) Pick handler from port type
pros Force PROS/V5 protocol
raw Raw serial (brain user port only; requires COBS disabled on brain)

Only one process may open a given serial port at a time. Close PROS terminal or other tools before running usb_test.py.

Architecture

Brain USB direct

Laptop ──USB──► Brain user port (/dev/cu.usbmodem…3)
                  └── PROS stream device (COBS, sout/serr topics)
  • Fast, stable
  • V5UserDevice read/write maps cleanly to program stdin/stdout
  • Same code path PROS terminal uses for brain user port

Controller USB tether

Laptop ──USB──► Controller system port (/dev/cu.usbmodem…2)
                  └── VEXnet ──► Brain
                        UserFifo CDC2 ext command 0x27
  • Single USB serial port on the controller (system port only)
  • Stdio must go through UserFifo packets, not the user debug port
  • Requires download radio channel for usable throughput
  • Much slower than brain-direct USB

See UserFifo + VEXnet (controller tether) for a full explanation.

flowchart LR
  subgraph direct [Brain direct]
    L1[Laptop] -->|USB user port| B1[Brain]
  end
  subgraph tether [Controller tether]
    L2[Laptop] -->|USB system port| C[Controller]
    C -->|VEXnet| B2[Brain]
  end
Loading

The brain program (src/main.cpp) is identical for both paths — it only uses getchar() / printf(). It does not detect how the host is connected. All controller-tether complexity lives in the host CLI (usb_test_cli/usb_test.py).

UserFifo + VEXnet (controller tether)

This section explains what happens when USB goes to the controller instead of the brain user port. See Protocol findings below for byte-level details and bugs discovered during testing.

End-to-end path

Host (usb_test.py)
    │  USB serial — V5 system protocol (CDC / CDC2 packets)
    ▼
Controller
    │  VEXnet radio link
    ▼
Brain
    │  PROS routes UserFifo data ↔ running program stdin/stdout
    ▼
Your program (getchar / printf)

The host never opens the brain’s user/debug port. Program I/O is forwarded over VEXnet using the UserFifo mechanism on the controller’s system port.

VEXnet (controller ↔ brain)

The controller and brain stay paired over VEXnet (wireless). For this POC the controller must be linked to the brain; the brain does not need its own USB cable.

Concept Role
Link state Controller must be connected to brain (CTRL:connected in program output, or PROS ControllerFlags.CONNECTED)
Pit channel Normal VEXnet mode — everyday radio use
Download channel Higher-bandwidth mode PROS uses for upload/terminal over controller

UserFifo I/O over controller USB is practical only on the download VEXnet channel. PROS switches with ft_transfer_channel('download') (CDC2 ext 0x10). That switch can briefly disturb the link — the CLI avoids switching back to pit on every exit for stability.

UserFifo (stdio over the system port)

On brain user USB, stdin/stdout use a dedicated serial pipe. On controller USB there is only the system port — no separate debug terminal. Program stdio is one service inside the V5 protocol, exposed as UserFifo (CDC2 extended command 0x27).

The same command handles read and write; the payload determines behavior.

Packet shape (host → device):

Byte Read (stdout) Write (stdin)
0 Channel (1 = stdio read side) Channel (2 per spec; 1 on tested hardware)
1 Read length Payload length
2+ (none) Payload bytes

The host wraps this in a V5 CDC2 packet (0x56 + ext id 0x27). The device responds with stdout bytes (reads) or ACK/NACK (writes).

Read stdout — host polls the brain for buffered program output:

Host sends:   [channel=1][read_len=0]
Brain returns: COBS-framed data (sout topic + payload)

Each poll is a round-trip over VEXnet, so controller tether is much slower than brain user USB. The host accumulates bytes until a COBS frame ends with \0, then decodes:

COBS frame →  "sout" (4 bytes) + payload (e.g. KEY:A, Hello World)

Write stdin — host sends bytes to the program:

Host sends:   [channel=1][len=N][N bytes]   (works on tested hardware)
              [channel=2][len=N][data…]    (spec-correct; NACK on tested hardware)

Bytes reach brain stdingetchar() in the brain program.

One read cycle vs one write cycle

Read stdout:

  1. Host opens controller system port (e.g. /dev/cu.usbmodem102)
  2. If needed, switch to download VEXnet channel
  3. Send UserFifo read: [1][0]
  4. Controller forwards request over VEXnet to brain
  5. Brain returns buffered stdout in the response
  6. Response travels VEXnet → controller → USB → host
  7. Host COBS-decodes sout frames and prints

Write stdin (then see echo):

  1. Host sends UserFifo write: [1][len][Hello World\n]
  2. Controller → VEXnet → brain → program stdin
  3. Brain echoes to stdout (same main.cpp as brain-direct path)
  4. Host must poll (read cycle above) to receive the echo

Three layers of “multiplexing”

Controller tether stacks several layers; brain-direct user USB only needs the last one:

Layer What it is Brain user USB Controller tether
1 VEXnet radio channel (pit vs download) Not used Required for UserFifo I/O
2 UserFifo channel byte (read stdout vs write stdin) Not used Required
3 COBS stream topics (sout / serr) Yes Yes (inside UserFifo payload)

This is why the brain-direct client approach fails on the controller port: you are on a system-only path that multiplexes many services, with stdio as one of them — not a dedicated stdin/stdout pipe.

What the CLI implements (controller path only)

run_controller_session() in usb_test.py:

  1. _controller_download_channel — switch to download VEXnet channel if needed; stay on it on exit
  2. _controller_user_fifo_read[ch1][read_len=0] (not PROS-cli’s 0x40)
  3. _controller_user_fifo_writech1-len with fallbacks; retries on timeout
  4. _controller_read_sout — buffer bytes, COBS decode, accept sout frames
  5. Link waits — do not write until controller reports connected to brain

Summary

Brain user USB Controller USB
USB interfaces 2 (system + user) 1 (system only)
Stdio transport Direct serial to program UserFifo 0x27 over system port
VEXnet in path? No Yes
Download channel switch? No Yes (for reliable I/O)
Host implementation V5UserDevice Custom UserFifo + COBS parser

Protocol findings

These were the main discoveries from reverse-engineering PROS-cli, vex-v5-serial, and testing on real hardware. For background on UserFifo and VEXnet, see the section above.

UserFifo packet (CDC2 ext 0x27) — implementation notes

One command handles both stdin write and stdout read. Layout:

Byte Read (stdout) Write (stdin)
0 Channel (1 = download/stdio) Channel (2 per spec; 1 on our hardware)
1 Read length (see below) Payload length
2+ (none) Payload bytes

Read (stdout) — vex-v5-serial uses [channel=1][read_len=0].

PROS-cli user_fifo_read() uses [channel=1][read_len=0x40]. On our hardware 0x40 leaked into brain stdin as @ (ASCII 0x40). Each read poll in the main loop re-injected that byte, which:

  1. Corrupted LCD (RX: @ instead of RX: Hello World)
  2. Polluted stdin → polluted echo → repeated garbage on stdout

Fix: custom read with read_len=0, not 0x40.

Write (stdin) — vex-v5-serial uses channel 2 with length prefix. Our controller NACKs channel 2; working formats:

Format Packet Notes
ch1-len [01][len][data…] Works on tested controller
ch1-read0 [01][00][data…] PROS-cli intended format (disabled in pros-cli)
ch2-len [02][len][data…] Spec-correct; NACK on tested hardware

PROS-cli user_fifo_write() is disabled (early return); this POC implements it.

COBS stream multiplexing

Brain stdout is COBS-framed with a 4-byte topic prefix:

  • sout — program stdout (what we read)
  • serr — stderr

Controller path: accumulate bytes until \0, COBS-decode, accept only sout frames.

Download radio channel

UserFifo I/O over controller tether needs the download VEXnet channel (high bandwidth). PROS upload/terminal switches with ft_transfer_channel('download').

Stability note: switching pit → download on every start and download → pit on every exit often drops the VEXnet link. This client:

  • Probes whether UserFifo already works (already on download from a prior run)
  • Switches to download only when needed
  • Leaves the controller on download channel by default on exit
  • Offers --restore-pit only when pit channel is needed (e.g. some PROS upload flows)

Problems encountered (chronological)

1. Wrong UserFifo read length (0x40)

Symptom: LCD showed RX: @; repeated @; garbled echo.
Cause: PROS user_fifo_read() sends read_len=0x40; that byte appeared on program stdin.
Fix: [channel=1][read_len=0] per vex-v5-serial.

2. Read polls polluting stdin → repeated garbage

Symptom: Lines like 7s0qgOshYmN"LHello World every few seconds.
Cause: Polluted stdin was echoed to stdout; controller fifo re-delivered the same buffer on each read poll.
Fix: Correct read format (above) broke the pollution loop.

3. Wrong write channel / format

Symptom: Partial messages (Helo World), mux noise before payload.
Cause: Writing on channel 1 (stdout/read side) mixed COBS/sout framing into stdin.
Fix: Prefer working write format (ch1-len on tested hardware); wait for VEXnet link before writing.

4. COBS mis-parsing (early client versions)

Symptom: Garbage even when brain output was clean.
Cause: Manual COBS parsing mis-aligned frames; PROS V5UserDevice treats failed decode as valid topic bytes.
Fix: Custom parser: split on \0, skip decode errors, require exact sout topic.

5. Download channel switching instability

Symptom: Controller lost brain link on start/stop; serial timeouts / crashes.
Cause: Repeated pit ↔ download transitions.
Fix: Stay on download between runs; probe before switching; retry writes; catch OSError; optional --restore-pit.

6. USB port contention

Symptom: Intermittent timeouts, access denied.
Cause: Only one process may open a port; PROS terminal and usb_test.py conflict on the same /dev/cu.usbmodem*.
Fix: Close other serial tools; brain system vs user ports are separate interfaces.

Current status

Path Send Receive Stability
Brain USB direct Excellent
Controller tether ✅ (ch1-len) Good after protocol fixes; slower; sensitive to link state

With fixes applied and filtering removed from the host client, stdout shows raw sout data — no garbage observed on tested hardware.

References

License

POC / research code for sharing with VEX engineering. PROS kernel and tooling remain under their respective licenses.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages