-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
strengthen abstraction and isolation (#1)
- `ApplicationService` - move the main working logic out of the main.rs loop into an "application service" - app service contains references to the static mutex-es for shared board values - main loop calls the app service inside the main loop - "Ports" - introduce traits for all things the "domain layer" - no hard-coded dependencies inside domain layer to the board - except for app service depending on `cortex_m` for mutex & critical section - structs adapting the ports to the board specifics in "infrastructure layer", one per trait - zero cost - compiled release binary was not increased but actually decreased in size - documentation - added various documenting comments
- Loading branch information
Showing
22 changed files
with
573 additions
and
325 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
use crate::{ | ||
domain::{ | ||
model::{AppMode, Instant, InteractionRequest, StateUpdateResult}, | ||
port::{Display, OutsideMessaging, RunningTimeClock, UserInterface}, | ||
}, | ||
error::{report_error, Error, NoControlsSnafu}, | ||
}; | ||
use core::cell::RefCell; | ||
use cortex_m::interrupt::{free, CriticalSection, Mutex}; | ||
|
||
/// application service to orchestrate the domain logic | ||
pub(crate) struct ApplicationService<'a, TClock, TDisplay, TUserInterface, TSerialBus> | ||
where | ||
TClock: RunningTimeClock + 'a, | ||
TDisplay: Display + 'a, | ||
TUserInterface: UserInterface + 'a, | ||
TSerialBus: OutsideMessaging + 'a, | ||
{ | ||
running_timer: &'a Mutex<RefCell<Option<TClock>>>, | ||
display: &'a Mutex<RefCell<Option<TDisplay>>>, | ||
controls: &'a Mutex<RefCell<Option<TUserInterface>>>, | ||
serial_bus: TSerialBus, | ||
} | ||
|
||
impl<'a, TClock, TDisplay, TUserInterface, TSerialBus> | ||
ApplicationService<'a, TClock, TDisplay, TUserInterface, TSerialBus> | ||
where | ||
TClock: RunningTimeClock + 'a, | ||
TDisplay: Display + 'a, | ||
TUserInterface: UserInterface + 'a, | ||
TSerialBus: OutsideMessaging + 'a, | ||
{ | ||
/// setup a new `ApplicationService` instance | ||
#[inline] | ||
pub(crate) fn new( | ||
running_timer: &'a Mutex<RefCell<Option<TClock>>>, | ||
display: &'a Mutex<RefCell<Option<TDisplay>>>, | ||
controls: &'a Mutex<RefCell<Option<TUserInterface>>>, | ||
serial_bus: TSerialBus, | ||
) -> Self { | ||
Self { | ||
running_timer, | ||
display, | ||
controls, | ||
serial_bus, | ||
} | ||
} | ||
|
||
/// run the next cycle of the main logic loop, returning the new state | ||
pub(crate) fn next_cycle(&mut self, mode: &AppMode) -> AppMode { | ||
let next = self | ||
.calculate_next_state(&mode) | ||
.unwrap_or_else(handle_runtime_error); | ||
self.show_mode(&next); | ||
|
||
next | ||
} | ||
|
||
/// calculate the next state: | ||
/// check what the user requested to do (by clicking on buttons) and | ||
/// let domain layer calculate the next state based on this input | ||
fn calculate_next_state(&mut self, mode: &AppMode) -> Result<AppMode, Error> { | ||
let (request, time) = free(|cs| (self.get_requested_interaction(cs), self.now(cs))); | ||
let StateUpdateResult { mode, message } = | ||
mode.handle_interaction_request(request?, time)?; | ||
|
||
if let Some(message) = message { | ||
self.serial_bus.send_result(message)?; | ||
} | ||
|
||
Ok(mode) | ||
} | ||
|
||
/// convenience method to read the current "running time" from the static timer object | ||
fn now(&self, cs: &CriticalSection) -> Option<Instant> { | ||
self.running_timer | ||
.borrow(cs) | ||
.borrow_mut() | ||
.as_mut() | ||
.map(|timer| timer.now()) | ||
} | ||
|
||
/// convenience wrapper to read the user interaction from the static controls object | ||
fn get_requested_interaction(&self, cs: &CriticalSection) -> Result<InteractionRequest, Error> { | ||
if let Some(controls) = self.controls.borrow(cs).borrow_mut().as_mut() { | ||
Ok(controls.requested_interaction()) | ||
} else { | ||
NoControlsSnafu.fail() | ||
} | ||
} | ||
|
||
/// convenience method to show the correct sprite for current mode on the display | ||
fn show_mode(&self, mode: &AppMode) { | ||
free(|cs| { | ||
let mut display = self.display.borrow(cs).borrow_mut(); | ||
let display = display | ||
.as_mut() | ||
.expect("Display must be set at this point. Need restart"); | ||
|
||
display.show_mode(mode); | ||
}); | ||
} | ||
} | ||
|
||
/// report an error that happened while executing the main loop | ||
/// and switch the AppMode appropriately to indicate it's in a failure state | ||
fn handle_runtime_error(err: Error) -> AppMode { | ||
report_error(err); | ||
AppMode::Error | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
/// defines all domain layer models processing the logic | ||
pub(crate) mod model; | ||
/// defines all dependencies of the domain layer which are implemented outside | ||
pub(crate) mod port; | ||
|
||
mod application_service; | ||
|
||
// only publish the ApplicationService itself from the application_service module, as it was defined here | ||
pub(crate) use application_service::ApplicationService; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
use crate::{ | ||
domain::model::{Instant, InteractionRequest, StateUpdateResult}, | ||
error::{Error, IncoherentTimestampsSnafu, NoTimerSnafu}, | ||
}; | ||
|
||
/// current state of the application logic (the "domain") | ||
#[derive(Debug, Copy, Clone, Default, PartialEq)] | ||
pub(crate) enum AppMode { | ||
/// app currently does nothing except idling | ||
#[default] | ||
Idle, | ||
/// the app has marked when time tracking started, waiting to finish it | ||
Running(Instant), | ||
/// the app ran into a (recoverable) error in the main loop | ||
Error, | ||
} | ||
|
||
impl AppMode { | ||
/// check what interaction the user requested to perform and calculate next state from that | ||
pub(crate) fn handle_interaction_request( | ||
&self, | ||
request: InteractionRequest, | ||
now: Option<Instant>, | ||
) -> Result<StateUpdateResult, Error> { | ||
match request { | ||
InteractionRequest::ToggleMode => { | ||
let Some(timestamp) = now else { | ||
return NoTimerSnafu.fail(); | ||
}; | ||
|
||
self.toggle_mode(timestamp) | ||
} | ||
InteractionRequest::Reset => Ok(StateUpdateResult::new(AppMode::Idle)), | ||
InteractionRequest::None => Ok(StateUpdateResult::new(*self)), | ||
} | ||
} | ||
|
||
/// user hit right button -> toggle between idle & running if possible | ||
/// sending the report over the serial bus if necessary | ||
#[inline(always)] | ||
fn toggle_mode(&self, timestamp: Instant) -> Result<StateUpdateResult, Error> { | ||
match self { | ||
AppMode::Idle => Ok(StateUpdateResult::new(AppMode::Running(timestamp))), | ||
AppMode::Running(start) => self.finish_report(start, timestamp), | ||
AppMode::Error => Ok(StateUpdateResult::new(*self)), | ||
} | ||
} | ||
|
||
/// user ended the timer, calculate duration and send it over the wire | ||
fn finish_report( | ||
&self, | ||
start_timestamp: &Instant, | ||
end_timestamp: Instant, | ||
) -> Result<StateUpdateResult, Error> { | ||
if start_timestamp > &end_timestamp { | ||
return IncoherentTimestampsSnafu { | ||
start: *start_timestamp, | ||
end: end_timestamp, | ||
} | ||
.fail(); | ||
} | ||
|
||
let duration = end_timestamp - *start_timestamp; | ||
Ok(StateUpdateResult::with_message( | ||
AppMode::Idle, | ||
duration.into(), | ||
)) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
/// measure of how long an action took | ||
#[repr(transparent)] | ||
pub(crate) struct Duration(u64); | ||
|
||
impl From<u64> for Duration { | ||
#[inline(always)] | ||
fn from(value: u64) -> Self { | ||
Self(value) | ||
} | ||
} | ||
|
||
impl Into<u64> for Duration { | ||
#[inline(always)] | ||
fn into(self) -> u64 { | ||
self.0 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
use crate::domain::model::Duration; | ||
use core::{ | ||
fmt::{Display, Formatter}, | ||
ops::Sub, | ||
}; | ||
|
||
/// timestamp in controller-local time | ||
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] | ||
#[repr(transparent)] | ||
pub(crate) struct Instant(u64); | ||
|
||
// display the instant | ||
impl Display for Instant { | ||
#[inline(always)] | ||
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { | ||
Display::fmt(&self.0, f) | ||
} | ||
} | ||
|
||
// create the instant from a u64 timer value | ||
impl From<u64> for Instant { | ||
#[inline(always)] | ||
fn from(value: u64) -> Self { | ||
Self(value) | ||
} | ||
} | ||
|
||
// extract the u64 timestamp from the instant | ||
impl Into<u64> for &Instant { | ||
#[inline(always)] | ||
fn into(self) -> u64 { | ||
self.0 | ||
} | ||
} | ||
|
||
// calculate different between 2 instances, creating a `Duration` | ||
impl Sub for Instant { | ||
type Output = Duration; | ||
|
||
#[inline(always)] | ||
fn sub(self, rhs: Self) -> Self::Output { | ||
(self.0 - rhs.0).into() | ||
} | ||
} |
Oops, something went wrong.