Next iteration with backoffs, tasks, basic skeleton for events
This commit is contained in:
80
src/backoff.rs
Normal file
80
src/backoff.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use embassy_time::{Duration, Timer};
|
||||
use log::warn;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Backoff {
|
||||
delay: Duration,
|
||||
attempts: Attempts,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Attempts {
|
||||
Finite(u64),
|
||||
Forever
|
||||
}
|
||||
|
||||
impl Iterator for Attempts {
|
||||
type Item = Self;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match *self {
|
||||
Attempts::Forever => Some(Attempts::Forever),
|
||||
Attempts::Finite(0) => None,
|
||||
Attempts::Finite(n) => {
|
||||
*self = Attempts::Finite(n - 1);
|
||||
Some(Attempts::Finite(n))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Backoff {
|
||||
pub fn from_millis(millis: u64) -> Self {
|
||||
Self {
|
||||
delay: Duration::from_millis(millis),
|
||||
attempts: Attempts::Finite(3),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_secs(secs: u64) -> Self {
|
||||
Self {
|
||||
delay: Duration::from_secs(secs),
|
||||
attempts: Attempts::Finite(3),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn forever(self) -> Self {
|
||||
Self {
|
||||
attempts: Attempts::Forever,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn attempt<F: AsyncFnMut() -> Result<T, E>, T, E: core::fmt::Debug>(self, mut f: F) -> Result<T, E> {
|
||||
let mut latest= f().await;
|
||||
let mut delay = self.delay;
|
||||
'outer: loop {
|
||||
for attempt in self.attempts {
|
||||
match &latest {
|
||||
Err(e) => {
|
||||
match attempt {
|
||||
Attempts::Finite(1) => {
|
||||
warn!("Operation failed on final attempt.");
|
||||
break 'outer
|
||||
}
|
||||
attempt => {
|
||||
warn!("Operation failed. Retrying attempt {attempt:?} after {delay}");
|
||||
Timer::after(delay).await;
|
||||
delay *= 2;
|
||||
latest = f().await
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => break 'outer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
latest
|
||||
}
|
||||
}
|
||||
232
src/bin/main.rs
232
src/bin/main.rs
@@ -6,51 +6,38 @@
|
||||
holding buffers for the duration of a data transfer."
|
||||
)]
|
||||
|
||||
use alloc::string::String;
|
||||
use embassy_embedded_hal::shared_bus::asynch::i2c::I2cDevice;
|
||||
use embassy_executor::Spawner;
|
||||
use embassy_sync::blocking_mutex::raw::NoopRawMutex;
|
||||
use embassy_sync::blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex};
|
||||
use embassy_sync::mutex::Mutex;
|
||||
use embassy_time::{Duration, Instant, Timer};
|
||||
use embedded_hal_async::i2c::I2c as _;
|
||||
use embassy_sync::pubsub::PubSubChannel;
|
||||
use esp_backtrace as _;
|
||||
use embassy_time::Delay;
|
||||
use esp_hal::i2c::master::{Config, I2c};
|
||||
use esp_hal::{Async, Blocking};
|
||||
use esp_hal::Async;
|
||||
use esp_hal::clock::CpuClock;
|
||||
use esp_hal::rmt::{ChannelCreator, Rmt};
|
||||
use esp_hal::rmt::Rmt;
|
||||
use esp_hal::time::Rate;
|
||||
use esp_hal::timer::systimer::SystemTimer;
|
||||
use esp_hal::timer::timg::TimerGroup;
|
||||
use esp_hal_smartled::{smart_led_buffer, SmartLedsAdapter};
|
||||
use esp_hal::peripherals::GPIO5;
|
||||
use figments::liber8tion::trig::sin8;
|
||||
use figments::prelude::*;
|
||||
use log::{info, warn};
|
||||
use mpu6050_dmp::address::Address;
|
||||
use mpu6050_dmp::calibration::CalibrationParameters;
|
||||
use mpu6050_dmp::gyro;
|
||||
use mpu6050_dmp::sensor_async::Mpu6050;
|
||||
use nmea::{Nmea, SentenceType};
|
||||
use renderbug_embassy::{as_milliwatts, brightness_for_mw};
|
||||
use smart_leds::{
|
||||
brightness,
|
||||
SmartLedsWrite,
|
||||
};
|
||||
use log::info;
|
||||
use static_cell::StaticCell;
|
||||
|
||||
use renderbug_embassy::{
|
||||
tasks::i2c::*,
|
||||
tasks::render::render,
|
||||
events::Notification
|
||||
};
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
// This creates a default app-descriptor required by the esp-idf bootloader.
|
||||
// For more information see: <https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/app_image_format.html#application-description>
|
||||
esp_bootloader_esp_idf::esp_app_desc!();
|
||||
|
||||
static I2C_BUS: StaticCell<Mutex<NoopRawMutex, I2c<'static, Async>>> = StaticCell::new();
|
||||
|
||||
static I2C_BUS: StaticCell<Mutex<CriticalSectionRawMutex, I2c<'static, Async>>> = StaticCell::new();
|
||||
static EVENT_BUS: StaticCell<PubSubChannel<NoopRawMutex, Notification, 4, 4, 4>> = StaticCell::new();
|
||||
#[esp_hal_embassy::main]
|
||||
async fn main(spawner: Spawner) {
|
||||
// generator version: 0.5.0
|
||||
|
||||
esp_println::logger::init_logger_from_env();
|
||||
|
||||
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
|
||||
@@ -63,6 +50,8 @@ async fn main(spawner: Spawner) {
|
||||
|
||||
info!("Embassy initialized!");
|
||||
|
||||
let events = EVENT_BUS.init(PubSubChannel::<NoopRawMutex, Notification, 4, 4, 4>::new());
|
||||
|
||||
let rng = esp_hal::rng::Rng::new(peripherals.RNG);
|
||||
let timer1 = TimerGroup::new(peripherals.TIMG0);
|
||||
let wifi_init =
|
||||
@@ -71,191 +60,16 @@ async fn main(spawner: Spawner) {
|
||||
.expect("Failed to initialize WIFI controller");
|
||||
|
||||
let frequency: Rate = Rate::from_mhz(80);
|
||||
let rmt: Rmt<'_, esp_hal::Blocking> = Rmt::new(peripherals.RMT, frequency)
|
||||
.expect("Failed to initialize RMT");
|
||||
|
||||
let rmt = Rmt::new(peripherals.RMT, frequency)
|
||||
.expect("Failed to initialize RMT").into_async();
|
||||
let rmt_channel = rmt.channel0;
|
||||
info!("Launching render task");
|
||||
spawner.must_spawn(render(events.subscriber().unwrap(), rmt_channel, peripherals.GPIO5));
|
||||
|
||||
spawner.spawn(render(rmt_channel, peripherals.GPIO5)).unwrap();
|
||||
|
||||
//static I2C_BUS: StaticCell<Mutex<NoopRawMutex, I2c<'static, Async>>> = StaticCell::new();
|
||||
let i2c = I2c::new(peripherals.I2C1, Config::default()).unwrap().with_scl(peripherals.GPIO36).with_sda(peripherals.GPIO33).into_async();
|
||||
info!("Launching i2c task");
|
||||
let i2c = I2c::new(peripherals.I2C1, Config::default().with_frequency(Rate::from_khz(400))).unwrap().with_scl(peripherals.GPIO36).with_sda(peripherals.GPIO33).into_async();
|
||||
let i2c_bus = I2C_BUS.init(Mutex::new(i2c));
|
||||
|
||||
//spawner.spawn(i2c_reader(i2c_bus)).unwrap();
|
||||
spawner.spawn(gps_task(I2cDevice::new(i2c_bus))).unwrap();
|
||||
//spawner.spawn(mpu_task(I2cDevice::new(i2c_bus))).unwrap();
|
||||
|
||||
// for inspiration have a look at the examples at https://github.com/esp-rs/esp-hal/tree/esp-hal-v1.0.0-rc.0/examples/src/bin
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn mpu_task(bus: I2cDevice<'static, NoopRawMutex, I2c<'static, Async>>) {
|
||||
info!("Initializing MPU");
|
||||
let mut sensor = Mpu6050::new(bus, Address::default()).await.unwrap();
|
||||
let mut delay = Delay;
|
||||
sensor.initialize_dmp(&mut delay).await.unwrap();
|
||||
sensor.calibrate(&mut delay, &CalibrationParameters::new(
|
||||
mpu6050_dmp::accel::AccelFullScale::G2,
|
||||
mpu6050_dmp::gyro::GyroFullScale::Deg2000,
|
||||
mpu6050_dmp::calibration::ReferenceGravity::ZN
|
||||
)).await.unwrap();
|
||||
sensor.set_sample_rate_divider(255).await.unwrap();
|
||||
info!("MPU is ready!");
|
||||
loop {
|
||||
let (accel_data, gyro_data) = sensor.motion6().await.unwrap();
|
||||
info!("Accel x={} y={} z={}", accel_data.x() as i32, accel_data.y() as i32, accel_data.z() as i32);
|
||||
info!("Gyro x={} y={} z={}", gyro_data.x() as i32, gyro_data.y() as i32, gyro_data.z() as i32);
|
||||
Timer::after_millis(50).await;
|
||||
}
|
||||
}
|
||||
|
||||
//use embedded_hal_async::i2c::I2c;
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn gps_task(mut i2c_bus: I2cDevice<'static, NoopRawMutex, I2c<'static, Async>>) {
|
||||
info!("Initializing GPS");
|
||||
//let bytes = "$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n";
|
||||
//let bytes = "$PMTK314,1,1,1,1,1,5,1,1,1,1,1,1,0,1,1,1,1*2C\r\n";
|
||||
// Enable a bunch of data? idk
|
||||
let bytes = "$PMTK314,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n";
|
||||
i2c_bus.write(0x10, bytes.as_bytes()).await.unwrap();
|
||||
|
||||
// 1hz updates
|
||||
let bytes = "$PMTK220,1000*1F\r\n";
|
||||
i2c_bus.write(0x10, bytes.as_bytes()).await.unwrap();
|
||||
|
||||
// 1hz position fix
|
||||
let bytes = "$PMTK300,1000,0,0,0,0*1C\r\n";
|
||||
i2c_bus.write(0x10, bytes.as_bytes()).await.unwrap();
|
||||
|
||||
// Antenna updates
|
||||
let bytes = "$PGCMD,33,1*6C\r\n";
|
||||
i2c_bus.write(0x10, bytes.as_bytes()).await.unwrap();
|
||||
|
||||
let mut strbuf = String::new();
|
||||
|
||||
let mut parser = Nmea::default();
|
||||
let mut parsing = false;
|
||||
info!("GPS is ready!");
|
||||
loop {
|
||||
let mut buf = [0; 1];
|
||||
i2c_bus.read(0x10, &mut buf).await.unwrap();
|
||||
if (buf[0] as char == '\n' || buf[0] as char == '\r') && !strbuf.is_empty() {
|
||||
if let Ok(result) = parser.parse(&strbuf) {
|
||||
log::info!("nmea={:?} raw={:?}", result, strbuf);
|
||||
log::info!("speed={:?} altitude={:?} lat={:?} lng={:?} fix={:?} nmea={:?}", parser.speed_over_ground, parser.altitude, parser.latitude, parser.longitude, parser.fix_type, parser);
|
||||
} else {
|
||||
log::warn!("Unhandled NMEA {:?}", strbuf);
|
||||
}
|
||||
strbuf = String::new();
|
||||
parsing = false;
|
||||
// Update frequency is 1hz, so we should never get an update faster than once per second
|
||||
Timer::after_secs(1).await;
|
||||
} else if strbuf.is_empty() && (buf[0] as char == '$' || buf[0] as char == '!') {
|
||||
parsing = true;
|
||||
strbuf.push(buf[0] as char);
|
||||
} else if parsing {
|
||||
strbuf.push(buf[0] as char);
|
||||
} else {
|
||||
// If there is no data ready for some reason, wait 500ms, which should place us at least somewhere after the next data frame is ready to read.
|
||||
Timer::after_millis(500).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn i2c_reader(mut i2c_bus: I2c<'static, Async>) {
|
||||
info!("Starting i2c bus");
|
||||
|
||||
// Enable status lines
|
||||
let bytes = "$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28";
|
||||
i2c_bus.write_async(0x10, bytes.as_bytes()).await.unwrap();
|
||||
|
||||
// 1hz updates
|
||||
let bytes = "$PMTK220,1000*1F";
|
||||
i2c_bus.write_async(0x10, bytes.as_bytes()).await.unwrap();
|
||||
|
||||
// Antenna updates
|
||||
let bytes = "$PGCMD,33,1*6C";
|
||||
i2c_bus.write_async(0x10, bytes.as_bytes()).await.unwrap();
|
||||
|
||||
let mut strbuf = String::new();
|
||||
|
||||
let mut parser = Nmea::default();
|
||||
let mut parsing = false;
|
||||
loop {
|
||||
let mut buf = [0; 1];
|
||||
i2c_bus.read_async(0x10, &mut buf).await.unwrap();
|
||||
if (buf[0] as char == '\n' || buf[0] as char == '\r') && !strbuf.is_empty() {
|
||||
//log::info!("buf={:?}", strbuf);
|
||||
if let Ok(result) = parser.parse(&strbuf) {
|
||||
log::info!("nmea={:?}", result);
|
||||
log::info!("speed={:?} altitude={:?} lat={:?} lng={:?} fix={:?} nmea={:?}", parser.speed_over_ground, parser.altitude, parser.latitude, parser.longitude, parser.fix_type, parser);
|
||||
} else {
|
||||
log::warn!("Unhandled NMEA {:?}", strbuf);
|
||||
}
|
||||
strbuf = String::new();
|
||||
parsing = false;
|
||||
} else if strbuf.is_empty() && (buf[0] as char == '$' || buf[0] as char == '!') {
|
||||
parsing = true;
|
||||
strbuf.push(buf[0] as char);
|
||||
} else if parsing {
|
||||
strbuf.push(buf[0] as char);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn render(rmt_channel: ChannelCreator<Blocking, 0>, gpio: GPIO5<'static>) {
|
||||
let rmt_buffer = smart_led_buffer!(255);
|
||||
|
||||
let mut target = SmartLedsAdapter::new(rmt_channel, gpio, rmt_buffer);
|
||||
|
||||
// Change this number to use a different number of LEDs
|
||||
let mut pixbuf = [Default::default(); 255];
|
||||
|
||||
// Change this to adjust the power available; the USB spec says 500ma is the standard limit, but sometimes you can draw more from a power brick
|
||||
const POWER_MA : u32 = 500;
|
||||
|
||||
// You probably don't need to change these values, unless your LED strip is somehow not 5 volts
|
||||
const POWER_VOLTS : u32 = 5;
|
||||
const MAX_POWER_MW : u32 = POWER_VOLTS * POWER_MA;
|
||||
|
||||
// This value is used as the 'seed' for rendering each frame, allowing us to do things like run the animation backwards, frames for double FPS, or even use system uptime for more human-paced animations
|
||||
let mut frame = 0;
|
||||
|
||||
info!("Rendering started!");
|
||||
|
||||
loop {
|
||||
let start = Instant::now();
|
||||
// Clear the pixbuf to black
|
||||
pixbuf.blank();
|
||||
|
||||
// Render the frame to the pixbuf, while also calculating the power consumption as we go
|
||||
let mut total_power = 0;
|
||||
for (coords, pix) in pixbuf.sample(&Rectangle::everything()) {
|
||||
*pix = Rgb::new(sin8(coords.x.wrapping_mul(3).wrapping_add(coords.y.wrapping_mul(3)).wrapping_add(frame)), 0, 0);
|
||||
total_power += as_milliwatts(pix);
|
||||
}
|
||||
|
||||
// Scale the total brightness down so we don't immediately trigger a brownout
|
||||
let scaled_brightness = brightness_for_mw(total_power, 255, MAX_POWER_MW);
|
||||
|
||||
// Finally, write out the rendered frame
|
||||
target.write(brightness(pixbuf.iter().cloned(), scaled_brightness)).expect("Failed to write to LEDs!");
|
||||
|
||||
let render_duration = Instant::now() - start;
|
||||
let render_budget = Duration::from_millis(16);
|
||||
|
||||
if render_duration < render_budget {
|
||||
let remaining_budget = render_budget - render_duration;
|
||||
Timer::after(remaining_budget).await;
|
||||
} else {
|
||||
warn!("Render stall! Frame took {render_duration:?}");
|
||||
}
|
||||
|
||||
// Increment the frame counter
|
||||
frame += 1;
|
||||
}
|
||||
spawner.spawn(mpu_task(events.dyn_publisher().unwrap(), I2cDevice::new(i2c_bus))).unwrap();
|
||||
spawner.spawn(gps_task(events.dyn_publisher().unwrap(), I2cDevice::new(i2c_bus))).unwrap();
|
||||
}
|
||||
133
src/display.rs
Normal file
133
src/display.rs
Normal file
@@ -0,0 +1,133 @@
|
||||
use figments::{hardware::{Brightness, Gamma}, mappings::linear::LinearSpace, power::AsMilliwatts, prelude::*, smart_leds::BrightnessWriter};
|
||||
use core::{fmt::Debug, ops::IndexMut};
|
||||
//use std::io::Write;
|
||||
|
||||
//use super::{Output};
|
||||
use figments::hardware::Output;
|
||||
use smart_leds::SmartLedsWriteAsync;
|
||||
|
||||
pub trait LinearPixbuf: Pixbuf + Sample<'static, LinearSpace, Output = Self::Format> + IndexMut<usize, Output = Self::Format> + AsRef<[Self::Format]> {}
|
||||
impl<T> LinearPixbuf for T where T: Pixbuf + Sample<'static, LinearSpace, Output = Self::Format> + IndexMut<usize, Output = Self::Format> + AsRef<[Self::Format]> {}
|
||||
|
||||
pub struct BikeOutput<T: SmartLedsWriteAsync> {
|
||||
pixbuf: [T::Color; 178],
|
||||
writer: BrightnessWriter<T>
|
||||
}
|
||||
|
||||
impl<T: SmartLedsWriteAsync> Debug for BikeOutput<T> {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_struct("BikeOutput").field("pixbuf", &core::any::type_name_of_val(&self.pixbuf)).field("writer", &core::any::type_name_of_val(&self.writer)).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SmartLedsWriteAsync> BikeOutput<T> where T::Color: HardwarePixel + Gamma + Fract8Ops, T::Error: core::fmt::Debug {
|
||||
pub fn new(target: T, max_mw: u32) -> Self {
|
||||
Self {
|
||||
pixbuf: [Default::default(); 178],
|
||||
writer: BrightnessWriter::new(target, max_mw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SmartLedsWriteAsync> Brightness for BikeOutput<T> where T::Color: HardwarePixel + Gamma + Fract8Ops, T::Error: core::fmt::Debug {
|
||||
fn set_brightness(&mut self, brightness: u8) {
|
||||
self.writer.set_brightness(brightness);
|
||||
}
|
||||
|
||||
fn set_on(&mut self, is_on: bool) {
|
||||
self.writer.set_on(is_on);
|
||||
}
|
||||
|
||||
fn set_gamma(&mut self, gamma: f32) {
|
||||
self.writer.set_gamma(gamma);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: SmartLedsWriteAsync + 'a> Output<'a, BikeSpace> for BikeOutput<T> where T::Color: 'static + HardwarePixel + Gamma + Fract8Ops, [T::Color; 178]: AsMilliwatts + Gamma + Copy, T::Error: core::fmt::Debug {
|
||||
async fn blank(&mut self) -> Result<(), Self::Error> {
|
||||
self.pixbuf.blank();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn commit(&mut self) -> Result<(), Self::Error> {
|
||||
self.writer.write(&self.pixbuf).await
|
||||
}
|
||||
|
||||
type HardwarePixel = T::Color;
|
||||
type Error = T::Error;
|
||||
}
|
||||
|
||||
impl<'a, T: SmartLedsWriteAsync> Sample<'a, BikeSpace> for BikeOutput<T> where T::Color: HardwarePixel + Gamma + Fract8Ops, [T::Color; 178]: AsMilliwatts + 'static {
|
||||
type Output = T::Color;
|
||||
|
||||
fn sample(&mut self, rect: &Rectangle<BikeSpace>) -> impl Iterator<Item = (Coordinates<BikeSpace>, &'a mut Self::Output)> {
|
||||
let bufref = unsafe {
|
||||
&mut *(&mut self.pixbuf as *mut [T::Color; 178])
|
||||
};
|
||||
BikeIter {
|
||||
pixbuf: bufref,
|
||||
cur: rect.top_left,
|
||||
end: rect.bottom_right
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Strip {
|
||||
start: usize,
|
||||
length: usize,
|
||||
}
|
||||
|
||||
const STRIP_MAP: [Strip; 6] = [
|
||||
Strip { start: 0, length: 28 },
|
||||
Strip { start: 28, length: 17 },
|
||||
Strip { start: 45, length: 14 },
|
||||
Strip { start: 59, length: 17 },
|
||||
Strip { start: 76, length: 14 },
|
||||
Strip { start: 90, length: 88 }
|
||||
];
|
||||
|
||||
#[derive(Clone, Copy, Default, Debug)]
|
||||
pub struct BikeSpace {}
|
||||
impl CoordinateSpace for BikeSpace {
|
||||
type Data = usize;
|
||||
}
|
||||
|
||||
pub struct BikeIter<'a, P: LinearPixbuf + Debug> {
|
||||
pixbuf: &'a mut P,
|
||||
cur: Coordinates<BikeSpace>,
|
||||
end: Coordinates<BikeSpace>
|
||||
}
|
||||
|
||||
impl<'a, P: LinearPixbuf + Debug> Debug for BikeIter<'a, P> {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_struct("BikeIter").field("cur", &self.cur).field("end", &self.end).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, P: LinearPixbuf + Debug> Iterator for BikeIter<'a, P> {
|
||||
type Item = (Coordinates<BikeSpace>, &'a mut P::Format);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.cur.y > self.end.y || self.cur.y >= STRIP_MAP.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let this_strip = &STRIP_MAP[self.cur.y];
|
||||
|
||||
let offset = this_strip.start + self.cur.x;
|
||||
let pixel_coords = Coordinates::new(self.cur.x, self.cur.y);
|
||||
|
||||
self.cur.x += 1;
|
||||
|
||||
if self.cur.x >= this_strip.length || self.cur.x > self.end.x {
|
||||
self.cur.x = 0;
|
||||
self.cur.y += 1;
|
||||
}
|
||||
|
||||
let bufref = unsafe {
|
||||
&mut *(self.pixbuf as *mut P)
|
||||
};
|
||||
|
||||
Some((pixel_coords, &mut bufref[offset]))
|
||||
}
|
||||
}
|
||||
19
src/events.rs
Normal file
19
src/events.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Scene {
|
||||
ParkedIdle, // Default state when booting up or waking up from long term (overnight, eg) parking, or entered when accelerometers and GPS both show zero motion for ~30 seconds
|
||||
StoplightIdle, // Entered when GPS speed is zero after decelerating
|
||||
Accelerating, // GPS speed is increasing
|
||||
Decelerating, // GPS speed is decreasing
|
||||
ParkedLongTerm, // GPS has not changed location in the last ~5 minutes
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Notification {
|
||||
SceneChange(Scene),
|
||||
WifiConnected,
|
||||
WifiDisconnected,
|
||||
GPSLost,
|
||||
GPSAcquired,
|
||||
BPMBeat
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
#![no_std]
|
||||
|
||||
pub mod display;
|
||||
pub mod backoff;
|
||||
pub mod events;
|
||||
pub mod tasks;
|
||||
extern crate alloc;
|
||||
|
||||
use rgb::Rgb;
|
||||
|
||||
/// Scales the requested brightness to stay within power consumption limits
|
||||
|
||||
136
src/tasks/i2c.rs
Normal file
136
src/tasks/i2c.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
use core::cell::RefCell;
|
||||
|
||||
use embassy_embedded_hal::shared_bus::asynch::i2c::I2cDevice;
|
||||
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, pubsub::DynPublisher};
|
||||
use embassy_time::{Delay, Timer};
|
||||
use embedded_hal_async::i2c::I2c as _;
|
||||
use esp_hal::{i2c::master::I2c, Async};
|
||||
use log::{info, warn};
|
||||
use mpu6050_dmp::{address::Address, error_async::Error, sensor_async::Mpu6050};
|
||||
use nmea::Nmea;
|
||||
use alloc::string::String;
|
||||
|
||||
use crate::{backoff::Backoff, events::Notification};
|
||||
|
||||
#[embassy_executor::task]
|
||||
pub async fn mpu_task(events: DynPublisher<'static, Notification>, bus: I2cDevice<'static, CriticalSectionRawMutex, I2c<'static, Async>>) {
|
||||
let backoff = Backoff::from_millis(5);
|
||||
let busref = RefCell::new(Some(bus));
|
||||
|
||||
backoff.forever().attempt::<_, (), ()>(async || {
|
||||
let mut sensor = backoff.attempt(async || {
|
||||
info!("Initializing MPU");
|
||||
match Mpu6050::new(busref.replace(None).unwrap(), Address::default()).await.map_err(|e| { e.i2c }) {
|
||||
Err(i2c) => {
|
||||
busref.replace(Some(i2c));
|
||||
Err(())
|
||||
},
|
||||
Ok(mut sensor) => {
|
||||
match backoff.attempt(async || { mpu_init(&mut sensor).await }).await {
|
||||
Err(_) => {
|
||||
busref.replace(Some(sensor.release()));
|
||||
Err(())
|
||||
},
|
||||
Ok(_) => Ok(sensor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}).await?;
|
||||
info!("MPU is ready!");
|
||||
let sensor_ref = &mut sensor;
|
||||
loop {
|
||||
match backoff.attempt(async || { sensor_ref.motion6().await }).await {
|
||||
Ok((accel_data, gyro_data)) => {
|
||||
info!("Accel x={} y={} z={}", accel_data.x() as i32, accel_data.y() as i32, accel_data.z() as i32);
|
||||
info!("Gyro x={} y={} z={}", gyro_data.x() as i32, gyro_data.y() as i32, gyro_data.z() as i32);
|
||||
Timer::after_millis(5000).await;
|
||||
},
|
||||
Err(e) => {
|
||||
warn!("Failed to read MPU motion data! {e:?}");
|
||||
busref.replace(Some(sensor.release()));
|
||||
return Err(());
|
||||
}
|
||||
};
|
||||
}
|
||||
}).await.unwrap();
|
||||
}
|
||||
|
||||
async fn mpu_init(sensor: &mut Mpu6050<I2cDevice<'static, CriticalSectionRawMutex, I2c<'static, Async>>>) -> Result<(), Error<I2cDevice<'static, CriticalSectionRawMutex, I2c<'static, Async>>>> {
|
||||
let mut delay = Delay;
|
||||
let backoff = Backoff::from_millis(3);
|
||||
info!("Resetting MPU");
|
||||
backoff.attempt(async || {sensor.reset(&mut delay).await}).await?;
|
||||
info!("Configuring sample rate");
|
||||
backoff.attempt(async || {sensor.set_sample_rate_divider(255).await}).await
|
||||
}
|
||||
|
||||
const GPS_TEST_DATA: &str = include_str!("../test.nmea");
|
||||
|
||||
#[embassy_executor::task]
|
||||
pub async fn gps_task(events: DynPublisher<'static, Notification>, mut i2c_bus: I2cDevice<'static, CriticalSectionRawMutex, I2c<'static, Async>>) {
|
||||
Backoff::from_secs(5).forever().attempt(async || {
|
||||
info!("Initializing GPS");
|
||||
// Enable a bunch of data? idk
|
||||
let bytes = "$PMTK314,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n";
|
||||
i2c_bus.write(0x10, bytes.as_bytes()).await?;
|
||||
|
||||
// 1hz updates
|
||||
let bytes = "$PMTK220,1000*1F\r\n";
|
||||
i2c_bus.write(0x10, bytes.as_bytes()).await?;
|
||||
|
||||
// 1hz position fix
|
||||
let bytes = "$PMTK300,1000,0,0,0,0*1C\r\n";
|
||||
i2c_bus.write(0x10, bytes.as_bytes()).await?;
|
||||
|
||||
// Antenna updates
|
||||
let bytes = "$PGCMD,33,1*6C\r\n";
|
||||
i2c_bus.write(0x10, bytes.as_bytes()).await
|
||||
}).await.unwrap();
|
||||
|
||||
let mut strbuf = String::new();
|
||||
|
||||
let mut parser = Nmea::default();
|
||||
let mut parsing = false;
|
||||
let mut has_lock = false;
|
||||
//let mut iter = GPS_TEST_DATA.as_bytes().iter();
|
||||
info!("GPS is ready!");
|
||||
loop {
|
||||
let mut buf = [0; 1];
|
||||
i2c_bus.read(0x10, &mut buf).await.unwrap();
|
||||
//buf[0] = *(iter.next().unwrap());
|
||||
if (buf[0] as char == '\n' || buf[0] as char == '\r') && !strbuf.is_empty() {
|
||||
if let Ok(result) = parser.parse(&strbuf) {
|
||||
if parser.fix_type.is_some() != has_lock {
|
||||
has_lock = parser.fix_type.is_some();
|
||||
if has_lock {
|
||||
events.publish(Notification::GPSAcquired).await;
|
||||
} else {
|
||||
events.publish(Notification::GPSLost).await;
|
||||
}
|
||||
}
|
||||
log::info!("nmea={result:?} raw={strbuf:?}");
|
||||
log::debug!("nmea={parser:?}");
|
||||
log::info!("speed={:?} altitude={:?} lat={:?} lng={:?} fix={:?}", parser.speed_over_ground, parser.altitude, parser.latitude, parser.longitude, parser.fix_type);
|
||||
for sat in parser.satellites() {
|
||||
info!("\t{} snr={:?} prn={:?}", sat.gnss_type(), sat.snr(), sat.prn())
|
||||
}
|
||||
} else {
|
||||
log::warn!("Unhandled NMEA {strbuf:?}");
|
||||
}
|
||||
strbuf = String::new();
|
||||
parsing = false;
|
||||
// Update frequency is 1hz, so we should never get an update faster than once per second
|
||||
Timer::after_secs(1).await;
|
||||
} else if strbuf.is_empty() && (buf[0] as char == '$' || buf[0] as char == '!') {
|
||||
parsing = true;
|
||||
strbuf.push(buf[0] as char);
|
||||
Timer::after_millis(3).await;
|
||||
} else if parsing {
|
||||
strbuf.push(buf[0] as char);
|
||||
Timer::after_millis(3).await;
|
||||
} else {
|
||||
// If there is no data ready for some reason, wait 500ms, which should place us at least somewhere after the next data frame is ready to read.
|
||||
Timer::after_millis(500).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
src/tasks/mod.rs
Normal file
2
src/tasks/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod i2c;
|
||||
pub mod render;
|
||||
68
src/tasks/render.rs
Normal file
68
src/tasks/render.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use embassy_sync::{blocking_mutex::raw::NoopRawMutex, pubsub::Subscriber};
|
||||
use embassy_time::{Duration, Instant, Timer};
|
||||
use esp_hal::{rmt::ChannelCreator, Async, peripherals::GPIO5};
|
||||
use esp_hal_smartled::{buffer_size_async, SmartLedsAdapterAsync};
|
||||
use figments::{hardware::Output, liber8tion::trig::sin8, prelude::Rectangle, render::Sample};
|
||||
use log::{info, warn};
|
||||
use rgb::Rgb;
|
||||
|
||||
use crate::{display::BikeOutput, events::Notification};
|
||||
|
||||
|
||||
#[embassy_executor::task]
|
||||
pub async fn render(mut events: Subscriber<'static, NoopRawMutex, Notification, 4, 4, 4>, rmt_channel: ChannelCreator<Async, 0>, gpio: GPIO5<'static>) {
|
||||
let rmt_buffer = [0u32; buffer_size_async(178)];
|
||||
|
||||
let target = SmartLedsAdapterAsync::new(rmt_channel, gpio, rmt_buffer);
|
||||
|
||||
// Change this number to use a different number of LEDs
|
||||
//let mut pixbuf = [Default::default(); 16];
|
||||
|
||||
// Change this to adjust the power available; the USB spec says 500ma is the standard limit, but sometimes you can draw more from a power brick
|
||||
const POWER_MA : u32 = 500;
|
||||
|
||||
// You probably don't need to change these values, unless your LED strip is somehow not 5 volts
|
||||
const POWER_VOLTS : u32 = 5;
|
||||
const MAX_POWER_MW : u32 = POWER_VOLTS * POWER_MA;
|
||||
|
||||
// This value is used as the 'seed' for rendering each frame, allowing us to do things like run the animation backwards, frames for double FPS, or even use system uptime for more human-paced animations
|
||||
let mut frame = 0;
|
||||
|
||||
//let mut target = BrightnessWriter::new(target, MAX_POWER_MW);
|
||||
let mut output = BikeOutput::new(target, MAX_POWER_MW);
|
||||
|
||||
info!("Rendering started!");
|
||||
|
||||
loop {
|
||||
/*if let Some(evt) = events.try_next_message() {
|
||||
|
||||
}*/
|
||||
let start = Instant::now();
|
||||
//pixbuf.blank();
|
||||
output.blank().await.expect("Failed to blank framebuf");
|
||||
|
||||
// Render the frame to the pixbuf, while also calculating the power consumption as we go
|
||||
for (coords, pix) in output.sample(&Rectangle::everything()) {
|
||||
*pix = Rgb::new(sin8(coords.x.wrapping_mul(3).wrapping_add(coords.y.wrapping_mul(3)).wrapping_add(frame)), 0, 0);
|
||||
}
|
||||
|
||||
// Finally, write out the rendered frame
|
||||
//target.write(pixbuf.iter().cloned()).await.expect("Could not write frame");
|
||||
//info!("frame");
|
||||
output.commit().await.expect("Failed to commit frame");
|
||||
//info!("commit");
|
||||
|
||||
let render_duration = Instant::now() - start;
|
||||
let render_budget = Duration::from_millis(16);
|
||||
|
||||
if render_duration < render_budget {
|
||||
let remaining_budget = render_budget - render_duration;
|
||||
Timer::after(remaining_budget).await;
|
||||
} else {
|
||||
warn!("Render stall! Frame took {}ms", render_duration.as_millis());
|
||||
}
|
||||
|
||||
// Increment the frame counter
|
||||
frame += 1;
|
||||
}
|
||||
}
|
||||
4368
src/test.nmea
Normal file
4368
src/test.nmea
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user