Skip to content

Commit

Permalink
strengthen abstraction and isolation (#1)
Browse files Browse the repository at this point in the history
- `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
kelko authored Oct 5, 2024
1 parent 56a69d2 commit 1d3782c
Show file tree
Hide file tree
Showing 22 changed files with 573 additions and 325 deletions.
206 changes: 93 additions & 113 deletions Cargo.lock

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions src/keret-adapter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ version = "0.1.0"
edition = "2021"

[dependencies]
snafu = "0.8.4"
clap = { version = "4.5.17", features = ["derive"] }
serialport = "4.5.0"
snafu = "0.8"
clap = { version = "4.5", features = ["derive"] }
serialport = "4.5.1"
keret-controller-transmit = { path = "../keret-controller-transmit" }
keret-service-transmit = { path = "../keret-service-transmit" }
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
Expand Down
4 changes: 2 additions & 2 deletions src/keret-controller-transmit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ version = "0.1.0"
edition = "2021"

[dependencies]
base64 = { version = "0.22.1", default-features = false }
snafu = { version = "0.8.4", default-features = false }
base64 = { version = "0.22", default-features = false }
snafu = { version = "0.8", default-features = false }
serde = { version = "1.0.210", default-features = false, features = ["derive"] }
postcard = { version = "1.0.10" }
heapless = "0.7.17"
Expand Down
6 changes: 3 additions & 3 deletions src/keret-controller/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ edition = "2021"
[dependencies]
microbit-v2 = { version = "0.15.1", features = ["embedded-hal-02"] }
cortex-m = { version = "0.7.7", features = ["critical-section-single-core"] }
cortex-m-rt = "0.7.0"
cortex-m-rt = "0.7.3"
rtt-target = { version = "0.5.0" }
panic-rtt-target = { version = "0.1.3" }
lsm303agr = "1.1.0"
nb = "1.1.0"
libm = "0.2.8"
heapless = "0.8.0"
heapless = "0.7.17"
tiny-led-matrix = "1.0.2"
embedded-hal = "1.0.0"
embedded-hal-nb = "1.0.0"
snafu = { version = "0.8.4", default-features = false }
snafu = { version = "0.8", default-features = false }
keret-controller-transmit = { path = "../keret-controller-transmit", default-features = false }
76 changes: 0 additions & 76 deletions src/keret-controller/src/domain.rs

This file was deleted.

110 changes: 110 additions & 0 deletions src/keret-controller/src/domain/application_service.rs
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
}
9 changes: 9 additions & 0 deletions src/keret-controller/src/domain/mod.rs
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;
69 changes: 69 additions & 0 deletions src/keret-controller/src/domain/model/app_mode.rs
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(),
))
}
}
17 changes: 17 additions & 0 deletions src/keret-controller/src/domain/model/duration.rs
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
}
}
44 changes: 44 additions & 0 deletions src/keret-controller/src/domain/model/instant.rs
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()
}
}
Loading

0 comments on commit 1d3782c

Please sign in to comment.