From 326c00261d16d8f0fba74c6ec0c59231818b3e40 Mon Sep 17 00:00:00 2001 From: kelko Date: Sat, 12 Oct 2024 10:39:59 +0200 Subject: [PATCH] Feature/domain unit tests (#2) * add unit-tests for domain core - extract domain core into own crate - make `[no_std]` on that crate conditional (not for testing) - add unit tests for all logic-bearing models - restructure app service in keret-controller, so it's not in a module `domain` but `application_service` (including ports / dependencies) * extract app service into own crate - new crate for app service and "port" traits (things the app service depend on) - remove free/Mutex handling from app service to make it testable - rework main to have whole app service in one mutex instead of the 3 individual mutexes * add unit-tests for app service --- Cargo.lock | 89 +++++ Cargo.toml | 6 +- src/keret-controller-appservice/Cargo.toml | 12 + .../src/app_service/mod.rs | 97 ++++++ .../src/app_service/test.rs | 309 ++++++++++++++++++ src/keret-controller-appservice/src/error.rs | 15 + src/keret-controller-appservice/src/lib.rs | 7 + .../src/ports.rs} | 29 +- src/keret-controller-domain/Cargo.toml | 7 + src/keret-controller-domain/src/app_mode.rs | 255 +++++++++++++++ src/keret-controller-domain/src/duration.rs | 55 ++++ src/keret-controller-domain/src/error.rs | 11 + src/keret-controller-domain/src/instant.rs | 109 ++++++ src/keret-controller-domain/src/lib.rs | 24 ++ src/keret-controller-domain/src/results.rs | 110 +++++++ src/keret-controller/Cargo.toml | 4 +- .../src/domain/application_service.rs | 110 ------- src/keret-controller/src/domain/mod.rs | 9 - .../src/domain/model/app_mode.rs | 69 ---- .../src/domain/model/duration.rs | 17 - .../src/domain/model/instant.rs | 44 --- src/keret-controller/src/domain/model/mod.rs | 68 ---- src/keret-controller/src/error.rs | 57 +--- .../src/infrastructure/controls.rs | 3 +- .../src/infrastructure/display/mod.rs | 12 +- .../src/infrastructure/display/sprites.rs | 25 +- .../{serialize.rs => serialize/bus.rs} | 17 +- .../src/infrastructure/serialize/error.rs | 43 +++ .../src/infrastructure/serialize/mod.rs | 5 + .../src/infrastructure/time.rs | 9 +- src/keret-controller/src/main.rs | 94 +++--- 31 files changed, 1264 insertions(+), 457 deletions(-) create mode 100644 src/keret-controller-appservice/Cargo.toml create mode 100644 src/keret-controller-appservice/src/app_service/mod.rs create mode 100644 src/keret-controller-appservice/src/app_service/test.rs create mode 100644 src/keret-controller-appservice/src/error.rs create mode 100644 src/keret-controller-appservice/src/lib.rs rename src/{keret-controller/src/domain/port.rs => keret-controller-appservice/src/ports.rs} (71%) create mode 100644 src/keret-controller-domain/Cargo.toml create mode 100644 src/keret-controller-domain/src/app_mode.rs create mode 100644 src/keret-controller-domain/src/duration.rs create mode 100644 src/keret-controller-domain/src/error.rs create mode 100644 src/keret-controller-domain/src/instant.rs create mode 100644 src/keret-controller-domain/src/lib.rs create mode 100644 src/keret-controller-domain/src/results.rs delete mode 100644 src/keret-controller/src/domain/application_service.rs delete mode 100644 src/keret-controller/src/domain/mod.rs delete mode 100644 src/keret-controller/src/domain/model/app_mode.rs delete mode 100644 src/keret-controller/src/domain/model/duration.rs delete mode 100644 src/keret-controller/src/domain/model/instant.rs delete mode 100644 src/keret-controller/src/domain/model/mod.rs rename src/keret-controller/src/infrastructure/{serialize.rs => serialize/bus.rs} (80%) create mode 100644 src/keret-controller/src/infrastructure/serialize/error.rs create mode 100644 src/keret-controller/src/infrastructure/serialize/mod.rs diff --git a/Cargo.lock b/Cargo.lock index e7807c1..0100a80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -388,6 +388,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + [[package]] name = "embedded-dma" version = "0.2.0" @@ -478,6 +484,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fragile" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" + [[package]] name = "futures-channel" version = "0.3.30" @@ -809,6 +821,8 @@ dependencies = [ "embedded-hal 1.0.0", "embedded-hal-nb", "heapless 0.7.17", + "keret-controller-appservice", + "keret-controller-domain", "keret-controller-transmit", "libm", "lsm303agr", @@ -820,6 +834,23 @@ dependencies = [ "tiny-led-matrix", ] +[[package]] +name = "keret-controller-appservice" +version = "0.1.0" +dependencies = [ + "keret-controller-domain", + "keret-controller-transmit", + "mockall", + "snafu", +] + +[[package]] +name = "keret-controller-domain" +version = "0.1.0" +dependencies = [ + "snafu", +] + [[package]] name = "keret-controller-transmit" version = "0.1.0" @@ -1020,6 +1051,32 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "mockall" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c28b3fb6d753d28c20e826cd46ee611fda1cf3cde03a443a974043247c065a" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "341014e7f530314e9a1fdbc7400b244efea7122662c96bfa248c31da5bfb2020" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.79", +] + [[package]] name = "nb" version = "0.1.3" @@ -1249,6 +1306,32 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "predicates" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" +dependencies = [ + "anstyle", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" + +[[package]] +name = "predicates-tree" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -1887,6 +1970,12 @@ dependencies = [ "futures-core", ] +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + [[package]] name = "thiserror" version = "1.0.64" diff --git a/Cargo.toml b/Cargo.toml index 63a2e5b..6568f2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,9 @@ [workspace] members = [ - "src/keret-controller", "src/keret-adapter", + "src/keret-controller", + "src/keret-controller-appservice", + "src/keret-controller-domain", "src/keret-controller-transmit", "src/keret-service-transmit", "src/keret-service", @@ -14,4 +16,4 @@ lto = true [profile.release.package.keret-controller] codegen-units = 1 -debug = true +#debug = true diff --git a/src/keret-controller-appservice/Cargo.toml b/src/keret-controller-appservice/Cargo.toml new file mode 100644 index 0000000..3cbb5ce --- /dev/null +++ b/src/keret-controller-appservice/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "keret-controller-appservice" +version = "0.1.0" +edition = "2021" + +[dependencies] +snafu = { version = "0.8", default-features = false } +keret-controller-domain = { path = "../keret-controller-domain" } +keret-controller-transmit = { path = "../keret-controller-transmit", default-features = false } + +[dev-dependencies] +mockall = "0.13.0" diff --git a/src/keret-controller-appservice/src/app_service/mod.rs b/src/keret-controller-appservice/src/app_service/mod.rs new file mode 100644 index 0000000..8a9f7af --- /dev/null +++ b/src/keret-controller-appservice/src/app_service/mod.rs @@ -0,0 +1,97 @@ +use crate::{ + error::{DomainErrorOccurredSnafu, SendingMessageToOutsideFailedSnafu}, + ports::{Display, OutsideMessaging, RunningTimeClock, UserInterface}, + Error, +}; +use keret_controller_domain::{AppMode, StateUpdateResult}; +use snafu::ResultExt; + +#[cfg(test)] +mod test; + +/// application service to orchestrate the domain logic +pub struct ApplicationService +where + TClock: RunningTimeClock, + TDisplay: Display, + TUserInterface: UserInterface, + TSerialBus: OutsideMessaging, + TReportFunc: FnMut(&Error) + Send + Sync, +{ + pub running_timer: TClock, + pub display: TDisplay, + pub controls: TUserInterface, + serial_bus: TSerialBus, + report_error: TReportFunc, +} + +impl + ApplicationService +where + TClock: RunningTimeClock, + TDisplay: Display, + TUserInterface: UserInterface, + TSerialBus: OutsideMessaging, + TReportFunc: FnMut(&Error) + Send + Sync, +{ + /// setup a new `ApplicationService` instance + #[inline] + pub fn new( + running_timer: TClock, + display: TDisplay, + controls: TUserInterface, + serial_bus: TSerialBus, + report_error: TReportFunc, + ) -> Self { + Self { + running_timer, + display, + controls, + serial_bus, + report_error, + } + } + + /// run the next cycle of the main logic loop, returning the new state + pub fn next_cycle(&mut self, mode: &AppMode) -> AppMode { + let next = self + .calculate_next_state(mode) + .unwrap_or_else(|e| self.handle_runtime_error(e)); + self.display.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> { + let request = self.controls.requested_interaction(); + let time = self.running_timer.now(); + + let StateUpdateResult { + mode, + result: message, + } = mode + .handle_interaction_request(request, time) + .context(DomainErrorOccurredSnafu)?; + + if let Some(message) = message { + self.serial_bus + .send_result(message) + .context(SendingMessageToOutsideFailedSnafu)?; + } + + Ok(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(&mut self, err: Error) -> AppMode { + (self.report_error)(&err); + AppMode::Error + } +} diff --git a/src/keret-controller-appservice/src/app_service/test.rs b/src/keret-controller-appservice/src/app_service/test.rs new file mode 100644 index 0000000..b1c3282 --- /dev/null +++ b/src/keret-controller-appservice/src/app_service/test.rs @@ -0,0 +1,309 @@ +use crate::ports::{Display, OutsideMessaging, RunningTimeClock, UserInterface}; +use crate::{ApplicationService, Error}; +use keret_controller_domain::{AppMode, Duration, Instant, InteractionRequest, TrackResult}; +use mockall::mock; +use mockall::predicate::*; +use snafu::Snafu; + +const FIRST_TIMESTAMP: u64 = 0xDA7A; +const DURATION: u64 = 10; +const SECOND_TIMESTAMP: u64 = FIRST_TIMESTAMP + DURATION; + +// errors used by the mocks + +#[derive(Debug, Snafu)] +#[snafu(visibility(pub(crate)))] +pub enum TestError { + #[snafu(display("Test Error during Send"))] + ErrorDuringSend, +} + +// create mocks of the ports +mock! { + MyUserInterface {} + + impl UserInterface for MyUserInterface { + fn requested_interaction(&mut self) -> InteractionRequest; + } +} + +mock! { + MyDisplay {} + + impl Display for MyDisplay { + fn show_mode(&mut self, mode: &AppMode); + } +} + +mock! { + MyOutsideMessaging {} + + impl OutsideMessaging for MyOutsideMessaging { + type Error = TestError; + fn send_result(&mut self, result: TrackResult) -> Result<(), TestError>; + } +} + +mock! { + MyClock {} + + impl RunningTimeClock for MyClock { + fn now(&mut self) -> Instant; + } +} + +fn noop_report(_err: &crate::Error) {} + +// tests + +// checks that all ports are called in one go, as this is how "mockall" works +#[test] +fn next_cycle_calls_ports() { + // arrange + let mut clock = MockMyClock::new(); + clock + .expect_now() + .once() + .returning(|| Instant::from(FIRST_TIMESTAMP)); + + let mut ui = MockMyUserInterface::new(); + ui.expect_requested_interaction() + .once() + .returning(|| InteractionRequest::None); + + let mut display = MockMyDisplay::new(); + display + .expect_show_mode() + .once() + .with(eq(AppMode::Idle)) + .return_const(()); + let mut bus = MockMyOutsideMessaging::new(); + bus.expect_send_result().never(); + + let mut service = ApplicationService::new(clock, display, ui, bus, &noop_report); + let mode = AppMode::Idle; + + // act + let _ = service.next_cycle(&mode); + + // assert -> automatically by mockall mocks +} + +#[test] +fn next_cycle_switches_to_running_using_timestamp() { + // arrange + let mut clock = MockMyClock::new(); + clock + .expect_now() + .once() + .returning(|| Instant::from(FIRST_TIMESTAMP)); + + let mut ui = MockMyUserInterface::new(); + ui.expect_requested_interaction() + .once() + .returning(|| InteractionRequest::ToggleMode); + + let mut display = MockMyDisplay::new(); + display + .expect_show_mode() + .once() + .with(eq(AppMode::Running(Instant::from(FIRST_TIMESTAMP)))) + .return_const(()); + let mut bus = MockMyOutsideMessaging::new(); + bus.expect_send_result().never(); + + let mut service = ApplicationService::new(clock, display, ui, bus, &noop_report); + let mode = AppMode::Idle; + + // act + let actual = service.next_cycle(&mode); + + // assert + assert_eq!(actual, AppMode::Running(Instant::from(FIRST_TIMESTAMP))); +} + +#[test] +fn next_cycle_sends_message_if_tracking_finished_and_returns_idle() { + // arrange + let mut clock = MockMyClock::new(); + clock + .expect_now() + .once() + .returning(|| Instant::from(SECOND_TIMESTAMP)); + + let mut ui = MockMyUserInterface::new(); + ui.expect_requested_interaction() + .once() + .returning(|| InteractionRequest::ToggleMode); + + let mut display = MockMyDisplay::new(); + display + .expect_show_mode() + .once() + .with(eq(AppMode::Idle)) + .return_const(()); + let mut bus = MockMyOutsideMessaging::new(); + bus.expect_send_result() + .once() + .with(eq(TrackResult::from(Duration::from(DURATION)))) + .returning(|_| Ok(())); + + let mut service = ApplicationService::new(clock, display, ui, bus, &noop_report); + let mode = AppMode::Running(Instant::from(FIRST_TIMESTAMP)); + + // act + let actual = service.next_cycle(&mode); + + // assert -> + automatically by mockall mocks + assert_eq!(actual, AppMode::Idle); +} + +#[test] +fn next_cycle_reports_error_on_inconsistent_timestamps() { + // arrange + let mut error_was_reported = false; + let mut clock = MockMyClock::new(); + clock + .expect_now() + .once() + .returning(|| Instant::from(FIRST_TIMESTAMP)); + + let mut ui = MockMyUserInterface::new(); + ui.expect_requested_interaction() + .once() + .returning(|| InteractionRequest::ToggleMode); + + let mut display = MockMyDisplay::new(); + display + .expect_show_mode() + .once() + .with(eq(AppMode::Error)) + .return_const(()); + let mut bus = MockMyOutsideMessaging::new(); + bus.expect_send_result().never(); + + let mut service = ApplicationService::new(clock, display, ui, bus, |error| { + error_was_reported = match error { + Error::DomainErrorOccurred { .. } => true, + _ => false, + }; + }); + let mode = AppMode::Running(Instant::from(SECOND_TIMESTAMP)); + + // act + let _ = service.next_cycle(&mode); + + // assert + assert_eq!(error_was_reported, true); +} + +#[test] +fn next_cycle_returns_error_on_inconsistent_timestamps() { + // arrange + let mut clock = MockMyClock::new(); + clock + .expect_now() + .once() + .returning(|| Instant::from(FIRST_TIMESTAMP)); + + let mut ui = MockMyUserInterface::new(); + ui.expect_requested_interaction() + .once() + .returning(|| InteractionRequest::ToggleMode); + + let mut display = MockMyDisplay::new(); + display + .expect_show_mode() + .once() + .with(eq(AppMode::Error)) + .return_const(()); + let mut bus = MockMyOutsideMessaging::new(); + bus.expect_send_result().never(); + + let mut service = ApplicationService::new(clock, display, ui, bus, &noop_report); + let mode = AppMode::Running(Instant::from(SECOND_TIMESTAMP)); + + // act + let actual = service.next_cycle(&mode); + + // assert -> + automatically by mockall mocks + assert_eq!(actual, AppMode::Error); +} + +#[test] +fn next_cycle_returns_error_when_sending_fails() { + // arrange + let mut clock = MockMyClock::new(); + clock + .expect_now() + .once() + .returning(|| Instant::from(SECOND_TIMESTAMP)); + + let mut ui = MockMyUserInterface::new(); + ui.expect_requested_interaction() + .once() + .returning(|| InteractionRequest::ToggleMode); + + let mut display = MockMyDisplay::new(); + display + .expect_show_mode() + .once() + .with(eq(AppMode::Error)) + .return_const(()); + let mut bus = MockMyOutsideMessaging::new(); + bus.expect_send_result() + .once() + .with(eq(TrackResult::from(Duration::from(DURATION)))) + .returning(|_| ErrorDuringSendSnafu.fail()); + + let mut service = ApplicationService::new(clock, display, ui, bus, &noop_report); + let mode = AppMode::Running(Instant::from(FIRST_TIMESTAMP)); + + // act + let actual = service.next_cycle(&mode); + + // assert -> + automatically by mockall mocks + assert_eq!(actual, AppMode::Error); +} + +#[test] +fn next_cycle_reports_error_when_sending_fails() { + // arrange + let mut error_was_reported = false; + let mut clock = MockMyClock::new(); + clock + .expect_now() + .once() + .returning(|| Instant::from(SECOND_TIMESTAMP)); + + let mut ui = MockMyUserInterface::new(); + ui.expect_requested_interaction() + .once() + .returning(|| InteractionRequest::ToggleMode); + + let mut display = MockMyDisplay::new(); + display + .expect_show_mode() + .once() + .with(eq(AppMode::Error)) + .return_const(()); + let mut bus = MockMyOutsideMessaging::new(); + bus.expect_send_result() + .once() + .with(eq(TrackResult::from(Duration::from(DURATION)))) + .returning(|_| ErrorDuringSendSnafu.fail()); + + let mut service = ApplicationService::new(clock, display, ui, bus, |error| { + error_was_reported = match error { + Error::SendingMessageToOutsideFailed { .. } => true, + _ => false, + }; + }); + let mode = AppMode::Running(Instant::from(FIRST_TIMESTAMP)); + + // act + let _ = service.next_cycle(&mode); + + // assert -> + automatically by mockall mocks + assert_eq!(error_was_reported, true); +} diff --git a/src/keret-controller-appservice/src/error.rs b/src/keret-controller-appservice/src/error.rs new file mode 100644 index 0000000..fee9577 --- /dev/null +++ b/src/keret-controller-appservice/src/error.rs @@ -0,0 +1,15 @@ +use snafu::Snafu; + +#[derive(Debug, Snafu)] +#[snafu(visibility(pub(crate)))] +pub enum Error +where + OutsideMessagingError: snafu::Error + 'static, +{ + #[snafu(display("Failed writing data to the serial port"))] + SendingMessageToOutsideFailed { source: OutsideMessagingError }, + #[snafu(display("Domain Error"))] + DomainErrorOccurred { + source: keret_controller_domain::Error, + }, +} diff --git a/src/keret-controller-appservice/src/lib.rs b/src/keret-controller-appservice/src/lib.rs new file mode 100644 index 0000000..5538419 --- /dev/null +++ b/src/keret-controller-appservice/src/lib.rs @@ -0,0 +1,7 @@ +#![cfg_attr(not(test), no_std)] +mod app_service; +mod error; +pub mod ports; + +pub use app_service::ApplicationService; +pub use error::Error; diff --git a/src/keret-controller/src/domain/port.rs b/src/keret-controller-appservice/src/ports.rs similarity index 71% rename from src/keret-controller/src/domain/port.rs rename to src/keret-controller-appservice/src/ports.rs index 8d7b048..97e6995 100644 --- a/src/keret-controller/src/domain/port.rs +++ b/src/keret-controller-appservice/src/ports.rs @@ -1,29 +1,26 @@ -use crate::domain::model::AppMode; -use crate::{ - domain::model::{Instant, InteractionRequest, TrackResult}, - error::Error, -}; +use keret_controller_domain::{AppMode, Instant, InteractionRequest, TrackResult}; -/// Keep track of the running time, producing an ever-increasing, never resetting timestamp -pub(crate) trait RunningTimeClock { - /// return the current timestamp - fn now(&mut self) -> Instant; +/// Show domain-specific content on the display +pub trait Display { + /// display a sprite associated with the given `AppMode` + fn show_mode(&mut self, mode: &AppMode); } /// Send domain-specific messages to the outside -pub(crate) trait OutsideMessaging { +pub trait OutsideMessaging { + type Error: snafu::Error + 'static; /// inform the outside of the time tracking result - fn send_result(&mut self, result: TrackResult) -> Result<(), Error>; + fn send_result(&mut self, result: TrackResult) -> Result<(), Self::Error>; } -/// Show domain-specific content on the display -pub(crate) trait Display { - /// display a sprite associated with the given `AppMode` - fn show_mode(&mut self, mode: &AppMode); +/// Keep track of the running time, producing an ever-increasing, never resetting timestamp +pub trait RunningTimeClock { + /// return the current timestamp + fn now(&mut self) -> Instant; } /// retrieve input from the user -pub(crate) trait UserInterface { +pub trait UserInterface { /// return the requested interface (as calculated from the inputs the user made) fn requested_interaction(&mut self) -> InteractionRequest; } diff --git a/src/keret-controller-domain/Cargo.toml b/src/keret-controller-domain/Cargo.toml new file mode 100644 index 0000000..4dfb3e8 --- /dev/null +++ b/src/keret-controller-domain/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "keret-controller-domain" +version = "0.1.0" +edition = "2021" + +[dependencies] +snafu = { version = "0.8", default-features = false } \ No newline at end of file diff --git a/src/keret-controller-domain/src/app_mode.rs b/src/keret-controller-domain/src/app_mode.rs new file mode 100644 index 0000000..d777398 --- /dev/null +++ b/src/keret-controller-domain/src/app_mode.rs @@ -0,0 +1,255 @@ +use crate::results::StateUpdateResult; +use crate::{error::IncoherentTimestampsSnafu, Error, Instant, InteractionRequest}; + +/// current state of the application logic (the "domain") +#[derive(Debug, Copy, Clone, Default, PartialEq)] +pub 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 fn handle_interaction_request( + &self, + request: InteractionRequest, + timestamp: Instant, + ) -> Result { + match request { + InteractionRequest::ToggleMode => 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 { + 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 { + if start_timestamp > &end_timestamp { + return IncoherentTimestampsSnafu { + start: *start_timestamp, + end: end_timestamp, + } + .fail(); + } + + let duration = end_timestamp - *start_timestamp; + Ok(StateUpdateResult::with_result( + AppMode::Idle, + duration.into(), + )) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{Duration, TrackResult}; + + const SOME_TIMESTAMP: u64 = 0xDA7A_u64; + const DIFFERENCE: u64 = 100; + const BIGGER_TIMESTAMP: u64 = SOME_TIMESTAMP + DIFFERENCE; + + #[test] + fn app_mode_default_is_idle() { + // act + let actual = AppMode::default(); + + // assert + assert_eq!(actual, AppMode::Idle); + } + + #[test] + fn app_mode_of_idle_handle_none_interaction_request_keeps_idle() { + // arrange + let mode = AppMode::Idle; + let interaction_request = InteractionRequest::None; + let timestamp: Instant = SOME_TIMESTAMP.into(); + + // act + let actual = mode.handle_interaction_request(interaction_request, Some(timestamp)); + + // assert + assert_eq!(actual, Ok(StateUpdateResult::new(AppMode::Idle))); + } + + #[test] + fn app_mode_of_idle_handle_reset_interaction_request_keeps_idle() { + // arrange + let mode = AppMode::Idle; + let interaction_request = InteractionRequest::Reset; + let timestamp: Instant = SOME_TIMESTAMP.into(); + + // act + let actual = mode.handle_interaction_request(interaction_request, Some(timestamp)); + + // assert + assert_eq!(actual, Ok(StateUpdateResult::new(AppMode::Idle))); + } + + #[test] + fn app_mode_of_idle_handle_toggle_interaction_request_returns_running() { + // arrange + let mode = AppMode::Idle; + let interaction_request = InteractionRequest::ToggleMode; + let timestamp: Instant = SOME_TIMESTAMP.into(); + + // act + let actual = mode.handle_interaction_request(interaction_request, Some(timestamp)); + + // assert + assert_eq!( + actual, + Ok(StateUpdateResult::new(AppMode::Running( + SOME_TIMESTAMP.into() + ))) + ); + } + + #[test] + fn app_mode_of_running_handle_none_interaction_request_keeps_running() { + // arrange + let mode = AppMode::Running(SOME_TIMESTAMP.into()); + let interaction_request = InteractionRequest::None; + let timestamp: Instant = BIGGER_TIMESTAMP.into(); + + // act + let actual = mode.handle_interaction_request(interaction_request, Some(timestamp)); + + // assert + assert_eq!( + actual, + Ok(StateUpdateResult::new(AppMode::Running( + SOME_TIMESTAMP.into() + ))) + ); + } + + #[test] + fn app_mode_of_running_handle_reset_interaction_request_returns_idle_without_result() { + // arrange + let mode = AppMode::Running(SOME_TIMESTAMP.into()); + let interaction_request = InteractionRequest::Reset; + let timestamp: Instant = BIGGER_TIMESTAMP.into(); + + // act + let actual = mode.handle_interaction_request(interaction_request, Some(timestamp)); + + // assert + assert_eq!(actual, Ok(StateUpdateResult::new(AppMode::Idle))); + } + + #[test] + fn app_mode_of_running_handle_toggle_interaction_request_returns_idle_with_result() { + // arrange + let mode = AppMode::Running(SOME_TIMESTAMP.into()); + let interaction_request = InteractionRequest::ToggleMode; + let timestamp: Instant = BIGGER_TIMESTAMP.into(); + + // act + let actual = mode.handle_interaction_request(interaction_request, Some(timestamp)); + + // assert + assert_eq!( + actual, + Ok(StateUpdateResult::with_result( + AppMode::Idle, + TrackResult::from(Duration::from(DIFFERENCE)) + )) + ); + } + + #[test] + fn app_mode_of_running_handle_toggle_interaction_request_with_smaller_end_returns_error() { + // arrange + let mode = AppMode::Running(BIGGER_TIMESTAMP.into()); + let interaction_request = InteractionRequest::ToggleMode; + let timestamp: Instant = SOME_TIMESTAMP.into(); + + // act + let actual = mode.handle_interaction_request(interaction_request, Some(timestamp)); + + // assert + assert_eq!( + actual, + Err(Error::IncoherentTimestamps { + start: BIGGER_TIMESTAMP.into(), + end: SOME_TIMESTAMP.into() + }) + ); + } + + #[test] + fn app_mode_of_error_handle_none_interaction_request_keeps_error() { + // arrange + let mode = AppMode::Error; + let interaction_request = InteractionRequest::None; + let timestamp: Instant = SOME_TIMESTAMP.into(); + + // act + let actual = mode.handle_interaction_request(interaction_request, Some(timestamp)); + + // assert + assert_eq!(actual, Ok(StateUpdateResult::new(AppMode::Error))); + } + + #[test] + fn app_mode_of_error_handle_toggle_interaction_request_keeps_error() { + // arrange + let mode = AppMode::Error; + let interaction_request = InteractionRequest::ToggleMode; + let timestamp: Instant = SOME_TIMESTAMP.into(); + + // act + let actual = mode.handle_interaction_request(interaction_request, Some(timestamp)); + + // assert + assert_eq!(actual, Ok(StateUpdateResult::new(AppMode::Error))); + } + + #[test] + fn app_mode_of_error_handle_result_interaction_request_returns_idle() { + // arrange + let mode = AppMode::Error; + let interaction_request = InteractionRequest::Reset; + let timestamp: Instant = SOME_TIMESTAMP.into(); + + // act + let actual = mode.handle_interaction_request(interaction_request, Some(timestamp)); + + // assert + assert_eq!(actual, Ok(StateUpdateResult::new(AppMode::Idle))); + } + + #[test] + fn any_app_mode_handle_toggle_interaction_request_without_timestamp_returns_error() { + // arrange + let mode = AppMode::Error; + let interaction_request = InteractionRequest::ToggleMode; + + // act + let actual = mode.handle_interaction_request(interaction_request, None); + + // assert + assert_eq!(actual, Err(Error::NoTimer)); + } +} diff --git a/src/keret-controller-domain/src/duration.rs b/src/keret-controller-domain/src/duration.rs new file mode 100644 index 0000000..693ad80 --- /dev/null +++ b/src/keret-controller-domain/src/duration.rs @@ -0,0 +1,55 @@ +/// measure of how long an action took +#[derive(Debug, PartialEq)] +#[repr(transparent)] +pub struct Duration(u64); + +impl From for Duration { + #[inline(always)] + fn from(value: u64) -> Self { + Self(value) + } +} + +impl From for u64 { + #[inline(always)] + fn from(val: Duration) -> Self { + val.0 + } +} + +#[cfg(test)] +mod test { + use super::*; + + const SOME_SECONDS: u64 = 0xDA7A_u64; + + #[test] + fn duration_from_u64_contains_value() { + // act + let actual = Duration::from(SOME_SECONDS); + + // assert + assert_eq!(actual.0, SOME_SECONDS); + } + + #[test] + fn u64_into_duration_contains_value() { + // act + let actual: Duration = SOME_SECONDS.into(); + + // assert + assert_eq!(actual.0, SOME_SECONDS); + } + + #[test] + fn duration_into_u64_returns_value() { + // arrange + let duration = Duration(SOME_SECONDS); + + // act + let actual: u64 = duration.into(); + + // assert + assert_eq!(actual, SOME_SECONDS); + } +} diff --git a/src/keret-controller-domain/src/error.rs b/src/keret-controller-domain/src/error.rs new file mode 100644 index 0000000..8426bc7 --- /dev/null +++ b/src/keret-controller-domain/src/error.rs @@ -0,0 +1,11 @@ +use crate::Instant; +use snafu::Snafu; + +#[derive(Debug, Snafu, PartialEq)] +#[snafu(visibility(pub(crate)))] +pub enum Error { + #[snafu(display("Incoherent timestamps. Started at {start} & ended at {end}"))] + IncoherentTimestamps { start: Instant, end: Instant }, + #[snafu(display("No timer initialized to read the time from"))] + NoTimer, +} diff --git a/src/keret-controller-domain/src/instant.rs b/src/keret-controller-domain/src/instant.rs new file mode 100644 index 0000000..caa2796 --- /dev/null +++ b/src/keret-controller-domain/src/instant.rs @@ -0,0 +1,109 @@ +use crate::Duration; +use core::{ + fmt::{Display, Formatter}, + ops::Sub, +}; + +/// timestamp in controller-local time +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] +#[repr(transparent)] +pub 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 for Instant { + #[inline(always)] + fn from(value: u64) -> Self { + Self(value) + } +} + +// extract the u64 timestamp from the instant +impl From<&Instant> for u64 { + #[inline(always)] + fn from(val: &Instant) -> Self { + val.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() + } +} + +#[cfg(test)] +mod test { + use super::*; + + const SOME_TIMESTAMP: u64 = 0xDA7A_u64; + const DIFFERENCE: u64 = 1; + const BIGGER_TIMESTAMP: u64 = SOME_TIMESTAMP + DIFFERENCE; + + #[test] + fn instant_from_u64_contains_value() { + // act + let actual = Instant::from(SOME_TIMESTAMP); + + // assert + assert_eq!(actual.0, SOME_TIMESTAMP); + } + + #[test] + fn u64_into_instant_contains_value() { + // act + let actual: Instant = SOME_TIMESTAMP.into(); + + // assert + assert_eq!(actual.0, SOME_TIMESTAMP); + } + + #[test] + fn instant_into_u64_returns_value() { + // arrange + let instant = Instant(SOME_TIMESTAMP); + + // act + let actual: u64 = (&instant).into(); + + // assert + assert_eq!(actual, SOME_TIMESTAMP); + } + + #[test] + fn instant_sub_instant_returns_duration_with_difference() { + // arrange + let from = Instant(SOME_TIMESTAMP); + let to = Instant(BIGGER_TIMESTAMP); + + // act + let actual = to - from; + + // assert + assert_eq!(actual, Duration::from(DIFFERENCE)); + } + + #[test] + #[should_panic] + fn instant_sub_bigger_instant_panics() { + // arrange + let from = Instant(SOME_TIMESTAMP); + let to = Instant(BIGGER_TIMESTAMP); + + // act + let _actual = from - to; + + // assert -> checked by `should_panic` + } +} diff --git a/src/keret-controller-domain/src/lib.rs b/src/keret-controller-domain/src/lib.rs new file mode 100644 index 0000000..856152d --- /dev/null +++ b/src/keret-controller-domain/src/lib.rs @@ -0,0 +1,24 @@ +#![cfg_attr(not(test), no_std)] +mod app_mode; +mod duration; +mod error; +mod instant; +mod results; + +// re-export everything relevant from the submodules as if it was directly coded here +// hides internal structure of the module +pub use app_mode::AppMode; +pub use duration::Duration; +pub use error::Error; +pub use instant::Instant; +pub use results::{StateUpdateResult, TrackResult}; + +/// enum to indicate the users desired interaction +/// which is calculated by which button was pressed +#[derive(Debug, Copy, Clone, Default)] +pub enum InteractionRequest { + #[default] + None, + ToggleMode, + Reset, +} diff --git a/src/keret-controller-domain/src/results.rs b/src/keret-controller-domain/src/results.rs new file mode 100644 index 0000000..90dcbba --- /dev/null +++ b/src/keret-controller-domain/src/results.rs @@ -0,0 +1,110 @@ +use crate::{AppMode, Duration}; + +/// the result of a time tracking action +#[repr(transparent)] +#[derive(Debug, PartialEq)] +pub struct TrackResult(pub Duration); + +// create a time tracking result using the given duration +impl From for TrackResult { + #[inline] + fn from(value: Duration) -> Self { + Self(value) + } +} + +// extract the duration as u64 +impl From for u64 { + #[inline] + fn from(val: TrackResult) -> Self { + val.0.into() + } +} + +/// result of calculating the next state +#[derive(Debug, PartialEq)] +pub struct StateUpdateResult { + /// the mode the controller is next + pub mode: AppMode, + + /// a result to send (if necessary) + pub result: Option, +} + +impl StateUpdateResult { + /// create a new updated state, without a message + #[inline] + pub(crate) fn new(mode: AppMode) -> Self { + Self { mode, result: None } + } + + /// create a new updated state and also include a message to be sent + #[inline] + pub(crate) fn with_result(mode: AppMode, message: TrackResult) -> Self { + Self { + mode, + result: Some(message), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + const SOME_DURATION: u64 = 0xDA7A_u64; + + #[test] + fn track_result_from_duration_contains_value() { + // arrange + let duration = Duration::from(SOME_DURATION); + + // act + let actual = TrackResult::from(duration); + + // assert + assert_eq!(actual.0, Duration::from(SOME_DURATION)); + } + + #[test] + fn track_result_into_u64_returns_value() { + // arrange + let result = TrackResult::from(Duration::from(SOME_DURATION)); + + // act + let actual: u64 = result.into(); + + // assert + assert_eq!(actual, SOME_DURATION); + } + + #[test] + fn new_state_update_result_contains_no_result() { + // arrange + let mode = AppMode::Idle; + + // act + let actual = StateUpdateResult::new(mode); + + // assert + assert_eq!(actual.mode, mode); + assert_eq!(actual.result, None); + } + + #[test] + fn state_update_result_with_message_contains_result() { + // arrange + let mode = AppMode::Idle; + let result = TrackResult::from(Duration::from(SOME_DURATION)); + + // act + let actual = StateUpdateResult::with_result(mode, result); + + // assert + assert_eq!(actual.mode, mode); + assert_eq!( + actual.result, + Some(TrackResult::from(Duration::from(SOME_DURATION))) + ); + } +} diff --git a/src/keret-controller/Cargo.toml b/src/keret-controller/Cargo.toml index 0444f48..3097c6f 100644 --- a/src/keret-controller/Cargo.toml +++ b/src/keret-controller/Cargo.toml @@ -17,4 +17,6 @@ tiny-led-matrix = "1.0.2" embedded-hal = "1.0.0" embedded-hal-nb = "1.0.0" snafu = { version = "0.8", default-features = false } -keret-controller-transmit = { path = "../keret-controller-transmit", default-features = false } \ No newline at end of file +keret-controller-appservice = { path = "../keret-controller-appservice" } +keret-controller-domain = { path = "../keret-controller-domain" } +keret-controller-transmit = { path = "../keret-controller-transmit", default-features = false } diff --git a/src/keret-controller/src/domain/application_service.rs b/src/keret-controller/src/domain/application_service.rs deleted file mode 100644 index 091784e..0000000 --- a/src/keret-controller/src/domain/application_service.rs +++ /dev/null @@ -1,110 +0,0 @@ -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>>, - display: &'a Mutex>>, - controls: &'a Mutex>>, - 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>>, - display: &'a Mutex>>, - controls: &'a Mutex>>, - 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 { - 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 { - 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 { - 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 -} diff --git a/src/keret-controller/src/domain/mod.rs b/src/keret-controller/src/domain/mod.rs deleted file mode 100644 index ad5d80b..0000000 --- a/src/keret-controller/src/domain/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -/// 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; diff --git a/src/keret-controller/src/domain/model/app_mode.rs b/src/keret-controller/src/domain/model/app_mode.rs deleted file mode 100644 index 7de8b92..0000000 --- a/src/keret-controller/src/domain/model/app_mode.rs +++ /dev/null @@ -1,69 +0,0 @@ -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, - ) -> Result { - 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 { - 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 { - 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(), - )) - } -} diff --git a/src/keret-controller/src/domain/model/duration.rs b/src/keret-controller/src/domain/model/duration.rs deleted file mode 100644 index 7efef48..0000000 --- a/src/keret-controller/src/domain/model/duration.rs +++ /dev/null @@ -1,17 +0,0 @@ -/// measure of how long an action took -#[repr(transparent)] -pub(crate) struct Duration(u64); - -impl From for Duration { - #[inline(always)] - fn from(value: u64) -> Self { - Self(value) - } -} - -impl From for u64 { - #[inline(always)] - fn from(val: Duration) -> Self { - val.0 - } -} diff --git a/src/keret-controller/src/domain/model/instant.rs b/src/keret-controller/src/domain/model/instant.rs deleted file mode 100644 index 644b1ac..0000000 --- a/src/keret-controller/src/domain/model/instant.rs +++ /dev/null @@ -1,44 +0,0 @@ -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 for Instant { - #[inline(always)] - fn from(value: u64) -> Self { - Self(value) - } -} - -// extract the u64 timestamp from the instant -impl From<&Instant> for u64 { - #[inline(always)] - fn from(val: &Instant) -> Self { - val.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() - } -} diff --git a/src/keret-controller/src/domain/model/mod.rs b/src/keret-controller/src/domain/model/mod.rs deleted file mode 100644 index 14e08aa..0000000 --- a/src/keret-controller/src/domain/model/mod.rs +++ /dev/null @@ -1,68 +0,0 @@ -mod app_mode; -mod duration; -mod instant; - -// re-export everything relevant from the submodules as if it was directly coded here -// hides internal structure of the module -pub(crate) use app_mode::AppMode; -pub(crate) use duration::Duration; -pub(crate) use instant::Instant; - -/// enum to indicate the users desired interaction -/// which is calculated by which button was pressed -#[derive(Debug, Copy, Clone, Default)] -pub(crate) enum InteractionRequest { - #[default] - None, - ToggleMode, - Reset, -} - -/// result of calculating the next state -pub(crate) struct StateUpdateResult { - /// the mode the controller is next - pub(crate) mode: AppMode, - - /// a message to send (if necessary) - pub(crate) message: Option, -} - -impl StateUpdateResult { - /// create a new updated state, without a message - #[inline] - fn new(mode: AppMode) -> Self { - Self { - mode, - message: None, - } - } - - /// create a new updated state and also include a message to be sent - #[inline] - fn with_message(mode: AppMode, message: TrackResult) -> Self { - Self { - mode, - message: Some(message), - } - } -} - -/// the result of a time tracking action -#[repr(transparent)] -pub(crate) struct TrackResult(pub Duration); - -// create a time tracking result using the given duration -impl From for TrackResult { - #[inline] - fn from(value: Duration) -> Self { - Self(value) - } -} - -// extract the duration as u64 -impl From for u64 { - #[inline] - fn from(val: TrackResult) -> Self { - val.0.into() - } -} diff --git a/src/keret-controller/src/error.rs b/src/keret-controller/src/error.rs index f51221e..9a91301 100644 --- a/src/keret-controller/src/error.rs +++ b/src/keret-controller/src/error.rs @@ -1,62 +1,19 @@ -use crate::domain::model::Instant; -use core::fmt::{Debug, Display, Formatter}; +use crate::infrastructure::serialize::SerialBusError; use rtt_target::rprintln; -use snafu::{Error as _, Snafu}; - -/// compatibility wrapper until core::error is used everywhere -#[repr(transparent)] -pub struct UarteError(microbit::hal::uarte::Error); - -impl UarteError { - pub(crate) fn new(error: microbit::hal::uarte::Error) -> Self { - Self(error) - } -} - -// Debug trait is used on errors to generate developer targeted information. Required by snafu::Error -impl Debug for UarteError { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - Debug::fmt(&self.0, f) - } -} - -// Display trait is used on errors to generate the error message itself. Required by snafu::Error -impl Display for UarteError { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - Debug::fmt(&self.0, f) - } -} - -// mark UarteError as compatible to snafu::Error trait -impl snafu::Error for UarteError {} +use snafu::Snafu; /// all errors which are generated inside this crate /// as enum, so handling code can easily match over it (if necessary) /// using snafu crate to auto-generate user-facing message which are sent over rtt #[derive(Debug, Snafu)] #[snafu(visibility(pub(crate)))] -pub(crate) enum Error { - #[snafu(display("Failed writing data to the serial port"))] - WritingToSerialPortFailed { - #[snafu(source(from(microbit::hal::uarte::Error, UarteError::new)))] - source: UarteError, - }, - #[snafu(display("Failed to deserialize message"))] - DeserializeMessageFailed { - source: keret_controller_transmit::Error, - }, - #[snafu(display("Incoherent timestamps. Started at {start} & ended at {end}"))] - IncoherentTimestamps { start: Instant, end: Instant }, +pub(crate) enum InitializationError { #[snafu(display("Failed to initialize the clock"))] ClockInitializationFailed, - #[snafu(display("No timer initialized to read the time from"))] - NoTimer, - #[snafu(display("No controls initialized to read requested interaction from"))] - NoControls, } /// send details of a top-level error over the rtt -pub(crate) fn report_error(err: Error) { +pub(crate) fn report_error(err: &dyn snafu::Error) { rprintln!("[ERROR] {}", err); let mut source = err.source(); @@ -69,3 +26,9 @@ pub(crate) fn report_error(err: Error) { source = inner.source(); } } + +// wrapper for app service errors +#[inline] +pub(crate) fn report_domain_error(err: &keret_controller_appservice::Error) { + report_error(err); +} diff --git a/src/keret-controller/src/infrastructure/controls.rs b/src/keret-controller/src/infrastructure/controls.rs index b908001..a129ac7 100644 --- a/src/keret-controller/src/infrastructure/controls.rs +++ b/src/keret-controller/src/infrastructure/controls.rs @@ -1,4 +1,5 @@ -use crate::domain::{model::InteractionRequest, port::UserInterface}; +use keret_controller_appservice::ports::UserInterface; +use keret_controller_domain::InteractionRequest; use microbit::{board::Buttons, hal::gpiote::Gpiote, pac}; /// reading and interpreting the button presses to calculate requested interaction diff --git a/src/keret-controller/src/infrastructure/display/mod.rs b/src/keret-controller/src/infrastructure/display/mod.rs index 966af1f..ef800cc 100644 --- a/src/keret-controller/src/infrastructure/display/mod.rs +++ b/src/keret-controller/src/infrastructure/display/mod.rs @@ -1,3 +1,4 @@ +use keret_controller_domain::AppMode; use microbit::{ display::nonblocking::Display as NonblockDisplay, gpio::DisplayPins, hal::timer::Instance, }; @@ -5,7 +6,7 @@ use tiny_led_matrix::Render; mod sprites; -use crate::domain::model::AppMode; +use crate::infrastructure::display::sprites::{ERROR_SPRITE, IDLE_SPRITE, RUNNING_SPRITE}; pub(crate) use sprites::FATAL_SPRITE; /// convenience abstraction of the BSP display module @@ -34,10 +35,15 @@ impl Display { } } -impl crate::domain::port::Display for Display { +impl keret_controller_appservice::ports::Display for Display { /// display a sprite associated with the given `AppMode` #[inline] fn show_mode(&mut self, app_mode: &AppMode) { - self.inner.show(app_mode); + let sprite = match app_mode { + AppMode::Idle => IDLE_SPRITE, + AppMode::Running(_) => RUNNING_SPRITE, + AppMode::Error => ERROR_SPRITE, + }; + self.inner.show(&sprite); } } diff --git a/src/keret-controller/src/infrastructure/display/sprites.rs b/src/keret-controller/src/infrastructure/display/sprites.rs index 234a478..11ae97a 100644 --- a/src/keret-controller/src/infrastructure/display/sprites.rs +++ b/src/keret-controller/src/infrastructure/display/sprites.rs @@ -1,18 +1,5 @@ -use crate::domain::model::AppMode; use tiny_led_matrix::Render; -/// mapping of an domain AppMode to the sprite that shall be shown on the display. -/// Render is a trait defined by the underlying Display module -impl Render for AppMode { - fn brightness_at(&self, x: usize, y: usize) -> u8 { - match self { - AppMode::Idle => IDLE_SPRITE[y][x], - AppMode::Running(_) => RUNNING_SPRITE[y][x], - AppMode::Error => ERROR_SPRITE[y][x], - } - } -} - /// simple struct to hold a 5x5 byte matrix which can be shown on the Display #[repr(transparent)] pub(crate) struct DisplayMode([[u8; 5]; 5]); @@ -24,31 +11,31 @@ impl Render for DisplayMode { } /// the sprite to show while the app idles ("pause" icon) -const IDLE_SPRITE: [[u8; 5]; 5] = [ +pub(super) const IDLE_SPRITE: DisplayMode = DisplayMode([ [5, 5, 0, 5, 5], [5, 5, 0, 5, 5], [5, 5, 0, 5, 5], [5, 5, 0, 5, 5], [5, 5, 0, 5, 5], -]; +]); /// the sprite to show while the app is running ("play" icon) -const RUNNING_SPRITE: [[u8; 5]; 5] = [ +pub(super) const RUNNING_SPRITE: DisplayMode = DisplayMode([ [0, 5, 0, 0, 0], [0, 5, 5, 0, 0], [0, 5, 5, 5, 0], [0, 5, 5, 0, 0], [0, 5, 0, 0, 0], -]; +]); /// the sprite to show if the app is in an error mode (exclamation mark) -const ERROR_SPRITE: [[u8; 5]; 5] = [ +pub(super) const ERROR_SPRITE: DisplayMode = DisplayMode([ [5, 5, 5, 5, 5], [0, 5, 5, 5, 0], [0, 0, 5, 0, 0], [0, 0, 0, 0, 0], [0, 0, 5, 0, 0], -]; +]); /// the sprite to show if the app ran into a fatal error it can't recover from /// (a large X) diff --git a/src/keret-controller/src/infrastructure/serialize.rs b/src/keret-controller/src/infrastructure/serialize/bus.rs similarity index 80% rename from src/keret-controller/src/infrastructure/serialize.rs rename to src/keret-controller/src/infrastructure/serialize/bus.rs index c0cc6bc..fd61502 100644 --- a/src/keret-controller/src/infrastructure/serialize.rs +++ b/src/keret-controller/src/infrastructure/serialize/bus.rs @@ -1,13 +1,14 @@ -use crate::{ - domain::model::TrackResult, - error::{DeserializeMessageFailedSnafu, Error, WritingToSerialPortFailedSnafu}, +use crate::infrastructure::serialize::error::{ + DeserializeMessageFailedSnafu, SerialBusError, WritingToSerialPortFailedSnafu, }; +use keret_controller_domain::TrackResult; use keret_controller_transmit::ActionReport; +use snafu::ResultExt; + use microbit::{ board::UartPins, hal::uarte::{Baudrate, Instance, Parity, Uarte}, }; -use snafu::ResultExt; /// convenience abstraction of the BSP serial bus #[repr(transparent)] @@ -29,7 +30,7 @@ impl SerialBus { } /// serialize the message and send if over the bus - fn send_report(&mut self, report: ActionReport) -> Result<(), Error> { + fn send_report(&mut self, report: ActionReport) -> Result<(), SerialBusError> { let serialized_message = report.as_message().context(DeserializeMessageFailedSnafu)?; self.serial @@ -46,9 +47,11 @@ impl SerialBus { } } -impl crate::domain::port::OutsideMessaging for SerialBus { +impl keret_controller_appservice::ports::OutsideMessaging for SerialBus { + type Error = SerialBusError; + /// send the duration as message via the serial bus - fn send_result(&mut self, result: TrackResult) -> Result<(), Error> { + fn send_result(&mut self, result: TrackResult) -> Result<(), Self::Error> { let report = ActionReport::new(result.into()); self.send_report(report) } diff --git a/src/keret-controller/src/infrastructure/serialize/error.rs b/src/keret-controller/src/infrastructure/serialize/error.rs new file mode 100644 index 0000000..2cc2763 --- /dev/null +++ b/src/keret-controller/src/infrastructure/serialize/error.rs @@ -0,0 +1,43 @@ +use core::fmt::{Debug, Display, Formatter}; +use snafu::Snafu; + +/// compatibility wrapper until core::error is used everywhere +#[repr(transparent)] +pub(crate) struct UarteError(microbit::hal::uarte::Error); + +impl UarteError { + pub(crate) fn new(error: microbit::hal::uarte::Error) -> Self { + Self(error) + } +} + +// Debug trait is used on errors to generate developer targeted information. Required by snafu::Error +impl Debug for UarteError { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + Debug::fmt(&self.0, f) + } +} + +// Display trait is used on errors to generate the error message itself. Required by snafu::Error +impl Display for UarteError { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + Debug::fmt(&self.0, f) + } +} + +// mark UarteError as compatible to snafu::Error trait +impl snafu::Error for UarteError {} + +#[derive(Debug, Snafu)] +#[snafu(visibility(pub(crate)))] +pub enum SerialBusError { + #[snafu(display("Failed writing data to the serial port"))] + WritingToSerialPortFailed { + #[snafu(source(from(microbit::hal::uarte::Error, UarteError::new)))] + source: UarteError, + }, + #[snafu(display("Failed to deserialize message"))] + DeserializeMessageFailed { + source: keret_controller_transmit::Error, + }, +} diff --git a/src/keret-controller/src/infrastructure/serialize/mod.rs b/src/keret-controller/src/infrastructure/serialize/mod.rs new file mode 100644 index 0000000..f7690d8 --- /dev/null +++ b/src/keret-controller/src/infrastructure/serialize/mod.rs @@ -0,0 +1,5 @@ +mod bus; +mod error; + +pub(crate) use bus::SerialBus; +pub(crate) use error::SerialBusError; diff --git a/src/keret-controller/src/infrastructure/time.rs b/src/keret-controller/src/infrastructure/time.rs index dbcdc61..d863e01 100644 --- a/src/keret-controller/src/infrastructure/time.rs +++ b/src/keret-controller/src/infrastructure/time.rs @@ -1,7 +1,6 @@ -use crate::{ - domain::{model::Instant, port::RunningTimeClock}, - error::{ClockInitializationFailedSnafu, Error}, -}; +use crate::error::{ClockInitializationFailedSnafu, InitializationError}; +use keret_controller_appservice::ports::RunningTimeClock; +use keret_controller_domain::Instant; use microbit::{ hal::rtc::RtcInterrupt, hal::rtc::{Instance, RtcCompareReg}, @@ -23,7 +22,7 @@ pub(crate) struct RunningTimer { impl RunningTimer { /// create a new instance, configuring the RTC and starting the CLOCK in low frequency - pub(crate) fn new(clock: CLOCK, rtc_component: T) -> Result { + pub(crate) fn new(clock: CLOCK, rtc_component: T) -> Result { Clocks::new(clock).start_lfclk(); let Ok(mut rtc) = Rtc::new(rtc_component, 511) else { diff --git a/src/keret-controller/src/main.rs b/src/keret-controller/src/main.rs index 00b9f56..bda14c7 100644 --- a/src/keret-controller/src/main.rs +++ b/src/keret-controller/src/main.rs @@ -5,24 +5,17 @@ // so the "main" method is not indicator for code entry, but below you will find #[entry] // also as there is no OS the Rust std lib can't be used, as it depends on libc/musl/something similar -mod domain; mod error; mod infrastructure; // importing elements (modules, structs, traits, ...) from other modules to be used in this file -/// convenience type alias to make code shorter/more readable -/// meant for those static values which exist once and used from interrupts and inside domain layer -type Singleton = Mutex>>; -type AppService<'a> = - ApplicationService<'a, RunningTimer, Display, InputControls, SerialBus>; - +use crate::error::report_domain_error; +use crate::infrastructure::serialize::SerialBusError; use crate::{ - domain::{model::AppMode, port::Display as _, ApplicationService}, - error::{report_error, Error}, + error::{report_error, InitializationError}, infrastructure::{ controls::InputControls, display::{Display, FATAL_SPRITE}, - serialize::SerialBus, time::RunningTimer, }, }; @@ -32,6 +25,11 @@ use cortex_m::{ prelude::_embedded_hal_blocking_delay_DelayMs, }; use cortex_m_rt::entry; +use infrastructure::serialize::SerialBus; +use keret_controller_appservice::{ + ports::Display as _, ApplicationService, Error as AppServiceError, +}; +use keret_controller_domain::AppMode; use microbit::{ board::Board, hal::{ @@ -43,20 +41,26 @@ use microbit::{ use panic_rtt_target as _; use rtt_target::rtt_init_print; -// the following three variables are static, as they need to be accessed +/// convenience type alias to make code shorter/more readable +/// meant for those static values which exist once and used from interrupts and inside domain layer +type Singleton = Mutex>>; + +/// convenience type alias for the big, complex app service with all types +type AppService<'a> = ApplicationService< + RunningTimer, + Display, + InputControls, + SerialBus, + fn(&AppServiceError), +>; + +// the following variable is static, as it needs to be accessed // by the main running code but also by the interrupts -// as both could happen concurrently they are wrapped in a `Mutex` allowing only one +// as both could happen concurrently it is wrapped in a `Mutex` allowing only one // concurrent access at a time (using Cortex hardware features) // and in a `RefCell` so we can call mutable methods on it (so-called "inner mutability") -/// the primary timer used to calculate the running time -static RUNNING_TIMER: Singleton> = Mutex::new(RefCell::new(None)); - -/// the display to show something on the LED matrix -static DISPLAY: Singleton> = Mutex::new(RefCell::new(None)); - -/// wrapper for input controls handling -static CONTROLS: Singleton = Mutex::new(RefCell::new(None)); +static APP_SERVICE: Singleton = Mutex::new(RefCell::new(None)); /// entry point for the application. Could have any name, `main` used to follow convention from C /// Initializes the controller as well as go into the execution loop. This method should never return @@ -72,17 +76,28 @@ fn main() -> ! { }; let mut mode = AppMode::Idle; - let (mut app_service, mut main_loop_timer) = initialize_app_service(board); + let mut main_loop_timer = initialize_board(board); // main execution loop, should never end loop { - mode = app_service.next_cycle(&mode); + free(|cs| { + let mut app_service = APP_SERVICE.borrow(cs).borrow_mut(); + let Some(app_service) = app_service.as_mut() else { + // if we don't have access on the App Service we don't have access on anything + // thus no way to display or send messages. Just panic and require hard restart + // this situation _should_ never arise, unless something is fatally flawed + panic!("App Service must exist by now. Needs hard restart"); + }; + mode = app_service.next_cycle(&mode); + }); main_loop_timer.delay_ms(500_u32); } } -/// initialize the board, creating all helper objects and put those necessary in the Mutexes -fn initialize_app_service<'a>(board: Board) -> (AppService<'a>, Timer) { +/// initialize the board, creating all helper objects and put the main "app service" in the mutex +/// also initializes the timer used to sleep on the main loop, as the passed in Board object +/// needs to be used in one place only, so everything board "owning" happens here +fn initialize_board(board: Board) -> Timer { let mut display = Display::new(board.TIMER1, board.display_pins); display.show_mode(&AppMode::Idle); @@ -106,21 +121,22 @@ fn initialize_app_service<'a>(board: Board) -> (AppService<'a>, Timer(mut display: Display, err: Error) -> ! { +fn handle_init_error(mut display: Display, err: InitializationError) -> ! { display.show_sprite(&FATAL_SPRITE); - report_error(err); + report_error(&err); loop { // don't let user interact if init failed @@ -137,8 +153,8 @@ fn handle_init_error(mut display: Display, err: Error) -> ! { #[interrupt] fn RTC1() { free(|cs| { - if let Some(timer) = RUNNING_TIMER.borrow(cs).borrow_mut().as_mut() { - timer.tick_timer(); + if let Some(app_service) = APP_SERVICE.borrow(cs).borrow_mut().as_mut() { + app_service.running_timer.tick_timer(); } }) } @@ -147,8 +163,8 @@ fn RTC1() { #[interrupt] fn TIMER1() { free(|cs| { - if let Some(display) = DISPLAY.borrow(cs).borrow_mut().as_mut() { - display.handle_display_event(); + if let Some(app_service) = APP_SERVICE.borrow(cs).borrow_mut().as_mut() { + app_service.display.handle_display_event(); } }) } @@ -157,8 +173,8 @@ fn TIMER1() { #[interrupt] fn GPIOTE() { free(|cs| { - if let Some(controls) = CONTROLS.borrow(cs).borrow_mut().as_mut() { - controls.check_input(); + if let Some(app_service) = APP_SERVICE.borrow(cs).borrow_mut().as_mut() { + app_service.controls.check_input(); } }) }