Overview

vexide internal documentation includes information for first time contributors, technical details about features we want to implement, technical explanations of already implemented features, current priorities, and more.

These docs are mainly for the vexide library, but the old pros-rs docs have been archived.

If you were looking for vexide user documentation, you should go to the main docs page.

Project info

vexide, our namesake library, is the successor to pros-rs. Instead of linking with and creating bindings for a library like PROS, vexide implements everything on its own. Every line of code in a vexide program is open source and written in Rust.

The major technical difference between vexide and pros-rs is that vexide directly calls VEXos jumptable functions without going through libv5rt's wrapper functions. This allows us to implement everything ourself instead of linking to libpros. vexide is also different from other Brain libraries in that it doesn't use an RTOS. vexide uses Rust async/await for multitasking. vexide programs are incredibly small for not being hot/cold linked and have higher stack and heap sizes than any other library.

Contributing

This chapter aims to give tips to first time vexide contributors. If you want to contribute to vexide, make sure you join the discord server! For info on contributing guidelines, please read CONTRIBUTING.md in the vexide repository.

Note

All development on pros-rs has stopped. We are now solely developing and maintaining vexide.

Project structure

Because vexide is split into multiple crates and multiple repositories, finding the part of vexide you want to work on can be a little overwhelming. To hopefully make it easier to find what you are looking for, here is a simple graph of the project structure:

Technical Implementations

vexide is an incredibly low level library and has many implementations that are very technical. This chapter aims to make our code easier to understand.

Startup

vexide produces working binaries without linking to any external libraries. This means that we have to write all of the code that gets run during startup. Luckily program startup is fairly simple, its just a pain to set up.

The boot process for programs works as such:

  1. VEXos loads the program into memory at 0x3800000
  2. VEXos reads the cold header and changes program behavior accordingly.
  3. VEXos starts program execution at 0x3800020 (the address just after the cold header)
  4. vexide zeroes the entire BSS section and sets the stack pointer to the bottom of the stack
  5. vexide initializes the heap allocator
  6. vexide starts the async executor and the users main function inside it

Explanations

The structure of a user program looks like this: program-anatomy

In order to explain the startup code, I will be going over every one of these six steps and how they are implemented.

Steps 1-3

Before we can even get to step one we need to get a program building correctly. The vexide-startup crate contains a v5.ld file that tells clang how to link vexide programs correctly. It tells the linker where cold memory is located and the locations of all of the sections necessary for running programs. The only parts of the linker script that are important to understand for the first three steps are in the .text section.

Specifically the .boot and .cold_magic sections are important. .boot is located at 0x3800020 and is the entrypoint to the program. .cold_magic is located at the very start of cold memory (0x3800000) and it stores information that VEXos uses to change the behavior of the program. In vexide, we call this "cold_magic" the cold header because it is a more fitting name. The name cold_magic originates from PROS, but it exists in vexcode under the name VCodeSig. The first four bytes of the cold header must be the string XVX5 encoded in ascii. The rest of the 32 bytes are used for various program options. For more detailed information, look at the docs for the ColdHeader struct in vexide.

After this build process, steps 1-3 work as intended.

Step 4

This step is the first, and only, time that assembly is required. Yay!

vexide sets the stack pointer with this inline assembly:

ldr sp, =__stack_start

This instruction loads the value of =__stack_start into sp (the stack pointer register). The symbol __stack_start is defined by the v5.ld linker script and the name is pretty self-explanatory.

The next task is to zero the BSS section. vexide uses a loop that increments a pointer to achieve this effect.

#![allow(unused)]
fn main() {
unsafe {
    let mut bss_start = addr_of_mut!(__bss_start);
    while bss_start < addr_of_mut!(__bss_end) {
        core::ptr::write_volatile(bss_start, 0);
        bss_start = bss_start.offset(1);
    }
}
}

You may think this seems horribly unsafe, and you would be right under normal circumstances; however, at this point nothing has set any values in the BSS section and it must be zeroed to avoid corruption.

Step 5-6

Steps five and six are pretty simple because at this point we can run any code that doesn't use the heap. In fact, we use these newfound capabilities to do the incredibly important task of printing our banner. 🏳️‍🌈

Currently, we use Talc for our heap allocator. We pass it a range of memory from the start of the heap defined in the linker script all the way to the end. Our linker script allocates the largest amount of memory that it can for the heap so vexide programs have the most memory of any V5 Brain library.

Spinning up the executor is straightforward. We just call vexide_async::block_on on the users main function.

And with that, we have a booting program!

Critical Section

vexide has a critical section implementation that is used to ensure that synchonization types are safe from interrupts.

We use the critical_section so that libraries that require a critical section implementation can easily access it.

When building for WASM, the critical section implementation is replaced with no-ops because the critical section is implemented using ARM assembly.

Implementation

When entering the critical section we want to disable all IRQ interrupts. What this means is that nothing can interrupt the flow of our code and mess with memory that needed to be untouched.

The first step that vexide takes when entering the critical section is checking if interrupts are already disabled.

