Skip to content

Fall Updates: Standard Library Support with vexide 0.8.0!


vexide logo drawn over a night sky

Hi everyone, we’re excited announce the largest update to vexide yet. This release introduces support for the Rust Standard Library, unit testing, running programs without a robot, improved error reporting, and much more!

vexide is a Rust library for programming VEX robots, empowering you to write safe and efficient code for your robot in the Rust programming language. Learn more.

vexide programs have historically required that Rust’s Standard Library (the std crate) be disabled. This forces us to use the stripped down core and alloc libraries instead, and limits the packages we can use on crates.io. This is because the std crate depends on features provided by an operating system, and nobody had bothered to write a port of the standard library for VEXos in the Rust compiler.

Rust Standard Library Support

pull request adding standard library support for armv7a-vex-v5

…until now! Over the last year we’ve been working on upstreaming support for VEX brains as an official compilation target and standard platform in the Rust language. You can now use the std crate and compile to a V5 brain using the armv7a-vex-v5 target in Rust!

main.rs
// This runs on a brain now.fn main() {    println!("Hello, World!");}

This unlocks a huge portion of the Rust ecosystem. Most libraries that require std now “just work”. You can use the std::fs API to write to the brain’s SDCard and use the real println! macro to print to the terminal. I’ve even managed to run Tokio on a V5 brain!

Tokio's single-threaded executor running on a V5 brain (real hardware).

Please don't try this at home.

Due to platform limitations, some parts of the standard library will return errors. Notably, std::thread::spawn will not work, filesystem access through std::fs is limited, and networking with std::net is unsupported. For a full list of what works and doesn’t work, see the target docs.

Slimming Down

In the past, vexide attempted to “fill in the gaps” left by our lack of a standard library with equivalent APIs. Now that we have the real thing, these APIs are redundant and have been removed.

Removed vexide 0.7.0 APIEquivalent std API
vexide::io::*std::io::*
vexide::fs::*std::fs::*
vexide::path:*std::path:*
vexide::program::{exit, abort}std::process::{exit, abort}
vexide::time::Instantstd::time::Instant
vexide::panic::*std::panic::*
vexide::float::*std::f32::*, std::f64::*

SDK Shenanigans

Before we go further, I should explain a major internal overhaul that we’ve been working towards for a while.

vexide and the standard library need some way to make calls to VEXos to control devices and handle I/O on the brain. This is traditionally done through the VEX SDK — a proprietary library shipped by VEX for VEXcode and partner developers (such as PROS). When vexide was first created, we made the decision to not use an SDK provided by VEX and instead implement our own version from scratch.

VEXos system architecture

However, this posed a challenge when we were porting the standard library. Shipping proprietary code in Rust itself wasn’t an option, but forcing people to use our own SDK wasn’t ideal either and could pose challenges down the road.

Bring Your Own SDK

Instead, we picked a third option — if you use the standard library, you are expected to link your own SDK. If you use vexide, it will provide one for you. This led to us completely modularizing how vexide links to its SDK. You can now pick from three different “backends” (providers) for an SDK that vexide can run on:

  • vex-sdk-jumptable is the custom reimplementation of the SDK used by previous vexide versions.
  • vex-sdk-vexcode will download the official proprietary SDK from VEX themselves, and link your project to it. This is downloaded from VEX’s servers, and not directly distributed with vexide due to licensing restrictions.
  • vex-sdk-pros will use the partner SDK inside of the PROS kernel as a provider for vexide’s SDK functions.

You can specify which backend to use in your Cargo.toml file by editing vexide’s feature flags:

Cargo.toml
vexide = "0.7.0"vexide = { version = "0.8.0", features = ["full", "vex-sdk-jumptable"] }

All of these options should provide equivalent functionality, but this setup ensures vexide remains future-proof.

Host Compilation & Unit Testing

Another advantage of modularizing the SDK in vexide is that we can now fake the underlying platform that vexide runs on. With vexide 0.8.0, you can now natively compile and run your robot’s codebase on your own computer. Here’s a screenshot of me running the clawbot example on my laptop without a robot:

clawbot example

To natively compile your robot code, enable the vex-sdk-mock feature on vexide in your Cargo.toml along with your existing SDK provider, and simply use cargo run instead of cargo v5 run:

Cargo.toml
vexide = { version = "0.8.0", features = ["full", "vex-sdk-jumptable"] }vexide = { version = "0.8.0", features = ["full", "vex-sdk-jumptable", "vex-sdk-mock"] }

Unit Tests

Being able to run our robot code on an actual host system means we can also support Rust’s testing features. You can now write and run unit tests against your robot logic without needing hardware on hand, integrate with CI pipelines, and catch bugs in your code earlier.

At this point in time, devices won’t do anything and will simply be disconnected at all times when using host compilation. This will change in the future, and we hope to add the ability to mock robot devices and entire subsystems in unit tests soon. We also hope to support emulating your robot’s brain screen, for testing GUIs and autonomous selectors without physical access to a brain.

use vexide::prelude::*;#{vexide::main}async fn main(peripherals: Peripherals) {    println!("Hello, world!");}#[cfg(test)]mod tests {    use vexide::prelude::*;    use std::time::Duration;    #[test]
The test attribute can now be used in vexide projects.
fn one_plus_one_equals_two() { assert!(1 + 1 == 2); } #[vexide::test]
Use the vexide::test macro to run tests with async code.
async fn async_test(_p: Peripherals) { sleep(Duration::from_millis(5)).await; println!("Hello"); assert!(4 + 4 == 8); }}

Error Reporting Improvements

If you’ve ever encountered this screen, you know you’re in for a fun time:

memory permission error

This is a data abort exception. It can happen when your program accesses memory in some way that the brain’s CPU doesn’t allow, and it often indicates that your program has undefined behavior. Think of it as the VEX equivalent of a segfault. These types of errors really suck to debug, and the error that VEXos throws up on screen often doesn’t give you all of the necessary information you need to find what caused it.

As of vexide 0.8.0, we’ll now optionally provide a more detailed report of many different CPU faults including data aborts, prefetch aborts, and undefined instruction exceptions.

improved abort handler

cargo v5 run --release
    Finished `release` profile [optimized] target(s) in 0.19s     Objcopy /home/tropical/Documents/GitHub/vexide/target/armv7a-vex-v5/release/examples/basic.bin     Running `slot_1.bin`go go gadget null pointer dereferenceData Abort exception at 0x3800ed0:Permission fault (MMU) while writing to 0x0registers at time of fault: r0: 0x0 r1: 0x0 r2: 0x0 r3: 0x0 r4: 0x1 r5: 0x0 r6: 0x0 r7: 0x7a00024 r8: 0x7a00034 r9: 0x7a0001cr10: 0x380fba8r11: 0x132r12: 0x3692594 sp: 0x79ffe60 lr: 0x3800e8c pc: 0x3800ed0stack backtrace:  0: 0x3800ecf  1: 0x3801a83  2: 0x38003f7help: this CPU fault indicates the misuse of unsafe code.      Use a symbolizer tool to determine the location of the crash.      (e.g. llvm-symbolizer -e ./target/armv7a-vex-v5/release/program_name 0x3800ed0)

This new error reporting system (which is enabled by default with the abort-handler feature flag) provides more helpful information for debugging and finding the location of these types of crashes, including:

  • The full cause of the CPU exception (including the type of operation that caused the fault).
  • A stack backtrace leading up to the function causing the exception. The addresses in this trace can be passed to a symbolizer program like llvm-symbolizer to find the exact line of code causing the abort.
  • For undefined instruction exceptions, the invalid instruction that caused the abort.
  • The CPU’s registers at the time of the fault.
  • The ability to print this data to the terminal and re-print it if your terminal wasn’t open when the program crashed.

As a reminder, vexide is designed to explicitly prevent these kinds of errors from happening and it should ideally be impossible to trigger these using only safe Rust code. That being said, if or when you do encounter them, we want to make the problem as easy to diagnose as possible.

