Skip to content
This repository has been archived by the owner on Jun 19, 2024. It is now read-only.

Commit

Permalink
Merge pull request #5 from pros-rs/async_runtime
Browse files Browse the repository at this point in the history
Async runtime
  • Loading branch information
Gavin-Niederman authored Dec 22, 2023
2 parents 66031da + 591fa84 commit 23c3454
Show file tree
Hide file tree
Showing 14 changed files with 617 additions and 49 deletions.
7 changes: 6 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ This is the todo list for the eventual 1.0.0 release of pros-rs
* [ ] Motors
* [x] Internal gearsets
* [ ] (Custom) Gear Ratios
* [ ] Async Runtime (ditch tasks)
* [X] Make Robot Functions Take Self
* [X] PID controllers
* [ ] Feedforward loops
Expand All @@ -32,6 +31,12 @@ This is the todo list for the eventual 1.0.0 release of pros-rs
* [X] Controller data
* [x] Controller printing
* [X] Link
* [X] Async runtime
* [X] Returning top level futures
* [X] Reactor
* [ ] More asynchronous APIs
* [ ] MPSC
* [X] Task Locals

## API

Expand Down
43 changes: 42 additions & 1 deletion pros-sys/src/rtos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ pub type task_t = *const core::ffi::c_void;
pub type task_fn_t = Option<unsafe extern "C" fn(arg1: *mut ::core::ffi::c_void)>;
pub type mutex_t = *const core::ffi::c_void;

const CURRENT_TASK: task_t = 0 as task_t;
const CURRENT_TASK: task_t = core::ptr::null();

extern "C" {
/** Gets the number of milliseconds since PROS initialized.
Expand Down Expand Up @@ -280,4 +280,45 @@ extern "C" {
\param mutex
Mutex to unlock.*/
pub fn mutex_delete(mutex: mutex_t);

/** Sets a value in a task's thread local storage array.
This function is intended for advanced users only.
Parameters:
xTaskToSet The handle of the task to which the thread local data is being written. A task can write to its own thread local data by using NULL as the parameter value.
xIndex The index into the thread local storage array to which data is being written.
The number of available array indexes is set by the configNUM_THREAD_LOCAL_STORAGE_POINTERS compile time configuration constant in FreeRTOSConfig.h.
pvValue The value to write into the index specified by the xIndex parameter.
Example usage:
See the examples provided on the thread local storage array documentation page. */
pub fn vTaskSetThreadLocalStoragePointer(
xTaskToSet: task_t,
xIndex: i32,
pvValue: *const core::ffi::c_void,
);

/** Retrieves a value from a task's thread local storage array.
This function is intended for advanced users only.
Parameters:
xTaskToQuery The handle of the task from which the thread local data is being read. A task can read its own thread local data by using NULL as the parameter value.
xIndex The index into the thread local storage array from which data is being read.
The number of available array indexes is set by the configNUM_THREAD_LOCAL_STORAGE_POINTERS compile time configuration constant in FreeRTOSConfig.h.
Returns:
The values stored in index position xIndex of the thread local storage array of task xTaskToQuery.
Example usage:
See the examples provided on the thread local storage array documentation page. */
pub fn pvTaskGetThreadLocalStoragePointer(
xTaskToQuery: task_t,
xIndex: i32,
) -> *const core::ffi::c_void;
}
6 changes: 6 additions & 0 deletions pros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ snafu = { version = "0.7.5", default-features = false, features = [
"rust_1_61",
] }
no_std_io = { version = "0.6.0", features = ["alloc"] }
futures = { version = "0.3.28", default-features = false, features = ["alloc"] }
slab = { version = "0.4.9", default-features = false }
hashbrown = { version = "0.14.1", default-features = true }
async-trait = "0.1.73"
async-task = { version = "4.5.0", default-features = false }
waker-fn = "1.1.1"

[target.'cfg(target_arch = "wasm32")'.dependencies]
dlmalloc = { version = "0.2.4", features = ["global"] }
25 changes: 19 additions & 6 deletions pros/examples/accessories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,25 @@
#![no_main]

use core::time::Duration;
use pros::prelude::*;
use pros::{prelude::*, task::delay};