#![allow(unused)]
fn main() {
let mut cpsr: u32;
asm!("mrs {0}, cpsr", out(reg) cpsr);
let masked = (cpsr & 0b10000000) == 0b10000000;
}

This block of code moves the value of the cspr register into the cspr variable and then checks if the 7th bit is a 1. If it is 1, IRQ interrupts are masked. It is important to know if interrupts are masked because nested critical sections shouldn't re-enable interrupts if an inner section is exited.

Next, we actually disable interrupts.

// Disable IRQs
cpsid i
// Synchronization barriers
dsb
isb

The synchronization barriers shown here are taken from the PROS FreeRTOS implementation.

When exiting the critical section, we first check if interrupts were disabled when the critical section was entered. This is where checking the value of cspr register comes into play. We run this assembly if the interrupts were disabled. It re-enables IRQ interrupts

// Re-enable IRQs
cpsie i
// Synchronization barriers
dsb
isb

Again the sychronization barriers are taken from PROS.

VEX SDK Tech Notes

The VEX V5's SDK is largely undocumented, making it difficult to develop software like vexide. This section includes notes on how to use various portions of the public SDK and is intended as a resource for developers creating software that targets the V5.

Display SDK Notes

The Vex V5 SDK contains functions for drawing to the V5’s 480 × 272 pixel LCD screen. The origin point of the display is at the top left, and the point (479, 271) is at the bottom right.

Code Signature

Several publicly released code signature options affect the operation of the display.

  • V5_SIG_OPTIONS_INDG (1 << 0): Inverts the background color to pure white.
  • V5_SIG_OPTIONS_THDG (1 << 2): If VEXos is using the Light theme, inverts the background color to pure white.

If both options are enabled at once, the themed option will win and the background will stay black if the V5 is using the Dark theme.

Foreground and background colors

