diff --git a/lib/tinyfsm/CMakeLists.txt b/lib/tinyfsm/CMakeLists.txt new file mode 100644 index 00000000..67111692 --- /dev/null +++ b/lib/tinyfsm/CMakeLists.txt @@ -0,0 +1 @@ +idf_component_register(INCLUDE_DIRS "include") diff --git a/lib/tinyfsm/COPYING b/lib/tinyfsm/COPYING new file mode 100644 index 00000000..72015de9 --- /dev/null +++ b/lib/tinyfsm/COPYING @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2012-2022 Axel Burri + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lib/tinyfsm/ChangeLog b/lib/tinyfsm/ChangeLog new file mode 100644 index 00000000..87e35bd2 --- /dev/null +++ b/lib/tinyfsm/ChangeLog @@ -0,0 +1,39 @@ +tinyfsm-0.3.3 + + * Remove size -B option in makefiles (unsupported on mac). + * Add library.json for PlatformIO. + * Tidy elevator example. + +tinyfsm-0.3.2 + + * Add TINYFSM_NOSTDLIB compile option. + * Remove static asserts on transit functions. + * Fix elevator example. + +tinyfsm-0.3.1 + + * Bugfix (workaround) for compiler bug in gcc < 7.0: define template + specialization for FSM_INITIAL_STATE in "namespace tinyfsm" block. + +tinyfsm-0.3.0 + + * Bugfix: set initial state on all state machines before entering + states in FsmList::start(). + * Add Fsm::start() function (identical interface as FsmList). + * Add reset() functionality to Fsm and FsmList class. + * Add MooreMachine and MealyMachine class. + * Add API examples: simple_switch, resetting_switch, multiple_switch, + debugging_switch, mealy_machine, moore_machine. + * Relax access specifiers for Fsm::state() access function. + * Remove debug code. + +tinyfsm-0.2.0 + + * Use Fsm::initialize() for initialization (instead of + Fsm::initial_state). + * Change license to MIT + +tinyfsm-0.1.0 + + * Initial revision + * Note that this release was originally named "v0.10" diff --git a/lib/tinyfsm/README.md b/lib/tinyfsm/README.md new file mode 100644 index 00000000..6eaf5b52 --- /dev/null +++ b/lib/tinyfsm/README.md @@ -0,0 +1,114 @@ +TinyFSM +======= + +TinyFSM is a simple finite state machine library for C++, designed for +optimal performance and low memory footprint. This makes it ideal for +real-time operating systems. The concept is very simple, allowing the +programmer to fully understand what is happening behind the scenes. It +provides a straightforward way of mapping your state machine charts +into source code. + +TinyFSM basically wraps event dispatching into function calls, making +event dispatching equally fast to calling (or even inlining) a +function. Even in the worst case, dispatching leads to nothing more +than a single vtable lookup and function call! + +Key Features: + + - Entry/exit actions + - Event actions + - Transition functions + - Transition conditions + - Event payload (classes) + - Inheritance of states and action functions + +TinyFSM benefits from the C++11 template metaprogramming features like +variadic templates, and does not depend on RTTI, exceptions or any +external library. + + +Official home page: + +Current version: `0.3.3` + + +Installation +------------ + +TinyFSM is a header-only library, no special installation steps are +needed. Just point your compiler to the "include" directory. + + +Documentation +------------- + +You can find the full documentation in the `doc/` directory: + + - [Introduction](/doc/10-Introduction.md) + - [Installation](/doc/20-Installation.md) + - [Concepts](/doc/30-Concepts.md) + - [Usage](/doc/40-Usage.md) + - [API](/doc/50-API.md) + +The docmentation is also available on the [official home +page](https://digint.ch/tinyfsm/doc/introduction.html). + + +### Code examples + + - [Elevator Project]: Documented example, two state machines with + shiny buttons, floor sensors and actors. + - [Simple Switch]: A generic switch with two states (on/off). + - [API examples] + + [Elevator Project]: /examples/elevator/ + [Simple Switch]: /examples/api/simple_switch.cpp + [API Examples]: /examples/api/ + + +Donate +------ + +So TinyFSM has proven useful for you? + +I will definitively continue to develop TinyFSM for free. If you want +to support me with a donation, you are welcome to do so! + +[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=QZQE9HY6QHDHS) + + +Development +----------- + +The source code for TinyFSM is managed using Git: + + git clone https://dev.tty0.ch/tinyfsm.git + +Mirror on GitHub: + + git clone https://github.com/digint/tinyfsm.git + +If you would like to contribute or have found bugs, visit the [TinyFSM +project page on GitHub] and use the [issues tracker] there. + + [TinyFSM project page on GitHub]: http://github.com/digint/tinyfsm + [issues tracker]: http://github.com/digint/tinyfsm/issues + + +Contact +------- + +For questions and suggestions regarding TinyFSM, success or failure +stories, and any other kind of feedback, please feel free to contact +the author (the email address can be found in the sources). + + +License +------- + +TinyFSM is [Open Source] software. It may be used for any purpose, +including commercial purposes, at absolutely no cost. It is +distributed under the terms of the [MIT license]. + + [Open Source]: http://www.opensource.org/docs/definition.html + [MIT license]: http://www.opensource.org/licenses/mit-license.html diff --git a/lib/tinyfsm/doc/10-Introduction.md b/lib/tinyfsm/doc/10-Introduction.md new file mode 100644 index 00000000..b9186f35 --- /dev/null +++ b/lib/tinyfsm/doc/10-Introduction.md @@ -0,0 +1,28 @@ +Introduction +============ + +TinyFSM is a simple finite state machine library for C++, designed for +optimal performance and low memory footprint. This makes it ideal for +real-time operating systems. The concept is very simple, allowing the +programmer to fully understand what is happening behind the scenes. It +provides a straightforward way of mapping your state machine charts +into source code. + +TinyFSM basically wraps event dispatching into function calls, making +event dispatching equally fast to calling (or even inlining) a +function. Even in the worst case, dispatching leads to nothing more +than a single vtable lookup and function call! + +Key Features +------------ + +- Entry/exit actions +- Event actions +- Transition functions +- Transition conditions +- Event payload (classes) +- Inheritance of states and action functions + +TinyFSM benefits from the C++11 template metaprogramming features like +variadic templates, and does not depend on RTTI, exceptions or any +external library. diff --git a/lib/tinyfsm/doc/20-Installation.md b/lib/tinyfsm/doc/20-Installation.md new file mode 100644 index 00000000..78fa3f1a --- /dev/null +++ b/lib/tinyfsm/doc/20-Installation.md @@ -0,0 +1,68 @@ +Installation +============ + +TinyFSM is an header-only library, no special installation steps are +needed. Just point your compiler to the "include" directory, and in +your source files: + + #include + + +Prerequisites +------------- + +TinyFSM requires a compiler supporting the C++11 language standard +("-std=c++11" in gcc). + +TinyFSM does not depend on RTTI, exceptions or any external library. +If you need to compile without standard libraries (e.g. in conjunction +with `-nostdlib` linker option), add `-DTINYFSM_NOSTDLIB` to the +compiler options: this removes all dependencies on the standard +library by disabling some compile-time type checks. + + +Building the Elevator Example +----------------------------- + +Change to the elevator example directory and compile the sources: + + $ cd examples/elevator + $ make + +Our elevator has call buttons on every floor, sensors reporting the +current position, and an alarm button for emergency. These actors can +be triggered via a simple command interface: + + $ ./elevator + Motor: stopped + Motor: stopped + c=Call, f=FloorSensor, a=Alarm, q=Quit ? + +Let's call the elevator to floor 2: + + c=Call, f=FloorSensor, a=Alarm, q=Quit ? c + Floor ? 2 + Motor: moving up + c=Call, f=FloorSensor, a=Alarm, q=Quit ? + +Now the elevator is moving up, and we need to trigger the floor sensor: + + c=Call, f=FloorSensor, a=Alarm, q=Quit ? f + Floor ? 1 + Reached floor 1 + c=Call, f=FloorSensor, a=Alarm, q=Quit ? f + Floor ? 2 + Reached floor 2 + Motor: stopped + c=Call, f=FloorSensor, a=Alarm, q=Quit ? + +Now we simulate a sensor defect: + + c=Call, f=FloorSensor, a=Alarm, q=Quit ? c + Floor ? 1 + Motor: moving down + c=Call, f=FloorSensor, a=Alarm, q=Quit ? f + Floor ? 2 + Floor sensor defect (expected 1, got 2) + *** calling maintenance *** + Motor: stopped diff --git a/lib/tinyfsm/doc/30-Concepts.md b/lib/tinyfsm/doc/30-Concepts.md new file mode 100644 index 00000000..b0853d6b --- /dev/null +++ b/lib/tinyfsm/doc/30-Concepts.md @@ -0,0 +1,38 @@ +Concepts +======== + +Keep it Simple +-------------- + +By design, TinyFSM implements only the very basics needed for +designing state machines. For many people, it is important to know +what a library is doing when making a decision for a specific library. + + +State Definition +---------------- + +States are derived classes from a base FSM state, providing react() +functions for every event, as well as entry() and exit() functions. + + +Event Dispatching +----------------- + +TinyFSM does not hold state/event function tables like most other +state machine processors do. Instead, it keeps a pointer to the +current state (having the type of the state machine base +class). Dispatching an event simply calls the react() function of the +current state, with the event class as argument. This results in a +single vtable lookup and a function call, which is very efficient! + +Event dispatching on an FsmList<> are simply dispatch() calls to all +state machines in the list. + + +Header-Only Library +------------------- + +The TinyFSM library consist entirely of header files containing +templates, and requires no separately-compiled library binaries or +special treatment when linking. diff --git a/lib/tinyfsm/doc/40-Usage.md b/lib/tinyfsm/doc/40-Usage.md new file mode 100644 index 00000000..457146b3 --- /dev/null +++ b/lib/tinyfsm/doc/40-Usage.md @@ -0,0 +1,244 @@ +Usage +===== + +Refer to the [API examples](/examples/api/) provided with the TinyFSM +package for a quick overview. Recommended starting points: + + - [Elevator Project]: Documented example, two state machines with + buttons, floor sensors and actors. + - [Simple Switch]: A generic switch with two states (on/off). + - [Moore Machine] and [Mealy Machine]: Basic, educational examples. + +For an example in an RTOS environment, see the [stm32f103stk-demo] of +the [OpenMPTL] project. Starting points: + + - [screen.hpp](https://github.com/digint/openmptl/tree/master/projects/stm32f103stk-demo/src/screen.hpp) + : TinyFSM declarations. + - [kernel.cpp](https://github.com/digint/openmptl/tree/master/projects/stm32f103stk-demo/src/kernel.cpp) + : Poll input and trigger events. + + [OpenMPTL]: https://digint.ch/openmptl/ + [stm32f103stk-demo]: https://github.com/digint/openmptl/tree/master/projects/stm32f103stk-demo + + +The examples in the documentation below are mainly based on the +[Elevator Project]. + + [Elevator Project]: /examples/elevator/ + [Simple Switch]: /examples/api/simple_switch.cpp + [Moore Machine]: /examples/api/moore_machine.cpp + [Mealy Machine]: /examples/api/mealy_machine.cpp + + +### 1. Declare Events + +Declare events that your state machine will listen to. Events are +classes derived from the tinyfsm::Event class. + +Example: + + struct FloorEvent : tinyfsm::Event + { + int floor; + }; + + struct Call : FloorEvent { }; + struct FloorSensor : FloorEvent { }; + struct Alarm : tinyfsm::Event { }; + +In the example above, we declare three events. Note that events are +regular classes, which are passed as arguments to the react() members +of a state class. In this example, we use a member variable "floor", +which is used to specify the floor number on "Call" and "FloorSensors" +events. + + +### 2. Declare the State Machine Class + +Declare your state machine class. State machines are classes derived +from the tinyfsm::Fsm template class, where T is the type name of the +state machine itself. + +You need to declare the following public members: + + - react() function for each event + - entry() and exit() functions + +Example: + + class Elevator + : public tinyfsm::Fsm + { + public: + /* default reaction for unhandled events */ + void react(tinyfsm::Event const &) { }; + + virtual void react(Call const &); + virtual void react(FloorSensor const &); + void react(Alarm const &); + + virtual void entry(void) { }; /* entry actions in some states */ + void exit(void) { }; /* no exit actions */ + }; + + +Note that you are free to declare the functions non-virtual if you +like. This has implications on the execution speed: In the example +above, the react(Alarm) function is declared non-virtual, as all states +share the same reaction for this event. This makes code execution +faster when dispatching the "Alarm" event, since no vtable lookup is +needed. + + +### 3. Declare the States + +Declare the states of your state machine. States are classes derived +from the state machine class. + +Note that state classes are *implicitly instantiated*. If you want to +reuse states in multiple state machines, you need to declare them as +templates (see `/examples/api/multiple_switch.cpp`). + +Example: + + class Panic + : public Elevator + { + void entry() override; + }; + + class Moving + : public Elevator + { + void react(FloorSensor const &) override; + }; + + class Idle + : public Elevator + { + void entry() override; + void react(Call const & e) override; + }; + + +In this example, we declare three states. Note that the "elevator" +example source code does not declare the states separately, but rather +defines the code directly in the declaration. + + +### 4. Implement Actions and Event Reactions + +In most cases, event reactions consist of one or more of the following +steps: + + - Change some local data + - Send events to other state machines + - Transit to different state + +**Important**: +Make sure that the `transit<>()` function call is the last command +executed within a reaction function! + +**Important**: +Don't use `transit<>()` in entry/exit actions! + +Example: + + void Idle::entry() { + send_event(MotorStop()); + } + + void Idle::react(Call const & e) { + dest_floor = e.floor; + + if(dest_floor == current_floor) + return; + + /* lambda function used for transition action */ + auto action = [] { + if(dest_floor > current_floor) + send_event(MotorUp()); + else if(dest_floor < current_floor) + send_event(MotorDown()); + }; + + transit(action); + }; + + +In this example, we use a lambda function as transition action. The +`transit<>()` function does the following: + + 1. Call the exit() function of the current state + 2. Call the the transition action if provided + 3. Change the current state to the new state + 4. Call the entry() function of the new state + +Note that you can also pass condition functions to the `transit<>()` +function. + + +### 5. Define the Initial State + +Use the macro `FSM_INITIAL_STATE(fsm, state)` for defining the initial +state (or "start state") of your state machine: + +Example: + + FSM_INITIAL_STATE(Elevator, Idle) + +This sets the current state of the "Elevator" state machine to "Idle". +More specifially, it defines a template specialization for +`Fsm::set_initial_state()`, setting the current state to +Idle. + + +### 6. Define Custom Initialization + +If you need to perform custom initialization, you can override the +reset() member function in your state machine class. If you are using +state variables, you can re-instantiate your states by calling +`tinyfsm::StateList::reset()`. + +Example: + + class Switch : public tinyfsm::Fsm + { + public: static void reset(void) { + tinyfsm::StateList::reset(); // reset all states + myvar = 0; + ... + } + ... + } + +Make sure to always set the current state, or you'll end up with a +null pointer dereference. + + +### 7. Use FsmList for Event Dispatching + +You might have noticed some calls to a send_event() function in the +example above. This is NOT a function provided with TinyFSM. Since +event dispatching can be implemented in several ways, TinyFSM leaves +this open to you. The "elevator" example implements the send_event() +function as *direct event dispatching*, without using event +queues. This has the advantage that execution is much faster, since no +RTTI is needed and the decision which function to call for an event +class is made at compile-time. On the other hand, special care has to +be taken when designing the state machines, in order to avoid loops. + +Code from "fsmlist.hpp": + + typedef tinyfsm::FsmList fsm_list; + + template + void send_event(E const & event) + { + fsm_list::template dispatch(event); + } + +Here, send_event() dispatches events to all state machines in the +list. It is important to understand that this approach comes with no +performance penalties at all, as long as the default reaction is +defined empty within the state machine declaration. diff --git a/lib/tinyfsm/doc/50-API.md b/lib/tinyfsm/doc/50-API.md new file mode 100644 index 00000000..44fcf172 --- /dev/null +++ b/lib/tinyfsm/doc/50-API.md @@ -0,0 +1,206 @@ +API Reference +============= + +`#include ` + + +Class Diagram +------------- + ....... + +--------------------------------------: T : + | tinyfsm::FsmList :.....: + +-----------------------------------------| + | [+] set_initial_state() <> | + | [+] reset() <> | + | [+] enter() <> | + | [+] start() <> | + | [+] dispatch(Event) <> | + +-----------------------------------------+ + + + ....... + +--------------------------------------: T : + | tinyfsm::Fsm :.....: + +-----------------------------------------| + | [+] state() <> | + | [+] set_initial_state() <> | + | [+] reset() <> | + | [+] enter() <> | + | [+] start() <> | + | [+] dispatch(Event) <> | + | [#] transit() | + | [#] transit(Action) | + | [#] transit(Action, Condition) | + +-----------------------------------------+ + # + | + | + +---------------------+ + | MyFSM | + +---------------------+ + | [+] entry() | + | [+] exit() | + | [+] react(EventX) | + | [+] react(EventY) | + | ... | + +---------------------+ + # + | + +-------------+-------------+ + | | | + +---------+ +---------+ +---------+ + | State_A | | State_B | | ... | + +---------+ +---------+ +---------+ + + + [#] protected + [+] public + [-] private + + +template< typename F > class Fsm +-------------------------------- + +### State Machine Functions + + * `template< typename S > static constexpr S & state(void)` + + Returns a reference to a (implicitly instantiated) state S. Allows + low-level access to all states; + + + * `static void set_initial_state(void)` + + Function prototype, must be defined (explicit template + specialization) for every state machine class (e.g. by using the + `FSM_INITIAL_STATE(fsm, state`) macro). Sets current state to + initial (start) state. + + + * `static void reset(void)` + + Empty function, can be overridden by state machine class in order to + perform custom initialization (e.g. set static state machine + variables, or reset states using `StateList::reset()`) + or directly via the `state()` instance). + + Note that this function is NOT called on start(). + + See example: `/examples/api/resetting_switch.cpp` + + * `static void enter(void)` + + Helper function, usually not needed to be used directly: + calls entry() function of current state. + + + * `static void start()` + + Sets the initial (start) state and calls its entry() function. + + + * `template< typename E > static void dispatch(E const &)` + + Dispatch an event to the current state of this state machine. + + +### State Transition Functions + + * `template< typename S > void transit(void)` + + Transit to a new state: + + 1. Call exit() function on current state + 2. Set new current state to S + 3. Call entry() function on new state + + + * `template< typename S, typename ActionFunction > void transit(ActionFunction)` + + Transit to a new state, with action function: + + 1. Call exit() function on current state + 2. Call ActionFunction + 3. Set new current state to S + 4. Call entry() function on new state + + + * `template< typename S, typename ActionFunction, typename ConditionFunction > void transit(ActionFunction, ConditionFunction)` + + Transit to a new state only if ConditionFunction returns true. + Shortcut for: `if(ConditionFunction()) transit(ActionFunction);`. + + +### Derived Classes + +#### template< typename F > class MooreMachine + +Moore state machines have entry actions, but no exit actions: + + * `virtual void entry(void) { }` + + Entry action, not enforcing. Can be enforced by declaring pure + virtual: `virtual void entry(void) = 0` + + * `void exit(void) { }` + + No exit actions. + +See example: `/examples/api/more_machine.cpp` + +#### template< typename F > class MealyMachine + +Mealy state machines do not have entry/exit actions: + + * `void entry(void) { }` + + No entry actions. + + * `void exit(void) { }` + + No exit actions. + +*Input actions* are modeled in react(), conditional dependent of event +type or payload and using `transit<>(ActionFunction)`. + +See example: `/examples/api/mealy_machine.cpp` + + +template< typename... FF > struct FsmList +----------------------------------------- + + * `static void set_initial_state(void)` + + Calls set_initial_state() on all state machines in the list. + + + * `static void reset()` + + Calls reset() on all state machines in the list. + + + * `static void enter()` + + Calls enter() on all state machines in the list. + + + * `static void start()` + + Sets the initial (start) state for all state machines in list, then + call all entry() functions. + + + * `template< typename E > static void dispatch(E const &)` + + Dispatch an event to the current state of all the state machines in + the list. + + +template< typename... SS > struct StateList +------------------------------------------- + + * `static void reset(void)` + + Re-instantiate all states in the list, using copy-constructor. + + See example: `/examples/api/resetting_switch.cpp` diff --git a/lib/tinyfsm/doc/60-Development.md b/lib/tinyfsm/doc/60-Development.md new file mode 100644 index 00000000..24ad0e14 --- /dev/null +++ b/lib/tinyfsm/doc/60-Development.md @@ -0,0 +1,26 @@ +Development +=========== + +Source Code Repository +---------------------- + +The source code for TinyFSM is managed using Git: + + git clone https://dev.tty0.ch/tinyfsm.git + +Mirror on GitHub: + + git clone https://github.com/digint/tinyfsm.git + + +How to Contribute +----------------- + +Your contributions are welcome! + +If you would like to contribute or have found bugs, visit the [TinyFSM +project page on GitHub] and use the [issues tracker] there, or contact +the author via email. + + [TinyFSM project page on GitHub]: http://github.com/digint/tinyfsm + [issues tracker]: http://github.com/digint/tinyfsm/issues diff --git a/lib/tinyfsm/doc/70-License.md b/lib/tinyfsm/doc/70-License.md new file mode 100644 index 00000000..6a4da72f --- /dev/null +++ b/lib/tinyfsm/doc/70-License.md @@ -0,0 +1,9 @@ +License +======= + +TinyFSM is [Open Source] software. It may be used for any purpose, +including commercial purposes, at absolutely no cost. It is +distributed under the terms of the [MIT license]. + + [Open Source]: http://www.opensource.org/docs/definition.html + [MIT license]: http://www.opensource.org/licenses/mit-license.html diff --git a/lib/tinyfsm/examples/api/Makefile b/lib/tinyfsm/examples/api/Makefile new file mode 100644 index 00000000..63b110e0 --- /dev/null +++ b/lib/tinyfsm/examples/api/Makefile @@ -0,0 +1,63 @@ +# Compiler prefix, in case your default compiler does not implement all C++11 features: +#CROSS = /opt/toolchain/x86_64-pc-linux-gnu-gcc-4.7.0/bin/x86_64-pc-linux-gnu- + +# HINT: g++ -Q -O2 --help=optimizers +OPTIMIZER = -Os + +CC = $(CROSS)gcc +CXX = $(CROSS)g++ +SIZE = size -d +RM = rm -f + +SRC_DIRS = . +INCLUDE = -I ../../include + +SRCS = $(wildcard $(addsuffix /*.cpp, $(SRC_DIRS))) +OBJS = $(SRCS:.cpp=.o) +DEPENDS = $(OBJS:.o=.d) + +EXE = $(SRCS:.cpp=) + + +#------------------------------------------------------------------------------ +# flags +# + +FLAGS += $(INCLUDE) +FLAGS += -MMD + +CXXFLAGS = $(FLAGS) +CXXFLAGS += $(OPTIMIZER) +CXXFLAGS += -std=c++11 +CXXFLAGS += -fno-exceptions +CXXFLAGS += -fno-rtti + +CXXFLAGS += -Wall -Wextra +CXXFLAGS += -Wctor-dtor-privacy +CXXFLAGS += -Wcast-align -Wpointer-arith -Wredundant-decls +CXXFLAGS += -Wshadow -Wcast-qual -Wcast-align -pedantic + +# Produce debugging information (for use with gdb) +#OPTIMIZER = -Og +#FLAGS += -g + +# Use LLVM +#CXX = $(CROSS)clang++ +#CXXFLAGS += -stdlib=libc++ +#LDFLAGS += -lc++ + + +.PHONY: all clean + +all: $(EXE) + +%: %.cpp + $(CXX) $(CXXFLAGS) -o $@ $< + $(SIZE) $@ + +clean: + $(RM) *.d + $(RM) $(EXE) + + +-include $(DEPENDS) diff --git a/lib/tinyfsm/examples/api/debugging_switch.cpp b/lib/tinyfsm/examples/api/debugging_switch.cpp new file mode 100644 index 00000000..065d0e13 --- /dev/null +++ b/lib/tinyfsm/examples/api/debugging_switch.cpp @@ -0,0 +1,122 @@ +#include +#include +#include + +struct Off; // forward declaration + + +// ---------------------------------------------------------------------------- +// Event Declarations +// +struct Toggle : tinyfsm::Event { }; // Event Declarations + + +// ---------------------------------------------------------------------------- +// State Machine Declaration +// +struct Switch +: tinyfsm::Fsm +{ + static void reset(void); + + // NOTE: on reset: "tinyfsm::StateList::reset()", copy + // constructor is used by default, so "this" points to neither + // "Off" nor "On" (see operator=() below). + Switch() : counter(0) { + std::cout << "* Switch()" << std::endl + << " this = " << this << std::endl; + } + + ~Switch() { + std::cout << "* ~Switch()" << std::endl + << " this = " << this << std::endl; + } + + Switch & operator=(const Switch & other) { + std::cout << "* operator=()" << std::endl + << " this = " << this << std::endl + << " other = " << &other << std::endl; + counter = other.counter; + return *this; + } + + virtual void react(Toggle const &) { }; + void entry(void); + void exit(void); + + int counter; +}; + +struct On : Switch { + void react(Toggle const &) override { transit(); }; +}; + +struct Off : Switch { + void react(Toggle const &) override { transit(); }; +}; + +FSM_INITIAL_STATE(Switch, Off) + + +// ---------------------------------------------------------------------------- +// State Machine Definitions +// +void Switch::reset() { + tinyfsm::StateList::reset(); +} + +void Switch::entry() { + counter++; + + // debugging only. properly designed state machines don't need this: + if(is_in_state()) { std::cout << "* On::entry()" << std::endl; } + else if(is_in_state()) { std::cout << "* Off::entry()" << std::endl; } + else assert(true); + + assert(current_state_ptr == this); + std::cout << " this (cur) = " << this << std::endl + << " state = " << &state() << std::endl + << " state = " << &state() << std::endl; +} + +void Switch::exit() { + assert(current_state_ptr == this); + std::cout << "* exit()" << std::endl + << " this (cur) = " << this << std::endl + << " state = " << &state() << std::endl + << " state = " << &state() << std::endl; +} + + +// ---------------------------------------------------------------------------- +// Main +// +int main() +{ + Switch::start(); + + while(1) + { + char c; + std::cout << "* main()" << std::endl + << " cur_counter = " << Switch::current_state_ptr->counter << std::endl + << " on_counter = " << Switch::state().counter << std::endl + << " off_counter = " << Switch::state().counter << std::endl; + + std::cout << std::endl << "t=Toggle, r=Restart, q=Quit ? "; + std::cin >> c; + switch(c) { + case 't': + Switch::dispatch(Toggle()); + break; + case 'r': + Switch::reset(); + Switch::start(); + break; + case 'q': + return 0; + default: + std::cout << "> Invalid input" << std::endl; + }; + } +} diff --git a/lib/tinyfsm/examples/api/mealy_machine.cpp b/lib/tinyfsm/examples/api/mealy_machine.cpp new file mode 100644 index 00000000..a4fadff6 --- /dev/null +++ b/lib/tinyfsm/examples/api/mealy_machine.cpp @@ -0,0 +1,70 @@ +#include +#include + +// ---------------------------------------------------------------------------- +// 1. Event Declarations +// +struct Toggle : tinyfsm::Event { }; + + +// ---------------------------------------------------------------------------- +// 2. State Machine Base Class Declaration +// +struct Switch : tinyfsm::MealyMachine +{ + /* pure virtual reaction (override required in all states) */ + virtual void react(Toggle const &) = 0; + + /* transition actions */ + static void OpenCircuit() { + std::cout << "* Opening ciruit (light goes OFF)" << std::endl; + } + static void CloseCircuit() { + std::cout << "* Closing ciruit (light goes ON)" << std::endl; + } +}; + + +// ---------------------------------------------------------------------------- +// 3. State Declarations +// +struct Off; // forward declaration + +struct On : Switch +{ + void react(Toggle const &) override { transit(OpenCircuit); }; +}; + +struct Off : Switch +{ + void react(Toggle const &) override { transit(CloseCircuit); }; +}; + +FSM_INITIAL_STATE(Switch, Off) + + +// ---------------------------------------------------------------------------- +// Main +// +int main() +{ + Switch::start(); + + std::cout << "> You are facing a light switch..." << std::endl; + while(1) + { + char c; + std::cout << std::endl << "t=Toggle, q=Quit ? "; + std::cin >> c; + switch(c) { + case 't': + std::cout << "> Toggling switch..." << std::endl; + Switch::dispatch(Toggle()); + break; + case 'q': + return 0; + default: + std::cout << "> Invalid input" << std::endl; + }; + } +} diff --git a/lib/tinyfsm/examples/api/moore_machine.cpp b/lib/tinyfsm/examples/api/moore_machine.cpp new file mode 100644 index 00000000..02a77da2 --- /dev/null +++ b/lib/tinyfsm/examples/api/moore_machine.cpp @@ -0,0 +1,64 @@ +#include +#include + +// ---------------------------------------------------------------------------- +// 1. Event Declarations +// +struct Toggle : tinyfsm::Event { }; + + +// ---------------------------------------------------------------------------- +// 2. State Machine Base Class Declaration +// +struct Switch : tinyfsm::MooreMachine +{ + /* pure virtual reaction (override required in all states) */ + virtual void react(Toggle const &) = 0; +}; + + +// ---------------------------------------------------------------------------- +// 3. State Declarations +// +struct Off; // forward declaration + +struct On : Switch +{ + void entry() override { std::cout << "* Closing ciruit (light goes ON)" << std::endl; }; + void react(Toggle const &) override { transit(); }; +}; + +struct Off : Switch +{ + void entry() override { std::cout << "* Opening ciruit (light goes OFF)" << std::endl; }; + void react(Toggle const &) override { transit(); }; +}; + +FSM_INITIAL_STATE(Switch, Off) + + +// ---------------------------------------------------------------------------- +// Main +// +int main() +{ + Switch::start(); + + std::cout << "> You are facing a light switch..." << std::endl; + while(1) + { + char c; + std::cout << std::endl << "t=Toggle, q=Quit ? "; + std::cin >> c; + switch(c) { + case 't': + std::cout << "> Toggling switch..." << std::endl; + Switch::dispatch(Toggle()); + break; + case 'q': + return 0; + default: + std::cout << "> Invalid input" << std::endl; + }; + } +} diff --git a/lib/tinyfsm/examples/api/multiple_switch.cpp b/lib/tinyfsm/examples/api/multiple_switch.cpp new file mode 100644 index 00000000..6bf844fb --- /dev/null +++ b/lib/tinyfsm/examples/api/multiple_switch.cpp @@ -0,0 +1,145 @@ +// +// In this example, we want to use the DefectiveSwitch FSM multiple +// times. As TinyFSM is all about templates, we need to declare it as +// a template class. +// +// This is a bit cumbersome, as the C++ syntax is really ugly when it +// comes to derived template classes. +// +#include +#include +#include /* rand */ + +template +class Off; // forward declaration + +static void DumpState(int inum, const char * state, int on_counter, int defect_level) { + std::cout << "* Switch[" << inum << "] is " << state << " (on_counter=" << on_counter << ", defect_level=" << defect_level << ")" << std::endl; +} + +// ---------------------------------------------------------------------------- +// 1. Event Declarations +// +struct Toggle : tinyfsm::Event { }; + + +// ---------------------------------------------------------------------------- +// 2. State Machine Base Class Declaration +// +template +class DefectiveSwitch +: public tinyfsm::Fsm< DefectiveSwitch > +{ +public: + static constexpr unsigned int defect_level = (inum * 2); + + static void reset(void) { + on_counter = 0; + } + + /* default reaction for unhandled events */ + void react(tinyfsm::Event const &) { }; + + virtual void react(Toggle const &) { }; + virtual void entry(void) { }; /* entry actions in some states */ + void exit(void) { }; /* no exit actions */ + +protected: + static unsigned int on_counter; +}; + +// state variable definitions +template +unsigned int DefectiveSwitch::on_counter{0}; + + +// ---------------------------------------------------------------------------- +// 3. State Declarations +// +template +class On +: public DefectiveSwitch +{ + // note: base class is not known in dependend template + using base = DefectiveSwitch; + void entry() override { + base::on_counter++; + DumpState(inum, "ON ", base::on_counter, base::defect_level); + }; + void react(Toggle const &) override { + base::template transit< Off >(); + }; +}; + + +template +class Off +: public DefectiveSwitch +{ + using base = DefectiveSwitch; + void entry() override { + DumpState(inum, "OFF", base::on_counter, base::defect_level); + }; + void react(Toggle const &) override { + if((rand() % (base::defect_level + 1)) == 0) + base::template transit< On >(); + else { + std::cout << "* Kzzz kzzzzzz" << std::endl; + base::template transit< Off >(); + } + }; +}; + +FSM_INITIAL_STATE(DefectiveSwitch<0>, Off<0> ) +FSM_INITIAL_STATE(DefectiveSwitch<1>, Off<1> ) +FSM_INITIAL_STATE(DefectiveSwitch<2>, Off<2> ) + + +// ---------------------------------------------------------------------------- +// 4. State Machine List Declaration +// + +using fsm_handle = tinyfsm::FsmList< + DefectiveSwitch<0>, + DefectiveSwitch<1>, + DefectiveSwitch<2> + >; + +template +void ToggleSingle() { + std::cout << "> Toggling switch " << inum << "..." << std::endl; + DefectiveSwitch::dispatch(Toggle()); +} + + +// ---------------------------------------------------------------------------- +// Main +// +int main() +{ + fsm_handle::start(); + + while(1) + { + char c; + std::cout << std::endl << "0,1,2=Toggle single, a=Toggle all, r=Restart, q=Quit ? "; + std::cin >> c; + switch(c) { + case '0': ToggleSingle<0>(); break; + case '1': ToggleSingle<1>(); break; + case '2': ToggleSingle<2>(); break; + case 'a': + std::cout << "> Toggling all switches..." << std::endl; + fsm_handle::dispatch(Toggle()); + break; + case 'r': + fsm_handle::reset(); + fsm_handle::start(); + break; + case 'q': + return 0; + default: + std::cout << "> Invalid input" << std::endl; + }; + } +} diff --git a/lib/tinyfsm/examples/api/resetting_switch.cpp b/lib/tinyfsm/examples/api/resetting_switch.cpp new file mode 100644 index 00000000..5642fc92 --- /dev/null +++ b/lib/tinyfsm/examples/api/resetting_switch.cpp @@ -0,0 +1,110 @@ +#include +#include + +class Off; // forward declaration + + +// ---------------------------------------------------------------------------- +// 1. Event Declarations +// +struct Toggle : tinyfsm::Event { }; + + +// ---------------------------------------------------------------------------- +// 2. State Machine Base Class Declaration +// +class Switch +: public tinyfsm::Fsm +{ + // entry(), exit() and react() are called from Fsm::transit() + // in derived states, make friends: + friend class tinyfsm::Fsm; + + /* default reaction for unhandled events */ + void react(tinyfsm::Event const &) { }; + + virtual void react(Toggle const &) { }; + virtual void entry(void) { }; /* entry actions in some states */ + void exit(void) { }; /* no exit actions */ + +public: + static void reset(void); /* implemented below */ +}; + + +// ---------------------------------------------------------------------------- +// 3. State Declarations +// +class On +: public Switch +{ + void entry() override { counter++; std::cout << "* Switch is ON, counter=" << counter << std::endl; }; + void react(Toggle const &) override { transit(); }; + int counter; + +public: + On() : counter(0) { std::cout << "** RESET State=On" << std::endl; } +}; + +class Off +: public Switch +{ + void entry() override { counter++; std::cout << "* Switch is OFF, counter=" << counter << std::endl; }; + void react(Toggle const &) override { transit(); }; + int counter; + +public: + Off() : counter(0) { std::cout << "** RESET State=Off" << std::endl; } +}; + + +void Switch::reset() { + std::cout << "** RESET Switch" << std::endl; + // Reset all states (calls constructor on all states in list) + tinyfsm::StateList::reset(); + + // Alternatively, make counter public above and reset the values + // here instead of using a copy-constructor with StateList<>: + //state().counter = 0; + //state().counter = 0; +} + +FSM_INITIAL_STATE(Switch, Off) + + +// ---------------------------------------------------------------------------- +// 4. State Machine List Declaration (dispatches events to multiple FSM's) +// +// In this example, we only have a single state machine, no need to use FsmList<>: +//using fsm_handle = tinyfsm::FsmList< Switch >; +using fsm_handle = Switch; + + +// ---------------------------------------------------------------------------- +// Main +// +int main() +{ + fsm_handle::start(); + + while(1) + { + char c; + std::cout << std::endl << "t=Toggle, r=Restart, q=Quit ? "; + std::cin >> c; + switch(c) { + case 't': + std::cout << "> Toggling switch..." << std::endl; + fsm_handle::dispatch(Toggle()); + break; + case 'r': + fsm_handle::reset(); + fsm_handle::start(); + break; + case 'q': + return 0; + default: + std::cout << "> Invalid input" << std::endl; + }; + } +} diff --git a/lib/tinyfsm/examples/api/simple_switch.cpp b/lib/tinyfsm/examples/api/simple_switch.cpp new file mode 100644 index 00000000..b20b2abd --- /dev/null +++ b/lib/tinyfsm/examples/api/simple_switch.cpp @@ -0,0 +1,85 @@ +#include +#include + +struct Off; // forward declaration + + +// ---------------------------------------------------------------------------- +// 1. Event Declarations +// +struct Toggle : tinyfsm::Event { }; + + +// ---------------------------------------------------------------------------- +// 2. State Machine Base Class Declaration +// +struct Switch : tinyfsm::Fsm +{ + virtual void react(Toggle const &) { }; + + // alternative: enforce handling of Toggle in all states (pure virtual) + //virtual void react(Toggle const &) = 0; + + virtual void entry(void) { }; /* entry actions in some states */ + void exit(void) { }; /* no exit actions */ + + // alternative: enforce entry actions in all states (pure virtual) + //virtual void entry(void) = 0; +}; + + +// ---------------------------------------------------------------------------- +// 3. State Declarations +// +struct On : Switch +{ + void entry() override { std::cout << "* Switch is ON" << std::endl; }; + void react(Toggle const &) override { transit(); }; +}; + +struct Off : Switch +{ + void entry() override { std::cout << "* Switch is OFF" << std::endl; }; + void react(Toggle const &) override { transit(); }; +}; + +FSM_INITIAL_STATE(Switch, Off) + + +// ---------------------------------------------------------------------------- +// 4. State Machine List Declaration (dispatches events to multiple FSM's) +// +// In this example, we only have a single state machine, no need to use FsmList<>: +//using fsm_handle = tinyfsm::FsmList< Switch >; +using fsm_handle = Switch; + + +// ---------------------------------------------------------------------------- +// Main +// +int main() +{ + // instantiate events + Toggle toggle; + + fsm_handle::start(); + + while(1) + { + char c; + std::cout << std::endl << "t=Toggle, q=Quit ? "; + std::cin >> c; + switch(c) { + case 't': + std::cout << "> Toggling switch..." << std::endl; + fsm_handle::dispatch(toggle); + // alternative: instantiating causes no overhead (empty declaration) + //fsm_handle::dispatch(Toggle()); + break; + case 'q': + return 0; + default: + std::cout << "> Invalid input" << std::endl; + }; + } +} diff --git a/lib/tinyfsm/examples/elevator/Makefile b/lib/tinyfsm/examples/elevator/Makefile new file mode 100644 index 00000000..4c979052 --- /dev/null +++ b/lib/tinyfsm/examples/elevator/Makefile @@ -0,0 +1,98 @@ +# Compiler prefix, in case your default compiler does not implement all C++11 features: +#CROSS = /opt/toolchain/x86_64-pc-linux-gnu-gcc-4.7.0/bin/x86_64-pc-linux-gnu- + +PROJECT = elevator + +# HINT: g++ -Q -O2 --help=optimizers +OPTIMIZER = -Os + +CC = $(CROSS)gcc +CXX = $(CROSS)g++ +AS = $(CROSS)gcc -x assembler-with-cpp +LD = $(CROSS)g++ +OBJCOPY = $(CROSS)objcopy +OBJDUMP = $(CROSS)objdump +SIZE = size -d +RM = rm -f +RM_R = rm -rf +CP = cp +MKDIR_P = mkdir -p +DOXYGEN = doxygen + + +SRC_DIRS = . +INCLUDE = -I ../../include + +SRCS = $(wildcard $(addsuffix /*.cpp, $(SRC_DIRS))) +OBJS = $(SRCS:.cpp=.o) +DEPENDS = $(OBJS:.o=.d) + +EXE = $(PROJECT) +MAP = $(PROJECT).map + + +#------------------------------------------------------------------------------ +# flags +# + +# commmon flags propagated to CFLAGS, CXXFLAGS, ASFLAGS (not LDFLAGS) +FLAGS += $(INCLUDE) +FLAGS += -MMD + +CXXFLAGS = $(FLAGS) +CXXFLAGS += $(OPTIMIZER) +CXXFLAGS += -std=c++11 +CXXFLAGS += -fno-exceptions +CXXFLAGS += -fno-rtti + +CXXFLAGS += -Wall -Wextra +CXXFLAGS += -Wctor-dtor-privacy +CXXFLAGS += -Wcast-align -Wpointer-arith -Wredundant-decls +CXXFLAGS += -Wshadow -Wcast-qual -Wcast-align -pedantic + +LDFLAGS += -fno-exceptions +LDFLAGS += -fno-rtti + +# Produce debugging information (for use with gdb) +#OPTIMIZER = -Og +#FLAGS += -g + +# Use LLVM +#CXX = $(CROSS)clang++ +#CXXFLAGS += -stdlib=libc++ +#LDFLAGS += -lc++ + +# Enable link-time optimizer +#CXXFLAGS += -flto +#LDFLAGS += -flto + +# Strip dead code (enable garbage collection) +#OPTIMIZER += -ffunction-sections -fdata-sections +#LDFLAGS += -Wl,$(if $(shell ld -v | grep GNU),--gc-sections,-dead_strip) + +# Enable automatic template instantiation at link time +#CXXFLAGS += -frepo +#LDFLAGS += -frepo + +# Create link map file +#LDFLAGS += -Wl,-Map="$(MAP)",--cref + + +.PHONY: all clean + +all: $(EXE) + +$(EXE): $(OBJS) + $(LD) $(OBJS) $(LDFLAGS) -o $(EXE) + $(SIZE) $@ + +%.o: %.cpp + $(CXX) -c $(CXXFLAGS) -o $@ $< + +clean: + $(RM) *.o + $(RM) *.d + $(RM) $(EXE) + + +-include $(DEPENDS) diff --git a/lib/tinyfsm/examples/elevator/README.md b/lib/tinyfsm/examples/elevator/README.md new file mode 100644 index 00000000..6de14ceb --- /dev/null +++ b/lib/tinyfsm/examples/elevator/README.md @@ -0,0 +1,86 @@ +Elevator Project +================ + +Example implementation of a simplified elevator logic, using [TinyFSM]. + + [TinyFSM]: https://digint.ch/tinyfsm/ + + +Overview +-------- + +Imagine a elevator having: + + - "Call" button on each floor, + - "Floor Sensor" on each floor, triggering an event as soon as the + elevator arrives there, + - "Alarm" button. + + +Implementation +-------------- + +The elevator example implements two state machines interacting with +each other: + + 1. Elevator + - State: Idle + - State: Moving + - State: Panic + + 2. Motor + - State: Stopped + - State: Up + - State: Down + + +A good state machine design avoids circular dependencies at all +cost: While the elevator sends events to the motor, the motor NEVER +sends events to the elevator (top-down only). + + +FAQ +--- + +Did you notice the motor starting twice? This is by design, let's +have a look at the call stack of fsm_list::start() in main.cpp: + + FsmList::start() + Motor::set_initial_state() + Motor::current_state = Stopped + Elevator::set_initial_state() + Elevator::current_state = Idle + Motor::enter() + Motor:Stopped->entry() + cout << "Motor: stopped" <-- HERE + Motor::direction = 0 + Elevator::enter() + Elevator:Idle->entry() + send_event(MotorStop) + Motor::react(MotorStop) + Motor:Stopped->transit + Motor:Stopped->exit() + Motor::current_state = Stopped + Motor:Stopped->entry() + cout << "Motor: stopped" <-- HERE + Motor::direction = 0 + Elevator::react(MotorStop) + +If we really had to work around this, we could either: + + 1. Change the initialization (bad design practice!) in main.cpp: + + - fsm_list::start(); + + fsm_list::set_initial_state(); + + Elevator::enter(); + + + 2. Modify the Motor:Stopped->entry() function in motor.cpp: + + class Stopped : public Motor { + void entry() override { + + if(direction == 0) + + return; + cout << "Motor: stopped" << endl; + direction = 0; + }; diff --git a/lib/tinyfsm/examples/elevator/elevator.cpp b/lib/tinyfsm/examples/elevator/elevator.cpp new file mode 100644 index 00000000..6e792f64 --- /dev/null +++ b/lib/tinyfsm/examples/elevator/elevator.cpp @@ -0,0 +1,115 @@ +#include + +#include "elevator.hpp" +#include "fsmlist.hpp" + +#include + +class Idle; // forward declaration + + +// ---------------------------------------------------------------------------- +// Transition functions +// + +static void CallMaintenance() { + std::cout << "*** calling maintenance ***" << std::endl; +} + +static void CallFirefighters() { + std::cout << "*** calling firefighters ***" << std::endl; +} + + +// ---------------------------------------------------------------------------- +// State: Panic +// + +class Panic +: public Elevator +{ + void entry() override { + send_event(MotorStop()); + } +}; + + +// ---------------------------------------------------------------------------- +// State: Moving +// + +class Moving +: public Elevator +{ + void react(FloorSensor const & e) override { + int floor_expected = current_floor + Motor::getDirection(); + if(floor_expected != e.floor) + { + std::cout << "Floor sensor defect (expected " << floor_expected << ", got " << e.floor << ")" << std::endl; + transit(CallMaintenance); + } + else + { + std::cout << "Reached floor " << e.floor << std::endl; + current_floor = e.floor; + if(e.floor == dest_floor) + transit(); + } + }; +}; + + +// ---------------------------------------------------------------------------- +// State: Idle +// + +class Idle +: public Elevator +{ + void entry() override { + send_event(MotorStop()); + } + + void react(Call const & e) override { + dest_floor = e.floor; + + if(dest_floor == current_floor) + return; + + /* lambda function used for transition action */ + auto action = [] { + if(dest_floor > current_floor) + send_event(MotorUp()); + else if(dest_floor < current_floor) + send_event(MotorDown()); + }; + + transit(action); + }; +}; + + +// ---------------------------------------------------------------------------- +// Base state: default implementations +// + +void Elevator::react(Call const &) { + std::cout << "Call event ignored" << std::endl; +} + +void Elevator::react(FloorSensor const &) { + std::cout << "FloorSensor event ignored" << std::endl; +} + +void Elevator::react(Alarm const &) { + transit(CallFirefighters); +} + +int Elevator::current_floor = Elevator::initial_floor; +int Elevator::dest_floor = Elevator::initial_floor; + + +// ---------------------------------------------------------------------------- +// Initial state definition +// +FSM_INITIAL_STATE(Elevator, Idle) diff --git a/lib/tinyfsm/examples/elevator/elevator.hpp b/lib/tinyfsm/examples/elevator/elevator.hpp new file mode 100644 index 00000000..be3d0053 --- /dev/null +++ b/lib/tinyfsm/examples/elevator/elevator.hpp @@ -0,0 +1,55 @@ +#ifndef ELEVATOR_HPP_INCLUDED +#define ELEVATOR_HPP_INCLUDED + +#include + + +// ---------------------------------------------------------------------------- +// Event declarations +// + +struct FloorEvent : tinyfsm::Event +{ + int floor; +}; + +struct Call : FloorEvent { }; +struct FloorSensor : FloorEvent { }; +struct Alarm : tinyfsm::Event { }; + + + +// ---------------------------------------------------------------------------- +// Elevator (FSM base class) declaration +// + +class Elevator +: public tinyfsm::Fsm +{ + /* NOTE: react(), entry() and exit() functions need to be accessible + * from tinyfsm::Fsm class. You might as well declare friendship to + * tinyfsm::Fsm, and make these functions private: + * + * friend class Fsm; + */ +public: + + /* default reaction for unhandled events */ + void react(tinyfsm::Event const &) { }; + + virtual void react(Call const &); + virtual void react(FloorSensor const &); + void react(Alarm const &); + + virtual void entry(void) { }; /* entry actions in some states */ + void exit(void) { }; /* no exit actions at all */ + +protected: + + static constexpr int initial_floor = 0; + static int current_floor; + static int dest_floor; +}; + + +#endif diff --git a/lib/tinyfsm/examples/elevator/fsmlist.hpp b/lib/tinyfsm/examples/elevator/fsmlist.hpp new file mode 100644 index 00000000..d340f8a1 --- /dev/null +++ b/lib/tinyfsm/examples/elevator/fsmlist.hpp @@ -0,0 +1,19 @@ +#ifndef FSMLIST_HPP_INCLUDED +#define FSMLIST_HPP_INCLUDED + +#include + +#include "elevator.hpp" +#include "motor.hpp" + +using fsm_list = tinyfsm::FsmList; + +/** dispatch event to both "Motor" and "Elevator" */ +template +void send_event(E const & event) +{ + fsm_list::template dispatch(event); +} + + +#endif diff --git a/lib/tinyfsm/examples/elevator/main.cpp b/lib/tinyfsm/examples/elevator/main.cpp new file mode 100644 index 00000000..46570153 --- /dev/null +++ b/lib/tinyfsm/examples/elevator/main.cpp @@ -0,0 +1,40 @@ +#include "fsmlist.hpp" + +#include + + +int main() +{ + fsm_list::start(); + + Call call; + FloorSensor sensor; + + while(1) + { + char c; + + std::cout << "c=Call, f=FloorSensor, a=Alarm, q=Quit ? "; + std::cin >> c; + switch(c) { + case 'c': + std::cout << "Floor ? "; + std::cin >> call.floor; + send_event(call); + break; + case 'f': + std::cout << "Floor ? "; + std::cin >> sensor.floor; + send_event(sensor); + break; + case 'a': + send_event(Alarm()); + break; + case 'q': + std::cout << "Thanks for playing!" << std::endl; + return 0; + default: + std::cout << "Invalid input" << std::endl; + }; + } +} diff --git a/lib/tinyfsm/examples/elevator/motor.cpp b/lib/tinyfsm/examples/elevator/motor.cpp new file mode 100644 index 00000000..b4668070 --- /dev/null +++ b/lib/tinyfsm/examples/elevator/motor.cpp @@ -0,0 +1,60 @@ +#include +#include "motor.hpp" +#include + + +// ---------------------------------------------------------------------------- +// Motor states +// + +class Stopped +: public Motor +{ + void entry() override { + std::cout << "Motor: stopped" << std::endl; + direction = 0; + }; +}; + +class Up +: public Motor +{ + void entry() override { + std::cout << "Motor: moving up" << std::endl; + direction = 1; + }; +}; + +class Down +: public Motor +{ + void entry() override { + std::cout << "Motor: moving down" << std::endl; + direction = -1; + }; +}; + + +// ---------------------------------------------------------------------------- +// Base State: default implementations +// + +void Motor::react(MotorStop const &) { + transit(); +} + +void Motor::react(MotorUp const &) { + transit(); +} + +void Motor::react(MotorDown const &) { + transit(); +} + +int Motor::direction{0}; + + +// ---------------------------------------------------------------------------- +// Initial state definition +// +FSM_INITIAL_STATE(Motor, Stopped) diff --git a/lib/tinyfsm/examples/elevator/motor.hpp b/lib/tinyfsm/examples/elevator/motor.hpp new file mode 100644 index 00000000..7d2447cf --- /dev/null +++ b/lib/tinyfsm/examples/elevator/motor.hpp @@ -0,0 +1,50 @@ +#ifndef MOTOR_HPP_INCLUDED +#define MOTOR_HPP_INCLUDED + +#include + + +// ---------------------------------------------------------------------------- +// Event declarations +// + +struct MotorUp : tinyfsm::Event { }; +struct MotorDown : tinyfsm::Event { }; +struct MotorStop : tinyfsm::Event { }; + + +// ---------------------------------------------------------------------------- +// Motor (FSM base class) declaration +// +class Motor +: public tinyfsm::Fsm +{ + /* NOTE: react(), entry() and exit() functions need to be accessible + * from tinyfsm::Fsm class. You might as well declare friendship to + * tinyfsm::Fsm, and make these functions private: + * + * friend class Fsm; + */ +public: + + /* default reaction for unhandled events */ + void react(tinyfsm::Event const &) { }; + + /* non-virtual declaration: reactions are the same for all states */ + void react(MotorUp const &); + void react(MotorDown const &); + void react(MotorStop const &); + + virtual void entry(void) = 0; /* pure virtual: enforce implementation in all states */ + void exit(void) { }; /* no exit actions at all */ + +protected: + + static int direction; + +public: + static int getDirection() { return direction; } +}; + + +#endif diff --git a/lib/tinyfsm/include/tinyfsm.hpp b/lib/tinyfsm/include/tinyfsm.hpp new file mode 100644 index 00000000..5a11f8ae --- /dev/null +++ b/lib/tinyfsm/include/tinyfsm.hpp @@ -0,0 +1,251 @@ +/* + * TinyFSM - Tiny Finite State Machine Processor + * + * Copyright (c) 2012-2022 Axel Burri + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/* --------------------------------------------------------------------- + * Version: 0.3.3 + * + * API documentation: see "../doc/50-API.md" + * + * The official TinyFSM website is located at: + * https://digint.ch/tinyfsm/ + * + * Author: + * Axel Burri + * --------------------------------------------------------------------- + */ + +#ifndef TINYFSM_HPP_INCLUDED +#define TINYFSM_HPP_INCLUDED + +#ifndef TINYFSM_NOSTDLIB +#include +#endif + +// #include +// #define DBG(str) do { std::cerr << str << std::endl; } while( false ) +// DBG("*** dbg_example *** " << __PRETTY_FUNCTION__); + +namespace tinyfsm +{ + + // -------------------------------------------------------------------------- + + struct Event { }; + + // -------------------------------------------------------------------------- + +#ifdef TINYFSM_NOSTDLIB + // remove dependency on standard library (silent fail!). + // useful in conjunction with -nostdlib option, e.g. if your compiler + // does not provide a standard library. + // NOTE: this silently disables all static_assert() calls below! + template + struct is_same_fsm { static constexpr bool value = true; }; +#else + // check if both fsm and state class share same fsmtype + template + struct is_same_fsm : std::is_same< typename F::fsmtype, typename S::fsmtype > { }; +#endif + + template + struct _state_instance + { + using value_type = S; + using type = _state_instance; + static S value; + }; + + template + typename _state_instance::value_type _state_instance::value; + + // -------------------------------------------------------------------------- + + template + class Fsm + { + public: + + using fsmtype = Fsm; + using state_ptr_t = F *; + + static state_ptr_t current_state_ptr; + + // public, leaving ability to access state instance (e.g. on reset) + template + static constexpr S & state(void) { + static_assert(is_same_fsm::value, "accessing state of different state machine"); + return _state_instance::value; + } + + template + static constexpr bool is_in_state(void) { + static_assert(is_same_fsm::value, "accessing state of different state machine"); + return current_state_ptr == &_state_instance::value; + } + + /// state machine functions + public: + + // explicitely specialized in FSM_INITIAL_STATE macro + static void set_initial_state(); + + static void reset() { }; + + static void enter() { + current_state_ptr->entry(); + } + + static void start() { + set_initial_state(); + enter(); + } + + template + static void dispatch(E const & event) { + current_state_ptr->react(event); + } + + + /// state transition functions + protected: + + template + void transit(void) { + static_assert(is_same_fsm::value, "transit to different state machine"); + current_state_ptr->exit(); + current_state_ptr = &_state_instance::value; + current_state_ptr->entry(); + } + + template + void transit(ActionFunction action_function) { + static_assert(is_same_fsm::value, "transit to different state machine"); + current_state_ptr->exit(); + // NOTE: do not send events in action_function definisions. + action_function(); + current_state_ptr = &_state_instance::value; + current_state_ptr->entry(); + } + + template + void transit(ActionFunction action_function, ConditionFunction condition_function) { + if(condition_function()) { + transit(action_function); + } + } + }; + + template + typename Fsm::state_ptr_t Fsm::current_state_ptr; + + // -------------------------------------------------------------------------- + + template + struct FsmList; + + template<> struct FsmList<> { + static void set_initial_state() { } + static void reset() { } + static void enter() { } + template + static void dispatch(E const &) { } + }; + + template + struct FsmList + { + using fsmtype = Fsm; + + static void set_initial_state() { + fsmtype::set_initial_state(); + FsmList::set_initial_state(); + } + + static void reset() { + F::reset(); + FsmList::reset(); + } + + static void enter() { + fsmtype::enter(); + FsmList::enter(); + } + + static void start() { + set_initial_state(); + enter(); + } + + template + static void dispatch(E const & event) { + fsmtype::template dispatch(event); + FsmList::template dispatch(event); + } + }; + + // -------------------------------------------------------------------------- + + template struct StateList; + template<> struct StateList<> { + static void reset() { } + }; + template + struct StateList + { + static void reset() { + _state_instance::value = S(); + StateList::reset(); + } + }; + + // -------------------------------------------------------------------------- + + template + struct MooreMachine : tinyfsm::Fsm + { + virtual void entry(void) { }; /* entry actions in some states */ + void exit(void) { }; /* no exit actions */ + }; + + template + struct MealyMachine : tinyfsm::Fsm + { + // input actions are modeled in react(): + // - conditional dependent of event type or payload + // - transit<>(ActionFunction) + void entry(void) { }; /* no entry actions */ + void exit(void) { }; /* no exit actions */ + }; + +} /* namespace tinyfsm */ + + +#define FSM_INITIAL_STATE(_FSM, _STATE) \ +namespace tinyfsm { \ + template<> void Fsm< _FSM >::set_initial_state(void) { \ + current_state_ptr = &_state_instance< _STATE >::value; \ + } \ +} + +#endif /* TINYFSM_HPP_INCLUDED */ diff --git a/lib/tinyfsm/library.json b/lib/tinyfsm/library.json new file mode 100644 index 00000000..c0f0a297 --- /dev/null +++ b/lib/tinyfsm/library.json @@ -0,0 +1,24 @@ +{ + "name": "tinyfsm", + "description": "A simple C++ finite state machine library", + "version": "0.3.3", + "repository": { + "type": "git", + "url": "https://dev.tty0.ch/tinyfsm.git" + }, + "homepage": "https://digint.ch/tinyfsm/", + "authors": { + "name": "Axel Burri", + "email": "axel@tty0.ch", + "url": "http://digint.ch", + "maintainer": true + }, + "license": "MIT", + "platforms": "*", + "frameworks": "*", + "build": { + "flags": [ + "-I include/" + ] + } +} diff --git a/src/app_console/CMakeLists.txt b/src/app_console/CMakeLists.txt new file mode 100644 index 00000000..f27adf3b --- /dev/null +++ b/src/app_console/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRCS "app_console.cpp" + INCLUDE_DIRS "include" + REQUIRES "dev_console" "events" "database") +target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/main/app_console.cpp b/src/app_console/app_console.cpp similarity index 87% rename from src/main/app_console.cpp rename to src/app_console/app_console.cpp index a85faaf0..80b8a856 100644 --- a/src/main/app_console.cpp +++ b/src/app_console/app_console.cpp @@ -8,7 +8,6 @@ #include #include -#include "audio_playback.hpp" #include "database.hpp" #include "esp_console.h" #include "esp_log.h" @@ -18,7 +17,7 @@ namespace console { static AppConsole* sInstance = nullptr; std::string toSdPath(const std::string& filepath) { - return std::string(drivers::kStoragePath) + "/" + filepath; + return std::string("/") + filepath; } int CmdListDir(int argc, char** argv) { @@ -55,6 +54,7 @@ void RegisterListDir() { esp_console_cmd_register(&cmd); } +/* int CmdPlayFile(int argc, char** argv) { static const std::string usage = "usage: play [file]"; if (argc != 2) { @@ -146,6 +146,7 @@ void RegisterAudioStatus() { .argtable = NULL}; esp_console_cmd_register(&cmd); } +*/ int CmdDbInit(int argc, char** argv) { static const std::string usage = "usage: db_init"; @@ -154,7 +155,12 @@ int CmdDbInit(int argc, char** argv) { return 1; } - sInstance->database_->Update(); + auto db = sInstance->database_.lock(); + if (!db) { + std::cout << "no database open" << std::endl; + return 1; + } + db->Update(); return 0; } @@ -176,15 +182,19 @@ int CmdDbSongs(int argc, char** argv) { return 1; } - std::unique_ptr> res( - sInstance->database_->GetSongs(5).get()); + auto db = sInstance->database_.lock(); + if (!db) { + std::cout << "no database open" << std::endl; + return 1; + } + std::unique_ptr> res(db->GetSongs(5).get()); while (true) { for (database::Song s : res->values()) { std::cout << s.tags().title.value_or("[BLANK]") << std::endl; } if (res->next_page()) { auto continuation = res->next_page().value(); - res.reset(sInstance->database_->GetPage(&continuation).get()); + res.reset(db->GetPage(&continuation).get()); } else { break; } @@ -209,18 +219,22 @@ int CmdDbDump(int argc, char** argv) { return 1; } + auto db = sInstance->database_.lock(); + if (!db) { + std::cout << "no database open" << std::endl; + return 1; + } + std::cout << "=== BEGIN DUMP ===" << std::endl; - std::unique_ptr> res( - sInstance->database_->GetDump(5).get()); + std::unique_ptr> res(db->GetDump(5).get()); while (true) { for (std::string s : res->values()) { std::cout << s << std::endl; } if (res->next_page()) { auto continuation = res->next_page().value(); - res.reset( - sInstance->database_->GetPage(&continuation).get()); + res.reset(db->GetPage(&continuation).get()); } else { break; } @@ -240,9 +254,8 @@ void RegisterDbDump() { esp_console_cmd_register(&cmd); } -AppConsole::AppConsole(audio::AudioPlayback* playback, - database::Database* database) - : playback_(playback), database_(database) { +AppConsole::AppConsole(std::weak_ptr database) + : database_(database) { sInstance = this; } AppConsole::~AppConsole() { @@ -251,10 +264,12 @@ AppConsole::~AppConsole() { auto AppConsole::RegisterExtraComponents() -> void { RegisterListDir(); + /* RegisterPlayFile(); RegisterToggle(); RegisterVolume(); RegisterAudioStatus(); + */ RegisterDbInit(); RegisterDbSongs(); RegisterDbDump(); diff --git a/src/main/app_console.hpp b/src/app_console/include/app_console.hpp similarity index 53% rename from src/main/app_console.hpp rename to src/app_console/include/app_console.hpp index 3a11d70c..eb079ccb 100644 --- a/src/main/app_console.hpp +++ b/src/app_console/include/app_console.hpp @@ -2,21 +2,17 @@ #include -#include "audio_playback.hpp" #include "console.hpp" #include "database.hpp" -#include "storage.hpp" namespace console { class AppConsole : public Console { public: - explicit AppConsole(audio::AudioPlayback* playback, - database::Database* database); + explicit AppConsole(std::weak_ptr database); virtual ~AppConsole(); - audio::AudioPlayback* playback_; - database::Database* database_; + std::weak_ptr database_; protected: virtual auto RegisterExtraComponents() -> void; diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt index 6361f827..d9c431ee 100644 --- a/src/audio/CMakeLists.txt +++ b/src/audio/CMakeLists.txt @@ -1,8 +1,8 @@ idf_component_register( SRCS "audio_decoder.cpp" "audio_task.cpp" "chunk.cpp" "fatfs_audio_input.cpp" "stream_message.cpp" "i2s_audio_output.cpp" "stream_buffer.cpp" - "audio_playback.cpp" "stream_event.cpp" "pipeline.cpp" "stream_info.cpp" + "stream_event.cpp" "pipeline.cpp" "stream_info.cpp" "audio_fsm.cpp" INCLUDE_DIRS "include" - REQUIRES "codecs" "drivers" "cbor" "result" "tasks" "span" "memory") + REQUIRES "codecs" "drivers" "cbor" "result" "tasks" "span" "memory" "tinyfsm" "database" "system_fsm") target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp new file mode 100644 index 00000000..fd57ef57 --- /dev/null +++ b/src/audio/audio_fsm.cpp @@ -0,0 +1,51 @@ +#include "audio_fsm.hpp" +#include "audio_decoder.hpp" +#include "audio_task.hpp" +#include "dac.hpp" +#include "fatfs_audio_input.hpp" +#include "i2s_audio_output.hpp" +#include "pipeline.hpp" + +namespace audio { + +drivers::GpioExpander* AudioState::sGpioExpander; +std::weak_ptr AudioState::sDac; +std::weak_ptr AudioState::sDatabase; + +std::unique_ptr AudioState::sFileSource; +std::unique_ptr AudioState::sI2SOutput; +std::vector> AudioState::sPipeline; + +auto AudioState::Init(drivers::GpioExpander* gpio_expander, + std::weak_ptr dac, + std::weak_ptr database) -> void { + sGpioExpander = gpio_expander; + sDac = dac; + sDatabase = database; +} + +namespace states { + +void Uninitialised::react(const system_fsm::BootComplete&) { + transit([&]() { + sFileSource.reset(new FatfsAudioInput()); + sI2SOutput.reset(new I2SAudioOutput(sGpioExpander, sDac)); + + // Perform initial pipeline configuration. + // TODO(jacqueline): Factor this out once we have any kind of dynamic + // reconfiguration. + AudioDecoder* codec = new AudioDecoder(); + sPipeline.emplace_back(codec); + + Pipeline* pipeline = new Pipeline(sPipeline.front().get()); + pipeline->AddInput(sFileSource.get()); + + task::StartPipeline(pipeline, sI2SOutput.get()); + }); +} + +} // namespace states + +} // namespace audio + +FSM_INITIAL_STATE(audio::AudioState, audio::states::Uninitialised) diff --git a/src/audio/audio_playback.cpp b/src/audio/audio_playback.cpp deleted file mode 100644 index c51e41fb..00000000 --- a/src/audio/audio_playback.cpp +++ /dev/null @@ -1,50 +0,0 @@ -#include "audio_playback.hpp" - -#include -#include -#include -#include - -#include "driver_cache.hpp" -#include "freertos/portmacro.h" - -#include "audio_decoder.hpp" -#include "audio_element.hpp" -#include "audio_task.hpp" -#include "chunk.hpp" -#include "fatfs_audio_input.hpp" -#include "gpio_expander.hpp" -#include "i2s_audio_output.hpp" -#include "pipeline.hpp" -#include "storage.hpp" -#include "stream_buffer.hpp" -#include "stream_info.hpp" -#include "stream_message.hpp" - -namespace audio { -AudioPlayback::AudioPlayback(drivers::DriverCache* drivers) - : file_source_(std::make_unique()), - i2s_output_(std::make_unique(drivers->AcquireGpios(), - drivers->AcquireDac())) { - AudioDecoder* codec = new AudioDecoder(); - elements_.emplace_back(codec); - - Pipeline* pipeline = new Pipeline(elements_.front().get()); - pipeline->AddInput(file_source_.get()); - - task::StartPipeline(pipeline, i2s_output_.get()); - // task::StartDrain(i2s_output_.get()); -} - -AudioPlayback::~AudioPlayback() {} - -auto AudioPlayback::Play(const std::string& filename) -> void { - // TODO: concurrency, yo! - file_source_->OpenFile(filename); -} - -auto AudioPlayback::LogStatus() -> void { - i2s_output_->Log(); -} - -} // namespace audio diff --git a/src/audio/i2s_audio_output.cpp b/src/audio/i2s_audio_output.cpp index 8c374351..d4ee7933 100644 --- a/src/audio/i2s_audio_output.cpp +++ b/src/audio/i2s_audio_output.cpp @@ -20,8 +20,8 @@ static const char* kTag = "I2SOUT"; namespace audio { I2SAudioOutput::I2SAudioOutput(drivers::GpioExpander* expander, - std::shared_ptr dac) - : expander_(expander), dac_(std::move(dac)), current_config_() { + std::weak_ptr dac) + : expander_(expander), dac_(dac.lock()), current_config_() { dac_->WriteVolume(127); // for testing dac_->SetSource(buffer()); } diff --git a/src/audio/include/audio_events.hpp b/src/audio/include/audio_events.hpp new file mode 100644 index 00000000..0c2e63cc --- /dev/null +++ b/src/audio/include/audio_events.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include "tinyfsm.hpp" + +#include "song.hpp" + +namespace audio { + +struct PlaySong : tinyfsm::Event { + database::SongId id; +}; + +} // namespace audio diff --git a/src/audio/include/audio_fsm.hpp b/src/audio/include/audio_fsm.hpp new file mode 100644 index 00000000..ea7f897d --- /dev/null +++ b/src/audio/include/audio_fsm.hpp @@ -0,0 +1,61 @@ +#pragma once +#include + +#include "audio_events.hpp" +#include "dac.hpp" +#include "database.hpp" +#include "display.hpp" +#include "fatfs_audio_input.hpp" +#include "gpio_expander.hpp" +#include "i2s_audio_output.hpp" +#include "storage.hpp" +#include "tinyfsm.hpp" + +#include "system_events.hpp" + +namespace audio { + +class AudioState : public tinyfsm::Fsm { + public: + static auto Init(drivers::GpioExpander* gpio_expander, + std::weak_ptr, + std::weak_ptr) -> void; + + virtual ~AudioState() {} + + virtual void entry() {} + virtual void exit() {} + + /* Fallback event handler. Does nothing. */ + void react(const tinyfsm::Event& ev) {} + + virtual void react(const system_fsm::BootComplete&) {} + virtual void react(const PlaySong&) {} + + protected: + static drivers::GpioExpander* sGpioExpander; + static std::weak_ptr sDac; + static std::weak_ptr sDatabase; + + static std::unique_ptr sFileSource; + static std::unique_ptr sI2SOutput; + static std::vector> sPipeline; +}; + +namespace states { + +class Uninitialised : public AudioState { + public: + void react(const system_fsm::BootComplete&) override; + using AudioState::react; +}; + +class Standby : public AudioState { + public: + void react(const PlaySong&) override {} + using AudioState::react; +}; + +} // namespace states + +} // namespace audio diff --git a/src/audio/include/audio_playback.hpp b/src/audio/include/audio_playback.hpp deleted file mode 100644 index cd4be3e7..00000000 --- a/src/audio/include/audio_playback.hpp +++ /dev/null @@ -1,50 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "audio_task.hpp" -#include "driver_cache.hpp" -#include "esp_err.h" -#include "fatfs_audio_input.hpp" -#include "i2s_audio_output.hpp" -#include "result.hpp" -#include "span.hpp" - -#include "audio_element.hpp" -#include "gpio_expander.hpp" -#include "storage.hpp" -#include "stream_buffer.hpp" - -namespace audio { - -/* - * Creates and links together audio elements into a pipeline. This is the main - * entrypoint to playing audio on the system. - */ -class AudioPlayback { - public: - explicit AudioPlayback(drivers::DriverCache* drivers); - ~AudioPlayback(); - - /* - * Begins playing the file at the given FatFS path. This will interrupt any - * currently in-progress playback. - */ - auto Play(const std::string& filename) -> void; - - auto LogStatus() -> void; - - // Not copyable or movable. - AudioPlayback(const AudioPlayback&) = delete; - AudioPlayback& operator=(const AudioPlayback&) = delete; - - private: - std::unique_ptr file_source_; - std::unique_ptr i2s_output_; - std::vector> elements_; -}; - -} // namespace audio diff --git a/src/audio/include/i2s_audio_output.hpp b/src/audio/include/i2s_audio_output.hpp index 07430777..53e1dd7a 100644 --- a/src/audio/include/i2s_audio_output.hpp +++ b/src/audio/include/i2s_audio_output.hpp @@ -18,7 +18,7 @@ namespace audio { class I2SAudioOutput : public IAudioSink { public: I2SAudioOutput(drivers::GpioExpander* expander, - std::shared_ptr dac); + std::weak_ptr dac); ~I2SAudioOutput(); auto Configure(const StreamInfo::Format& format) -> bool override; diff --git a/src/drivers/CMakeLists.txt b/src/drivers/CMakeLists.txt index 413e1ea0..25fdc187 100644 --- a/src/drivers/CMakeLists.txt +++ b/src/drivers/CMakeLists.txt @@ -1,6 +1,6 @@ idf_component_register( SRCS "touchwheel.cpp" "dac.cpp" "gpio_expander.cpp" "battery.cpp" "storage.cpp" "i2c.cpp" - "spi.cpp" "display.cpp" "display_init.cpp" "driver_cache.cpp" "samd.cpp" + "spi.cpp" "display.cpp" "display_init.cpp" "samd.cpp" INCLUDE_DIRS "include" REQUIRES "esp_adc" "fatfs" "result" "lvgl" "span") target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/drivers/display.cpp b/src/drivers/display.cpp index f8594a5a..2888903c 100644 --- a/src/drivers/display.cpp +++ b/src/drivers/display.cpp @@ -61,7 +61,7 @@ extern "C" void FlushDataCallback(lv_disp_drv_t* disp_drv, instance->OnLvglFlush(disp_drv, area, color_map); } -auto Display::create(GpioExpander* expander, +auto Display::Create(GpioExpander* expander, const displays::InitialisationData& init_data) -> Display* { ESP_LOGI(kTag, "Init I/O pins"); diff --git a/src/drivers/driver_cache.cpp b/src/drivers/driver_cache.cpp deleted file mode 100644 index 650e6f16..00000000 --- a/src/drivers/driver_cache.cpp +++ /dev/null @@ -1,43 +0,0 @@ -#include "driver_cache.hpp" - -#include -#include - -#include "display.hpp" -#include "display_init.hpp" -#include "storage.hpp" -#include "touchwheel.hpp" - -namespace drivers { - -DriverCache::DriverCache() : gpios_(std::make_unique()) {} -DriverCache::~DriverCache() {} - -auto DriverCache::AcquireGpios() -> GpioExpander* { - return gpios_.get(); -} - -auto DriverCache::AcquireDac() -> std::shared_ptr { - return Acquire(dac_, [&]() -> AudioDac* { - return AudioDac::create(AcquireGpios()).value_or(nullptr); - }); -} - -auto DriverCache::AcquireDisplay() -> std::shared_ptr { - return Acquire(display_, [&]() -> Display* { - return Display::create(AcquireGpios(), displays::kST7735R); - }); -} - -auto DriverCache::AcquireStorage() -> std::shared_ptr { - return Acquire(storage_, [&]() -> SdStorage* { - return SdStorage::create(AcquireGpios()).value_or(nullptr); - }); -} - -auto DriverCache::AcquireTouchWheel() -> std::shared_ptr { - return Acquire(touchwheel_, - [&]() -> TouchWheel* { return new TouchWheel(); }); -} - -} // namespace drivers diff --git a/src/drivers/include/battery.hpp b/src/drivers/include/battery.hpp index d50bb20f..302271b2 100644 --- a/src/drivers/include/battery.hpp +++ b/src/drivers/include/battery.hpp @@ -11,6 +11,7 @@ namespace drivers { class Battery { public: + static auto Create() -> Battery* { return new Battery(); } Battery(); ~Battery(); diff --git a/src/drivers/include/display.hpp b/src/drivers/include/display.hpp index 9e4a0224..f8a28503 100644 --- a/src/drivers/include/display.hpp +++ b/src/drivers/include/display.hpp @@ -22,7 +22,7 @@ class Display { * over SPI. This never fails, since unfortunately these display don't give * us back any kind of signal to tell us we're actually using them correctly. */ - static auto create(GpioExpander* expander, + static auto Create(GpioExpander* expander, const displays::InitialisationData& init_data) -> Display*; Display(GpioExpander* gpio, spi_device_handle_t handle); diff --git a/src/drivers/include/driver_cache.hpp b/src/drivers/include/driver_cache.hpp deleted file mode 100644 index c56ebc3f..00000000 --- a/src/drivers/include/driver_cache.hpp +++ /dev/null @@ -1,54 +0,0 @@ -#pragma once - -#include -#include - -#include "dac.hpp" -#include "display.hpp" -#include "gpio_expander.hpp" -#include "storage.hpp" -#include "touchwheel.hpp" - -namespace drivers { - -class DriverCache { - private: - std::unique_ptr gpios_; - std::weak_ptr dac_; - std::weak_ptr display_; - std::weak_ptr storage_; - std::weak_ptr touchwheel_; - // TODO(jacqueline): Haptics, samd - - std::mutex mutex_; - - template - auto Acquire(std::weak_ptr ptr, F factory) -> std::shared_ptr { - std::shared_ptr acquired = ptr.lock(); - if (acquired) { - return acquired; - } - - std::lock_guard lock(mutex_); - - acquired = ptr.lock(); - if (acquired) { - return acquired; - } - acquired.reset(factory()); - ptr = acquired; - return acquired; - } - - public: - DriverCache(); - ~DriverCache(); - - auto AcquireGpios() -> GpioExpander*; - auto AcquireDac() -> std::shared_ptr; - auto AcquireDisplay() -> std::shared_ptr; - auto AcquireStorage() -> std::shared_ptr; - auto AcquireTouchWheel() -> std::shared_ptr; -}; - -} // namespace drivers diff --git a/src/drivers/include/gpio_expander.hpp b/src/drivers/include/gpio_expander.hpp index cd3719a0..de1c8aba 100644 --- a/src/drivers/include/gpio_expander.hpp +++ b/src/drivers/include/gpio_expander.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -28,6 +29,8 @@ namespace drivers { */ class GpioExpander { public: + static auto Create() -> GpioExpander* { return new GpioExpander(); } + GpioExpander(); ~GpioExpander(); diff --git a/src/drivers/include/samd.hpp b/src/drivers/include/samd.hpp index 4ba5877c..8fe87c66 100644 --- a/src/drivers/include/samd.hpp +++ b/src/drivers/include/samd.hpp @@ -6,6 +6,8 @@ namespace drivers { class Samd { public: + static auto Create() -> Samd* { return new Samd(); } + Samd(); ~Samd(); diff --git a/src/drivers/include/storage.hpp b/src/drivers/include/storage.hpp index c19ec935..1b51ff7d 100644 --- a/src/drivers/include/storage.hpp +++ b/src/drivers/include/storage.hpp @@ -25,7 +25,7 @@ class SdStorage { FAILED_TO_MOUNT, }; - static auto create(GpioExpander* gpio) -> cpp::result; + static auto Create(GpioExpander* gpio) -> cpp::result; SdStorage(GpioExpander* gpio, esp_err_t (*do_transaction)(sdspi_dev_handle_t, sdmmc_command_t*), diff --git a/src/drivers/include/touchwheel.hpp b/src/drivers/include/touchwheel.hpp index 2bbc8e94..9daf572e 100644 --- a/src/drivers/include/touchwheel.hpp +++ b/src/drivers/include/touchwheel.hpp @@ -17,6 +17,7 @@ struct TouchWheelData { class TouchWheel { public: + static auto Create() -> TouchWheel* { return new TouchWheel(); } TouchWheel(); ~TouchWheel(); diff --git a/src/drivers/storage.cpp b/src/drivers/storage.cpp index d90bd811..49b817f2 100644 --- a/src/drivers/storage.cpp +++ b/src/drivers/storage.cpp @@ -49,7 +49,7 @@ static esp_err_t do_transaction(sdspi_dev_handle_t handle, } } // namespace callback -auto SdStorage::create(GpioExpander* gpio) -> cpp::result { +auto SdStorage::Create(GpioExpander* gpio) -> cpp::result { gpio->set_pin(GpioExpander::SD_CARD_POWER_ENABLE, 0); gpio->set_pin(GpioExpander::SD_MUX_EN_ACTIVE_LOW, 0); gpio->set_pin(GpioExpander::SD_MUX_SWITCH, GpioExpander::SD_MUX_ESP); diff --git a/src/events/CMakeLists.txt b/src/events/CMakeLists.txt new file mode 100644 index 00000000..dd99c70a --- /dev/null +++ b/src/events/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRCS "event_queue.cpp" + INCLUDE_DIRS "include" + REQUIRES "tinyfsm") +target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/events/event_queue.cpp b/src/events/event_queue.cpp new file mode 100644 index 00000000..b0bae857 --- /dev/null +++ b/src/events/event_queue.cpp @@ -0,0 +1,23 @@ +#include "event_queue.hpp" + +#include "freertos/FreeRTOS.h" +#include "freertos/queue.h" + +namespace events { + +static const std::size_t kMaxPendingEvents = 16; + +EventQueue::EventQueue() + : handle_(xQueueCreate(kMaxPendingEvents, sizeof(WorkItem*))) {} + +auto EventQueue::Service(TickType_t max_wait_time) -> bool { + WorkItem* item; + if (xQueueReceive(handle_, &item, max_wait_time)) { + (*item)(); + delete item; + return true; + } + return false; +} + +} // namespace events diff --git a/src/events/include/event_queue.hpp b/src/events/include/event_queue.hpp new file mode 100644 index 00000000..3eb59570 --- /dev/null +++ b/src/events/include/event_queue.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include + +#include "freertos/FreeRTOS.h" +#include "freertos/portmacro.h" +#include "freertos/queue.h" +#include "tinyfsm.hpp" + +namespace events { + +typedef std::function WorkItem; + +class EventQueue { + public: + static EventQueue& GetInstance() { + static EventQueue instance; + return instance; + } + + template + auto Dispatch(const Event& ev) -> void { + WorkItem* item = new WorkItem( + [=]() { tinyfsm::FsmList::template dispatch(ev); }); + xQueueSend(handle_, &item, portMAX_DELAY); + } + + auto Service(TickType_t max_wait_time) -> bool; + + EventQueue(EventQueue const&) = delete; + void operator=(EventQueue const&) = delete; + + private: + EventQueue(); + + QueueHandle_t handle_; +}; + +template +auto Dispatch(const Event& ev) -> void { + EventQueue& queue = EventQueue::GetInstance(); + queue.Dispatch(ev); +} + +} // namespace events diff --git a/src/main/CMakeLists.txt b/src/main/CMakeLists.txt index 524a7d30..c557a7df 100644 --- a/src/main/CMakeLists.txt +++ b/src/main/CMakeLists.txt @@ -1,5 +1,5 @@ idf_component_register( - SRCS "main.cpp" "app_console.cpp" + SRCS "main.cpp" INCLUDE_DIRS "." - REQUIRES "audio" "drivers" "dev_console" "drivers" "database" "ui") + REQUIRES "audio" "ui" "system_fsm" "events") target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/main/main.cpp b/src/main/main.cpp index 29ac2c7f..df1eb8b2 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -1,114 +1,18 @@ -#include -#include -#include - -#include -#include -#include - -#include "driver/gpio.h" -#include "driver/i2c.h" -#include "driver/sdspi_host.h" -#include "driver/spi_common.h" -#include "driver/spi_master.h" -#include "driver_cache.hpp" -#include "esp_freertos_hooks.h" -#include "esp_heap_caps.h" -#include "esp_intr_alloc.h" -#include "esp_log.h" #include "freertos/portmacro.h" -#include "freertos/projdefs.h" -#include "hal/gpio_types.h" -#include "hal/spi_types.h" -#include "app_console.hpp" -#include "audio_playback.hpp" -#include "battery.hpp" -#include "dac.hpp" -#include "database.hpp" -#include "display.hpp" -#include "display_init.hpp" -#include "gpio_expander.hpp" -#include "i2c.hpp" -#include "lvgl_task.hpp" -#include "samd.hpp" -#include "spi.hpp" -#include "storage.hpp" -#include "touchwheel.hpp" +#include "tinyfsm.hpp" -static const char* TAG = "MAIN"; +#include "audio_fsm.hpp" +#include "event_queue.hpp" +#include "system_fsm.hpp" +#include "ui_fsm.hpp" extern "C" void app_main(void) { - ESP_LOGI(TAG, "Initialising peripherals"); - - ESP_ERROR_CHECK(drivers::init_i2c()); - ESP_ERROR_CHECK(drivers::init_spi()); - std::unique_ptr drivers = - std::make_unique(); - - ESP_LOGI(TAG, "Init GPIOs"); - drivers::GpioExpander* expander = drivers->AcquireGpios(); - - ESP_LOGI(TAG, "Init SAMD comms"); - drivers::Samd samd; - ESP_LOGI(TAG, "It might have worked? Let's read something!"); - auto res = samd.ReadChargeStatus(); - if (res) { - ESP_LOGI(TAG, "Charge status is %d", static_cast(*res)); - } else { - ESP_LOGI(TAG, "no charge status?"); - } - - ESP_LOGI(TAG, "Enable power rails for development"); - expander->with( - [&](auto& gpio) { gpio.set_pin(drivers::GpioExpander::AMP_EN, 1); }); - - ESP_LOGI(TAG, "Init battery measurement"); - drivers::Battery* battery = new drivers::Battery(); - ESP_LOGI(TAG, "it's reading %d mV!", (int)battery->Millivolts()); - - ESP_LOGI(TAG, "Init SD card"); - auto storage = drivers->AcquireStorage(); - if (!storage) { - ESP_LOGE(TAG, "Failed! Do you have an SD card?"); - } - - ESP_LOGI(TAG, "Init touch wheel"); - std::shared_ptr touchwheel = - drivers->AcquireTouchWheel(); - - std::atomic lvgl_quit; - TaskHandle_t lvgl_task_handle; - ui::StartLvgl(drivers.get(), &lvgl_quit, &lvgl_task_handle); - - std::unique_ptr playback; - if (storage) { - ESP_LOGI(TAG, "Init audio pipeline"); - playback = std::make_unique(drivers.get()); - } - - ESP_LOGI(TAG, "Init database"); - std::unique_ptr db; - auto db_res = database::Database::Open(); - if (db_res.has_value()) { - db.reset(db_res.value()); - } - - ESP_LOGI(TAG, "Waiting for background tasks before launching console..."); - vTaskDelay(pdMS_TO_TICKS(1000)); - - ESP_LOGI(TAG, "Launch console"); - console::AppConsole console(playback.get(), db.get()); - console.Launch(); + tinyfsm::FsmList::start(); - uint8_t prev_position = 0; + auto& queue = events::EventQueue::GetInstance(); while (1) { - touchwheel->Update(); - auto wheel_data = touchwheel->GetTouchWheelData(); - if (wheel_data.wheel_position != prev_position) { - prev_position = wheel_data.wheel_position; - ESP_LOGI(TAG, "Touch wheel pos: %u", prev_position); - } - vTaskDelay(pdMS_TO_TICKS(100)); + queue.Service(portMAX_DELAY); } } diff --git a/src/system_fsm/CMakeLists.txt b/src/system_fsm/CMakeLists.txt new file mode 100644 index 00000000..829ad320 --- /dev/null +++ b/src/system_fsm/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRCS "system_fsm.cpp" "running.cpp" "booting.cpp" + INCLUDE_DIRS "include" + REQUIRES "tinyfsm" "drivers" "database" "ui" "result" "events" "audio") +target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/system_fsm/booting.cpp b/src/system_fsm/booting.cpp new file mode 100644 index 00000000..a2a920c8 --- /dev/null +++ b/src/system_fsm/booting.cpp @@ -0,0 +1,88 @@ +#include "assert.h" +#include "audio_fsm.hpp" +#include "core/lv_obj.h" +#include "display_init.hpp" +#include "esp_err.h" +#include "esp_log.h" +#include "event_queue.hpp" +#include "gpio_expander.hpp" +#include "lvgl/lvgl.h" +#include "spi.hpp" +#include "system_events.hpp" +#include "system_fsm.hpp" +#include "ui_fsm.hpp" + +#include "i2c.hpp" +#include "touchwheel.hpp" + +namespace system_fsm { +namespace states { + +static const char kTag[] = "BOOT"; + +auto Booting::entry() -> void { + ESP_LOGI(kTag, "beginning tangara boot"); + ESP_LOGI(kTag, "installing bare minimum drivers"); + + // I2C and SPI are both always needed. We can't even power down or show an + // error without these. + ESP_ERROR_CHECK(drivers::init_i2c()); + ESP_ERROR_CHECK(drivers::init_spi()); + + // These drivers are the bare minimum to even show an error. If these fail, + // then the system is completely hosed. + sGpioExpander.reset(drivers::GpioExpander::Create()); + assert(sGpioExpander != nullptr); + + // Start bringing up LVGL now, since we have all of its prerequisites. + ESP_LOGI(kTag, "starting ui"); + lv_init(); + sDisplay.reset(drivers::Display::Create(sGpioExpander.get(), + drivers::displays::kST7735R)); + assert(sDisplay != nullptr); + + // The UI FSM now has everything it needs to start setting up. Do this now, + // so that we can properly show the user any errors that appear later. + ui::UiState::Init(sGpioExpander.get(), sTouch, sDisplay, sDatabase); + events::Dispatch(DisplayReady()); + + // These drivers are required for normal operation, but aren't critical for + // booting. We will transition to the error state if these aren't present. + ESP_LOGI(kTag, "installing required drivers"); + sSamd.reset(drivers::Samd::Create()); + sTouch.reset(drivers::TouchWheel::Create()); + + auto dac_res = drivers::AudioDac::create(sGpioExpander.get()); + if (dac_res.has_error() || !sSamd || !sTouch) { + events::Dispatch( + FatalError()); + return; + } + sDac.reset(dac_res.value()); + + // These drivers are initialised on boot, but are recoverable (if weird) if + // they fail. + ESP_LOGI(kTag, "installing extra drivers"); + sBattery.reset(drivers::Battery::Create()); + + // All drivers are now loaded, so we can finish initing the other state + // machines. + audio::AudioState::Init(sGpioExpander.get(), sDac, sDatabase); + + events::Dispatch( + BootComplete()); +} + +auto Booting::react(const BootComplete& ev) -> void { + ESP_LOGE(kTag, "bootup completely successfully"); + // It's possible that the SAMD is currently exposing the SD card as a USB + // device. Make sure we don't immediately try to claim it. + if (sSamd && sSamd->ReadUsbMscStatus() == + drivers::Samd::UsbMscStatus::kAttachedMounted) { + transit(); + } + transit(); +} + +} // namespace states +} // namespace system_fsm diff --git a/src/system_fsm/include/system_events.hpp b/src/system_fsm/include/system_events.hpp new file mode 100644 index 00000000..77242531 --- /dev/null +++ b/src/system_fsm/include/system_events.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include "tinyfsm.hpp" + +namespace system_fsm { + +struct DisplayReady : tinyfsm::Event {}; + +/* + * Sent by SysState when the system has finished with its boot and self-test, + * and is now ready to run normally. + */ +struct BootComplete : tinyfsm::Event {}; + +/* + * May be sent by any component to indicate that the system has experienced an + * unrecoverable error. This should be used sparingly, as it essentially brings + * down the device. + */ +struct FatalError : tinyfsm::Event {}; + +/* + * Sent before unmounting the system storage. Storage will not be unmounted + * until each reaction to this even has returned. FSMs should immediately cease + * their usage of storage. + * + * May be emitted either by UiState in response to user action, or by SysState + * as a part of either entering low-power standby or powering off. + */ +struct StorageUnmountRequested : tinyfsm::Event {}; + +/* + * Sent by SysState when the system storage has been successfully mounted. + */ +struct StorageMounted : tinyfsm::Event {}; + +struct StorageError : tinyfsm::Event {}; + +namespace internal { + +/* + * Sent when the actual unmount operation should be performed. Always dispatched + * by SysState in response to StoragePrepareToUnmount. + */ +struct ReadyToUnmount : tinyfsm::Event {}; + +} // namespace internal + +} // namespace system_fsm diff --git a/src/system_fsm/include/system_fsm.hpp b/src/system_fsm/include/system_fsm.hpp new file mode 100644 index 00000000..c537c6de --- /dev/null +++ b/src/system_fsm/include/system_fsm.hpp @@ -0,0 +1,90 @@ +#pragma once + +#include + +#include "battery.hpp" +#include "dac.hpp" +#include "database.hpp" +#include "display.hpp" +#include "gpio_expander.hpp" +#include "samd.hpp" +#include "storage.hpp" +#include "tinyfsm.hpp" +#include "touchwheel.hpp" + +#include "system_events.hpp" + +namespace system_fsm { + +/* + * State machine for the overall system state. Responsible for managing + * peripherals, and bringing the rest of the system up and down. + */ +class SystemState : public tinyfsm::Fsm { + public: + virtual ~SystemState() {} + + virtual void entry() {} + virtual void exit() {} + + /* Fallback event handler. Does nothing. */ + void react(const tinyfsm::Event& ev) {} + + void react(const FatalError&); + + virtual void react(const DisplayReady&) {} + virtual void react(const BootComplete&) {} + virtual void react(const StorageUnmountRequested&) {} + virtual void react(const internal::ReadyToUnmount&) {} + virtual void react(const StorageMounted&) {} + + protected: + static std::shared_ptr sGpioExpander; + static std::shared_ptr sSamd; + + static std::shared_ptr sTouch; + static std::shared_ptr sBattery; + static std::shared_ptr sStorage; + static std::shared_ptr sDisplay; + static std::shared_ptr sDac; + static std::shared_ptr sDatabase; +}; + +namespace states { + +/* + * Initial state. Initialises peripherals, starts up lvgl, checks everything + * looks good. + */ +class Booting : public SystemState { + public: + void entry() override; + + void react(const BootComplete&) override; + using SystemState::react; +}; + +/* + * Most common state. Everything is going full bore! + */ +class Running : public SystemState { + public: + void entry() override; + void exit() override; + + void react(const StorageUnmountRequested&) override; + void react(const internal::ReadyToUnmount&) override; + using SystemState::react; +}; + +class Unmounted : public SystemState {}; + +/* + * Something unrecoverably bad went wrong. Shows an error (if possible), awaits + * reboot. + */ +class Error : public SystemState {}; + +} // namespace states + +} // namespace system_fsm diff --git a/src/system_fsm/running.cpp b/src/system_fsm/running.cpp new file mode 100644 index 00000000..e55989f1 --- /dev/null +++ b/src/system_fsm/running.cpp @@ -0,0 +1,54 @@ + +#include "result.hpp" + +#include "audio_fsm.hpp" +#include "event_queue.hpp" +#include "storage.hpp" +#include "system_events.hpp" +#include "system_fsm.hpp" +#include "ui_fsm.hpp" + +namespace system_fsm { +namespace states { + +/* + * Ensure the storage and database are both available. If either of these fails + * to open, then we assume it's an issue with the underlying SD card. + */ +void Running::entry() { + auto storage_res = drivers::SdStorage::Create(sGpioExpander.get()); + if (storage_res.has_error()) { + events::Dispatch( + StorageError()); + return; + } + sStorage.reset(storage_res.value()); + + auto database_res = database::Database::Open(); + if (database_res.has_error()) { + events::Dispatch( + StorageError()); + return; + } + sDatabase.reset(database_res.value()); + + events::Dispatch( + StorageMounted()); +} + +void Running::exit() { + sDatabase.reset(); + sStorage.reset(); +} + +void Running::react(const StorageUnmountRequested& ev) { + events::Dispatch( + internal::ReadyToUnmount()); +} + +void Running::react(const internal::ReadyToUnmount& ev) { + transit(); +} + +} // namespace states +} // namespace system_fsm diff --git a/src/system_fsm/system_fsm.cpp b/src/system_fsm/system_fsm.cpp new file mode 100644 index 00000000..c529df5e --- /dev/null +++ b/src/system_fsm/system_fsm.cpp @@ -0,0 +1,24 @@ +#include "system_fsm.hpp" +#include "system_events.hpp" + +namespace system_fsm { + +std::shared_ptr SystemState::sGpioExpander; +std::shared_ptr SystemState::sSamd; + +std::shared_ptr SystemState::sTouch; +std::shared_ptr SystemState::sBattery; +std::shared_ptr SystemState::sStorage; +std::shared_ptr SystemState::sDisplay; +std::shared_ptr SystemState::sDac; +std::shared_ptr SystemState::sDatabase; + +void SystemState::react(const FatalError& err) { + if (!is_in_state()) { + transit(); + } +} + +} // namespace system_fsm + +FSM_INITIAL_STATE(system_fsm::SystemState, system_fsm::states::Booting) diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index bdccbbdb..13c1e7e3 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -1,5 +1,5 @@ idf_component_register( - SRCS "lvgl_task.cpp" + SRCS "lvgl_task.cpp" "ui_fsm.cpp" INCLUDE_DIRS "include" - REQUIRES "drivers") + REQUIRES "drivers" "lvgl" "tinyfsm" "events" "system_fsm" "database") target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/ui/include/lvgl_task.hpp b/src/ui/include/lvgl_task.hpp index ca3fc771..32e01b7f 100644 --- a/src/ui/include/lvgl_task.hpp +++ b/src/ui/include/lvgl_task.hpp @@ -2,16 +2,18 @@ #include #include +#include #include "freertos/FreeRTOS.h" #include "freertos/task.h" -#include "driver_cache.hpp" +#include "display.hpp" +#include "touchwheel.hpp" namespace ui { -auto StartLvgl(drivers::DriverCache* drivers, - std::atomic* quit, - TaskHandle_t* handle) -> bool; +auto StartLvgl(std::weak_ptr touch_wheel, + std::weak_ptr display, + std::atomic* quit) -> bool; } // namespace ui diff --git a/src/ui/include/ui_events.hpp b/src/ui/include/ui_events.hpp new file mode 100644 index 00000000..2df3dc5f --- /dev/null +++ b/src/ui/include/ui_events.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include "tinyfsm.hpp" + +namespace ui { + +// TODO(jacqueline): is this needed? is this good? +/* + * Event emitted by the main task on heartbeat. + */ +struct OnStorageChange : tinyfsm::Event { + bool is_mounted; +}; + +struct OnSystemError : tinyfsm::Event {}; + +} // namespace ui diff --git a/src/ui/include/ui_fsm.hpp b/src/ui/include/ui_fsm.hpp new file mode 100644 index 00000000..3c06b8a0 --- /dev/null +++ b/src/ui/include/ui_fsm.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include + +#include "database.hpp" +#include "display.hpp" +#include "storage.hpp" +#include "tinyfsm.hpp" +#include "touchwheel.hpp" + +#include "system_events.hpp" + +namespace ui { + +class UiState : public tinyfsm::Fsm { + public: + static auto Init(drivers::GpioExpander* gpio_expander, + std::weak_ptr touchwheel, + std::weak_ptr display, + std::weak_ptr database) -> void; + + virtual ~UiState() {} + + virtual void entry() {} + virtual void exit() {} + + /* Fallback event handler. Does nothing. */ + void react(const tinyfsm::Event& ev) {} + + virtual void react(const system_fsm::DisplayReady&) {} + virtual void react(const system_fsm::BootComplete&) {} + + protected: + static drivers::GpioExpander* sGpioExpander; + static std::weak_ptr sTouchWheel; + static std::weak_ptr sDisplay; + static std::weak_ptr sDatabase; + static std::atomic sTaskQuit; +}; + +namespace states { + +class PreBoot : public UiState { + public: + void react(const system_fsm::DisplayReady&) override; + using UiState::react; +}; + +class Splash : public UiState { + public: + void react(const system_fsm::BootComplete&) override; + using UiState::react; +}; + +class Interactive : public UiState {}; + +class FatalError : public UiState {}; + +} // namespace states + +} // namespace ui diff --git a/src/ui/lvgl_task.cpp b/src/ui/lvgl_task.cpp index 12dfd34e..70df9f72 100644 --- a/src/ui/lvgl_task.cpp +++ b/src/ui/lvgl_task.cpp @@ -23,10 +23,10 @@ #include "misc/lv_color.h" #include "misc/lv_style.h" #include "misc/lv_timer.h" +#include "touchwheel.hpp" #include "widgets/lv_label.h" #include "display.hpp" -#include "driver_cache.hpp" #include "gpio_expander.hpp" namespace ui { @@ -38,27 +38,23 @@ auto tick_hook(TimerHandle_t xTimer) -> void { } struct LvglArgs { - drivers::DriverCache* drivers; + std::weak_ptr touch_wheel; + std::weak_ptr display; std::atomic* quit; }; void LvglMain(void* voidArgs) { LvglArgs* args = reinterpret_cast(voidArgs); - drivers::DriverCache* drivers = args->drivers; + std::weak_ptr weak_touch_wheel = args->touch_wheel; + std::weak_ptr weak_display = args->display; + std::atomic* quit = args->quit; delete args; { - ESP_LOGI(kTag, "init lvgl"); - lv_init(); - - // LVGL has been initialised, so we can now start reporting ticks to it. TimerHandle_t tick_timer = xTimerCreate("lv_tick", pdMS_TO_TICKS(1), pdTRUE, NULL, &tick_hook); - ESP_LOGI(kTag, "init display"); - std::shared_ptr display = drivers->AcquireDisplay(); - lv_style_t style; lv_style_init(&style); lv_style_set_text_color(&style, LV_COLOR_MAKE(0xFF, 0, 0)); @@ -74,7 +70,9 @@ void LvglMain(void* voidArgs) { while (!quit->load()) { lv_timer_handler(); - vTaskDelay(pdMS_TO_TICKS(10)); + // 30 FPS + // TODO(jacqueline): make this dynamic + vTaskDelay(pdMS_TO_TICKS(33)); } // TODO(robin? daniel?): De-init the UI stack here. @@ -82,8 +80,6 @@ void LvglMain(void* voidArgs) { lv_style_reset(&style); xTimerDelete(tick_timer, portMAX_DELAY); - - lv_deinit(); } vTaskDelete(NULL); @@ -93,11 +89,12 @@ static const size_t kLvglStackSize = 8 * 1024; static StaticTask_t sLvglTaskBuffer = {}; static StackType_t sLvglStack[kLvglStackSize] = {0}; -auto StartLvgl(drivers::DriverCache* drivers, - std::atomic* quit, - TaskHandle_t* handle) -> bool { +auto StartLvgl(std::weak_ptr touch_wheel, + std::weak_ptr display, + std::atomic* quit) -> bool { LvglArgs* args = new LvglArgs(); - args->drivers = drivers; + args->touch_wheel = touch_wheel; + args->display = display; args->quit = quit; return xTaskCreateStaticPinnedToCore(&LvglMain, "LVGL", kLvglStackSize, diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp new file mode 100644 index 00000000..8ca201e2 --- /dev/null +++ b/src/ui/ui_fsm.cpp @@ -0,0 +1,38 @@ +#include "ui_fsm.hpp" +#include "display.hpp" +#include "lvgl_task.hpp" +#include "system_events.hpp" +#include "touchwheel.hpp" + +namespace ui { + +drivers::GpioExpander* UiState::sGpioExpander; +std::weak_ptr UiState::sTouchWheel; +std::weak_ptr UiState::sDisplay; +std::weak_ptr UiState::sDatabase; +std::atomic UiState::sTaskQuit; + +auto UiState::Init(drivers::GpioExpander* gpio_expander, + std::weak_ptr touchwheel, + std::weak_ptr display, + std::weak_ptr database) -> void { + sGpioExpander = gpio_expander; + sTouchWheel = touchwheel; + sDisplay = display; + sDatabase = database; +} + +namespace states { + +void PreBoot::react(const system_fsm::DisplayReady& ev) { + transit([&]() { StartLvgl(sTouchWheel, sDisplay, &sTaskQuit); }); +} + +void Splash::react(const system_fsm::BootComplete& ev) { + transit(); +} + +} // namespace states +} // namespace ui + +FSM_INITIAL_STATE(ui::UiState, ui::states::PreBoot) diff --git a/tools/cmake/common.cmake b/tools/cmake/common.cmake index 1c24c5db..9eb599d1 100644 --- a/tools/cmake/common.cmake +++ b/tools/cmake/common.cmake @@ -13,6 +13,7 @@ list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/libtags") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/lvgl") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/result") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/span") +list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/tinyfsm") include($ENV{IDF_PATH}/tools/cmake/project.cmake)