Compare commits
10 commits
4fb98ee9c5
...
d878b71eec
Author | SHA1 | Date | |
---|---|---|---|
d878b71eec | |||
af62ec315d | |||
06f4ba549b | |||
29e3d21996 | |||
045eada9d3 | |||
ce049b81d2 | |||
5f3f5e63b2 | |||
e7c9f5c5cc | |||
5935541ab2 | |||
e0dd01ad17 |
8
.woodpecker.yml
Normal file
8
.woodpecker.yml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
pipeline:
|
||||||
|
build:
|
||||||
|
image: rust
|
||||||
|
commands:
|
||||||
|
- rustup override set nightly
|
||||||
|
- rustup target add thumbv7m-none-eabi
|
||||||
|
- cargo install flip-link
|
||||||
|
- cargo build --release
|
855
Cargo.lock
generated
855
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
14
Cargo.toml
14
Cargo.toml
|
@ -6,12 +6,12 @@ edition = "2021"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
embassy-util = { version = "0.1.0", git = "https://github.com/embassy-rs/embassy.git", features = ["defmt"] }
|
embassy-sync = { version = "0.2.0", git = "https://github.com/embassy-rs/embassy.git", features = ["defmt"] }
|
||||||
embassy-executor = { version = "0.1.0", git = "https://github.com/embassy-rs/embassy.git", features = ["defmt", "integrated-timers"] }
|
embassy-futures = { version = "0.1.0", git = "https://github.com/embassy-rs/embassy.git", features = ["defmt"] }
|
||||||
embassy-time = { version = "0.1.0", git = "https://github.com/embassy-rs/embassy.git", features = ["defmt", "defmt-timestamp-uptime", "tick-32768hz"] }
|
embassy-executor = { version = "0.2.0", git = "https://github.com/embassy-rs/embassy.git", features = ["arch-cortex-m", "executor-thread", "defmt", "integrated-timers"] }
|
||||||
embassy-stm32 = { version = "0.1.0", git = "https://github.com/embassy-rs/embassy.git", features = ["nightly", "defmt", "stm32f103c8", "unstable-pac", "memory-x", "time-driver-any"] }
|
embassy-time = { version = "0.1.1", git = "https://github.com/embassy-rs/embassy.git", features = ["defmt", "defmt-timestamp-uptime"] }
|
||||||
|
embassy-stm32 = { version = "0.1.0", git = "https://github.com/embassy-rs/embassy.git", features = ["nightly", "defmt", "stm32f103c8", "unstable-pac", "memory-x", "time-driver-any", "unstable-traits"] }
|
||||||
embassy-usb = { version = "0.1.0", git = "https://github.com/embassy-rs/embassy.git", features = ["defmt"] }
|
embassy-usb = { version = "0.1.0", git = "https://github.com/embassy-rs/embassy.git", features = ["defmt"] }
|
||||||
embassy-usb-serial = { version = "0.1.0", git = "https://github.com/embassy-rs/embassy.git", features = ["defmt"] }
|
|
||||||
|
|
||||||
defmt = "0.3.2"
|
defmt = "0.3.2"
|
||||||
defmt-rtt = "0.3.2"
|
defmt-rtt = "0.3.2"
|
||||||
|
@ -19,10 +19,8 @@ panic-probe = { version = "0.3.0"}
|
||||||
|
|
||||||
cortex-m = { version = "0.7.6", features = ["critical-section-single-core"] }
|
cortex-m = { version = "0.7.6", features = ["critical-section-single-core"] }
|
||||||
cortex-m-rt = "0.7.0"
|
cortex-m-rt = "0.7.0"
|
||||||
embedded-hal = "0.2.6"
|
|
||||||
futures = { version = "0.3.17", default-features = false, features = ["async-await"] }
|
|
||||||
heapless = { version = "0.7.5", default-features = false, features = ['ufmt-impl']}
|
heapless = { version = "0.7.5", default-features = false, features = ['ufmt-impl']}
|
||||||
nb = "1.0.0"
|
|
||||||
ufmt = "0.2.0"
|
ufmt = "0.2.0"
|
||||||
|
|
||||||
ssd1306 = "0.7.1"
|
ssd1306 = "0.7.1"
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
use embassy_stm32::i2c;
|
use embassy_stm32::i2c;
|
||||||
use embassy_stm32::peripherals;
|
use embassy_stm32::peripherals;
|
||||||
|
use embassy_stm32::bind_interrupts;
|
||||||
|
use embassy_stm32::dma::NoDma;
|
||||||
use embassy_stm32::time::Hertz;
|
use embassy_stm32::time::Hertz;
|
||||||
use embassy_time::{Duration, Timer};
|
use embassy_time::{Duration, Timer};
|
||||||
use embassy_util::blocking_mutex::raw::ThreadModeRawMutex;
|
use embassy_sync::blocking_mutex::raw::ThreadModeRawMutex;
|
||||||
use embassy_util::channel::mpmc::Receiver;
|
use embassy_sync::channel::Receiver;
|
||||||
use embassy_util::{select, Either};
|
use embassy_futures::select::{select, Either};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
use embedded_graphics::{
|
use embedded_graphics::{
|
||||||
mono_font::{
|
mono_font::{
|
||||||
|
@ -24,6 +28,10 @@ use ssd1306::{prelude::*, I2CDisplayInterface, Ssd1306};
|
||||||
|
|
||||||
use crate::{AzElPair, RotorState};
|
use crate::{AzElPair, RotorState};
|
||||||
|
|
||||||
|
bind_interrupts!(struct Irqs {
|
||||||
|
I2C1_EV => i2c::InterruptHandler<peripherals::I2C1>;
|
||||||
|
});
|
||||||
|
|
||||||
#[embassy_executor::task]
|
#[embassy_executor::task]
|
||||||
pub async fn display_task(
|
pub async fn display_task(
|
||||||
i2c1: peripherals::I2C1,
|
i2c1: peripherals::I2C1,
|
||||||
|
@ -31,7 +39,7 @@ pub async fn display_task(
|
||||||
scl: peripherals::PB7,
|
scl: peripherals::PB7,
|
||||||
cmd_receiver: Receiver<'static, ThreadModeRawMutex, RotorState, 1>,
|
cmd_receiver: Receiver<'static, ThreadModeRawMutex, RotorState, 1>,
|
||||||
) {
|
) {
|
||||||
let i2c = i2c::I2c::new(i2c1, sda, scl, Hertz::hz(100_000), i2c::Config::default());
|
let i2c = i2c::I2c::new(i2c1, sda, scl, Irqs, NoDma, NoDma, Hertz(100_000), Default::default());
|
||||||
|
|
||||||
let interface = I2CDisplayInterface::new(i2c);
|
let interface = I2CDisplayInterface::new(i2c);
|
||||||
|
|
||||||
|
@ -50,9 +58,11 @@ pub async fn display_task(
|
||||||
let mut rotor_state = RotorState {
|
let mut rotor_state = RotorState {
|
||||||
actual_pos: AzElPair { az: 0, el: 0 },
|
actual_pos: AzElPair { az: 0, el: 0 },
|
||||||
setpoint_pos: AzElPair { az: 0, el: 0 },
|
setpoint_pos: AzElPair { az: 0, el: 0 },
|
||||||
stopped: false,
|
stopped: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut activity_indicator = 0;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
display.clear();
|
display.clear();
|
||||||
|
|
||||||
|
@ -72,23 +82,38 @@ pub async fn display_task(
|
||||||
.draw(&mut display)
|
.draw(&mut display)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
Rectangle::new(Point::new(0, 19), Size::new(128, 23))
|
if !rotor_state.stopped {
|
||||||
.into_styled(style_filled)
|
Rectangle::new(Point::new(0, 19), Size::new(128, 23))
|
||||||
.draw(&mut display)
|
.into_styled(style_filled)
|
||||||
.unwrap();
|
.draw(&mut display)
|
||||||
|
.unwrap();
|
||||||
|
};
|
||||||
|
|
||||||
|
let setpoint_style = if !rotor_state.stopped {
|
||||||
|
text_large_inv
|
||||||
|
} else {
|
||||||
|
text_large
|
||||||
|
};
|
||||||
|
|
||||||
tmp.clear();
|
tmp.clear();
|
||||||
uwrite!(tmp, "AZ: {}", rotor_state.setpoint_pos.az).unwrap();
|
uwrite!(tmp, "AZ: {}", rotor_state.setpoint_pos.az).unwrap();
|
||||||
Text::new(&tmp, Point::new(1, 30), text_large_inv)
|
Text::new(&tmp, Point::new(1, 30), setpoint_style)
|
||||||
.draw(&mut display)
|
.draw(&mut display)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
tmp.clear();
|
tmp.clear();
|
||||||
uwrite!(tmp, "EL: {}", rotor_state.setpoint_pos.el).unwrap();
|
uwrite!(tmp, "EL: {}", rotor_state.setpoint_pos.el).unwrap();
|
||||||
Text::new(&tmp, Point::new(64, 30), text_large_inv)
|
Text::new(&tmp, Point::new(64, 30), setpoint_style)
|
||||||
.draw(&mut display)
|
.draw(&mut display)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
if rotor_state.stopped || activity_indicator < 19 {
|
||||||
|
display.set_pixel(127, activity_indicator, true);
|
||||||
|
} else {
|
||||||
|
display.set_pixel(127, activity_indicator, false);
|
||||||
|
}
|
||||||
|
activity_indicator = (activity_indicator + 1) % 32;
|
||||||
|
|
||||||
display.flush().unwrap();
|
display.flush().unwrap();
|
||||||
|
|
||||||
let result = select(
|
let result = select(
|
||||||
|
|
14
src/main.rs
14
src/main.rs
|
@ -2,9 +2,7 @@
|
||||||
#![no_main]
|
#![no_main]
|
||||||
#![feature(type_alias_impl_trait)]
|
#![feature(type_alias_impl_trait)]
|
||||||
|
|
||||||
use core::fmt::Write;
|
use defmt::Format;
|
||||||
|
|
||||||
use defmt::{panic, Format};
|
|
||||||
use defmt_rtt as _;
|
use defmt_rtt as _;
|
||||||
use panic_probe as _;
|
use panic_probe as _;
|
||||||
|
|
||||||
|
@ -13,9 +11,8 @@ use embassy_stm32::gpio::{Level, Output, Speed};
|
||||||
use embassy_stm32::time::Hertz;
|
use embassy_stm32::time::Hertz;
|
||||||
use embassy_stm32::Config;
|
use embassy_stm32::Config;
|
||||||
use embassy_time::{Duration, Timer};
|
use embassy_time::{Duration, Timer};
|
||||||
use embassy_util::blocking_mutex::raw::ThreadModeRawMutex;
|
use embassy_sync::blocking_mutex::raw::ThreadModeRawMutex;
|
||||||
use embassy_util::channel::mpmc::Channel;
|
use embassy_sync::channel::Channel;
|
||||||
use embassy_util::Forever;
|
|
||||||
|
|
||||||
mod display;
|
mod display;
|
||||||
use display::display_task;
|
use display::display_task;
|
||||||
|
@ -74,10 +71,13 @@ async fn main(spawner: Spawner) {
|
||||||
|
|
||||||
spawner
|
spawner
|
||||||
.spawn(movement_task(
|
.spawn(movement_task(
|
||||||
|
p.ADC1,
|
||||||
|
p.PA0,
|
||||||
|
p.PA1,
|
||||||
|
p.PA2,
|
||||||
p.PA3,
|
p.PA3,
|
||||||
p.PA4,
|
p.PA4,
|
||||||
p.PA5,
|
p.PA5,
|
||||||
p.PA6,
|
|
||||||
CMD_CHAN.receiver(),
|
CMD_CHAN.receiver(),
|
||||||
POS_CHAN.sender(),
|
POS_CHAN.sender(),
|
||||||
STATE_CHAN.sender(),
|
STATE_CHAN.sender(),
|
||||||
|
|
152
src/movement.rs
152
src/movement.rs
|
@ -1,91 +1,189 @@
|
||||||
|
use embassy_stm32::adc::Adc;
|
||||||
use embassy_stm32::gpio::{Level, Output, Speed};
|
use embassy_stm32::gpio::{Level, Output, Speed};
|
||||||
use embassy_stm32::peripherals;
|
use embassy_stm32::peripherals;
|
||||||
use embassy_time::{Duration, Instant, Timer};
|
use embassy_time::{with_timeout, Delay, Duration, Timer};
|
||||||
use embassy_util::blocking_mutex::raw::ThreadModeRawMutex;
|
use embassy_sync::blocking_mutex::raw::ThreadModeRawMutex;
|
||||||
use embassy_util::channel::mpmc::{Receiver, Sender};
|
use embassy_sync::channel::{Receiver, Sender};
|
||||||
use embassy_util::{select, Either};
|
use embassy_futures::select::{select, Either};
|
||||||
|
use embassy_futures::join::join;
|
||||||
use futures::future::join;
|
use heapless::Vec;
|
||||||
|
|
||||||
use crate::usb::Gs232Cmd;
|
use crate::usb::Gs232Cmd;
|
||||||
use crate::{AzElPair, RotorState};
|
use crate::{AzElPair, RotorState};
|
||||||
|
|
||||||
|
// ADC reading for azimuth 0°
|
||||||
|
const AZ_MIN_READING: f32 = 0.0;
|
||||||
|
// ADC reading for azimuth 360°
|
||||||
|
const AZ_MAX_READING: f32 = 4096.0;
|
||||||
|
// Range of motion for azimuth (0 to AZ_RANGE)
|
||||||
|
const AZ_RANGE: f32 = 360.0;
|
||||||
|
// Tolerance for the azimuth setpoint
|
||||||
|
const AZ_SLOP : i16 = 1;
|
||||||
|
|
||||||
|
// ADC reading for elevation 0°
|
||||||
|
const EL_MIN_READING: f32 = 0.0;
|
||||||
|
// ADC reading for elevation 360°
|
||||||
|
const EL_MAX_READING: f32 = 4096.0;
|
||||||
|
// Range of motion for elevantion (0 to EL_RANGE)
|
||||||
|
const EL_RANGE: f32 = 180.0;
|
||||||
|
// Tolerance for the elevation setpoint
|
||||||
|
const EL_SLOP : i16 = 1;
|
||||||
|
|
||||||
#[embassy_executor::task]
|
#[embassy_executor::task]
|
||||||
pub async fn movement_task(
|
pub async fn movement_task(
|
||||||
cw_pin: peripherals::PA3,
|
adc1: peripherals::ADC1,
|
||||||
ccw_pin: peripherals::PA4,
|
mut az_pin: peripherals::PA0,
|
||||||
up_pin: peripherals::PA5,
|
mut el_pin: peripherals::PA1,
|
||||||
down_pin: peripherals::PA6,
|
cw_pin: peripherals::PA2,
|
||||||
|
ccw_pin: peripherals::PA3,
|
||||||
|
up_pin: peripherals::PA4,
|
||||||
|
down_pin: peripherals::PA5,
|
||||||
cmd_receiver: Receiver<'static, ThreadModeRawMutex, Gs232Cmd, 1>,
|
cmd_receiver: Receiver<'static, ThreadModeRawMutex, Gs232Cmd, 1>,
|
||||||
pos_sender: Sender<'static, ThreadModeRawMutex, AzElPair, 1>,
|
pos_sender: Sender<'static, ThreadModeRawMutex, AzElPair, 1>,
|
||||||
state_sender: Sender<'static, ThreadModeRawMutex, RotorState, 1>,
|
state_sender: Sender<'static, ThreadModeRawMutex, RotorState, 1>,
|
||||||
) {
|
) {
|
||||||
|
// Initialize the rotor state
|
||||||
let mut rotor_state = RotorState {
|
let mut rotor_state = RotorState {
|
||||||
actual_pos: AzElPair { az: 0, el: 0 },
|
actual_pos: AzElPair { az: 0, el: 0 },
|
||||||
setpoint_pos: AzElPair { az: 0, el: 0 },
|
setpoint_pos: AzElPair { az: 0, el: 0 },
|
||||||
stopped: false,
|
stopped: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Setup output pins for moving the rotor
|
||||||
let mut cw_pin = Output::new(cw_pin, Level::Low, Speed::Low);
|
let mut cw_pin = Output::new(cw_pin, Level::Low, Speed::Low);
|
||||||
let mut ccw_pin = Output::new(ccw_pin, Level::Low, Speed::Low);
|
let mut ccw_pin = Output::new(ccw_pin, Level::Low, Speed::Low);
|
||||||
let mut up_pin = Output::new(up_pin, Level::Low, Speed::Low);
|
let mut up_pin = Output::new(up_pin, Level::Low, Speed::Low);
|
||||||
let mut down_pin = Output::new(down_pin, Level::Low, Speed::Low);
|
let mut down_pin = Output::new(down_pin, Level::Low, Speed::Low);
|
||||||
|
|
||||||
|
// Setup the ADC for reading the rotor positions
|
||||||
|
let mut adc = Adc::new(adc1, &mut Delay);
|
||||||
|
|
||||||
|
// Do an initial ADC reading to initialize the averages
|
||||||
|
let az_reading = adc.read(&mut az_pin) as f32;
|
||||||
|
let el_reading = adc.read(&mut el_pin) as f32;
|
||||||
|
let mut az_average = Average::new(az_reading);
|
||||||
|
let mut el_average = Average::new(el_reading);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
// Wait until either a new command has been received or 100ms have elapsed
|
||||||
match select(
|
match select(
|
||||||
cmd_receiver.recv(),
|
cmd_receiver.recv(),
|
||||||
Timer::after(Duration::from_millis(100)),
|
Timer::after(Duration::from_millis(100)),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
// A new command has been received. This task only cares about MoveTo and Stop.
|
||||||
Either::First(cmd) => match cmd {
|
Either::First(cmd) => match cmd {
|
||||||
|
// Move to command. Update the setpoint pair in the rotor state
|
||||||
Gs232Cmd::MoveTo(pair) => {
|
Gs232Cmd::MoveTo(pair) => {
|
||||||
rotor_state.setpoint_pos = pair;
|
rotor_state.setpoint_pos = pair;
|
||||||
rotor_state.stopped = false;
|
rotor_state.stopped = false;
|
||||||
}
|
}
|
||||||
|
// Stop command. Set the stopped flag.
|
||||||
Gs232Cmd::Stop => {
|
Gs232Cmd::Stop => {
|
||||||
rotor_state.stopped = true;
|
rotor_state.stopped = true;
|
||||||
}
|
}
|
||||||
|
// Everthing elese is an noop.
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Second case of the select statement. Timer has elapsed.
|
||||||
Either::Second(_) => {
|
Either::Second(_) => {
|
||||||
if !rotor_state.stopped && rotor_state.actual_pos.az < rotor_state.setpoint_pos.az {
|
// First read the current rotor position
|
||||||
rotor_state.actual_pos.az += 1;
|
let az_reading = adc.read(&mut az_pin) as f32;
|
||||||
|
let el_reading = adc.read(&mut el_pin) as f32;
|
||||||
|
|
||||||
|
// Apply the averaging filters
|
||||||
|
az_average.add(az_reading);
|
||||||
|
el_average.add(el_reading);
|
||||||
|
|
||||||
|
// Calculate the position in degreee
|
||||||
|
let az_actual = (az_average.average() - AZ_MIN_READING)
|
||||||
|
/ (AZ_MAX_READING - AZ_MIN_READING)
|
||||||
|
* AZ_RANGE;
|
||||||
|
let el_actual = (el_average.average() - EL_MIN_READING)
|
||||||
|
/ (EL_MAX_READING - EL_MIN_READING)
|
||||||
|
* EL_RANGE;
|
||||||
|
|
||||||
|
// Update the rotor state
|
||||||
|
rotor_state.actual_pos.az = az_actual as u16;
|
||||||
|
rotor_state.actual_pos.el = el_actual as u16;
|
||||||
|
|
||||||
|
let delta_az =
|
||||||
|
rotor_state.setpoint_pos.az as i16 - rotor_state.actual_pos.az as i16;
|
||||||
|
let delta_el =
|
||||||
|
rotor_state.setpoint_pos.el as i16 - rotor_state.actual_pos.el as i16;
|
||||||
|
|
||||||
|
if !rotor_state.stopped && delta_az > AZ_SLOP {
|
||||||
|
// Azimuth needs to move clockwise
|
||||||
cw_pin.set_high();
|
cw_pin.set_high();
|
||||||
ccw_pin.set_low();
|
ccw_pin.set_low();
|
||||||
} else if !rotor_state.stopped
|
} else if !rotor_state.stopped && delta_az < -AZ_SLOP {
|
||||||
&& rotor_state.actual_pos.az > rotor_state.setpoint_pos.az
|
// Azimuth needs to move counter clockwise
|
||||||
{
|
|
||||||
rotor_state.actual_pos.az -= 1;
|
|
||||||
cw_pin.set_low();
|
cw_pin.set_low();
|
||||||
ccw_pin.set_high();
|
ccw_pin.set_high();
|
||||||
} else {
|
} else {
|
||||||
|
// Either azimuth is on the setpoint or the rotor has beend stopped.
|
||||||
cw_pin.set_low();
|
cw_pin.set_low();
|
||||||
ccw_pin.set_low();
|
ccw_pin.set_low();
|
||||||
}
|
}
|
||||||
|
|
||||||
if !rotor_state.stopped && rotor_state.actual_pos.el < rotor_state.setpoint_pos.el {
|
if !rotor_state.stopped && delta_el > EL_SLOP {
|
||||||
rotor_state.actual_pos.el += 1;
|
// Elevation needs to move up
|
||||||
up_pin.set_high();
|
up_pin.set_high();
|
||||||
down_pin.set_low();
|
down_pin.set_low();
|
||||||
} else if !rotor_state.stopped
|
} else if !rotor_state.stopped && delta_el < -EL_SLOP {
|
||||||
&& rotor_state.actual_pos.el > rotor_state.setpoint_pos.el
|
// Elevation needs to move down
|
||||||
{
|
|
||||||
rotor_state.actual_pos.el -= 1;
|
|
||||||
up_pin.set_low();
|
up_pin.set_low();
|
||||||
down_pin.set_high();
|
down_pin.set_high();
|
||||||
} else {
|
} else {
|
||||||
|
// Either elevation is on the setpoint or the rotor has beend stopped.
|
||||||
up_pin.set_low();
|
up_pin.set_low();
|
||||||
down_pin.set_low();
|
down_pin.set_low();
|
||||||
}
|
}
|
||||||
|
|
||||||
join(
|
// Send the state to the display task and the position usb.
|
||||||
pos_sender.send(rotor_state.actual_pos),
|
// Use timeouts to prevent blocking if display or usb task are unresponsive.
|
||||||
state_sender.send(rotor_state),
|
let _ = join(
|
||||||
|
with_timeout(
|
||||||
|
Duration::from_millis(100),
|
||||||
|
pos_sender.send(rotor_state.actual_pos),
|
||||||
|
),
|
||||||
|
with_timeout(Duration::from_millis(100), state_sender.send(rotor_state)),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
//state_sender.send(rotor_state).await;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Simple sliding average filter
|
||||||
|
struct Average {
|
||||||
|
pos: usize,
|
||||||
|
data: Vec<f32, 5>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Average {
|
||||||
|
// Create a new filter and prefill the state using an initial value
|
||||||
|
fn new(initial: f32) -> Average {
|
||||||
|
let mut data: Vec<f32, 5> = Vec::new();
|
||||||
|
data.resize(5, initial).unwrap();
|
||||||
|
Average { pos: 0, data }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds a new value to internal state
|
||||||
|
fn add(&mut self, sample: f32) {
|
||||||
|
self.data[self.pos] = sample;
|
||||||
|
self.pos = (self.pos + 1) % self.data.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the average value from the internal state
|
||||||
|
fn average(&self) -> f32 {
|
||||||
|
let mut sum = 0.0;
|
||||||
|
for sample in &self.data {
|
||||||
|
sum += sample;
|
||||||
|
}
|
||||||
|
|
||||||
|
sum / self.data.len() as f32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
89
src/usb.rs
89
src/usb.rs
|
@ -1,21 +1,25 @@
|
||||||
use defmt::Format;
|
use defmt::Format;
|
||||||
|
|
||||||
use embassy_stm32::interrupt;
|
|
||||||
use embassy_stm32::peripherals;
|
use embassy_stm32::peripherals;
|
||||||
|
use embassy_stm32::{bind_interrupts, usb};
|
||||||
use embassy_stm32::usb::Driver;
|
use embassy_stm32::usb::Driver;
|
||||||
use embassy_usb::Builder;
|
use embassy_usb::Builder;
|
||||||
use embassy_usb_serial::{CdcAcmClass, State};
|
use embassy_usb::class::cdc_acm::{CdcAcmClass, State};
|
||||||
use embassy_util::blocking_mutex::raw::ThreadModeRawMutex;
|
use embassy_sync::blocking_mutex::raw::ThreadModeRawMutex;
|
||||||
use embassy_util::channel::mpmc::{Receiver, Sender};
|
use embassy_sync::channel::{Receiver, Sender};
|
||||||
use embassy_util::{select, Either};
|
use embassy_futures::select::{select, Either};
|
||||||
|
use embassy_futures::join::join;
|
||||||
use futures::future::join;
|
|
||||||
|
|
||||||
use heapless::String;
|
use heapless::String;
|
||||||
use ufmt::uwrite;
|
use ufmt::uwrite;
|
||||||
|
|
||||||
use crate::AzElPair;
|
use crate::AzElPair;
|
||||||
|
|
||||||
|
bind_interrupts!(struct Irqs {
|
||||||
|
USB_LP_CAN1_RX0 => usb::InterruptHandler<peripherals::USB>;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
#[embassy_executor::task]
|
#[embassy_executor::task]
|
||||||
pub async fn usb_task(
|
pub async fn usb_task(
|
||||||
usb: peripherals::USB,
|
usb: peripherals::USB,
|
||||||
|
@ -24,12 +28,10 @@ pub async fn usb_task(
|
||||||
cmd_sender: Sender<'static, ThreadModeRawMutex, Gs232Cmd, 1>,
|
cmd_sender: Sender<'static, ThreadModeRawMutex, Gs232Cmd, 1>,
|
||||||
pos_receiver: Receiver<'static, ThreadModeRawMutex, AzElPair, 1>,
|
pos_receiver: Receiver<'static, ThreadModeRawMutex, AzElPair, 1>,
|
||||||
) {
|
) {
|
||||||
let irq = interrupt::take!(USB_LP_CAN1_RX0);
|
let driver = Driver::new(usb, Irqs, dp_pin, dm_pin);
|
||||||
let driver = Driver::new(usb, irq, dp_pin, dm_pin);
|
|
||||||
|
|
||||||
// Create embassy-usb Config
|
// Create embassy-usb Config
|
||||||
let config = embassy_usb::Config::new(0xc0de, 0xcafe);
|
let config = embassy_usb::Config::new(0xc0de, 0xcafe);
|
||||||
//config.max_packet_size_0 = 64;
|
|
||||||
|
|
||||||
// Create embassy-usb DeviceBuilder using the driver and config.
|
// Create embassy-usb DeviceBuilder using the driver and config.
|
||||||
// It needs some buffers for building the descriptors.
|
// It needs some buffers for building the descriptors.
|
||||||
|
@ -38,7 +40,7 @@ pub async fn usb_task(
|
||||||
let mut bos_descriptor = [0; 256];
|
let mut bos_descriptor = [0; 256];
|
||||||
let mut control_buf = [0; 7];
|
let mut control_buf = [0; 7];
|
||||||
|
|
||||||
let mut state = State::new();
|
let mut usb_state = State::new();
|
||||||
|
|
||||||
let mut builder = Builder::new(
|
let mut builder = Builder::new(
|
||||||
driver,
|
driver,
|
||||||
|
@ -46,41 +48,51 @@ pub async fn usb_task(
|
||||||
&mut device_descriptor,
|
&mut device_descriptor,
|
||||||
&mut config_descriptor,
|
&mut config_descriptor,
|
||||||
&mut bos_descriptor,
|
&mut bos_descriptor,
|
||||||
&mut control_buf,
|
&mut control_buf
|
||||||
None,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create classes on the builder.
|
// Create classes on the builder.
|
||||||
let mut class = CdcAcmClass::new(&mut builder, &mut state, 64);
|
let mut class = CdcAcmClass::new(&mut builder, &mut usb_state, 64);
|
||||||
|
|
||||||
// Build the builder.
|
// Build the builder.
|
||||||
let mut usb = builder.build();
|
let mut usb = builder.build();
|
||||||
|
|
||||||
// Do stuff with the class!
|
// Create a future to handle incomming usb packets
|
||||||
let usb_handler_fut = async {
|
let usb_handler_fut = async {
|
||||||
|
// Initialize the current position in case we get a B or C command,
|
||||||
|
// before we get the first the update via pos_receiver
|
||||||
let mut current_pos = AzElPair { az: 0, el: 0 };
|
let mut current_pos = AzElPair { az: 0, el: 0 };
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
// No much used doing anything until we have a usb connection
|
||||||
class.wait_connection().await;
|
class.wait_connection().await;
|
||||||
defmt::info!("USB connected");
|
defmt::info!("USB connected");
|
||||||
|
// Allocate a space for incomming usb data packets
|
||||||
let mut packet = [0; 64];
|
let mut packet = [0; 64];
|
||||||
|
// Allocate a string to act as buffer to pares the packets linewise
|
||||||
let mut buffer: String<64> = String::new();
|
let mut buffer: String<64> = String::new();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let n = match select(class.read_packet(&mut packet), pos_receiver.recv()).await {
|
let n = match select(class.read_packet(&mut packet), pos_receiver.recv()).await {
|
||||||
|
// The read_packet furture returned either usb data or an error.
|
||||||
Either::First(res) => match res {
|
Either::First(res) => match res {
|
||||||
|
// In case of an error break the loop and treat it like an usb disconnect
|
||||||
Ok(n) => n,
|
Ok(n) => n,
|
||||||
|
// In case of an error break the loop and treat it like an usb disconnect
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
defmt::error!("Unable to read packet: {}", err);
|
defmt::error!("Unable to read packet: {}", err);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// The pos_receiver future returned a position update from moment task.
|
||||||
|
// Just update position and restart loop.
|
||||||
Either::Second(pair) => {
|
Either::Second(pair) => {
|
||||||
current_pos = pair;
|
current_pos = pair;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Append the data in the packet buffer to the buffer string
|
||||||
for byte in &packet[..n] {
|
for byte in &packet[..n] {
|
||||||
if buffer.len() == 64 {
|
if buffer.len() == 64 {
|
||||||
buffer.clear();
|
buffer.clear();
|
||||||
|
@ -88,91 +100,115 @@ pub async fn usb_task(
|
||||||
buffer.push(*byte as char).unwrap();
|
buffer.push(*byte as char).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the buffer string contains a '\r'
|
||||||
let line_end = match buffer.rfind('\r') {
|
let line_end = match buffer.rfind('\r') {
|
||||||
|
// Carriage return found, keep the index
|
||||||
Some(n) => n,
|
Some(n) => n,
|
||||||
|
// No carriage return, wait for the next package
|
||||||
_ => continue,
|
_ => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
defmt::info!("Line buffer: {:x}", buffer.as_bytes());
|
// The is a non-zero amount of characters before the carriage return
|
||||||
|
|
||||||
if line_end > 0 {
|
if line_end > 0 {
|
||||||
|
// Try the parse the slice leading up to linend as a GS323 command
|
||||||
let cmd = parse_command(&buffer.as_str()[..line_end]);
|
let cmd = parse_command(&buffer.as_str()[..line_end]);
|
||||||
defmt::info!("Command: {}", cmd);
|
defmt::info!("Command: {}", cmd);
|
||||||
|
|
||||||
|
// Reverse some space for a respose to the command
|
||||||
let mut resp: String<16> = String::new();
|
let mut resp: String<16> = String::new();
|
||||||
match cmd {
|
match cmd {
|
||||||
Gs232Cmd::GetAl => {
|
// Get Azimuth command. Respond with last known azimuth
|
||||||
|
Gs232Cmd::GetAz => {
|
||||||
uwrite!(&mut resp, "AZ={}\r", current_pos.az).unwrap();
|
uwrite!(&mut resp, "AZ={}\r", current_pos.az).unwrap();
|
||||||
}
|
}
|
||||||
Gs232Cmd::GetEz => {
|
// Get Elevation comman. Respond with last known elevation
|
||||||
|
Gs232Cmd::GetEl => {
|
||||||
uwrite!(&mut resp, "EL={}\r", current_pos.el).unwrap();
|
uwrite!(&mut resp, "EL={}\r", current_pos.el).unwrap();
|
||||||
}
|
}
|
||||||
Gs232Cmd::GetAlEz => {
|
// Get Azimuth and Elevation. Respond with last known pair
|
||||||
|
Gs232Cmd::GetAzEl => {
|
||||||
uwrite!(&mut resp, "AZ={} EL={}\r", current_pos.az, current_pos.el)
|
uwrite!(&mut resp, "AZ={} EL={}\r", current_pos.az, current_pos.el)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
// Move to command. Send to movement task. Respond with empty line.
|
||||||
Gs232Cmd::MoveTo(_) => {
|
Gs232Cmd::MoveTo(_) => {
|
||||||
cmd_sender.send(cmd).await;
|
cmd_sender.send(cmd).await;
|
||||||
resp.push_str("\r").unwrap();
|
resp.push_str("\r").unwrap();
|
||||||
}
|
}
|
||||||
|
// Stop command. Send to movement task. Respond with empty line.
|
||||||
Gs232Cmd::Stop => {
|
Gs232Cmd::Stop => {
|
||||||
cmd_sender.send(cmd).await;
|
cmd_sender.send(cmd).await;
|
||||||
resp.push_str("\r").unwrap();
|
resp.push_str("\r").unwrap();
|
||||||
}
|
}
|
||||||
|
// Unknown command or parser error. Complain and do nothing.
|
||||||
_ => {
|
_ => {
|
||||||
defmt::error!("Uknown command: {}", &buffer.as_str()[..line_end]);
|
defmt::error!("Uknown command: {}", &buffer.as_str()[..line_end]);
|
||||||
resp.push_str("Unkown command!\r").unwrap();
|
resp.push_str("Unkown command!\r").unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Write the response back via USB
|
||||||
match class.write_packet(resp.as_bytes()).await {
|
match class.write_packet(resp.as_bytes()).await {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
|
// Error treat like broken usb connection
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
defmt::error!("Unable to write packet: {}", err);
|
defmt::error!("Unable to write packet: {}", err);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
// Drop the processed line from the buffer
|
||||||
buffer = String::from(&buffer.as_str()[line_end + 1..]);
|
buffer = String::from(&buffer.as_str()[line_end + 1..]);
|
||||||
}
|
}
|
||||||
defmt::info!("USB disconnected");
|
defmt::info!("USB disconnected");
|
||||||
|
// USB connection is broken, so better stop the rotor.
|
||||||
|
cmd_sender.send(Gs232Cmd::Stop).await;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Run the ubs and handler future both to completion.
|
||||||
|
// None of the ever completes, but they will still be polled continously.
|
||||||
join(usb.run(), usb_handler_fut).await;
|
join(usb.run(), usb_handler_fut).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enum for the GS232 commands
|
||||||
#[derive(Format, PartialEq)]
|
#[derive(Format, PartialEq)]
|
||||||
pub enum Gs232Cmd {
|
pub enum Gs232Cmd {
|
||||||
Unkown,
|
Unkown,
|
||||||
GetAl,
|
GetAz,
|
||||||
GetEz,
|
GetEl,
|
||||||
GetAlEz,
|
GetAzEl,
|
||||||
MoveTo(AzElPair),
|
MoveTo(AzElPair),
|
||||||
Stop,
|
Stop,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse a GS232 commmand from a string slice
|
||||||
fn parse_command(data: &str) -> Gs232Cmd {
|
fn parse_command(data: &str) -> Gs232Cmd {
|
||||||
match data.chars().nth(0).unwrap() {
|
match data.chars().nth(0).unwrap() {
|
||||||
'B' => {
|
'B' => {
|
||||||
|
// Get Az command. Format 'B\r'
|
||||||
if data.len() == 1 {
|
if data.len() == 1 {
|
||||||
Gs232Cmd::GetAl
|
Gs232Cmd::GetAz
|
||||||
} else {
|
} else {
|
||||||
Gs232Cmd::Unkown
|
Gs232Cmd::Unkown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
'C' => {
|
'C' => {
|
||||||
|
// Get AZ and EL. Format 'C2\r'
|
||||||
if data.len() == 2 && data.chars().nth(1).unwrap() as char == '2' {
|
if data.len() == 2 && data.chars().nth(1).unwrap() as char == '2' {
|
||||||
Gs232Cmd::GetAlEz
|
Gs232Cmd::GetAzEl
|
||||||
|
// Get EL only 'C\r'
|
||||||
} else if data.len() == 1 {
|
} else if data.len() == 1 {
|
||||||
Gs232Cmd::GetEz
|
Gs232Cmd::GetEl
|
||||||
} else {
|
} else {
|
||||||
Gs232Cmd::Unkown
|
Gs232Cmd::Unkown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
'W' => {
|
'W' => {
|
||||||
|
// Set position 'Waaa eee\r' with azimuth aaa and elevation eee.
|
||||||
|
// Fortunately rotcld will prepend zeros, so there will always be 3 digits per number.
|
||||||
if data.len() == 8 {
|
if data.len() == 8 {
|
||||||
if let Ok(az) = data[1..4].parse::<u16>() {
|
if let Ok(az) = data[1..4].parse::<u16>() {
|
||||||
if let Ok(el) = data[5..].parse::<u16>() {
|
if let Ok(el) = data[5..].parse::<u16>() {
|
||||||
|
@ -189,6 +225,7 @@ fn parse_command(data: &str) -> Gs232Cmd {
|
||||||
}
|
}
|
||||||
|
|
||||||
'S' => {
|
'S' => {
|
||||||
|
// Stop command. Format 'S\r'
|
||||||
if data.len() == 1 {
|
if data.len() == 1 {
|
||||||
Gs232Cmd::Stop
|
Gs232Cmd::Stop
|
||||||
} else {
|
} else {
|
||||||
|
|
Loading…
Reference in a new issue