Foreground and background colors can be set using vexDisplayForegroundColor and vexDisplayBackgroundColor. These colors will then be used for all future display calls unless changed again. In all programs, the foreground starts off-white (#c0c0ff). By default, the background starts black (#000000) but this isn’t guaranteed and can be changed using the program’s code signature. Colors are u32s in the format 0x00RRGGBB where R, G, and B are the red, green, and blue components.

Erasing

vexDisplayErase acts similarly to vexDisplayRectFill but uses the background color instead and always fills the entire screen. As with other drawing-related functions, portions of the screen that are outside the clip region will not be modified.

Scrolling

vexDisplayScroll*-form methods move a region of the screen nLine pixels upwards, without affecting portions of the screen outside the specified scroll region. Since nLine is a signed integer, a negative value will move the pixels in the region downwards instead. Pixels that move outside the region being scrolled are discarded, and any portions of the region that no longer have a value after the operation are set to the background color.

The vexDisplayScroll function scrolls the region defined by all the pixels whose y-axis coordinate is within the range [nStartLine, 272). Consider a brain with the following image displayed on the screen:

The V5 display with a gradient background before scroll has been applied. A green line illustrates the bottom of the display.

Running vexDisplayScroll with nStartLines set to 150 and nLines set to 50 would result in the following:

The V5 display after scrolling. The green line has been moved up.

The operation does not edit the top nStartLine (150) pixels because they are outside the scroll region. The pixels inside the scroll region are moved up by nLines (50) pixels (and discarded if they would modify a portion of the screen outside said scroll region). The operation leaves nLines (50) pixels of empty space at the bottom, which is subsequently filled with the background color (in this case it was set to a shade of red).

vexDisplayScrollRect works similarly, but allows you to specify a more detailed scroll region. The parameters passed to the function are inclusive, so the points you passed will be included inside the scroll region. However, it appears to be somewhat bugged at the time of writing: it overwrites one too many lines, setting the bottommost row of scroll data to the background color.

The V5 display after a rectangular region has been scrolled. Pixels that left the region were not drawn. The SDK call deleted 1 too many rows of scroll data when drawing due to an off-by-one error.

Copying image buffers

vexDisplayCopyRect is used to copy many pixels to the display at once from a pixel buffer outside the display. Each u32 element in the buffer is considered a pixel and is parsed in the same format used by vexDisplayForegroundColor. The function allows you to specify the region to write to, the pointer to your image buffer, and the stride (or the number of u32 pixels in your buffer per 1 row).

Rectangles

The parameters of vexDisplayRect*-form methods are inclusive, meaning that the coordinate pairs you specify are inside the rectangle that is created. Thus, the area of a rectangle created with this set of SDK functions is (1 + x2 - x1) * (1 + y2 - y1) pixels.

Circles

Circles are not antialiased.

Text

V5 display after a call to vexDisplayStringAt: the coordinate parameters are the upper left corner of the string printed

Text can be displayed using one of the many printf-style functions in the Display SDK. Most of them have 2 variants: line-based, and coordinate-based. The origin of coordinate-based text is its top-left corner. Line-based text functions are equivalent to calling their coordinate-based form with the x-coordinate 0 and the y-coordinate 34 + (20 * nLineNumber).

Transparent text can be drawn using vexDisplayPrintf with its bOpaque parameter set to 0.

V5 display with transparent text

Image reading

Using the SDK, images can be parsed from the BMP and PNG container formats to their raw pixel buffers for use with vexDisplayCopyRect. vexImage* functions require the caller to allocate a data buffer that the parsed pixel data will be stored in and then pass it as an out-parameter. Thus, the caller must commit to a maximum image size and pre-allocate a buffer that could hold that largest possible image before calling interacting with this portion of the SDK. The maxh and maxw parameters used by these SDK functions must, when multiplied, equal the number of u32 pixels that could fit in the data buffer.

The v5_image struct

The v5_image struct is used as an out-parameter by the vexImage* family of SDK functions.

Its fields consist of:

  • data: This field must be set before the read operation as a pointer to the pre-allocated pixel buffer. After an image read operation, said image’s pixels are written to the location specified by this field.
  • width and height: These will be set by the SDK by the end of a successful image read operation and are the definitive width and height of the image that was loaded. In contrast to maxh and maxw, these convey the actual number of pixels written rather than the maximum capacity of the pixel buffer.
  • p: This field is only set by the SDK after a vexImageBmpRead call and appears to point to the first pixel of the second row in the pixel buffer.

BMP images

BMP-formatted images can be read using the vexImageBmpRead function. It returns 0 on failure and 1 on success.

A BMP-formatted image intended for testing, displayed on the VEX V5 screen

BMP reading safety

  • ibuf must be null, OR the buffer it points to must begin with a well-formed BMP image. Because the function does not take a buffer length argument, the only way it could know the size of the image is by relying on the header of ibuf being valid.
  • oBuf must point to an initialized v5_image struct or null.
  • (*oBuf).data must point to a mutable allocated image buffer that is at least maxw * maxh * 4 bytes long or be null.

BMP reading postconditions

  • If the image read operation failed, the function returns 0.
  • If the image read operation was successful, the function returns 1 and oBuf is updated as described in The v5_image struct.

PNG images

PNG-formatted images can be read using the vexImagePngRead function. It returns 0 on failure and 1 on success.

A PNG-formatted image intended for testing, displayed on the VEX V5 screen

PNG reading safety

  • ibuf must be null, OR point to a buffer of at least length ibuflen
  • oBuf must point to an initialized v5_image struct or null.
  • (*oBuf).data must point to a mutable allocated image buffer that is at least maxw * maxh * 4 bytes long or be null.

PNG reading postconditions

  • If the image read operation failed, the function returns 0.
  • If the image read operation was successful, the function returns 1 and oBuf is updated as described in The v5_image struct.

Clip regions

The clip region is the region of the screen that the current VEX thread can modify. It can be updated using the vexDisplayClipRegionSet function, or changed for a different thread using vexDisplayClipRegionSetWithIndex. In that case, the index parameter must a 0-indexed thread ID.

Double buffering

By default, LCD updates are written directly to the display. In cases where the display is updating quickly (such as in a loop), this can cause flickering issues due to the lack of VSync. Calling vexDisplayRender every frame resolves this issue by enabling double-buffer mode and only updating the display when it is called. Later calling vexDisplayDoubleBufferDisable will return the display to its direct mode.

When calling vexDisplayRender, enabling the bVsyncWait parameter will sleep the current thread until the screen is ready to refresh, and enabling the bRunScheduler parameter will run background tasks while waiting.

Font and text size

The functions vexDisplayTextSize and vexDisplayFontNamedSet do not appear to affect font or text size in a way that is accessible to the user.

Hard To Implement Features

There are many features that we would love to have in vexide, but don't have implemented. Often these features are not implemented because they are very technical or too hard to implement. This chapter goes over these features and describes what we know about them, why we haven't implemented them yet, why we want them in the first place, and possibly more depending on the feature.

Hot/Cold Linking

Hot/Cold linking is a feature of VEXos that allows for a user program to be split into two files, named the hot and cold binaries. The purpose of splitting your binary up into two files is that the code in the libraries you use, PROS, pros-rs, vexide, VEXCode, etc. can be uploaded once in the cold binary and never again. User code ends up in the hot binary. The hot binary is the only binary that is uploaded when you make changes. When working with a large project, hot/cold linking can save a huge amount of time spent uploading. A PROS V3 project can take over 30 seconds longer to upload when using a monolith binary. Currently vexide only supports monolith style linking which puts all code into one binary.

Stack Unwinding

When a Rust program encounters an unrecoverable error, it panic!()s. The behavior of a panic is intentionally not defined in Rust, and may vary based on the platform and strategy.

In general, Rust defines two "panic strategies":

  • abort: After printing a stacktrace (defined by a #[panic_handler]), the program will exit immediately. No memory cleanup is performed. This form of panic is truly unrecoverable.
  • unwind: Rather than exiting immediately, the program's stack memory will be unwound, allowing for a more graceful exit. Every active struct instance's Drop implementation will be called, and panics created outside of the main thread can be caught and handled.

no_std and Panics: The current approach.

Panic behavior is platform-dependent. On a more "traditional" platform target, we have the luxury of an operating system with I/O utilities and a well-defined allocator. On an embedded target such as armv7a-VEXos-eabi (the platform target defined by vexide), Rust takes no assumptions and leaves the panic implementation up to us.

In order to build a barebones Rust program on bare metal, we must define a panic handler:

#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
    loop {}
}

In this (extremely barebones) example, if panic!() were to be called, the panic handler would simply spinlock the CPU indefinitely. If this were an enviornment where libc was available (PROS has access to newlib libc) we could call libc::exit(1).

vexide currently defines a basic panic handler under the assumption of the abort strategy:

  • It prints a panic message over stdout through serial.
  • It optionally displays a brief error message on the brain screen.
  • Finally, it exits the user program using PROS' wrapper over over the vexSystemExitRequest() SDK call.

The panic implementation does not perform stack unwinding or any cleanup, as armv7a-VEXos-eabi's panic_strategy is currently abort.

Unwinding on ARM

As it turns out, it might be possible to support "panic_strategy": "unwind" on our bare metal ARM target. PROS links against libgcc, which defines primitives for dealing with exception handling, including the parts needed to implement an unwinding panic handler in Rust. Unfortunately, stack unwinding has a very complicated and advanced implementation. The best we have to go off of is the libpanic implementation over libgcc used internally by rustc. This piece of code is a pretty obscure part of Rust's runtime. It's not well documented and is supposedly reverse-engineered from other language implementations.

Essentially, in order to implement an unwinding panic, we must first override the eh_personality language item which is called by Rust internally when panicking using unwind. The #[eh_personality] routine is then responsible for hooking into libgcc's ARM unwinding functions for actually unwinding the stack. These functions are actually part of the Itanium C++ Exception Handling ABI.

In a previous attempt to reimplement unwinding panics, that item looked something like this:

#![feature(lang_items)]

#[lang = "eh_personality"]
#[no_mangle]
unsafe extern "C" fn rust_eh_personality(
    state: _Unwind_State,
    exception_object: *mut _Unwind_Exception,
    context: *mut _Unwind_Context,
) -> _Unwind_Reason_Code {
  ...
}

PROS Additionally has its own unwinding exception handler.

Other Alternatives

The unwinding crate provides pure-rust implementations of exception handlers that we could use in place of a libgcc implementation. Unfortunately, ARM support isn't available in this crate yet.

VEX Simulators

VEX V5 simulators are useful for debugging user programs without access to a real robot. This chapter describes the vexide/pros-rs ecosystem of simulator-related applications and how to write compatible software.

Components of a Simulator

The following components are the most essential parts of a VEX simulator. Applications might implement one or more of these, or fill a supporting role not listed.

Code Execution Backend (the "Simulator")

The code executor is probably the most important part of the simulator. Its job is to interpret user programs along with inputs like joystick controls and peripherals state in order to decide what the robot would do in real life (serial output, peripherals output, warnings, panics, etc). It might be implemented using a virtual machine, using a WebAssembly engine, or through dynamic library loading.

There are several code executors for VEX simulators, in varying stages of completion. Here are a few of the ones I know about:

Physics Backend

This is a piece of software that decides robot and peripheral state (including velocity, temperature, etc.). It tells the code execution backend about the world state so that the simulated robot code can react to it.

Frontend

The simulator frontend is usually the only part the user sees. It combines information from the physics backend and output from the code executor to show and overview of the simulation's progress. It also sends user input (e.g. joystick/touch) to the code executor to act accordingly.

  • vexide/pros-simulator-gui

    A cross-platform Tauri application, it uses pros-simulator's frontend interface to start and manage a compatible simulator.

Vexide Simulator Protocol

The Vexide Simulator Protocol enables communication between VEX robot simulators and user-facing frontends using a JSON-based protocol.

Specification

This specification details the Vexide Simulator Protocol, used by tools in the vexide ecosystem to achieve interoperability between simulators and their frontend. It is primarily intended to inform library developers creating software intended to be compatible with tools utilizing the Protocol.

Requirements

The keywords “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this section are to be interpreted as defined in RFC 2119.

Overall Operation

Vexide Simulator Protocol (the "Protocol") is a newline-delimited JSON-based protocol intended to facilitate communication between robot simulators and frontends like GUIs. All communication MUST be encoded in the JSON Lines format.

Implementors of the Protocol SHALL recognize two unique roles in a given session: the Code Execution Backend (the "Simulator" or "Backend") and the Frontend. A Vexide Simulator Protocol session begins with the creation of an I/O stream between the Simulator and the Frontend. Implementors SHOULD support sessions over the standard streams of the Simulator using its standard input and standard output and MAY support other methods such as Unix domain sockets or Transmission Control Protocol (TCP) streams.

Throughout a given session, the Code Execution Backend and the Frontend send JSON messages over the underlying stream. Messages sent by the Backend are referred to as Events because they are used to notify the Frontend of changes in the simulator world. Messages sent by the Frontend are referred to as Commands because they are used to control the behavior of the Simulator.

Standard Stream Considerations

When using standard streams to communicate, data sent by the Backend over its standard error stream MUST be considered unstructured logs by the Frontend and SHOULD be made accessible to users. This requirement is made to aid the discoverability of Simulator error messages and improve the troubleshooting experience.

Data Type Format

Events and commands sent using the Protocol contain various JSON data types analogous to Rust type values. Implementors of the Protocol MUST decode and encode data types to a format compatible with the Serde library configured to use externally tagged enums.

Specific Commands and Events

The specific commands and events that implementors may use are currently defined in the Commands and Events sections, respectively.

Protocol Timeline

A session begins with the Frontend sending a Handshake command containing the maximum Protocol version it is compatible with (the current version is 1), as well as an array of string IDs containing an unspecified list of extensions (i.e. deviations from the specification) that it is compatible with. If the Simulator is not compatible with the protocol version sent by the Frontend, it SHOULD immediately close the stream, ending the session. To continue the session, it MUST send a Handshake event containing the protocol version and extensions that will be used henceforth. The protocol version MUST be less than or equal to the one sent by the Frontend, and the extensions array MUST only contain values that the Frontend indicated it was compatible with. Unknown fields in the handshake SHALL be ignored by all implementations.

The handshake is finished when both the Frontend and Backend have sent Handshake messages. Events and commands other than Handshake MUST NOT be sent until the handshake has finished.

After the handshake has finished, the Frontend MUST send ConfigureDevice commands for each peripheral that is already configured to make them available to the robot code. The Frontend SHOULD also send a CompetitionMode command containing the desired starting competition mode, as well as any other commands necessary to set the Simulator to the desired starting state.

If the Frontend does not send a CompetitionMode command, the Backend SHALL default to a competition mode with the following fields:

  • enabled: true
  • connected: false
  • mode: Driver
  • is_competition: false

Before continuing the session, the Backend MUST handle setup commands sent by the Frontend and at any point send a Ready event when it is capable of immediately beginning code execution. To continue the simulation, the Frontend MUST wait for the Backend's Ready event, and then send the StartExecution commands. The Backend SHOULD ignore StartExecution commands received before it has signaled it is ready.

After receiving the StartExecution command, the Backend SHALL take the steps necessary to begin executing robot code and SHOULD send events as necessary to notify the Frontend of changes to the state of the simulation.

If the robot code exits for any reason, the Simulator MUST send an Exited event, followed by ending the session by closing the underlying stream. If the code exited due to a fatal error, the Simulator SHOULD precede its Exited event with an Error-level Log event containing a concise explanation of what went wrong. If the Simulator ends the session after the handshake by closing the underlying stream without first sending an Exited event, the Frontend SHOULD handle the situation as an internal error in the Simulator.

Code Signatures

Backends SHOULD send a VCodeSig event containing the robot program's Code Signature (i.e. "Cold Header") as soon as possible after the handshake has finished. Code signatures MUST be encoded in the Base64 format.

Here is the output of hexdump -C on a code signature commonly used by vexide programs:

00000000  58 56 58 35 02 00 00 00  00 00 00 00 00 00 00 00  |XVX5............|
00000010  00 00 00 00 00 00 00 00                           |........|
00000018

This code signature would be encoded by a compatible Simulator as the following Base64-encoded JSON string literal:

"WFZYNQIAAAAAAAAAAAAAAAAAAAAAAAAA"

Logging

Backends MAY send human-readable Log events at any time after the handshake is completed. Frontends SHOULD make these logs available to users and MAY store them for later retrieval. Frontends MAY also implement filtering to reduce clutter caused by log levels that tend to be more verbose.

Example Session

This example session is provided as a resource to help developers imagine one of the possibilities of a valid exchange using the Protocol. Multi-line JSON, as well as comments preceded by a double-slash (//), are not valid in a true implementation and are used to aid the reader in understanding.

// The Frontend begins the session by sending a handshake.
// Frontend:
{ "Handshake": { "version": 1, "extensions": [] } }
// The Backend agrees on using version 1 without extensions.
// Backend:
{ "Handshake": { "version": 1, "extensions": [] } }
//
// The Frontend begins configuring devices.
// Frontend:
{ "ConfigureDevice": {
  "port": 1,
  "device": { "Motor": {
    "physical_gearset": "Red",
    "moment_of_inertia": 1.0
  } }
} }
{ "ConfigureDevice": {
  "port": 2,
  "device": { "Motor": {
    "physical_gearset": "Green",
    "moment_of_inertia": 1.0
  } }
} }
// Meanwhile, the Backend has finished loading the robot program's code signature.
// Backend:
{ "VCodeSig": "WFZYNQIAAAAAAAAAAAAAAAAAAAAAAAAA" }
// The Frontend continues setup.
// Frontend:
{ "ConfigureDevice": {
  "port": 3,
  "device": { "Motor": {
    "physical_gearset": "Red",
    "moment_of_inertia": 2.0
  } }
} }
{ "CompetitionMode": {
  "enabled": true,
  "mode": "Driver",
  "connected": true,
  "is_competition": false
} }
// The Frontend has finished configuring devices and is now waiting for the
// Backend to be ready.
//
// The Backend is now ready to execute the robot code.
// Backend:
"Ready"
//
// The user has indicated the robot code should start.
// Frontend:
"StartExecution"
// The robot program has sent a line over the serial port.
// Backend:
{ "Serial": {
  "channel": 1,
  "data": "SGVsbG8gV29ybGQhCg=="
} }
// The robot program is moving a motor.
// Backend:
{ "DeviceUpdate": {
  "port": 1,
  "status": { "Motor": {
    "velocity": 1.0,
    "reversed": false,
    "power_draw": 1.0,
    "torque_output": 1.0,
    "flags": 0,
    "position": 2.5,
    "target_position": null,
    "voltage": 5.0,
    "gearset": "Red",
    "brake_mode": "Brake"
  } }
} }
// The user has changed the competition mode.
// Frontend:
{ "CompetitionMode": {
  "enabled": false,
  "mode": "Driver",
  "connected": true,
  "is_competition": false
} }
//
// The robot code has exited.
// Backend:
"Exited"

Communication

The code executor and frontend talk over a stream in newline-delimited JSON format.

The backend sends Events which represent a change in simulator state. These are used by the frontend to correctly display the state of the simulated program.

The frontend sends Commands to the code executor to control the robot code environment, simulating changes in robot hardware (like controller input and LCD touch events) or competition phase.

When using Standard I/O to communicate, data sent by the simulator engine over stderr must be considered unstructured logs and should be accessible to the user.

Events

Events are sent from the simulator backend to the frontend to describe simulator state changes.

  • Handshake: Specifies the version of the Protocol used, as well as extensions that the Backend is compatible with. Fields:
    • version: Nonzero integer. Compatible implementations MUST set this to the value 1.
    • extensions: String array
  • ScreenDraw: Draw something on the screen. ScreenDraw fields:
  • ScreenClear: Fill the entire screen with one color. ScreenClear fields:
  • ScreenDoubleBufferMode: Set the double-buffer mode of the screen. When double buffer mode is enabled draw calls should be stored in a intermediate buffer and flushed to the screen only once a ScreenRender event is sent. ScreenDoubleBufferMode fields:
    • enable: boolean
  • ScreenRender: Flush the screens double buffer if screen double buffering is enabled.
  • VCodeSig: The cold header ("Code Signature") of the robot program, encoded as a base-64 string.
  • Ready: The backend is ready to start executing user code.
  • Exited: The backend has stopped exiting user code and will be terminating.
  • Serial: Data that has been flushed from the serial FIFO buffer. Serial fields:
    • channel: integer
    • data: Base64-encoded string
  • DeviceUpdate: State regarding an ADI or Smart device has changed. DeviceUpdate fields:
  • Battery: New statistics about the current state of the robot battery are ready. Fields:
    • voltage: The output voltage of the battery. Float value in Volts.
    • current: The current draw of the battery. Float value in Amps.
    • capacity: The remaining energy capacity of the battery. Float value in Watt-Hours.
  • RobotPose: The physically simulated robot has moved. RobotPose fields:
    • x: float
    • y: float
  • RobotState: The state of the physically simulated robot has changed. Implementation is TBD.
  • Log: Log a message. This is not to be used in place of serial. This is purely for messages from the backend itself. Log fields:
    • level: LogLevel
    • message: UTF8 encoded string.
  • VEXLinkConnect: Tell the, currently nonexistent, VEXLink server to open a VEXLink connection. VEXLinkConnect fields:
  • VEXLinkDisconnect: Tell the VEXLink server that the VEXLink connection has been terminated. Fields:

Commands

Commands are sent from the frontend to the backend and signal for specific actions to be performed.

  • Handshake: Specifies the version of the Protocol used, as well as extensions that the Frontend is compatible with. Fields:
    • version: Nonzero integer. Compatible implementations MUST set this to the value 1.
    • extensions: String array
  • Touch: Touch a point on the screen. Only one touch can be registered on the Brain display. Touch fields:
  • ControllerUpdate: Updates the current state of the controller. A ControllerUpdate enum.
  • USD: Mount or unmount a directory as the V5's SD Card. Robot programs will be able to read and write to this directory. Fields:
    • root: string (path to sd card's root), or null to unmount
  • VEXLinkOpened: The VEXLink server has successfully opened a connection. Fields:
  • VEXLinkClosed: The VEXLink server has closed a VEXLink connection. Fields:
  • CompetitionMode: Update the competition mode. Fields:
    • enabled: boolean
    • connected: boolean
    • mode: CompMode
    • is_competition: boolean
  • ConfigureDevice: Configure a port as a specified device. Fields:
  • AdiInput: Set the input voltage for an ADI port. Implementation is TBD. Fields:
  • StartExecution: Start executing user code. This should be treated as a no-op by the backend until it sends a Ready Event.
  • SetBatteryCapacity: Updates the remaining battery capacity to a new Watt-Hours value. If this command is not sent, the simulator must default to a value of 14 Watt-Hours, the maximum capacity of the VEX V5 battery. This value may be used by the simulator to help calculate the voltage of the battery. Fields:
    • capacity: float in Watt-Hours

Data types

Several different enums and structs are used for communication to and from the backend.

All enum datatypes are encoded using externally tagged representation; see Serde enum-representations.

DrawCommand

An enum with these variants:

  • Fill: Fill a shape on the screen. Fields:
  • Stroke: Draw the outline of a shape on the screen. Fields:
  • CopyBuffer: Draw a pixel buffer to the screen with a given stride and start and ending coordinates. CopyBuffer fields:
    • top_left: Point
    • bottom_right: Point
    • stride: nonzero integer
    • buffer: image buffer of 32-bit RGB pixels as a base64-encoded string

Shape

An enum that describes a shape to be drawn on the screen. Shape has these variants:

  • Rectangle: Rectangles are drawn starting at the top left coordinate and extending to the bottom right coordinate. Rectangle fields:
  • Circle: Circles are drawn with a coordinate at the center of the circle and a radius. This variant has these fields:
    • center: Point
    • radius: integer
  • Pixel: Draw a single pixel at a coordinate. Pixel fields:

Point

A struct that stores a pixel coordinate. The origin is at the top left of the screen. Point fields:

  • x: integer
  • y: integer

DeviceStatus

An enum representing the state of a device. Every time the state of a device on any port changes, this enum will be sent. This type should be considered "non-exhaustive" and variants may be added without causing a breaking change. DeviceStatus variants:

  • Motor: The state of the motor has changed. Motor fields:
    • velocity: float in radians per second.
    • reversed: boolean
    • power_draw: float in Watts
    • torque_output: float in Nm
    • flags: A 32-bit integer bitfield containing the motor flags that will be provided to the robot code. VEX V5 uses this to signal motor faults. Implementors should set this to 0.
    • position: float in radians
    • target_position: optional float in radians
    • voltage: float in Volts.
    • gearset: MotorGearSet
    • brake_mode: MotorBrakeMode

MotorGearSet

Represents the gearset of a smart motor device. Variants:

  • Red: 36-1 ratio
  • Green: 18-1 ratio
  • Blue: 6-1 ratio

MotorBrakeMode

Represents the brake mode of a smart motor device. Variants:

  • Coast
  • Brake
  • Hold

MotorFlags

TBD

LinkMode

An enum representing the type of VEXLink connection. LinkMode variants:

  • Manager
  • Worker

TouchEvent

An enum representing how a touch on the Brain display has changed. TouchEvent variants:

  • Released: The screen is no longer being pressed.
  • Pressed: A new touch has been registered on the screen or the screen has been held but not long enough to start sending Held events.
  • Held: Sent in place of Pressed events once a timeout has been reached. I cannot find the exact timeout for this, but it can easily be tested with a simple program.

Port

An enum containing a Smart port or ADI port. Port variants:

SmartPort

An integer with the constraints 0 ≤ integer ≤ 20.

AdiPort

An integer with the constraints 0 ≤ integer ≤ 7.

CompMode

An enum representing the current phase of competition. Variants:

  • Auto
  • Driver

Color

A struct that represents an rgb8 color. Color fields:

  • r: 8 bit integer
  • g: 8 bit integer
  • b: 8 bit integer

LogLevel

An enum representing the importance of a log message. Variants:

  • Trace: jumptable calls and other verbose messages
  • Info: non-critical informational messages
  • Warn: possible issues or errors that do not directly affect the simulation
  • Error: issues and errors that degrade the simulation

ControllerUpdate

An enum representing a method of accessing a controller's status. If the server recieves a ControllerUpdate command while in a disabled or autonomous mode, it must store the state recieved even though the robot code cannot access it yet. Variants:

  • Raw: ControllerState. If this variant is recieved, the server must stop updating the controller's status using a previously recieved UUID and only provide the robot code with the most recently recieved raw state.
  • UUID: string, the UUID of a hardware gamepad such that it can be accessed via SDL2. If this variant is recieved, the server must discard any previously recieved Raw data and continue updating its internal representation of the controller using the specified UUID even if no more ControllerUpdate commands are sent.

ControllerState

The current state of a controller's inputs. Fields:

  • axis1: integer with range of [-127, 127]
  • axis2: integer with range of [-127, 127]
  • axis3: integer with range of [-127, 127]
  • axis4: integer with range of [-127, 127]
  • button_l1: boolean
  • button_l2: boolean
  • button_r1: boolean
  • button_r2: boolean
  • button_up: boolean
  • button_down: boolean
  • button_left: boolean
  • button_right: boolean
  • button_x: boolean
  • button_b: boolean
  • button_y: boolean
  • button_a: boolean
  • button_sel: boolean
  • battery_level: integer with range of [0, battery_capacity]
  • button_all: boolean
  • flags: A 32-bit integer bitfield containing the controller flags that will be provided to the robot code. Implementors should set this to 0.
  • battery_capacity: positive integer

Device

An enum of the configurations for a peripheral device. This type should be considered "non-exhaustive" and variants may be added without causing a breaking change. Variants:

  • Motor: The configuration for a motor peripheral. Fields:
    • physical_gearset: MotorGearSet
    • moment_of_inertia: float in kilogram meters squared. Controls the acceleration of the motor when it applies a set amount of torque.

pros-rs

This chapter is for archived docs specifically related to pros-rs.

Technical Implementations

Some features of pros-rs required technical or hard to follow implementations. In order to make them easier to understand, this chapter aims to explain the more difficult ones.

Task Local Storage

Pros-rs uses a custom made TLS (Task Local Storage) implementation to allow for arbitrary TLS entries. FreeRTOS TLS has a maximum number of entries. It is configurable, but the default is only 5. Because the FreeRTOS TLS api is horribly unsafe and the maximum number of entries is 5, we use a custom implementation based off of the FreeRTOS api.

The custom implementation works by placing a pointer to a pros-rs TLS type in the FreeRTOS TLS. Pros-rs puts this always pointer in slot 0 of tls so that we can easily find it.
Here is a flow chart that goes over the basic idea of the implementation:
pros-tls flowchart In order to guarantee that every LocalKey has a unique index into the pros-rs TLS that works on every task, a static atomic u32 is used as a semaphore. Every time a LocalKey is created it will get the current value of the next index semaphore and increment it by one. the index that it got from the semaphore is guaranteed to be unique on all threads because the semaphore is static and atomic.

Drawbacks

Unfortunately, because of the fundamental design of this implementation, in order to access any value in TLS you must go through two pointers. Also when a task is destroyed for any reason, all entries in pros-rs TLS are leaked and cannot be freed. The leaking issue could be fixed by using Box::from_raw and dropping the returned value when the TLS struct is dropped, but that hasn't been implemented just yet.

Task Spawning

The FreeRTOS task spawning API in PROS works as follows:

  • It takes a C calling convention function pointer to a function with one argument of type void*.
  • It takes a void* that should be passed to the given function.
  • It takes general information about the task. This includes stack size and priority.

You can pass the pros-rs task spawning API any type implementing FnOnce() + Send + 'static and general info about the task. This is significantly better because it means that you can use closures and rust calling convention function pointers. You can avoid code like this:

#![allow(unused)]
fn main() {
unsafe extern "C" fn simple_task(args: *mut core::ffi::c_void) {
    let args: Box<String> = unsafe { Box::from_raw(args as *mut _) };
    loop {
        println!("{}", args);
    }
}
}

and instead write code like this:

#![allow(unused)]
fn main() {
let message = "Hi";
let simple_task = move || {
    loop {
        println!("{}", message);
    }
};
}

Internally, pros-rs is taking your functions,1 putting them on the heap, and then passing them through the arguments pointer to a pros-rs extern "C" function that calls your function in the newly created task. The majority takes place in TaskEntrypoint, specifically the TaskEntrypoint::cast_and_call_external function.

Drawbacks

The biggest drawback of this implementation is that the functions passed by the user need to be copied to the heap. If you pass a closure that closes over 100MB of data, pros-rs will copy 100MB of data to the heap just to spawn your task. Honestly speaking this is a tiny slowdown and it would take a pretty huge performance issue to outweigh the ease of use that this implementation brings.

Hard To Implement Features

There are many features that we would love to have in pros-rs, but don't have implemented. Often these features are not implemented because they are very technical or too hard to implement. This chapter goes over these features and describes what we know about them, why we haven't implemented them yet, why we want them in the first place, and possibly more depending on the feature.

GCC independence

The pros-rs build process (the armv7a-VEXos-eabi target and cargo-pros) depends on the Arm GNU toolchain (arm-none-eabi-gcc) for linking and transforming output ELF executables produced by Rust into binaries that can be run on a brain. This dependency is less than ideal because the Arm GNU toolchain is BIG. Uncompressed, it's about 1.7GiB which is pretty abysmal. Ideally we would be using only LLVM binutils and cargo-binutils as most users will already have these installed if they are using Rust.

Attempts have been made at using only llvm for linking, stripping, and objcopying (see PR #84). Ultimately all attempts have failed because of a libgcc dependent unwinding implementation in PROS and strange, likely linker related, memory access violation issues in FreeRTOS task switching code. It could still technically be done if we manually patched libpros' unwinding implementation to be independent of libgcc and figured out what what caused the memory access violations, however this would be difficult and would require a huge amount of testing.

If you are up to the the task of getting a pros-rs project running on a brain without arm-none-eabi-gcc or libgcc, please take a shot at it. Getting this working would be amazing.

TLDR

In order to not depend on GCC or libgcc we need to:

  • patch libpros with a custom unwinding solution
  • get programs linked with LLVM tools to not segfault
  • strip everything related to libgcc from libpros
  • update the V5 linker scripts to work with LLVM

PR #84 does a couple of these things, so check it out if you want to try getting this working.