Other Changes in vexide 0.8.0

Custom Encoder Support

It’s fairly common for VEXU teams to use off-the-shelf shaft encoders rather than buying sensors sold by VEX. vexide currently supports VEX’s old Optical Shaft Encoders through the AdiEncoder API, and while this is sort of compatible with custom hardware already, commonly used sensors like the AMT102-V often have much higher resolution than VEX’s optical encoders (which have a resolution of 360 ticks/rev). In the past, you’d have to multiply the output of AdiEncoder by a constant to get a usable angle reading, which was rather annoying.

In vexide 0.8.0, we now provide support for encoders with custom resolutions. AdiEncoder now takes a const-generic argument for the number of encoder ticks in a full revolution. The position reading will be automatically scaled appropriately for the encoder’s TPR.

For example, you can read from an encoder with a resolution of 8192 ticks/rev like this:

use vexide::prelude::*;const ENCODER_TPR: u32 = 8192;#[vexide::main]async fn main(peripherals: Peripherals) {    let enc = AdiEncoder::<ENCODER_TPR>::new(        peripherals.adi_a,        peripherals.adi_b    );    println!("Encoder position: {:?}", enc.position().unwrap().as_degrees());}

We also provide an AdiOpticalEncoder type alias for if you’re using VEX’s optical encoder. This simply resolves to AdiEncoder<360>, and should behave identically to the previous AdiEncoder API in vexide 0.7.0 and below.

use vexide::prelude::*;#[vexide::main]async fn main(peripherals: Peripherals) {    let enc = AdiOpticalEncoder::new(        peripherals.adi_a,        peripherals.adi_b    );    println!("Encoder position: {:?}", enc.position().unwrap().as_degrees());}

Task-local Storage (TLS)

vexide now supports the ability to create async task-local static data. That’s a lot of of word salad, so let’s look at what this practically means.

You can define a task-local static variable by wrapping it in the new task_local! macro. Let’s make a counter:

use std::cell::Cell;vexide::task::task_local! {    static COUNT: Cell<u32> = Cell::new(1);}

This looks a lot like a normal static, but when we spawn an async task, each task will get its own unique version of COUNT starting at 1. In other words, value of COUNT is local to each task accessing it.

{...}use vexide::prelude::*;use std::{cell::Cell, std::time::Duration};vexide::task::task_local! { static COUNT: Cell<u32> = Cell::new(1);}
let task = spawn(async { loop { println!("Spawned task count: {}", COUNT.get()); COUNT.set(COUNT.get() + 1); sleep(Duration::from_millis(100)).await; }});loop { println!("Main task count: {}", COUNT.get()); COUNT.set(COUNT.get() + 1); sleep(Duration::from_millis(100)).await;}
Main task count: 1Spawned task count: 1Main task count: 2Spawned task count: 2Main task count: 3Spawned task count: 3Main task count: 4Spawned task count: 4Main task count: 5Spawned task count: 5Main task count: 6Spawned task count: 6

Task-local statics have a few advantages over regular global statics. Because each task gets its own isolated instance, they don’t require any form of synchronization (no need for a Mutex). They can also safely store types that wouldn’t be allowed in global statics, such as types that don’t implement Sync.

Low-level User Program Information

We’ve added new functions for retrieving some niche information about the current user program. The new code_signature and linked_file functions in vexide::program allow you to read the currently running program’s code signature and file link address:

let code_sig = vexide::program::code_signature();let linked_file_ptr = vexide::program::linked_file();println!("Program owner: {:?}", code_sig.owner());println!("Link address: {:#x}", linked_file_ptr as usize);

These functions aren’t something that you should realistically need to use or care about in most cases, but are useful to have for low-level development.

cargo-v5 0.12.0

New Contributors

vexide is a community project maintained for free by open-source contributors. We’d like to thank the following new contributors to the project:

  • fibonacci61 wrote an initial implementation of our new async Task-local Storage system in vexide’s async runtime.
  • slipperking fixed some bugs in vexide’s AI Vision API and helped with documentation.

Thanks again for your contributions!

colored stars