#[derive(Debug, Default)]
struct ExampleRobot;
impl Robot for ExampleRobot {
fn opcontrol(&mut self) -> pros::Result {
#[async_trait]
impl AsyncRobot for ExampleRobot {
async fn opcontrol(&mut self) -> pros::Result {
let handle = pros::async_runtime::spawn(async {
for _ in 0..5 {
println!("Hello from async!");
sleep(Duration::from_millis(1000)).await;
}
});

pros::async_runtime::block_on(handle);

// Create a new motor plugged into port 2. The motor will brake when not moving.
let motor = Motor::new(2, BrakeMode::Brake)?;
motor.wait_until_stopped().await?;
// Create a controller, specifically controller 1.
let controller = Controller::Master;

Expand All @@ -27,7 +38,9 @@ impl Robot for ExampleRobot {

// Sleep the task as to not steal processing time from the OS.
// This should always be done in any loop, including loops in the main task.
sleep(Duration::from_millis(20));
// Because this is a real FreeRTOS task this is not the sleep function used elsewhere in this example.
// This sleep function will block the entire task, including the async executor! (There isn't one running here, but there is in the main task.)
delay(Duration::from_millis(20));
});

loop {
Expand All @@ -39,11 +52,11 @@ impl Robot for ExampleRobot {
println!("Vision objs {}", vision.nth_largest_object(0)?.middle_x);

// Once again, sleep.
sleep(Duration::from_millis(20));
sleep(Duration::from_millis(20)).await;
}
}
}
robot!(ExampleRobot);
async_robot!(ExampleRobot);

fn left_button_callback() {
println!("Left button pressed!");
Expand Down
16 changes: 16 additions & 0 deletions pros/examples/basic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#![no_std]
#![no_main]

use pros::prelude::*;

#[derive(Default)]
pub struct Robot;
#[async_trait]
impl AsyncRobot for Robot {
async fn opcontrol(&mut self) -> pros::Result {
println!("basic example");

Ok(())
}
}
async_robot!(Robot);
95 changes: 95 additions & 0 deletions pros/src/async_runtime/executor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
use core::{
cell::RefCell,
future::Future,
pin::Pin,
sync::atomic::{AtomicBool, Ordering},
task::{Context, Poll},
};

use alloc::{collections::VecDeque, sync::Arc};
use async_task::{Runnable, Task};
use waker_fn::waker_fn;

use crate::os_task_local;

use super::reactor::Reactor;

os_task_local! {
pub(crate) static EXECUTOR: Executor = Executor::new();
}

pub(crate) struct Executor {
queue: RefCell<VecDeque<Runnable>>,
pub(crate) reactor: RefCell<Reactor>,
}

impl !Send for Executor {}
impl !Sync for Executor {}

impl Executor {
pub fn new() -> Self {
Self {
queue: RefCell::new(VecDeque::new()),
reactor: RefCell::new(Reactor::new()),
}
}

pub fn spawn<T>(&'static self, future: impl Future<Output = T> + 'static) -> Task<T> {
// SAFETY: `runnable` will never be moved off this thread or shared with another thread because of the `!Send + !Sync` bounds on `Self`.
// Both `future` and `schedule` are `'static` so they cannot be used after being freed.
// TODO: Make sure that the waker can never be sent off the thread.
let (runnable, task) = unsafe {
async_task::spawn_unchecked(future, |runnable| {
self.queue.borrow_mut().push_back(runnable)
})
};

runnable.schedule();

task
}

pub(crate) fn tick(&self) -> bool {
self.reactor.borrow_mut().tick();

let runnable = {
let mut queue = self.queue.borrow_mut();
queue.pop_front()
};
match runnable {
Some(runnable) => {
runnable.run();
true
}
None => false,
}
}

pub fn block_on<R>(&self, mut task: Task<R>) -> R {
let woken = Arc::new(AtomicBool::new(true));

let waker = waker_fn({
let woken = woken.clone();
move || woken.store(true, Ordering::Relaxed)
});
let mut cx = Context::from_waker(&waker);

loop {
if woken.swap(false, Ordering::Relaxed) {
if let Poll::Ready(output) = Pin::new(&mut task).poll(&mut cx) {
return output;
}
}

self.tick();
}
}

pub fn complete(&self) {
loop {
if !self.tick() {
break;
}
}
}
}
27 changes: 27 additions & 0 deletions pros/src/async_runtime/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use core::future::Future;

use async_task::Task;

pub(crate) mod executor;
pub(crate) mod reactor;

/// Runs a future in the background without having to await it
/// To get the the return value you can await a task.
pub fn spawn<T>(future: impl Future<Output = T> + 'static) -> Task<T> {
executor::EXECUTOR.with(|e| e.spawn(future))
}

/// Blocks the current task untill a return value can be extracted from the provided future.
/// Does not poll all futures to completion.
/// If you want to complete all futures, use the [`complete_all`] function.
pub fn block_on<F: Future + 'static>(future: F) -> F::Output {
executor::EXECUTOR.with(|e| e.block_on(spawn(future)))
}

/// Completes all tasks.
/// Return values can be extracted from the futures by awaiting any [`Tasks`]s you have not detached.
pub fn complete_all() {
executor::EXECUTOR.with(|e| {
e.complete();
})
}
37 changes: 37 additions & 0 deletions pros/src/async_runtime/reactor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use core::task::Waker;

use alloc::collections::BTreeMap;

pub struct Sleepers {
sleepers: BTreeMap<u32, Waker>,
}

impl Sleepers {
pub fn push(&mut self, waker: Waker, target: u32) {
self.sleepers.insert(target, waker);
}

pub fn pop(&mut self) -> Option<Waker> {
self.sleepers.pop_first().map(|(_, waker)| waker)
}
}

pub struct Reactor {
pub(crate) sleepers: Sleepers,
}

impl Reactor {
pub fn new() -> Self {
Self {
sleepers: Sleepers {
sleepers: BTreeMap::new(),
},
}
}

pub fn tick(&mut self) {
if let Some(sleeper) = self.sleepers.pop() {
sleeper.wake()
}
}
}
Loading

0 comments on commit 23c3454

Please sign in to comment.