bump a lot of big changes I dont want to break down into individual commits
This commit is contained in:
18
src/tasks/demo.rs
Normal file
18
src/tasks/demo.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use embassy_sync::channel::DynamicSender;
|
||||
use embassy_time::Timer;
|
||||
|
||||
use crate::events::{Notification, Scene};
|
||||
|
||||
|
||||
#[embassy_executor::task]
|
||||
pub async fn demo_task(ui: DynamicSender<'static, Notification>) {
|
||||
Timer::after_secs(10).await;
|
||||
ui.send(Notification::SceneChange(Scene::Idle)).await;
|
||||
Timer::after_secs(10).await;
|
||||
loop {
|
||||
for scene in [Scene::Accelerating, Scene::Ready, Scene::Decelerating, Scene::Ready] {
|
||||
Timer::after_secs(8).await;
|
||||
ui.send(Notification::SceneChange(scene)).await
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,7 @@ use nmea::Nmea;
|
||||
|
||||
use crate::{backoff::Backoff, events::Measurement};
|
||||
|
||||
#[allow(dead_code)] //FIXME: Allow switching to this via configure option
|
||||
const GPS_TEST_DATA: &str = include_str!("../test.nmea");
|
||||
|
||||
// FIXME: We need a way to put the GPS to sleep when the system goes to sleep
|
||||
#[embassy_executor::task]
|
||||
pub async fn gps_task(events: DynamicSender<'static, Measurement>, mut i2c_bus: I2cDevice<'static, CriticalSectionRawMutex, I2c<'static, Async>>) {
|
||||
Backoff::from_secs(5).forever().attempt::<_, (), ()>(async || {
|
||||
@@ -40,16 +38,15 @@ pub async fn gps_task(events: DynamicSender<'static, Measurement>, mut i2c_bus:
|
||||
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.map_err(|_| { Err::<(), ()>(()) }).ok();
|
||||
//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) {
|
||||
match parser.fix_type {
|
||||
None if has_lock => {
|
||||
// TODO: Send a Measurement::SensorOffline(SensorSource::GPS) here instead
|
||||
events.send(Measurement::GPS(None)).await;
|
||||
has_lock = false
|
||||
},
|
||||
@@ -58,6 +55,7 @@ pub async fn gps_task(events: DynamicSender<'static, Measurement>, mut i2c_bus:
|
||||
if !has_lock {
|
||||
has_lock = true;
|
||||
}
|
||||
// TODO: Send a Measurement::SensorOnline(SensorSource::GPS) here instead
|
||||
|
||||
//TODO: 4 satellites seems to be "Some" fix, 6 is a perfect fix
|
||||
//TODO: Only send updates when we get the correct nmea sentence
|
||||
@@ -66,11 +64,11 @@ pub async fn gps_task(events: DynamicSender<'static, Measurement>, mut i2c_bus:
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
log::trace!("nmea={result:?} raw={strbuf:?}");
|
||||
log::trace!("nmea={parser:?}");
|
||||
log::trace!("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())
|
||||
trace!("\t{} snr={:?} prn={:?}", sat.gnss_type(), sat.snr(), sat.prn())
|
||||
}
|
||||
} else {
|
||||
log::warn!("Unhandled NMEA {strbuf:?}");
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
#[cfg(feature="mpu")]
|
||||
pub mod mpu;
|
||||
#[cfg(feature="gps")]
|
||||
pub mod gps;
|
||||
pub mod render;
|
||||
pub mod motion;
|
||||
pub mod gps;
|
||||
pub mod ui;
|
||||
#[cfg(feature="radio")]
|
||||
pub mod wifi;
|
||||
#[cfg(feature="simulation")]
|
||||
pub mod simulation;
|
||||
pub mod simulation;
|
||||
pub mod predict;
|
||||
pub mod demo;
|
||||
@@ -1,203 +1,31 @@
|
||||
use embassy_sync::channel::{DynamicReceiver, DynamicSender};
|
||||
use nalgebra::{Rotation3, Vector2, Vector3, ComplexField};
|
||||
|
||||
use crate::{ego::{alignment::{Direction, DirectionEstimator, SensorAlignment}, heading::HeadingEstimator, kalman::{GpsMeasurement, Kalman}}, events::{Measurement, Notification, Prediction, Telemetry}, CircularBuffer};
|
||||
use log::*;
|
||||
|
||||
#[embassy_executor::task]
|
||||
pub async fn motion_task(src: DynamicReceiver<'static, Measurement>, ui_sink: DynamicSender<'static, Notification>, telemetry_sink: DynamicSender<'static, Telemetry>) {
|
||||
//TODO: is this the right value? how do update frequencies work?
|
||||
let dt = 0.01; // 10 Hz
|
||||
let mut kf = Kalman::new(dt);
|
||||
use crate::{ego::engine::BikeStates, events::{Measurement, Notification, Prediction}};
|
||||
|
||||
let mut recent_accel: CircularBuffer<Vector3<f32>, 100> = CircularBuffer::default();
|
||||
let mut gravity_align = None;
|
||||
let mut forwards_align = DirectionEstimator::new(Direction::UpX);
|
||||
let mut last_fix = Vector2::default();
|
||||
let mut reference_fix = None;
|
||||
let mut heading = HeadingEstimator::new(0.0);
|
||||
#[embassy_executor::task]
|
||||
pub async fn motion_task(src: DynamicReceiver<'static, Measurement>, ui_sink: DynamicSender<'static, Notification>, prediction_sink: DynamicSender<'static, Prediction>) {
|
||||
let mut states = BikeStates::default();
|
||||
|
||||
loop {
|
||||
let next_measurement = src.receive().await;
|
||||
let _ = telemetry_sink.try_send(Telemetry::Measurement(next_measurement));
|
||||
debug!("measurement={next_measurement:?}");
|
||||
match next_measurement {
|
||||
Measurement::IMU { accel, gyro } => {
|
||||
recent_accel.insert(accel);
|
||||
// Require a gravity alignment first
|
||||
if gravity_align.is_none() {
|
||||
gravity_align = {
|
||||
let align = SensorAlignment::from_samples(&recent_accel.data, crate::ego::alignment::Direction::DnZ);
|
||||
if align.bias.norm() < 9.764 || align.bias.norm() > 9.834 {
|
||||
None
|
||||
} else {
|
||||
ui_sink.send(Notification::CalibrationComplete).await;
|
||||
Some(align)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Minimum motion threshold
|
||||
if accel.xy().magnitude() >= 0.8 {
|
||||
// Try to build a new 'forwards' alignment
|
||||
forwards_align.update(accel);
|
||||
|
||||
// Rotate the measurement into the gravity-down frame
|
||||
let (body_accel, body_gyro) = if let Some(ref adj) = gravity_align {
|
||||
(adj.apply(&accel), adj.apply(&gyro))
|
||||
} else {
|
||||
(accel, gyro)
|
||||
};
|
||||
|
||||
// Then rotate the other two axes to produce the final body frame
|
||||
let forwards_adjusted = forwards_align.apply(&body_accel);
|
||||
heading.update(body_gyro.z, 0.1); // TODO: need to correctly use time values for dt
|
||||
let heading_rotation = Rotation3::from_axis_angle(&Vector3::z_axis(), heading.heading());
|
||||
|
||||
let enu_rotated = heading_rotation * forwards_adjusted;
|
||||
|
||||
// FIXME: the last thing to add here is rotating the acceleration measurements into the ENU frame.
|
||||
let m = Vector3::new(enu_rotated.x, enu_rotated.y, body_gyro.z);
|
||||
kf.predict(&m);
|
||||
} else {
|
||||
// Otherwise, we are standing stationary and should insert accel=0 data into the model
|
||||
kf.update_zupt();
|
||||
}
|
||||
}
|
||||
states.insert_imu(accel, gyro);
|
||||
states.commit(&prediction_sink, &ui_sink).await;
|
||||
},
|
||||
Measurement::GPS(Some(gps_pos)) => {
|
||||
match reference_fix {
|
||||
None => {
|
||||
reference_fix = Some(gps_pos);
|
||||
last_fix = gps_pos;
|
||||
ui_sink.send(Notification::GPSAcquired).await;
|
||||
},
|
||||
Some(coords) => {
|
||||
|
||||
let est = kf.state();
|
||||
info!("pos=\t({},\t{})\tvel=({},\t{})\theading={}\t({})\tforwards={:?}", est[0], est[1], est[2], est[3], est[4], heading.heading(), forwards_align.apply(&Vector3::new(1.0, 0.0, 0.0)));
|
||||
if last_fix != coords {
|
||||
let gps_heading = last_fix.angle(&coords);
|
||||
heading.correct(gps_heading as f32, 0.9);
|
||||
}
|
||||
|
||||
// Convert GPS coordinates into a meter distance away from the known fix location
|
||||
let dx = (gps_pos.x - coords.x) * coords.x.cos() * 6371000.0;
|
||||
let dy = (gps_pos.y - coords.y) * 6371000.0;
|
||||
kf.update(&GpsMeasurement::new(dx as f32, dy as f32));
|
||||
info!("GPS={gps_pos:?} d=({dx}, {dy}) heading={}", heading.heading().to_degrees());
|
||||
|
||||
last_fix = coords;
|
||||
}
|
||||
}
|
||||
states.insert_gps(gps_pos);
|
||||
states.commit(&prediction_sink, &ui_sink).await;
|
||||
},
|
||||
Measurement::GPS(None) => {
|
||||
reference_fix = None;
|
||||
ui_sink.send(Notification::GPSLost).await;
|
||||
}
|
||||
}
|
||||
|
||||
let est = kf.state();
|
||||
if est[2].abs() + est[3].abs() >= 5.0 {
|
||||
//info!("pos=\t({},\t{})\tvel=({},\t{})\theading={}\tforwards={:?}", est[0], est[1], est[2], est[3], est[4], forwards_align.apply(&Vector3::new(1.0, 0.0, 0.0)));
|
||||
}
|
||||
|
||||
let _ = telemetry_sink.try_send(Telemetry::Prediction(Prediction::Location(est.xy())));
|
||||
}
|
||||
}
|
||||
|
||||
/*#[embassy_executor::task]
|
||||
pub async fn _motion_task(src: DynamicReceiver<'static, Measurement>, sink: DynamicSender<'static, Notification>, telemetry_sink: DynamicSender<'static, Measurement>) {
|
||||
//TODO: is this the right value? how do update frequencies work?
|
||||
let dt = 0.01; // 10 Hz
|
||||
let mut kf = Kalman::new(dt);
|
||||
let mut is_moving = false;
|
||||
let mut heading = HeadingEstimator::new(0.0);
|
||||
|
||||
//TODO: Accelerating/decelerating events need thresholds, maybe a PID to detect? PID setpoint can be a running average speed, and if the calculated error is above/below a threshold, it means we are accelerating/decelerating
|
||||
|
||||
let mut last_fix = None;
|
||||
let mut last_idle = Instant::now();
|
||||
let mut is_maybe_idling = false;
|
||||
let mut is_idle = false;
|
||||
let mut is_parked = false;
|
||||
|
||||
loop {
|
||||
match src.receive().await {
|
||||
Measurement::IMU{ accel, gyro } => {
|
||||
//info!("IMU update {imu_accel:?}");
|
||||
//info!("m=\t({:?}, {:?})\tyaw={:?}", imu_accel.x, imu_accel.y, imu_accel.z);
|
||||
//kf.predict(&Control::new(imu_accel.x, imu_accel.y, imu_accel.z));
|
||||
// TODO: Also needs corrected using GPS headings via rotation from body frame into world frame
|
||||
//heading.heading();
|
||||
heading.update(accel.z, 0.01);
|
||||
|
||||
//info!("{}", imu_accel.xy().magnitude());
|
||||
if accel.xy().magnitude() >= 0.8 {
|
||||
//info!("magnitude {imu_accel:?} {}", imu_accel.xy().magnitude());
|
||||
kf.predict(&accel);
|
||||
if is_parked {
|
||||
info!("Wake up!");
|
||||
sink.send(Notification::SceneChange(crate::events::Scene::ParkedIdle)).await;
|
||||
is_maybe_idling = false;
|
||||
is_parked = false;
|
||||
is_idle = false;
|
||||
}
|
||||
} else {
|
||||
//info!("Stationary!!");
|
||||
kf.update_zupt();
|
||||
|
||||
if !is_maybe_idling {
|
||||
is_maybe_idling = true;
|
||||
last_idle = Instant::now();
|
||||
} else if !is_idle && Instant::now() - last_idle >= Duration::from_secs(15) {
|
||||
info!("Parked idle");
|
||||
sink.send(Notification::SceneChange(crate::events::Scene::ParkedIdle)).await;
|
||||
is_idle = true;
|
||||
} else if !is_parked && Instant::now() - last_idle >= Duration::from_secs(60 * 3) {
|
||||
info!("Parked long term!");
|
||||
sink.send(Notification::SceneChange(crate::events::Scene::ParkedLongTerm)).await;
|
||||
is_parked = true;
|
||||
}
|
||||
}
|
||||
// TODO: Rotate the acceleration values into the World frame using GPS heading values
|
||||
|
||||
// X means forwards, Y is left/right slide, Z is vertical movement
|
||||
if accel.x > 1.0 {
|
||||
//sink.send(Notification::SceneChange(crate::events::Scene::Accelerating)).await;
|
||||
} else if accel.x < -1.0 {
|
||||
//sink.send(Notification::SceneChange(crate::events::Scene::Decelerating)).await;
|
||||
}
|
||||
|
||||
let _ = telemetry_sink.try_send(Measurement::IMU { accel, gyro });
|
||||
}
|
||||
Measurement::GPS(gps_pos) => {
|
||||
//info!("GPS update! {gps_pos:?}");
|
||||
match last_fix {
|
||||
None => last_fix = Some(gps_pos),
|
||||
Some(coords) => {
|
||||
let dx = (gps_pos.x - coords.x) * coords.x.cos() * 6371000.0;
|
||||
let dy = (gps_pos.y - coords.y) * 6371000.0;
|
||||
kf.update(&GpsMeasurement::new(dx as f32, dy as f32));
|
||||
}
|
||||
}
|
||||
|
||||
let _ = telemetry_sink.try_send(Measurement::GPS(gps_pos));
|
||||
}
|
||||
}
|
||||
|
||||
let est = kf.state();
|
||||
//let _ = telemetry_sink.try_send(Measurement::Prediction(*est));
|
||||
if est[2].abs() + est[3].abs() >= 0.2 {
|
||||
info!("pos=\t({},\t{})\tvel=({},\t{})\theading=({},\t{})", est[0], est[1], est[2], est[3], est[4], est[5]);
|
||||
}
|
||||
|
||||
if est[2].abs() > 1.0 {
|
||||
if !is_moving {
|
||||
is_moving = true;
|
||||
sink.send(Notification::SceneChange(crate::events::Scene::Accelerating)).await;
|
||||
}
|
||||
} else if is_moving {
|
||||
is_moving = false;
|
||||
sink.send(Notification::SceneChange(crate::events::Scene::StoplightIdle)).await;
|
||||
states.has_gps_fix.set(false);
|
||||
},
|
||||
// FIXME: This needs harmonized with the automatic data timeout from above, somehow?
|
||||
Measurement::SensorOnline(source) => warn!("Sensor {source:?} is online!"),
|
||||
Measurement::SensorOffline(source) => warn!("Sensor {source:?} is offline!"),
|
||||
Measurement::SimulationProgress(source, duration, _pct) => debug!("{source:?} simulation time: {}", duration.as_secs()),
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
@@ -2,14 +2,15 @@ use core::cell::RefCell;
|
||||
|
||||
use embassy_embedded_hal::shared_bus::asynch::i2c::I2cDevice;
|
||||
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, channel::DynamicSender};
|
||||
use embassy_time::{Delay, Timer};
|
||||
use embassy_time::{Delay, Timer, Instant};
|
||||
use esp_hal::{i2c::master::I2c, Async};
|
||||
use log::{info, warn};
|
||||
use log::*;
|
||||
use mpu6050_dmp::{address::Address, calibration::CalibrationParameters, error_async::Error, sensor_async::Mpu6050};
|
||||
use nalgebra::Vector3;
|
||||
use core::f32::consts::PI;
|
||||
use crate::events::SensorSource;
|
||||
|
||||
use crate::{backoff::Backoff, ego::tilt::TiltEstimator, events::Measurement};
|
||||
use crate::{backoff::Backoff, events::Measurement};
|
||||
|
||||
const G: f32 = 9.80665;
|
||||
const SENS_2G: f32 = 16384.0;
|
||||
@@ -27,43 +28,6 @@ fn gyro_raw_to_rads(raw: i16) -> f32 {
|
||||
(raw as f32 / 16.4) * (DEG2RAD)
|
||||
}
|
||||
|
||||
/*/// Rotate body accel into ENU and remove gravity; returns (east, north, up) linear accel in m/s^2
|
||||
pub fn accel_body_to_enu_lin(body_acc: (f32,f32,f32), roll: f32, pitch: f32, yaw: f32) -> (f32,f32,f32) {
|
||||
let (ax, ay, az) = body_acc;
|
||||
|
||||
let (sr, cr) = roll.sin_cos();
|
||||
let (sp, cp) = pitch.sin_cos();
|
||||
let (sy, cy) = yaw.sin_cos();
|
||||
|
||||
// Build R = Rz(yaw) * Ry(pitch) * Rx(roll)
|
||||
// multiply R * a_body
|
||||
let r11 = cy*cp;
|
||||
let r12 = cy*sp*sr - sy*cr;
|
||||
let r13 = cy*sp*cr + sy*sr;
|
||||
|
||||
let r21 = sy*cp;
|
||||
let r22 = sy*sp*sr + cy*cr;
|
||||
let r23 = sy*sp*cr - cy*sr;
|
||||
|
||||
let r31 = -sp;
|
||||
let r32 = cp*sr;
|
||||
let r33 = cp*cr;
|
||||
|
||||
// a_nav = R * a_body
|
||||
let ax_nav = r11*ax + r12*ay + r13*az; // east
|
||||
let ay_nav = r21*ax + r22*ay + r23*az; // north
|
||||
let az_nav = r31*ax + r32*ay + r33*az; // up
|
||||
|
||||
// remove gravity (nav frame gravity points down in NED; in our ENU 'up' axis positive up)
|
||||
// If accel measured gravity as +9.8 on sensor when static and z down convention used above,
|
||||
// then az_nav will include +g in 'up' direction; subtract G.
|
||||
let ax_lin = ax_nav;
|
||||
let ay_lin = ay_nav;
|
||||
let az_lin = az_nav - G; // linear acceleration in up axis
|
||||
|
||||
(ax_lin, ay_lin, az_lin)
|
||||
}*/
|
||||
|
||||
#[embassy_executor::task]
|
||||
pub async fn mpu_task(events: DynamicSender<'static, Measurement>, bus: I2cDevice<'static, CriticalSectionRawMutex, I2c<'static, Async>>) {
|
||||
let backoff = Backoff::from_millis(5);
|
||||
@@ -71,7 +35,7 @@ pub async fn mpu_task(events: DynamicSender<'static, Measurement>, bus: I2cDevic
|
||||
|
||||
backoff.forever().attempt::<_, (), ()>(async || {
|
||||
let mut sensor = backoff.forever().attempt(async || {
|
||||
info!("Initializing MPU");
|
||||
warn!("Initializing connection to MPU");
|
||||
match Mpu6050::new(busref.replace(None).unwrap(), Address::default()).await.map_err(|e| { e.i2c }) {
|
||||
Err(i2c) => {
|
||||
busref.replace(Some(i2c));
|
||||
@@ -91,8 +55,7 @@ pub async fn mpu_task(events: DynamicSender<'static, Measurement>, bus: I2cDevic
|
||||
|
||||
let sensor_ref = &mut sensor;
|
||||
|
||||
info!("MPU is ready!");
|
||||
let _tilt = TiltEstimator::new(0.3);
|
||||
events.send(Measurement::SensorOnline(SensorSource::IMU)).await;
|
||||
//TODO: Need to read in a constant buffer of accelerometer measurements, which we can then use to determine where "forward" points in the body frame when converting from the sensor frame.
|
||||
// From there, we can rotate the body frame into the world frame using gps headings to generate a compass north
|
||||
fn lowpass(prev: f32, current: f32, alpha: f32) -> f32 {
|
||||
@@ -122,12 +85,6 @@ pub async fn mpu_task(events: DynamicSender<'static, Measurement>, bus: I2cDevic
|
||||
);
|
||||
prev_accel = adjusted;
|
||||
|
||||
// Apply current rotation and accel values to rotate our values back into the ENU frame
|
||||
/*tilt.update(0.01, (filtered.x, filtered.y, filtered.z), (adjusted_gyro.x, adjusted_gyro.y, adjusted_gyro.z));
|
||||
let (roll, pitch, yaw) = tilt.angles();
|
||||
let (ax_e, ay_n, az_up) = accel_body_to_enu_lin((filtered.x,filtered.y,filtered.z), roll, pitch, yaw);
|
||||
|
||||
let measured = Vector3::new(ax_e, ay_n, az_up);*/
|
||||
events.send(
|
||||
Measurement::IMU {
|
||||
accel: filtered,
|
||||
@@ -137,7 +94,7 @@ pub async fn mpu_task(events: DynamicSender<'static, Measurement>, bus: I2cDevic
|
||||
Timer::after_millis(10).await;
|
||||
},
|
||||
Err(e) => {
|
||||
warn!("Failed to read MPU motion data! {e:?}");
|
||||
error!("Failed to read MPU motion data! {e:?}");
|
||||
busref.replace(Some(sensor.release()));
|
||||
return Err(());
|
||||
}
|
||||
@@ -147,18 +104,28 @@ pub async fn mpu_task(events: DynamicSender<'static, Measurement>, bus: I2cDevic
|
||||
}
|
||||
|
||||
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(10);
|
||||
info!("Initializing MPU");
|
||||
backoff.attempt(async || { sensor.initialize_dmp(&mut delay).await }).await?;
|
||||
backoff.attempt(async || { sensor.set_digital_lowpass_filter(mpu6050_dmp::config::DigitalLowPassFilter::Filter3).await }).await?;
|
||||
info!("Calibrating MPU");
|
||||
backoff.attempt(async || {
|
||||
// TODO: Store calibration data with set_accel_calibration, set_gyro_calibration
|
||||
sensor.calibrate(&mut delay, &CalibrationParameters::new(
|
||||
mpu6050_dmp::accel::AccelFullScale::G2,
|
||||
mpu6050_dmp::gyro::GyroFullScale::Deg2000,
|
||||
mpu6050_dmp::calibration::ReferenceGravity::Zero
|
||||
)).await.map(|_| { Ok(()) })
|
||||
}).await?
|
||||
if cfg!(feature="wokwi") {
|
||||
warn!("Initializing simulated MPU");
|
||||
Ok(())
|
||||
} else {
|
||||
let mut delay = Delay;
|
||||
let backoff = Backoff::from_millis(10);
|
||||
info!("Initializing DMP");
|
||||
let start = Instant::now();
|
||||
backoff.attempt(async || { sensor.initialize_dmp(&mut delay).await }).await?;
|
||||
backoff.attempt(async || { sensor.set_digital_lowpass_filter(mpu6050_dmp::config::DigitalLowPassFilter::Filter1).await }).await?;
|
||||
info!("DMP ready in {}ms", start.as_millis());
|
||||
info!("Calibrating MPU");
|
||||
let start = Instant::now();
|
||||
backoff.attempt(async || {
|
||||
// TODO: Store calibration data with set_accel_calibration, set_gyro_calibration
|
||||
sensor.calibrate(&mut delay, &CalibrationParameters::new(
|
||||
mpu6050_dmp::accel::AccelFullScale::G2,
|
||||
mpu6050_dmp::gyro::GyroFullScale::Deg2000,
|
||||
mpu6050_dmp::calibration::ReferenceGravity::Zero
|
||||
)).await
|
||||
}).await?;
|
||||
info!("MPU calibrated in {}ms", start.as_millis());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
108
src/tasks/predict.rs
Normal file
108
src/tasks/predict.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
|
||||
use embassy_sync::channel::{DynamicReceiver, DynamicSender};
|
||||
use embassy_time::Duration;
|
||||
use log::*;
|
||||
|
||||
use crate::{ego::engine::{gps_to_local_meters_haversine, MotionState}, events::{Notification, Prediction, Scene}, idle::IdleClock};
|
||||
|
||||
#[embassy_executor::task]
|
||||
pub async fn prediction_task(prediction_src: DynamicReceiver<'static, Prediction>, notify: DynamicSender<'static, Notification>) {
|
||||
let mut last_velocity = 0.0;
|
||||
let mut first_position = None;
|
||||
let mut last_position = Default::default();
|
||||
let mut parking_timer = IdleClock::new(Duration::from_secs(10));
|
||||
let mut sleep_timer = IdleClock::new(Duration::from_secs(30));
|
||||
let mut stationary = true;
|
||||
loop {
|
||||
let d = first_position.map(|x| {
|
||||
gps_to_local_meters_haversine(&x, &last_position).norm()
|
||||
});
|
||||
if let Ok(next_evt) = embassy_time::with_timeout(Duration::from_secs(1), prediction_src.receive()).await {
|
||||
match next_evt {
|
||||
Prediction::WakeRequested => {
|
||||
if sleep_timer.wake() {
|
||||
warn!("Wake requested during sleep");
|
||||
notify.send(Notification::WakeUp).await;
|
||||
|
||||
// Also reset the parking timer
|
||||
parking_timer.wake();
|
||||
} else if parking_timer.wake() {
|
||||
info!("Wake requested while parked");
|
||||
// If we weren't asleep but we were parked, then switch back to the Ready state and turn on the lights
|
||||
notify.send(Notification::SetHeadlight(true)).await;
|
||||
notify.send(Notification::SetBrakelight(true)).await;
|
||||
notify.send(Notification::SceneChange(Scene::Ready)).await;
|
||||
}
|
||||
}
|
||||
Prediction::Velocity(v) => {
|
||||
last_velocity = v;
|
||||
|
||||
if v > 5.0 && stationary {
|
||||
notify.send(Notification::Beat).await;
|
||||
}
|
||||
// TODO: Probably makes sense to only print this based on an IdleTimer, so that a long period of slightly variable movement doesn't get lost, but we can still report values to the UI / telemetry outputs
|
||||
//info!("Velocity predict: velocity={v}\tpos={last_position:?}\tdistance={d:?}");
|
||||
},
|
||||
Prediction::Location(loc) => {
|
||||
if first_position.is_none() {
|
||||
info!("Got location={loc:?}");
|
||||
first_position = Some(loc);
|
||||
}
|
||||
last_position = loc;
|
||||
}
|
||||
Prediction::Motion(motion) => {
|
||||
info!("Motion predict:\t{motion:?}\tvelocity={last_velocity}\tpos={last_position:?}\tdistance={d:?}");
|
||||
if sleep_timer.wake() {
|
||||
notify.send(Notification::WakeUp).await;
|
||||
notify.send(Notification::SetHeadlight(true)).await;
|
||||
notify.send(Notification::SetBrakelight(true)).await
|
||||
}
|
||||
|
||||
if parking_timer.wake() {
|
||||
notify.send(Notification::SetHeadlight(true)).await;
|
||||
notify.send(Notification::SetBrakelight(true)).await
|
||||
}
|
||||
match motion {
|
||||
MotionState::Accelerating => {
|
||||
if stationary {
|
||||
// If we are going from standing still to immediately accelerating, first transition to the 'ready' scene
|
||||
notify.send(Notification::SceneChange(Scene::Ready)).await;
|
||||
}
|
||||
notify.send(Notification::SceneChange(Scene::Accelerating)).await;
|
||||
stationary = false;
|
||||
},
|
||||
MotionState::Decelerating => {
|
||||
if stationary {
|
||||
// If we are going from standing still to immediately accelerating, first transition to the 'ready' scene
|
||||
notify.send(Notification::SceneChange(Scene::Ready)).await;
|
||||
}
|
||||
notify.send(Notification::SceneChange(Scene::Decelerating)).await;
|
||||
stationary = false;
|
||||
},
|
||||
MotionState::Steady => {
|
||||
notify.send(Notification::SceneChange(Scene::Ready)).await;
|
||||
stationary = false;
|
||||
},
|
||||
MotionState::Stationary => {
|
||||
notify.send(Notification::SceneChange(Scene::Ready)).await;
|
||||
stationary = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: Need a way to detect if sensors are dead for some reason. Probably should be done in the motion engine, since it would be a prediction?
|
||||
|
||||
if stationary {
|
||||
if parking_timer.check() {
|
||||
notify.send(Notification::SceneChange(Scene::Idle)).await;
|
||||
notify.send(Notification::SetHeadlight(false)).await;
|
||||
notify.send(Notification::SetBrakelight(false)).await
|
||||
}
|
||||
|
||||
if sleep_timer.check() {
|
||||
notify.send(Notification::Sleep).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,20 @@
|
||||
use embassy_time::{Duration, Instant, Timer};
|
||||
use esp_hal::{gpio::AnyPin, rmt::Rmt, time::Rate};
|
||||
use esp_hal::{gpio::AnyPin, rmt::Rmt, time::Rate, timer::timg::Wdt};
|
||||
use esp_hal_smartled::{buffer_size_async, SmartLedsAdapterAsync};
|
||||
use figments::{hardware::{Brightness, OutputAsync}, prelude::Hsv, surface::{BufferedSurfacePool, Surfaces}};
|
||||
use figments::{prelude::*, surface::{BufferedSurfacePool, Surfaces}};
|
||||
use figments_render::gamma::GammaCurve;
|
||||
use figments_render::output::{GammaCorrected, OutputAsync};
|
||||
use log::{info, warn};
|
||||
use rgb::Rgba;
|
||||
use nalgebra::ComplexField;
|
||||
use alloc::sync::Arc;
|
||||
|
||||
use crate::{display::{BikeOutputAsync, BikeSpace}, events::DisplayControls};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Uniforms {
|
||||
pub frame: usize,
|
||||
pub primary_color: Hsv
|
||||
}
|
||||
use crate::{display::{BikeOutput, SegmentSpace, Uniforms}, events::DisplayControls};
|
||||
|
||||
//TODO: Import the bike surfaces from renderbug-prime, somehow make those surfaces into tasks
|
||||
|
||||
#[embassy_executor::task]
|
||||
pub async fn render(rmt: esp_hal::peripherals::RMT<'static>, gpio: AnyPin<'static>, surfaces: BufferedSurfacePool<Uniforms, BikeSpace, Rgba<u8>>, controls: &'static DisplayControls) {
|
||||
pub async fn render(rmt: esp_hal::peripherals::RMT<'static>, gpio: AnyPin<'static>, surfaces: BufferedSurfacePool<Uniforms, SegmentSpace, Rgba<u8>>, controls: Arc<DisplayControls>, mut wdt: Wdt<esp_hal::peripherals::TIMG0<'static>>) {
|
||||
let frequency: Rate = Rate::from_mhz(80);
|
||||
let rmt = Rmt::new(rmt, frequency)
|
||||
.expect("Failed to initialize RMT").into_async();
|
||||
@@ -33,7 +30,8 @@ pub async fn render(rmt: esp_hal::peripherals::RMT<'static>, gpio: AnyPin<'stati
|
||||
|
||||
// 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;
|
||||
|
||||
const MAX_POWER_MW : u32 = if cfg!(feature="max-usb-power") { u32::MAX } else { 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 uniforms = Uniforms {
|
||||
@@ -41,29 +39,42 @@ pub async fn render(rmt: esp_hal::peripherals::RMT<'static>, gpio: AnyPin<'stati
|
||||
..Uniforms::default()
|
||||
};
|
||||
|
||||
let mut output = BikeOutputAsync::new(target, MAX_POWER_MW);
|
||||
let mut output = BikeOutput::new(target, MAX_POWER_MW, controls.clone());
|
||||
output.set_gamma(GammaCurve::new(1.3));
|
||||
//#[cfg(not(feature="wokwi"))]
|
||||
//output.set_gamma(GammaCurve::new(2.1));
|
||||
|
||||
info!("Rendering started! {}ms since boot", Instant::now().as_millis());
|
||||
controls.render_is_running.signal(true);
|
||||
|
||||
const FPS: u64 = 60;
|
||||
const FPS: u64 = 80;
|
||||
const RENDER_BUDGET: Duration = Duration::from_millis(1000 / FPS);
|
||||
|
||||
loop {
|
||||
// FIXME: need to put the rendering loop into a deep sleep when the display is off
|
||||
let start = Instant::now();
|
||||
output.blank_async().await.expect("Failed to blank framebuf");
|
||||
|
||||
output.blank();
|
||||
|
||||
surfaces.render_to(&mut output, &uniforms);
|
||||
|
||||
// Apply hardware controls
|
||||
output.set_on(controls.on.load(core::sync::atomic::Ordering::Relaxed));
|
||||
output.set_brightness(controls.brightness.load(core::sync::atomic::Ordering::Relaxed));
|
||||
|
||||
// Finally, write out the rendered frame
|
||||
output.commit_async().await.expect("Failed to commit frame");
|
||||
|
||||
let render_duration = Instant::now() - start;
|
||||
|
||||
if !controls.is_on() {
|
||||
warn!("Renderer is sleeping zzzz");
|
||||
//controls.render_is_running.signal(false);
|
||||
output.blank();
|
||||
wdt.disable();
|
||||
controls.wait_for_on().await;
|
||||
wdt.feed();
|
||||
wdt.enable();
|
||||
warn!("Renderer is awake !!!!");
|
||||
//controls.render_is_running.signal(true);
|
||||
}
|
||||
|
||||
if render_duration < RENDER_BUDGET {
|
||||
let remaining_budget = RENDER_BUDGET - render_duration;
|
||||
uniforms.frame += 1;
|
||||
@@ -74,5 +85,9 @@ pub async fn render(rmt: esp_hal::peripherals::RMT<'static>, gpio: AnyPin<'stati
|
||||
// If we took longer than the budget, we need to drop some frames to catch up
|
||||
uniforms.frame += dropped_count;
|
||||
}
|
||||
|
||||
uniforms.primary_color.hue = uniforms.primary_color.hue.wrapping_add(1);
|
||||
|
||||
wdt.feed();
|
||||
}
|
||||
}
|
||||
@@ -1,160 +1,63 @@
|
||||
use embassy_sync::channel::DynamicSender;
|
||||
use embassy_time::{Duration, Timer};
|
||||
use nalgebra::{Vector2, Vector3};
|
||||
use csv_core::{ReadFieldResult, Reader};
|
||||
use log::*;
|
||||
|
||||
use crate::events::Measurement;
|
||||
|
||||
const ACCELEROMETER_DATA: &str = include_str!("../../test-data/accelerometer.csv");
|
||||
//const GYRO_DATA: &str = include_str!("../../test-data/gyro.csv");
|
||||
const GPS_DATA: &str = include_str!("../../test-data/gps.csv");
|
||||
|
||||
struct SimDataReader<'a> {
|
||||
reader: Reader,
|
||||
buf: &'a str,
|
||||
read_offset: usize
|
||||
}
|
||||
|
||||
impl<'a> SimDataReader<'a> {
|
||||
pub fn new(buf: &'a str) -> Self {
|
||||
Self {
|
||||
buf,
|
||||
read_offset: 0,
|
||||
reader: Reader::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for SimDataReader<'a> {
|
||||
type Item = &'a str;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let mut out_buf = [0u8; 128];
|
||||
match self.reader.read_field(&self.buf.as_bytes()[self.read_offset..(self.read_offset + 128).min(self.buf.len())], &mut out_buf) {
|
||||
(ReadFieldResult::Field { record_end: _ }, read_size, write_size) => {
|
||||
let start = self.read_offset;
|
||||
self.read_offset += read_size;
|
||||
Some(&self.buf[start..(start + write_size)])
|
||||
},
|
||||
(ReadFieldResult::End, _, _) => None,
|
||||
err => panic!("{:?}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy)]
|
||||
enum MotionFields {
|
||||
#[default]
|
||||
Time,
|
||||
SecondsElapsed,
|
||||
Z,
|
||||
Y,
|
||||
X,
|
||||
End
|
||||
}
|
||||
|
||||
impl MotionFields {
|
||||
pub fn next(self) -> Self {
|
||||
match self {
|
||||
Self::Time => Self::SecondsElapsed,
|
||||
Self::SecondsElapsed => Self::Z,
|
||||
Self::Z => Self::Y,
|
||||
Self::Y => Self::X,
|
||||
Self::X => Self::End,
|
||||
Self::End => panic!("No more fields")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy)]
|
||||
enum GpsFields {
|
||||
#[default]
|
||||
Time,
|
||||
SecondsElapsed,
|
||||
BearingAcc,
|
||||
SpeedAcc,
|
||||
VertAcc,
|
||||
HorizAcc,
|
||||
Speed,
|
||||
Bearing,
|
||||
Altitude,
|
||||
Longitude,
|
||||
Latitude,
|
||||
End
|
||||
}
|
||||
|
||||
impl GpsFields {
|
||||
pub fn next(self) -> Self {
|
||||
match self {
|
||||
Self::Time => Self::SecondsElapsed,
|
||||
Self::SecondsElapsed => Self::BearingAcc,
|
||||
Self::BearingAcc => Self::SpeedAcc,
|
||||
Self::SpeedAcc => Self::VertAcc,
|
||||
Self::VertAcc => Self::HorizAcc,
|
||||
Self::HorizAcc => Self::Speed,
|
||||
Self::Speed => Self::Bearing,
|
||||
Self::Bearing => Self::Altitude,
|
||||
Self::Altitude => Self::Longitude,
|
||||
Self::Longitude => Self::Latitude,
|
||||
Self::Latitude => Self::End,
|
||||
Self::End => panic!("No more fields")
|
||||
}
|
||||
}
|
||||
}
|
||||
use crate::events::{Measurement, SensorSource};
|
||||
use crate::Breaker;
|
||||
|
||||
#[embassy_executor::task]
|
||||
pub async fn motion_simulation_task(events: DynamicSender<'static, Measurement>) {
|
||||
let mut accel = Vector3::default();
|
||||
let mut cur_field = MotionFields::default();
|
||||
let mut last_stamp = 0.0;
|
||||
let mut cur_delta = Duration::from_ticks(0);
|
||||
let reader = SimDataReader::new(ACCELEROMETER_DATA);
|
||||
for field in reader {
|
||||
match cur_field {
|
||||
MotionFields::SecondsElapsed => {
|
||||
let secs = field.parse::<f64>().unwrap();
|
||||
cur_delta = Duration::from_millis(((secs - last_stamp) * 1000.0) as u64);
|
||||
last_stamp = secs;
|
||||
}
|
||||
MotionFields::X => accel.x = field.parse::<f32>().unwrap(),
|
||||
MotionFields::Y => accel.y = field.parse().unwrap(),
|
||||
MotionFields::Z => accel.z = field.parse().unwrap(),
|
||||
_ => ()
|
||||
}
|
||||
cur_field = cur_field.next();
|
||||
if let MotionFields::End = cur_field {
|
||||
cur_field = MotionFields::default();
|
||||
events.send(Measurement::IMU{ accel, gyro: Vector3::default() }).await;
|
||||
Timer::after(cur_delta).await;
|
||||
let mut rd = rmp::decode::Bytes::new(include_bytes!("../../test-data/motion.msgpack"));
|
||||
let mut runtime_secs = Breaker::default();
|
||||
let mut runtime = Duration::default();
|
||||
events.send(Measurement::SensorOnline(SensorSource::IMU)).await;
|
||||
while let Ok(size) = rmp::decode::read_array_len(&mut rd) {
|
||||
assert_eq!(size, 7, "Expected 7 fields, but only found {size}");
|
||||
let delay = embassy_time::Duration::from_millis((rmp::decode::read_f64(&mut rd).unwrap() * 1000.0) as u64);
|
||||
let accel = Vector3::new(
|
||||
rmp::decode::read_f64(&mut rd).unwrap() as f32,
|
||||
rmp::decode::read_f64(&mut rd).unwrap() as f32,
|
||||
rmp::decode::read_f64(&mut rd).unwrap() as f32,
|
||||
);
|
||||
let gyro = Vector3::new(
|
||||
rmp::decode::read_f64(&mut rd).unwrap() as f32,
|
||||
rmp::decode::read_f64(&mut rd).unwrap() as f32,
|
||||
rmp::decode::read_f64(&mut rd).unwrap() as f32,
|
||||
);
|
||||
runtime += delay;
|
||||
runtime_secs.set(runtime.as_secs());
|
||||
if runtime_secs.read_tripped().is_some() {
|
||||
events.send(Measurement::SimulationProgress(SensorSource::IMU, runtime, 0.0)).await;
|
||||
}
|
||||
Timer::after(delay).await;
|
||||
events.send(Measurement::IMU{ accel, gyro }).await;
|
||||
}
|
||||
events.send(Measurement::SensorOffline(SensorSource::IMU)).await;
|
||||
warn!("End of motion recording");
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
pub async fn location_simulation_task(events: DynamicSender<'static, Measurement>) {
|
||||
let mut coords = Vector2::default();
|
||||
let mut cur_field = GpsFields::default();
|
||||
let mut last_stamp = 0.0;
|
||||
let mut cur_delta = Duration::from_ticks(0);
|
||||
let reader = SimDataReader::new(GPS_DATA);
|
||||
for field in reader {
|
||||
match cur_field {
|
||||
GpsFields::SecondsElapsed => {
|
||||
let secs = field.parse::<f64>().unwrap();
|
||||
cur_delta = Duration::from_millis(((secs - last_stamp) * 1000.0) as u64);
|
||||
last_stamp = secs;
|
||||
}
|
||||
GpsFields::Latitude => coords.x = field.parse::<f64>().unwrap(),
|
||||
GpsFields::Longitude => coords.y = field.parse().unwrap(),
|
||||
_ => ()
|
||||
}
|
||||
|
||||
cur_field = cur_field.next();
|
||||
if let GpsFields::End = cur_field {
|
||||
cur_field = GpsFields::default();
|
||||
Timer::after(cur_delta).await;
|
||||
events.send(Measurement::GPS(Some(coords))).await;
|
||||
let mut rd = rmp::decode::Bytes::new(include_bytes!("../../test-data/gps.msgpack"));
|
||||
let mut runtime_secs = Breaker::default();
|
||||
let mut runtime = Duration::default();
|
||||
events.send(Measurement::SensorOnline(SensorSource::GPS)).await;
|
||||
while let Ok(size) = rmp::decode::read_array_len(&mut rd) {
|
||||
assert_eq!(size, 3, "Expected 3 fields, but only found {size}");
|
||||
let delay = embassy_time::Duration::from_millis((rmp::decode::read_f64(&mut rd).unwrap() * 1000.0) as u64);
|
||||
let coords = Vector2::new(
|
||||
rmp::decode::read_f64(&mut rd).unwrap(),
|
||||
rmp::decode::read_f64(&mut rd).unwrap()
|
||||
);
|
||||
runtime += delay;
|
||||
runtime_secs.set(runtime.as_secs());
|
||||
if runtime_secs.read_tripped().is_some() {
|
||||
events.send(Measurement::SimulationProgress(SensorSource::GPS, runtime, 0.0)).await;
|
||||
}
|
||||
Timer::after(delay).await;
|
||||
events.send(Measurement::GPS(Some(coords))).await;
|
||||
}
|
||||
events.send(Measurement::SensorOffline(SensorSource::GPS)).await;
|
||||
warn!("End of GPS recording");
|
||||
}
|
||||
369
src/tasks/ui.rs
369
src/tasks/ui.rs
@@ -1,272 +1,243 @@
|
||||
use embassy_sync::channel::DynamicReceiver;
|
||||
use embassy_time::{Duration, Instant, Timer};
|
||||
use figments::{liber8tion::interpolate::Fract8, prelude::*};
|
||||
use embassy_time::Duration;
|
||||
use figments::prelude::*;
|
||||
use rgb::{Rgb, Rgba};
|
||||
use log::*;
|
||||
use nalgebra::ComplexField;
|
||||
|
||||
use crate::{display::BikeSpace, events::{DisplayControls, Notification, Scene}, shaders::*, tasks::render::Uniforms};
|
||||
use alloc::sync::Arc;
|
||||
use core::fmt::Debug;
|
||||
use futures::join;
|
||||
|
||||
use crate::{animation::{AnimDisplay, AnimatedSurface, Animation}, display::{SegmentSpace, Uniforms}, events::{DisplayControls, Notification, Scene, SensorSource}, shaders::*};
|
||||
|
||||
pub struct Ui<S: Surface> {
|
||||
// Background layer provides an always-running background for everything to draw on
|
||||
background: S,
|
||||
background: AnimatedSurface<S>,
|
||||
|
||||
// Tail and panels provide content
|
||||
tail: S,
|
||||
panels: S,
|
||||
tail: AnimatedSurface<S>,
|
||||
panels: AnimatedSurface<S>,
|
||||
|
||||
motion: AnimatedSurface<S>,
|
||||
|
||||
// Notification layer sits on top of the content, and is used for transient event notifications (gps lost, wifi found, etc)
|
||||
notification:S,
|
||||
notification: AnimatedSurface<S>,
|
||||
|
||||
// Headlight and brakelight layers can only be overpainted by the bootsplash overlay layer
|
||||
headlight: S,
|
||||
brakelight: S,
|
||||
headlight: AnimatedSurface<S>,
|
||||
brakelight: AnimatedSurface<S>,
|
||||
|
||||
// The overlay covers everything and is used to implement a power-on and power-off animation.
|
||||
overlay:S,
|
||||
headlight_on: bool,
|
||||
brakelight_on: bool,
|
||||
display: &'static DisplayControls
|
||||
overlay: AnimatedSurface<S>,
|
||||
display: Arc<DisplayControls>
|
||||
}
|
||||
|
||||
impl<S: Surface<Uniforms = Uniforms, CoordinateSpace = BikeSpace, Pixel = Rgba<u8>>> Ui<S> {
|
||||
pub fn new(surfaces: &mut impl Surfaces<S::CoordinateSpace, Surface = S>, display: &'static DisplayControls) -> Self {
|
||||
impl<S: Debug + Surface<Uniforms = Uniforms, CoordinateSpace = SegmentSpace, Pixel = Rgba<u8>>> Ui<S> {
|
||||
pub fn new<SS: Surfaces<SegmentSpace, Surface = S>>(surfaces: &mut SS, display: Arc<DisplayControls>) -> Self where SS::Error: Debug {
|
||||
Self {
|
||||
background: SurfaceBuilder::build(surfaces)
|
||||
.rect(Rectangle::everything())
|
||||
.shader(Background::default())
|
||||
.opacity(32)
|
||||
.visible(false)
|
||||
.finish().unwrap(),
|
||||
.finish().unwrap().into(),
|
||||
tail: SurfaceBuilder::build(surfaces)
|
||||
.rect(Rectangle::new_from_coordinates(0, 5, 255, 5))
|
||||
.opacity(96)
|
||||
.shader(Tail::default())
|
||||
.visible(false)
|
||||
.finish().unwrap(),
|
||||
.finish().unwrap().into(),
|
||||
panels: SurfaceBuilder::build(surfaces)
|
||||
.rect(Rectangle::new_from_coordinates(0, 1, 255, 4))
|
||||
.opacity(128)
|
||||
.shader(Panel::default())
|
||||
.visible(false)
|
||||
.finish().unwrap(),
|
||||
.finish().unwrap().into(),
|
||||
motion: SurfaceBuilder::build(surfaces)
|
||||
.rect(Rectangle::everything())
|
||||
.shader(Movement::default())
|
||||
.visible(false)
|
||||
.finish().unwrap().into(),
|
||||
notification: SurfaceBuilder::build(surfaces)
|
||||
.rect(Rectangle::everything())
|
||||
.shader(Background::default())
|
||||
.visible(false)
|
||||
.finish().unwrap(),
|
||||
.finish().unwrap().into(),
|
||||
// FIXME: Headlight, brakelight, and the overlay should all be handled as part of the safety UI
|
||||
headlight: SurfaceBuilder::build(surfaces)
|
||||
.rect(Rectangle::new_from_coordinates(0, 0, 255, 0))
|
||||
.shader(Headlight::default())
|
||||
.visible(false)
|
||||
.finish().unwrap(),
|
||||
.finish().unwrap().into(),
|
||||
brakelight: SurfaceBuilder::build(surfaces)
|
||||
.rect(Brakelight::rectangle())
|
||||
.shader(Brakelight::default())
|
||||
.visible(false)
|
||||
.finish().unwrap(),
|
||||
.finish().unwrap().into(),
|
||||
overlay: SurfaceBuilder::build(surfaces)
|
||||
.rect(Rectangle::everything())
|
||||
.visible(true)
|
||||
.opacity(0)
|
||||
.shader(Thinking::default())
|
||||
.finish().unwrap(),
|
||||
headlight_on: false,
|
||||
brakelight_on: false,
|
||||
.visible(false)
|
||||
.finish().unwrap().into(),
|
||||
display
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn flash_notification_color(&mut self, color: Rgb<u8>) {
|
||||
self.notification.set_shader(Background::from_color(color));
|
||||
self.notification.set_opacity(0);
|
||||
let fade_in = Animation::default().from(0).to(255).duration(Duration::from_millis(30));
|
||||
let pulse_out = Animation::default().from(255).to(60).duration(Duration::from_millis(100));
|
||||
let pulse_in = Animation::default().from(0).to(255).duration(Duration::from_millis(100));
|
||||
let fade_out = Animation::default().from(255).to(0).duration(Duration::from_secs(2));
|
||||
info!("Flashing notification {color}");
|
||||
|
||||
self.notification.set_visible(true);
|
||||
// Fade in to full on over 1s
|
||||
Self::animate_duration(Duration::from_secs(1), |pct| {
|
||||
self.notification.set_opacity(pct);
|
||||
}).await;
|
||||
|
||||
// Pulse quickly 3 times
|
||||
for _ in 0..3 {
|
||||
// Brief dimming back to half brightness for half a sec
|
||||
Self::animate_duration(Duration::from_millis(100), |pct| {
|
||||
self.notification.set_opacity(100 + pct/2);
|
||||
}).await;
|
||||
self.notification.set_shader(Background::from_color(color));
|
||||
|
||||
// Back to full
|
||||
Self::animate_duration(Duration::from_millis(100), |pct| {
|
||||
self.notification.set_opacity(100 + (255 - pct) / 2);
|
||||
}).await;
|
||||
fade_in.apply(&mut self.notification).await;
|
||||
|
||||
// Pulse quickly 5 times
|
||||
for _ in 0..5 {
|
||||
pulse_out.apply(&mut self.notification).await;
|
||||
pulse_in.apply(&mut self.notification).await;
|
||||
}
|
||||
|
||||
// Back to off
|
||||
Self::animate_duration(Duration::from_secs(3), |pct| {
|
||||
self.notification.set_opacity(255 - pct);
|
||||
}).await;
|
||||
fade_out.apply(&mut self.notification).await;
|
||||
self.notification.set_visible(false);
|
||||
}
|
||||
|
||||
async fn animate_duration<F: FnMut(Fract8)>(duration: Duration, mut f: F) {
|
||||
let start = Instant::now();
|
||||
let end = start + duration;
|
||||
let full_duration = duration.as_millis() as f64;
|
||||
loop {
|
||||
let now = Instant::now();
|
||||
if now > end {
|
||||
break
|
||||
} else {
|
||||
let pct = (now - start).as_millis() as f64 / full_duration;
|
||||
let frac_pct = (255.0 * pct) as u8;
|
||||
f(frac_pct);
|
||||
Timer::after_millis(5).await;
|
||||
}
|
||||
}
|
||||
pub async fn sleep(&mut self) {
|
||||
info!("Running sleep sequence");
|
||||
let fade_out = Animation::default().duration(Duration::from_secs(1)).from(255).to(0);
|
||||
let mut disp_anim = AnimDisplay(&self.display);
|
||||
fade_out.apply(&mut disp_anim).await;
|
||||
|
||||
// Reset layers to the initial state now that the display should be off
|
||||
[
|
||||
&mut *self.tail,
|
||||
&mut *self.panels,
|
||||
&mut *self.background,
|
||||
&mut *self.motion
|
||||
].set_opacity(0);
|
||||
[
|
||||
&mut *self.tail,
|
||||
&mut *self.panels,
|
||||
&mut *self.background,
|
||||
&mut *self.motion
|
||||
].set_visible(false);
|
||||
|
||||
info!("Turning off display");
|
||||
self.display.set_on(false);
|
||||
// Wait for the display hardware to actually turn off, before we return to process the next event, which could cause funky behaviors.
|
||||
// FIXME: also deadlocks :(
|
||||
//self.display.render_is_running.wait().await;
|
||||
info!("Display is now sleeping.");
|
||||
}
|
||||
|
||||
pub fn expo_in(t: f32) -> f32 {
|
||||
if t <= 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
2f32.powf(10.0 * t - 10.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expo_out(t: f32) -> f32 {
|
||||
if 1.0 <= t {
|
||||
1.0
|
||||
} else {
|
||||
1.0 - 2f32.powf(-10.0 * t)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn startup_fade_sequence(&mut self) {
|
||||
pub async fn wakeup(&mut self) {
|
||||
info!("Running startup fade sequence");
|
||||
// Start with a completely transparent overlay, which then fades in over 1 second
|
||||
self.overlay.set_opacity(0);
|
||||
|
||||
info!("Turning on display");
|
||||
// Turn on the display hardware
|
||||
self.display.set_brightness(0);
|
||||
self.display.set_on(true);
|
||||
// Wait for the renderer to start running again
|
||||
// FIXME: This deadlocks :(
|
||||
//self.display.render_is_running.wait().await;
|
||||
|
||||
let fade_in = Animation::default().duration(Duration::from_secs(3)).from(0).to(255);
|
||||
let fade_out = Animation::default().duration(Duration::from_secs(1)).from(255).to(0);
|
||||
let mut disp_anim = AnimDisplay(&self.display);
|
||||
info!("Fade in overlay");
|
||||
self.overlay.set_visible(true);
|
||||
Self::animate_duration(Duration::from_secs(2), |pct| {
|
||||
self.overlay.set_opacity((Self::expo_in(pct as f32 / 255.0) * 255.0) as u8);
|
||||
}).await;
|
||||
// When the overlay is fully opaque and all lower layers will be hidden, turn them on
|
||||
self.apply_scene(Scene::Startup).await;
|
||||
Timer::after_secs(1).await;
|
||||
// Then fade out the overlay over 3 seconds, allowing the base animations to be shown
|
||||
Self::animate_duration(Duration::from_secs(1), |pct| {
|
||||
self.overlay.set_opacity((Self::expo_in((255 - pct) as f32 / 255.0) * 255.0) as u8);
|
||||
}).await;
|
||||
join!(
|
||||
fade_in.apply(&mut self.overlay),
|
||||
fade_in.apply(&mut disp_anim)
|
||||
);
|
||||
|
||||
info!("Flipping on surfaces");
|
||||
// Flip on all the layers
|
||||
[
|
||||
&mut *self.panels,
|
||||
&mut *self.tail,
|
||||
&mut *self.background,
|
||||
&mut *self.motion
|
||||
].set_visible(true);
|
||||
|
||||
// Enter the startup scene
|
||||
self.apply_scene(Scene::Ready).await;
|
||||
|
||||
info!("Fade out overlay");
|
||||
fade_out.apply(&mut self.overlay).await;
|
||||
self.overlay.set_visible(false);
|
||||
info!("Fade sequence completed");
|
||||
}
|
||||
|
||||
pub async fn set_headlight_on(&mut self, is_on: bool) {
|
||||
if self.headlight_on != is_on {
|
||||
self.brakelight_on = is_on;
|
||||
if is_on {
|
||||
info!("Turning on headlight");
|
||||
self.headlight.set_opacity(0);
|
||||
self.headlight.set_visible(true);
|
||||
Self::animate_duration(Duration::from_secs(1), |pct| {
|
||||
self.headlight.set_opacity(pct);
|
||||
}).await;
|
||||
self.headlight.set_opacity(255);
|
||||
} else {
|
||||
info!("Turning off headlight");
|
||||
Self::animate_duration(Duration::from_secs(1), |pct| {
|
||||
self.headlight.set_opacity(255 - pct);
|
||||
}).await;
|
||||
self.headlight.set_visible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn set_brakelight_on(&mut self, is_on: bool) {
|
||||
if self.brakelight_on != is_on {
|
||||
self.brakelight_on = is_on;
|
||||
if is_on {
|
||||
info!("Turning on brakelight");
|
||||
self.brakelight.set_opacity(0);
|
||||
self.brakelight.set_visible(true);
|
||||
Self::animate_duration(Duration::from_millis(300), |pct| {
|
||||
self.brakelight.set_opacity(pct);
|
||||
}).await;
|
||||
self.brakelight.set_opacity(255);
|
||||
} else {
|
||||
info!("Turning off brakelight");
|
||||
Self::animate_duration(Duration::from_millis(300), |pct| {
|
||||
self.brakelight.set_opacity(255 - pct);
|
||||
}).await;
|
||||
self.brakelight.set_visible(false);
|
||||
}
|
||||
}
|
||||
info!("Wakeup complete!");
|
||||
}
|
||||
|
||||
// TODO: Brakelight should only be toggled when actually braking or stationary
|
||||
pub async fn apply_scene(&mut self, next_scene: Scene) {
|
||||
info!("Activating scene {next_scene:?}");
|
||||
match next_scene {
|
||||
Scene::Startup => {
|
||||
self.display.on.store(true, core::sync::atomic::Ordering::Relaxed);
|
||||
self.display.brightness.store(255, core::sync::atomic::Ordering::Relaxed);
|
||||
self.background.set_visible(true);
|
||||
self.tail.set_visible(true);
|
||||
self.panels.set_visible(true);
|
||||
Scene::Ready => {
|
||||
let tail = Animation::default().duration(Duration::from_millis(300)).to(96);
|
||||
let panels = Animation::default().duration(Duration::from_millis(300)).to(128);
|
||||
let bg = Animation::default().duration(Duration::from_millis(300)).to(32);
|
||||
let motion = Animation::default().duration(Duration::from_secs(1)).to(0);
|
||||
join!(
|
||||
tail.apply(&mut self.tail),
|
||||
panels.apply(&mut self.panels),
|
||||
bg.apply(&mut self.background),
|
||||
motion.apply(&mut self.motion)
|
||||
);
|
||||
self.background.set_shader(Background::default());
|
||||
},
|
||||
Scene::ParkedIdle => {
|
||||
// Ensure the display is on
|
||||
self.display.on.store(true, core::sync::atomic::Ordering::Relaxed);
|
||||
Self::animate_duration(Duration::from_secs(1), |pct| {
|
||||
self.display.brightness.store(128.scale8(255 - pct), core::sync::atomic::Ordering::Relaxed);
|
||||
}).await;
|
||||
// Hide the content; only notifications will remain
|
||||
self.background.set_visible(true);
|
||||
self.tail.set_visible(false);
|
||||
self.panels.set_visible(false);
|
||||
},
|
||||
Scene::ParkedLongTerm => {
|
||||
// For long-term parking, we turn off the safety lights
|
||||
self.set_headlight_on(false).await;
|
||||
self.set_brakelight_on(false).await;
|
||||
// Then we turn the display off completely
|
||||
Self::animate_duration(Duration::from_secs(1), |pct| {
|
||||
self.display.brightness.store(255 - pct, core::sync::atomic::Ordering::Relaxed);
|
||||
}).await;
|
||||
self.display.on.store(false, core::sync::atomic::Ordering::Relaxed);
|
||||
Scene::Idle => {
|
||||
// FIXME: The safety UI task should handle setting the display brightness to 50% here
|
||||
self.background.set_shader(Thinking::default());
|
||||
let fg_fade = Animation::default().duration(Duration::from_millis(300)).to(0);
|
||||
let bg_fade = Animation::default().duration(Duration::from_millis(300)).to(128);
|
||||
|
||||
// FIXME: The scenes shouldn't be touching the brake/headlights at all here. In fact, they should be dealt with in a whole separate task from the main UI, maybe running on the motion prediction executor
|
||||
join!(
|
||||
fg_fade.apply(&mut self.tail),
|
||||
fg_fade.apply(&mut self.panels),
|
||||
bg_fade.apply(&mut self.background),
|
||||
fg_fade.apply(&mut self.motion)
|
||||
);
|
||||
},
|
||||
Scene::Accelerating => {
|
||||
self.display.on.store(true, core::sync::atomic::Ordering::Relaxed);
|
||||
self.display.brightness.store(255, core::sync::atomic::Ordering::Relaxed);
|
||||
self.tail.set_shader(Thinking::default());
|
||||
self.panels.set_shader(Panel::default());
|
||||
self.motion.set_shader(Movement::default());
|
||||
Animation::default().duration(Duration::from_secs(1)).to(255).apply(&mut self.motion).await;
|
||||
},
|
||||
Scene::Decelerating => {
|
||||
self.display.on.store(true, core::sync::atomic::Ordering::Relaxed);
|
||||
self.display.brightness.store(255, core::sync::atomic::Ordering::Relaxed);
|
||||
self.panels.set_shader(Thinking::default());
|
||||
self.tail.set_shader(Tail::default());
|
||||
}
|
||||
_ => {
|
||||
warn!("Unimplemented scene {next_scene:?}!");
|
||||
self.motion.set_shader(Movement::default().reversed());
|
||||
Animation::default().duration(Duration::from_secs(1)).to(255).apply(&mut self.motion).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn on_event(&mut self, event: Notification) {
|
||||
info!("UI Event: {event:?}");
|
||||
match event {
|
||||
// Apply the scene when it is changed
|
||||
Notification::SceneChange(_) => (), // We already log this inside apply_scene()
|
||||
evt => info!("UI event: {evt:?}")
|
||||
}
|
||||
match event {
|
||||
// TODO: We probably also want some events to indicate when the ESP has no calibration data or otherwise needs re-calibrated and is waiting for the bike to stand still
|
||||
Notification::SensorOnline(SensorSource::IMU) => self.flash_notification_color(Rgb::new(0, 255, 0)).await,
|
||||
Notification::SensorOffline(SensorSource::GPS) => self.flash_notification_color(Rgb::new(255, 0, 0)).await,
|
||||
Notification::SensorOnline(SensorSource::GPS) => self.flash_notification_color(Rgb::new(0, 255, 255)).await,
|
||||
|
||||
// Scene change
|
||||
Notification::SceneChange(scene) => self.apply_scene(scene).await,
|
||||
|
||||
// TODO: We probably also want some events to indicate when the ESP has no calibration data or otherwise needs re-calibrated and is waiting for the bike to stand still
|
||||
Notification::CalibrationComplete => self.flash_notification_color(Rgb::new(0, 255, 0)).await,
|
||||
Notification::GPSLost => self.flash_notification_color(Rgb::new(255, 0, 0)).await,
|
||||
Notification::GPSAcquired => self.flash_notification_color(Rgb::new(0, 255, 255)).await,
|
||||
Notification::WifiConnected => self.flash_notification_color(Rgb::new(0, 0, 255)).await,
|
||||
Notification::WifiDisconnected => self.flash_notification_color(Rgb::new(128, 0, 255)).await,
|
||||
|
||||
// Toggling head and brake lights
|
||||
Notification::SetBrakelight(is_on) => self.set_brakelight_on(is_on).await,
|
||||
Notification::SetHeadlight(is_on) => self.set_headlight_on(is_on).await
|
||||
Notification::SetBrakelight(is_on) => self.brakelight.set_on(is_on).await,
|
||||
Notification::SetHeadlight(is_on) => self.headlight.set_on(is_on).await,
|
||||
|
||||
Notification::Sleep => self.sleep().await,
|
||||
Notification::WakeUp => self.wakeup().await,
|
||||
|
||||
Notification::SensorsOffline => {
|
||||
self.flash_notification_color(Rgb::new(255, 0, 0)).await;
|
||||
self.flash_notification_color(Rgb::new(0, 255, 0)).await;
|
||||
self.flash_notification_color(Rgb::new(0, 0, 255)).await;
|
||||
}
|
||||
_ => ()
|
||||
|
||||
// Other event ideas:
|
||||
// - Bike has crashed, or is laid down
|
||||
@@ -281,23 +252,23 @@ impl<S: Surface<Uniforms = Uniforms, CoordinateSpace = BikeSpace, Pixel = Rgba<u
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature="headless")]
|
||||
pub type UiSurfacePool = NullBufferPool<NullSurface<Uniforms, SegmentSpace, Rgba<u8>>>;
|
||||
#[cfg(not(feature="headless"))]
|
||||
pub type UiSurfacePool = BufferedSurfacePool<Uniforms, SegmentSpace, Rgba<u8>>;
|
||||
|
||||
#[embassy_executor::task]
|
||||
pub async fn ui_main(events: DynamicReceiver<'static, Notification>, mut ui: Ui<BufferedSurface<Uniforms, BikeSpace, Rgba<u8>>>) {
|
||||
pub async fn ui_main(events: DynamicReceiver<'static, Notification>, mut ui: Ui<<UiSurfacePool as Surfaces<SegmentSpace>>::Surface>) {
|
||||
// Wait for the renderer to start running
|
||||
ui.display.render_is_running.wait().await;
|
||||
//ui.display.render_is_running.wait().await;
|
||||
|
||||
// Run the fade sequence
|
||||
ui.startup_fade_sequence().await;
|
||||
// Run the wake sequence, and turn on the lights
|
||||
ui.wakeup().await;
|
||||
// FIXME: Moving the safety lights into another task will let us turn them both on in parallel with the wakeup sequence
|
||||
ui.headlight.set_on(true).await;
|
||||
ui.brakelight.set_on(true).await;
|
||||
|
||||
// Briefly turn on the brakelight while the headlight turns on
|
||||
ui.set_brakelight_on(true).await;
|
||||
// Turn on the headlight when we are finished
|
||||
ui.set_headlight_on(true).await;
|
||||
// Turn off the brakelight
|
||||
ui.set_brakelight_on(false).await;
|
||||
|
||||
// Flash white to indicate we are ready
|
||||
ui.flash_notification_color(Rgb::new(255, 255, 255)).await;
|
||||
// Enter the event loop
|
||||
loop {
|
||||
ui.on_event(events.receive().await).await;
|
||||
}
|
||||
|
||||
147
src/tasks/wifi.rs
Normal file
147
src/tasks/wifi.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
use bleps::{ad_structure::{create_advertising_data, AdStructure, BR_EDR_NOT_SUPPORTED, LE_GENERAL_DISCOVERABLE}, attribute_server::{AttributeServer, NotificationData}, gatt, Ble, HciConnector};
|
||||
use embassy_sync::channel::DynamicReceiver;
|
||||
use embassy_time::Timer;
|
||||
use esp_hal::timer::AnyTimer;
|
||||
use esp_wifi::ble::controller::BleConnector;
|
||||
use log::*;
|
||||
|
||||
use crate::events::Notification;
|
||||
|
||||
pub async fn ble_task(notify: DynamicReceiver<'static, Notification>, wifi_init: &esp_wifi::EspWifiController<'_>, bluetooth_device: esp_hal::peripherals::BT<'static>) {
|
||||
info!("Setting up BLE stack");
|
||||
let connector = BleConnector::new(wifi_init, bluetooth_device);
|
||||
let get_millis = || esp_hal::time::Instant::now().duration_since_epoch().as_millis();
|
||||
let hci = HciConnector::new(connector, get_millis);
|
||||
let mut ble = Ble::new(&hci);
|
||||
ble.init().unwrap();
|
||||
ble.cmd_set_le_advertising_parameters().unwrap();
|
||||
ble.cmd_set_le_advertising_data(
|
||||
create_advertising_data(&[
|
||||
AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED),
|
||||
AdStructure::ServiceUuids16(&[Uuid::Uuid16(0x0001)]),
|
||||
AdStructure::CompleteLocalName("Renderbug!")
|
||||
])
|
||||
.unwrap()
|
||||
).unwrap();
|
||||
ble.cmd_set_le_advertise_enable(true).unwrap();
|
||||
|
||||
let mut wf1 = |_offset: usize, data: &[u8]| {
|
||||
info!("Read serial data! {data:?}");
|
||||
};
|
||||
|
||||
// Other useful characteristics:
|
||||
// 0x2A67 - Location and speed
|
||||
// 0x2A00 - Device name
|
||||
// 0x2B90 - Device time
|
||||
// Permitted characteristics:
|
||||
// Acceleration
|
||||
// Force
|
||||
// Length
|
||||
// Linear position
|
||||
// Rotational speed
|
||||
// Temperature
|
||||
// Torque
|
||||
// Useful app that logs data: https://github.com/a2ruan/ArduNetApp?tab=readme-ov-file
|
||||
// Requires service 4fafc201-1fb5-459e-8fcc-c5c9c331914b, characteristic beb5483e-36e1-4688-b7f5-ea07361b26a8
|
||||
let s = &b""[..];
|
||||
gatt!([service {
|
||||
uuid: "6E400001-B5A3-F393-E0A9-E50E24DCCA9E", // Nordic UART
|
||||
characteristics: [
|
||||
characteristic {
|
||||
uuid: "6E400003-B5A3-F393-E0A9-E50E24DCCA9E", // TX from device, everything is sent as notifications
|
||||
notify: true,
|
||||
name: "tx",
|
||||
value: s
|
||||
},
|
||||
characteristic {
|
||||
uuid: "6E400002-B5A3-F393-E0A9-E50E24DCCA9E", // RX from phone
|
||||
write: wf1
|
||||
},
|
||||
]
|
||||
}]);
|
||||
let mut rng = bleps::no_rng::NoRng;
|
||||
let mut srv = AttributeServer::new(&mut ble, &mut gatt_attributes, &mut rng);
|
||||
|
||||
info!("BLE running!");
|
||||
// TODO: Somehow need to recreate the attributeserver after disconnecting?
|
||||
loop {
|
||||
let notification = match notify.try_receive() {
|
||||
Err(_) => None,
|
||||
Ok(Notification::Beat) => Some("beat"),
|
||||
//TODO: Should make Telemetry values serde-encodable
|
||||
/*Ok(Telemetry::Measurement(Measurement::IMU { accel, gyro })) => {
|
||||
let json_data = json!({
|
||||
"x": accel.x,
|
||||
"y": accel.y,
|
||||
"z": accel.z,
|
||||
"gx": gyro.x,
|
||||
"gy": gyro.y,
|
||||
"gz": gyro.z
|
||||
});
|
||||
let json_buf = serde_json::to_string(&json_data).unwrap();
|
||||
Some(json_buf)
|
||||
},
|
||||
Ok(Telemetry::Measurement(Measurement::GPS(Some(measurement)))) => {
|
||||
info!("gps telemetry");
|
||||
let json_data = json!({
|
||||
"lat": measurement.x,
|
||||
"lng": measurement.y
|
||||
});
|
||||
let json_buf = serde_json::to_string(&json_data).unwrap();
|
||||
Some(json_buf)
|
||||
},*/
|
||||
_ => None
|
||||
};
|
||||
|
||||
match notification {
|
||||
None => {
|
||||
srv.do_work().unwrap();
|
||||
Timer::after_millis(5).await;
|
||||
},
|
||||
Some(serial_data) => {
|
||||
for chunk in serial_data.as_bytes().chunks(20) {
|
||||
srv.do_work_with_notification(Some(NotificationData::new(tx_handle, chunk))).unwrap();
|
||||
}
|
||||
srv.do_work_with_notification(Some(NotificationData::new(tx_handle, &b"\n"[..]))).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Wifi task needs to know when there is data to upload, so it only connects when needed.
|
||||
#[embassy_executor::task]
|
||||
pub async fn wireless_task(notify: DynamicReceiver<'static, Notification>, timer: AnyTimer<'static>, rng: esp_hal::peripherals::RNG<'static>, _wifi_device: esp_hal::peripherals::WIFI<'static>, bluetooth_device: esp_hal::peripherals::BT<'static>) {
|
||||
let rng = esp_hal::rng::Rng::new(rng);
|
||||
let wifi_init =
|
||||
esp_wifi::init(timer, rng).expect("Failed to initialize WIFI/BLE controller");
|
||||
|
||||
ble_task(notify, &wifi_init, bluetooth_device).await;
|
||||
|
||||
/*
|
||||
loop {
|
||||
let (mut wifi, _interfaces) = esp_wifi::wifi::new(&wifi_init, wifi_device)
|
||||
.expect("Failed to initialize WIFI controller"); }
|
||||
|
||||
loop {
|
||||
//let results = wifi.scan_n_async(16).await.unwrap();
|
||||
wifi.set_configuration(&esp_wifi::wifi::Configuration::Client(
|
||||
ClientConfiguration {
|
||||
ssid: "The Frequency".to_string(),
|
||||
auth_method: esp_wifi::wifi::AuthMethod::WPA2Personal,
|
||||
password: "thepasswordkenneth".to_string(),
|
||||
..Default::default()
|
||||
}
|
||||
)).unwrap();
|
||||
|
||||
if wifi.connect_async().await.is_ok() {
|
||||
info!("Connected to wifi!");
|
||||
while wifi.is_connected().unwrap() {
|
||||
Timer::after_secs(60).await;
|
||||
}
|
||||
info!("Disconnected.");
|
||||
}
|
||||
Timer::after_secs(30).await;
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
Reference in New Issue
Block a user