Hans M. und ich ham vor über 8 Jahren mal eine Regelung für sein Ferienhaus gebaut: https://stefan.box2code.de/2018/05/11/warm-wasser-steuerung/
Neulich kamen wir mal wieder auf das Thema und ham entschieden dass wir das heute viel einfacher, besser und vorallem schneller könnten. Tatsächlich ham wir bis jetzt nur 2 Wochenenden dafür gebraucht

Die neue Regelung basiert jetzt auf Linux mit QT/QML-Applikation. Zu finden hier: https://bitbucket.org/bobbery/wwc2/src/main/

Alles in Allem besteht die ganze Anwendung aus unter 500 Zeilen einfachen QML-Code der unglaublich primitiv und daher wahrscheinlich auch gut zu warten ist.
Der C++ – Code enthält nur ein paar Treiber, mit denen man aus dem QML-Code heraus leicht die Hardware ansprechen kann.

Die Applikation ist noch nicht ganz fertig (es fehlt noch die eigentliche Regelschleife), da sich ein Großteil der Hardware grad noch in Italien befindet. Ich schreib hier wenn gar fertig und mit Hardware getestet

Das Hauptbild ist einfach mit Inkscape und Cliparts von https://openclipart.org/ gezeichnet

Natürlich lassen sich die Parameter der Regelung leicht über TouchPad ändern und beiben nach Neustart erhalten:

Du kannst dir das Repo klonen und erstmal einfach im QtCreator auf deinem PC ausführen. Etwas interessanter ist aber wie man das Ding auf den Linux-Controller bekommt und wie man am geschicktesten die Hardware anbindet

Die Hardware

  • RPI-Zero 2W: weil der schön klein und billig is 🙂
  • HDMI-TS-Display 7″ – bekommt man für um die 40€ – über HDMI und USB angeschlossen
  • Einen MAX31865 – mit dem kann man einfache Widerstands-Temperatursensoren (PT-100) auslesen – wir brauchen 4 Sensoren aber nur einen MAX31865 da wir die Sensoren einfach über Relais multiplexen
  • Eine 8-Fach Relaiskarte – bekommt man für unter 10€
  • Einen PCF8574 – mit dem bekommen wir die Relaiskarte angesteuert
  • Einen Flusssensor, der impulse liefert wenn Wasser durchfließt

Alles in allem unter 100€ Material. Das Gehäuse ist einfach mit dem 3D-Drucker gedruckt.

Befestigt ist das Ganze mit etwas Bauschaum. Wenn du dich fragst warum Bauschaum: Wir hatten auf Arbeit mal bei einem Prototypen ein Display mit Heißkleber fixiert. Leider stand das ganze dann mal für eine Weile in der Hitze von Miami und viel auseinander 🙁 Deshalb auf keinen Fall mehr Heißkleber 🙂

Wir benutzen am RPI nur wenige Pins:

  • GPIO23 als Eingang für den Flusssensor (auf Tastaturtaste F12 gemapped)
  • SDA und SCL (GPIO2 & 3) zum Ansteuern der Relaiskarte über PCF8574 (da man mehrere Bausteine an einem Bus betreiben kann könnten wir damit sogar bis zu 64 Relais schalten)
  • MOSI, MISO, SCK, CS (GPIO8-11) zum Auslesen der PT-100 Sensoren über MAX31865

Normalerweise bau ich mir fast immer ein eigenes Linux-Image mit buildroot. Da hier aber Einfachheit zählt und die Bootzeit ziemlich egal ist, nehmen wir einfach ein fertiges Raspberry Pi OS Lite

Linux Image vorbereiten

Am einfachsten geht das über den Raspberry Pi Imager. Sorge beim Flashen nur dafür, dass SSH aktiviert und mit deinem WLAN verbunden wird.

Läuft dein PI, dann verbinde dich über SSH und installiere mit sudo apt install qt6-*{dev}* cmake Alles was wir so brauchen.

Ändere deine /boot/firmware/config.txt auf folgenden Inhalt

dtparam=spi=on
dtparam=i2c_arm=on
dtoverlay=vc4-kms-v3d
dtoverlay=gpio-key,gpio=23,active_low=1,gpio_pull=up,keycode=88 # mapped to F12

Danach ist ein Reboot angebracht

Kopiere den Quellcode in dein Heimatverzeichnis auf dem PI (mit scp, FileZilla, WinSCP, o.ä.)
Mache dir ein Verzeichnis build in deinem Heimatverzeichnis auf dem PI und führe aus dem Verzeichnis heraus cmake ../wwc2; make aus.

Jetzt kannst du die Applikation mit ~/build/appwwc2 -platform eglfs starten

Damit das beim starten automatisch passiert, kannst du den Befehl an deine ~/.bashrc am Ende anhängen

Aktiviere noch schnell über sudo raspi-config den Auto-Login

Wenn du jetzt neu startest, sollte nach dem Booten die Applikation laufen

(optional) Image wartbar machen

Damit du auch außerhalb deines WLANs auf deinen PI zugreifen kannst, packst du in deine /etc/profile die Zeile nmcli device wifi hotspot ssid wwc2 password 12345678 so macht dein PI einen WLAN-Hotspot auf. SSH funktioniert dann über die Adresse 10.42.0.1

Ich möcht in diesem Beitrag zeigen, dass man Signalerarbeitung auch ohne viel Theorie und Mathematik hinbekommt und hab dafür eine Bibliothek geschrieben, die klein, supereinfach zu verwenden und vorallem auf einem ESP mit Micropython ihren Dienst tuen kann.

Continue reading

Mein Vermieter Hans fragte mich neulich ob man irgendwie die Daten seines Ergometers (ein altes Daum ergo_bike) in seinen neuen Fahrradcomputer ( Einen Garmin Edge 820 ) bekommen kann,
da er in seinem Fahrradcomputer gerne seine gesamten Fitnessdaten für Rennrad und Ergometer hätte

Bei Daum bekommt man für das ergo_bike eine schöne Schnittstellenbeschreibung:

Man kann erkennen, dass die sog. Run_Daten alles enthalten was man so braucht – Leistung, Trittfrequenz, Geschwindigkeit und Puls

Auf der Rückseite des Cockpits findet man den zugehörigen Stecker (oben):

Der Stecker sieht zwar wie RS-232 aus, hat aber etwas andere Pegel (Daum verwendet statt max232 2 Optokoppler für RX und TX) – Deswegen ein PullUp an pin 2 des MAX232.

GND und VCC hab ich der Übersichtlichkeit halber nicht eingezeichnet (und natürlich weil ich faul bin 🙂 )

Das war schon die Hardware – du wirst dich wundern warum 2 ESP32. Leider hat der Fahrradcomputer mehrere Sensoren pro Gerät nicht verstanden und es ging nicht anders 🙁

Nach etwas Micropython hats dann funktioniert:

Code Power-Sensor ( für Trittfrequenz, Leistung und Kadenz )

(oberer ESP32)

Tut mir leid, ist etwas schnell und schlampig geschrieben aber erfüllt seinen Zweck ganz gut

Du brauchst Micropython und die Datei https://github.com/micropython/micropython/blob/master/examples/bluetooth/ble_advertising.py aus den Micropython-Beispielen auf deinem Device. Natürlich hab ich mich auch großzügig an den Beispielen dort bedient – ist doch klar 🙂

Das Protokoll zwischen Fahrradcomputern und Sensoren ist am besten beschrieben unter https://www.bluetooth.com/specifications/specs/gatt-specification-supplement-5/
und natürlich viele viele andere Typen von Sensoren (z.B. Temperatur, Luftfeuchte …) man findet für fast Alles was. Vielleicht kannst du hier was brauchen für dein Sensorprojekt – es ist sehr leicht BLE-Sensoren zu implementieren ( besonders mit Micropython, finde ich 🙂 )

import bluetooth
import random
import struct
import time
from ble_advertising import advertising_payload

from micropython import const

from machine import UART

uart = UART(2, 9600)                         
uart.init(9600, bits=8, parity=None, stop=1) 

_IRQ_CENTRAL_CONNECT = const(1)
_IRQ_CENTRAL_DISCONNECT = const(2)
_IRQ_GATTS_INDICATE_DONE = const(20)

_FLAG_READ = const(0x0002)
_FLAG_NOTIFY = const(0x0010)
_FLAG_INDICATE = const(0x0020)

_UUID_POWER = bluetooth.UUID(0x1818)

_POWER_CHAR = (
    bluetooth.UUID(0x2A63),
    _FLAG_READ | _FLAG_NOTIFY | _FLAG_INDICATE,
)
_POWER_FEAT_CHAR = (
    bluetooth.UUID(0x2A65),
    _FLAG_READ | _FLAG_NOTIFY | _FLAG_INDICATE,
)
_POWER_LOC_CHAR = (
    bluetooth.UUID(0x2A5D),
    _FLAG_READ | _FLAG_NOTIFY,
)
_SERVICE_POWER = (
    _UUID_POWER,
    (_POWER_CHAR,_POWER_FEAT_CHAR,_POWER_LOC_CHAR,),
)

SERVICES = (_SERVICE_POWER,)

class BLEErgo:
    def __init__(self, ble, name="HansErgoPower"):
        self._ble = ble
        self._ble.active(True)
        self._ble.irq(self._irq)
        ((self._handlePower, self._handlePowerFeat,self._handlePowerLoc,),) = self._ble.gatts_register_services(SERVICES)
        self._connections = set()
        self._payload = advertising_payload(
            name=name, services=[_UUID_POWER]
        )
        self._advertise()
        self.lastCrankRev = 0
        self.lastWheelRev = 0
        self.lastCrankEvent = 0
        self.lastWheelEvent = 0
        
        dat = bytearray()
        dat.append(0x0C) # speed and cadence
        dat.append(0x00)
        dat.append(0x00)
        dat.append(0x00)

        self._ble.gatts_write(self._handlePowerFeat, dat)
        
        dat = bytearray()
        dat.append(6) # right crank
        
        self._ble.gatts_write(self._handlePowerLoc, dat)

    def _irq(self, event, data):
        if event == _IRQ_CENTRAL_CONNECT:
            conn_handle, _, _ = data
            self._connections.add(conn_handle)
        elif event == _IRQ_CENTRAL_DISCONNECT:
            conn_handle, _, _ = data
            self._connections.remove(conn_handle)
            self._advertise()
        elif event == _IRQ_GATTS_INDICATE_DONE:
            conn_handle, value_handle, status = data

    def setPower(self, pwr, rpm, speed, notify=False, indicate=False):
        
        self.lastWheelRev += 1
        self.lastCrankRev += 1
        self.lastCrankEvent += int(60 / rpm * 1024)
        
        rnd = int(15500 / speed)
        self.lastWheelEvent += rnd
        
        print(rnd)
     
        data = bytearray()
        data.append(0x30) # wheel and crank rev present
        data.append(0x00)        
        data+= ((int(pwr)).to_bytes(2, 'little'))
        data+= ((int(self.lastWheelRev)).to_bytes(4, 'little'))
        data+= ((int(self.lastWheelEvent)).to_bytes(2, 'little'))
        data+= ((int(self.lastCrankRev)).to_bytes(2, 'little'))
        data+= ((int(self.lastCrankEvent)).to_bytes(2, 'little'))
        data.append(0x00)
        data.append(0x00)
        data.append(0x00)
        data.append(0x00)
        data.append(0x00)

        self._ble.gatts_write(self._handlePower, data)
        if notify or indicate:
            for conn_handle in self._connections:
                if notify:
                    self._ble.gatts_notify(conn_handle, self._handlePower)
                if indicate:
                    self._ble.gatts_indicate(conn_handle, self._handlePower)


    def _advertise(self, interval_us=100000):
        self._ble.gap_advertise(interval_us, adv_data=self._payload)



def app():
    ble = bluetooth.BLE()
    ergo = BLEErgo(ble)

    while True:
        dat = bytearray()
        dat.append(0x40)
        dat.append(0x00)
        uart.write(dat)
        time.sleep_ms(10)
        dat = bytearray()
        while uart.any():
             dat += uart.read()
        
        if len(dat) > 14:
            speed = dat[7]
            cadence = dat[6]
            power = dat[5] * 5
            pulse = dat[14]
            if cadence == 0:
                cadence = 1
            if speed == 0:
                speed = 1

            print ("Leistung: " + str(power))
            print ("Geschwindigkeit: " + str(speed))
            print ("Kadenz: " + str(cadence))
            
            ergo.setPower(power, cadence, speed, notify=True, indicate=False)
        else:
            ergo.setPower(42, 42, 42, notify=True, indicate=False)
        
        time.sleep_ms(1000)

if __name__ == "__main__":
    app()

Code Speed-Sensor:

(unterer ESP32)

import bluetooth
import random
import struct
import time
from ble_advertising import advertising_payload

from micropython import const

from machine import UART

uart = UART(2, 9600)                         
uart.init(9600, bits=8, parity=None, stop=1) 

_IRQ_CENTRAL_CONNECT = const(1)
_IRQ_CENTRAL_DISCONNECT = const(2)
_IRQ_GATTS_INDICATE_DONE = const(20)

_FLAG_WRITE = const(0x0001)
_FLAG_READ = const(0x0002)
_FLAG_NOTIFY = const(0x0010)
_FLAG_INDICATE = const(0x0020)

_UUID_SPEED = bluetooth.UUID(0x1816)

_SPEED_CHAR = (
    bluetooth.UUID(0x2A5B),
    _FLAG_NOTIFY,
)
_SPEED_FEAT_CHAR = (
    bluetooth.UUID(0x2A5C),
    _FLAG_READ,
)
_SPEED_LOC_CHAR = (
    bluetooth.UUID(0x2A5D),
    _FLAG_READ,
)
_SPEED_CONTROL_CHAR = (
    bluetooth.UUID(0x2A55),
    _FLAG_WRITE | _FLAG_INDICATE,
)
_SPEED_CONTROL_CHAR = (
    bluetooth.UUID(0x2A55),
    _FLAG_WRITE | _FLAG_INDICATE,
)

_SERVICE_SPEED = (
    _UUID_SPEED,
    (_SPEED_CHAR,_SPEED_FEAT_CHAR,_SPEED_LOC_CHAR,_SPEED_CONTROL_CHAR,),
)

SERVICES = (_SERVICE_SPEED,)

class BLEErgo:
    def __init__(self, ble, name="HansErgoSpeed"):
        self._ble = ble
        self._ble.active(True)
        self._ble.irq(self._irq)
        ((self._handleSpeed,self._handleSpeedFeat,self._handleSpeedLoc,self._handleSpeedControl,),) = self._ble.gatts_register_services(SERVICES)
        self._connections = set()
        self._payload = advertising_payload(
            name=name, services=[_UUID_SPEED]
        )
        self._advertise()
        self.lastWheelEvent = 0
        self.lastWheelRev = 0
        
        dat = bytearray()
        dat.append(0x01) # speed 
        dat.append(0x00)

        self._ble.gatts_write(self._handleSpeedFeat, dat)
        
        dat = bytearray()
        dat.append(12) # rear wheel
        
        self._ble.gatts_write(self._handleSpeedLoc, dat)

        dat = bytearray()
        dat.append(0x02) 
        dat.append(0x00) 
        
        self._ble.gatts_write(self._handleSpeedControl, dat)


    def _irq(self, event, data):
        if event == _IRQ_CENTRAL_CONNECT:
            conn_handle, _, _ = data
            self._connections.add(conn_handle)
        elif event == _IRQ_CENTRAL_DISCONNECT:
            conn_handle, _, _ = data
            self._connections.remove(conn_handle)
            self._advertise()
        elif event == _IRQ_GATTS_INDICATE_DONE:
            conn_handle, value_handle, status = data

    def setSpeed(self, speed, notify=False, indicate=False):
        
        self.lastWheelRev += 1
        self.lastWheelEvent += int(15500 / speed / 2)
     
        data = bytearray()
        data.append(0x01) # wheel rev present      
        data+= ((int(self.lastWheelRev)).to_bytes(4, 'little'))
        data+= ((int(self.lastWheelEvent)).to_bytes(2, 'little'))
        
        self._ble.gatts_write(self._handleSpeed, data)
        if notify or indicate:
            for conn_handle in self._connections:
                if notify:
                    self._ble.gatts_notify(conn_handle, self._handleSpeed)
                if indicate:
                    self._ble.gatts_indicate(conn_handle, self._handleSpeed)


    def _advertise(self, interval_us=100000):
        self._ble.gap_advertise(interval_us, adv_data=self._payload)



def app():
    ble = bluetooth.BLE()
    ergo = BLEErgo(ble)
    dat = bytearray()
    while True:
        
        while uart.any():
             dat += uart.read()
        
        if len(dat) > 14:
            speed = dat[7]
            cadence = dat[6]
            power = dat[5] * 5
            pulse = dat[14]
            if cadence == 0:
                cadence = 1
            if speed == 0:
                speed = 1
            dat = bytearray()

            print ("Geschwindigkeit: " + str(speed))

            ergo.setSpeed(speed, notify=True, indicate=False)
            time.sleep_ms(100)
        else:
            #ergo.setSpeed(42, notify=True, indicate=False)
            time.sleep_ms(1000)

if __name__ == "__main__":
    app()

Ich hab mir vor einiger Zeit ein HANMATEK HO52S Taschenoszi gekauft und bin total begeistert von dem Gerät. Analogbandbreite von 50 MHz, 2 Kanäle und einen integrierten Funktionsgenerator – und das ganze für unter 200 €

Das einzige von dem ich nicht begeistert war, war die PC-Software dazu. Leider nur Windows und unter VirtualBox gings leider auch ned 🙁 Hab mir die Anleitung angeschaut und gesehen, dass man damit eh nicht sehr viel machen kann außer Daten vom Oszi runterzuladen. Eine wirkliche Bedienung des DSOs wäre damit garnicht möglich gewesen.

Hab mir das Gerät etwas genauer angeschaut und wollte rausfinden ob es irgendeine Möglichkeit gibt es auch über den PC anzusprechen. Nach etwas Google hab ich schnell herausgefunden, dass die Firmware des Geräts eigentlich von Owon kommt. Für einige Geräte von Owon gibt es eine Schnittstellendoku im Netz.

Zu meiner großen Freude benutzt das Gerät das supereinfache SCPI-Protokoll über USB – Und ich hatte schon ein Ziel für meinen Urlaub 🙂

Das Bild indem die Kurven dargestellt werden, hab ich einfach vom Oszi selber – Screenshot gemacht, 3x vergrößert und alle Werte wegradiert 🙂 Dann einfach die Werte von der Schnittstelle drüberzeichnen.

Die Software müsste auch für andere ähnliche DSOs von Owon und OEMs funktionieren. Ich würde mich freuen von dir zu hören, wenn du ein anderes Gerät erfolgreich damit zum Laufen bekommst.

Der Quellcode (natürlich GPL) liegt unter https://bitbucket.org/bobbery/qpocketscope (ist sehr einfach nur ca. 300 LOC)

Als erstes kommen natürlich die Linux-Nutzer dran:

https://stefan.box2code.de/huge_files/QPocketScope_2024_11_05.tar.gz

Im Archiv findest du ein AppImage, dass du einfach ausführen kannst. Leider musst du noch (als root) install.sh ausführen. Das fügt eine udev-Rule hinzu ohne die eine kommunikation mit dem DSO nicht möglich ist (zumindest als nicht-root).

Die Windows-Benutzer müssen sich noch etwas gedulden. Ich brauche jemand mit einem Windows-Rechner zum Testen, da es leider unter VirtualBox nicht geht (die SW vom Hersteller aber auch nicht).
Natürlich würde ich mich riesig freuen, wenn ein/e Windows-Nutzer/in mit DSO dies liest und sich zur Verfügung stellt 🙂

Viel Spaß damit – euer Stefan

StateMachines sind das Brot und Butter eines (embedded) Softwareentwicklers. Die meisten StateMachine Frameworks für C++ sind aber sehr kompliziert und verwenden unglaublich viel Templates oder hängen von irgendwelchen Bibliotheken ab. Hab grad Urlaub und hab mir gedacht ich bau mir jetzt endlich ein eigenes Statemachine-Framework. Überraschenderweise war das recht schnell erledigt (ca. 2 Tage). So viel muss ein StateMachine-Framework ja eigentlich garned können hab ich gemerkt. Was mich sehr gewundert hat ist, dass sowas wie parallele States in vielen Frameworks garnicht möglich sind oder sehr aufwendig. Rausgekommen sind im ersten Schuss etwa 100 Zeilen Code in einer einzigen Headerdatei. Auch wenn ich beim Testen recht leicht auf 100% Coverage kam, bin ich nicht sicher, dass das Ding fehlerfrei ist. Also erstmal vorsichtig damit – Wenn du Fehler findest, bitte her damit. Ich hab bestimmt ein paar versteckt 🙂

Features

  • Eventbasiert
  • Einfache Initialisierung (alles an einer Stelle bzw. Methode)
  • SubStates und parallelle States
  • Single header mit nur etwa 130 LOC – reinkopieren und loslegen – keine Abhängigkeiten zu irgendwelchen Bibliotheken – STL only
  • Interface mit nur einer Methode die lediglich Events entgegennimmt
  • Fast keine Templates – C++11 – auch für alte Compiler
  • Geeignet für Embedded-Systeme (auch Bare-Metal mit wenig RAM, FreeRTOS, Arduino, mbed)
  • Verschachtelbar – mehrere StateMachines in einem System einfach möglich
  • Wenig Speicherbedarf – eine einfache StateMachine braucht ca. 1,4 kB (gemessen auf ESP32 mit Arduino)
  • Schnell – Stateübergang in 5 us auf PC. Auf ESP32 in 75 us (ohne Logging)
  • Initialisierung aller States, Transitions und Reactions in nur einer überschriebenen Methode
  • Einfach zu verbinden mit dem Rest des Systems – kleine Aktionen über Lambdas die mit dem „Rest“ verknüpfen
  • So einfach, dass durch das kleine Beispiel bereits getestet mit 100% LineCoverage (coverage als html liegt mit im Repo)
  • Update 2024-11-04: Hab einen Timer-Mechanismus eingebaut (ist zwar kein standard StateMachine-Feature, hab ich aber schon oft gebraucht)

Das Repo liegt hier: https://bitbucket.org/bobbery/mystatemachine hab qmake verwendet, damit mans leicht im QtCreator aufmachen und übersetzen kann. Braucht natürlich kein Qt – aber für ein Makefile für jede Plattform bin ich zu faul und qmake hab ich viel lieber als CMake

Das ist die StateMachine – ist einfach nur ein Header mit knapp über 100 LOC

/* Copyright 2024 stefan.box2code.de
 *            
 * Use of this source code is governed by an MIT-style
 * license that can be found in the LICENSE file or at
 * https://opensource.org/licenses/MIT.
 */
#ifndef STATEMACHINE_H
#define STATEMACHINE_H

#include <functional>
#include <iostream>
#include <map>
#include <set>
#include <string>
#include <vector>

#define LOG_EVENTS
#define LOG_STATES

template<class StateID, class EventID>
class StateMachine
{
public:
    StateMachine() {}

    void react(EventID e) { react_p(e); }
    void handleTimerTick() { handleTimerTick_p(); }

    virtual void init() = 0; // overwrite it to create your machine

    virtual ~StateMachine() {}

protected:
    void react_p(EventID e)
    {
#ifdef LOG_EVENTS
        if (m_eventNames.find(e) != m_eventNames.end()) {
            std::cout << "on" << m_eventNames.find(e)->second << "()" << std::endl;
        }
#endif
        bool transitOk = true;
        for (StateID sid : m_activeStates) {
            if (m_reactions.find(std::make_pair(sid, e)) != m_reactions.end()) {
                if (!m_reactions.find(std::make_pair(sid, e))->second()) {
                    transitOk = false;
                }
            }
        }
        if (transitOk) {
            transit(e);
        }
    }

    void addInitialState(StateID sid, const std::string &name, StateID parent = StateID::NumStates)
    {
        m_stateNames[sid] = name;
        if (parent != StateID::NumStates) {
            m_childStates[parent].insert(sid);
        } else {
            m_activeStates.insert(sid);
        }
        m_initials[parent].insert(sid);
    }
    void addState(StateID sid, const std::string &name, StateID parent = StateID::NumStates)
    {
        m_stateNames[sid] = name;
        if (parent != StateID::NumStates) {
            m_childStates[parent].insert(sid);
        }
    }
    void addEventName(EventID eid, const std::string &name) { m_eventNames[eid] = name; }
    void addReaction(StateID sid, EventID eid, std::function<bool()> fun)
    {
        m_reactions.emplace(std::make_pair(sid, eid), fun);
    }
    void addEntry(StateID sid, std::function<bool()> fun) { m_entry.emplace(sid, fun); }
    void addExit(StateID sid, std::function<bool()> fun) { m_exit.emplace(sid, fun); }
    void addTransition(StateID sid, EventID eid, StateID target)
    {
        m_transitions.emplace(std::make_pair(sid, eid), target);
    }

    void transit(EventID e)
    {
        std::set<StateID> newActiveStates;
        for (StateID id : m_activeStates) {
            if (m_transitions.find(std::make_pair(id, e)) != m_transitions.end()) {
                StateID activated = m_transitions.find(std::make_pair(id, e))->second;
                newActiveStates.insert(activated);
                if (m_initials.find(activated) != m_initials.end()) {
                    for (StateID subInitial : m_initials.find(activated)->second) {
                        newActiveStates.insert(subInitial);
                    }
                }
            } else {
                newActiveStates.insert(id);
            }
        }

        std::set<StateID> deactivated;
        for (StateID id : m_activeStates) {
            if (newActiveStates.count(id) == 0) {
                deactivated.insert(id);
            }
        }

        for (StateID id : deactivated) {
            if (m_childStates.find(id) != m_childStates.end()) {
                for (StateID chId : m_childStates.at(id)) {
                    newActiveStates.erase(chId);
                    if (m_exit.find(chId) != m_exit.end()) {
                        m_exit.at(chId)();
                    }
                }
            }
            if (m_exit.find(id) != m_exit.end()) {
#ifdef LOG_STATES
                std::cout << "onExit() of " << m_stateNames.at(id) << std::endl;
#endif
                m_exit.at(id)();
            }
        }

        for (StateID id : newActiveStates) {
            if (m_activeStates.count(id) == 0) {
                if (m_entry.find(id) != m_entry.end()) {
#ifdef LOG_STATES
                    std::cout << "onEntry() of " << m_stateNames.at(id) << std::endl;
#endif
                    m_entry.at(id)();
                }
            }
        }

        m_activeStates = newActiveStates;
#ifdef LOG_STATES
        std::cout << "Current States: ";
        for (StateID id : m_activeStates) {
            if (m_stateNames.find(id) != m_stateNames.end()) {
                std::cout << m_stateNames.find(id)->second << " ";
            }
        }
        std::cout << std::endl;
#endif
    }

    void handleTimerTick_p()
    {
        bool timerExpired = false;
        for (auto timer : m_timers) {
            if (timer.second < 0) {
                continue;
            }
            m_timers[timer.first] = --timer.second;
            if (timer.second == 0) {        // timer is expired
                m_timers[timer.first] = -1; // timer is stopped
                timerExpired = true;
            }
        }
        if (timerExpired) {
            react_p(EventID::TimerExpired);
        }
    }

    bool isTimerExpired(size_t timerNum)
    {
        if (m_timers.find(timerNum) == m_timers.end()) {
            return false;
        }
        return m_timers.at(timerNum) < 0;
    }

    void startTimer(size_t timerNum, int timeInMs) { m_timers[timerNum] = timeInMs; }

    void stopTimer(size_t timerNum) { m_timers[timerNum] = -1; }

    std::map<StateID, std::string> m_stateNames;
    std::map<EventID, std::string> m_eventNames;
    std::map<StateID, std::set<StateID>> m_initials;
    std::map<std::pair<StateID, EventID>, StateID> m_transitions;
    std::set<StateID> m_activeStates;
    std::map<StateID, std::set<StateID>> m_childStates;
    typedef std::function<bool()> reactFun;
    std::map<std::pair<StateID, EventID>, reactFun> m_reactions;
    std::map<StateID, reactFun> m_entry;
    std::map<StateID, reactFun> m_exit;
    std::map<size_t, int> m_timers;
};

#endif // STATEMACHINE_H

Und ein Besispiel

Das Beispiel ist bewusst einfach und hoffentlich leicht verständlich. Eigentlich wollt ich nur was was mit dem man gut parallelle States demonstrieren kann. Das Diagramm ist mit dem QtCreator gezeichnet:

/* Copyright 2024 stefan.box2code.de
 *            
 * Use of this source code is governed by an MIT-style
 * license that can be found in the LICENSE file or at
 * https://opensource.org/licenses/MIT.
 */

#include "statemachine.h"

#define MEASURE_TIME

#ifdef MEASURE_TIME
#include <chrono>
#endif

enum class MyStateID {
    Startup,
    Running,
    Lampe1An,
    Lampe1Aus,
    Lampe2An,
    Lampe2Aus,
    NumStates
}; // you should always add NumStates on end
enum class MyEventID {
    TimerExpired, // must always be there
    On,
    Off,
    Toggle,
    NumEvents
}; // you should always add NumEvents on end

class MyApi // The Interface to the Rest of your Application
{
public:
    virtual bool checkBatteryFull()
    {
        std::cout << "Implement battery check" << std::endl;
        return false;
    }
    virtual void turnOnLed() { std::cout << "Implement turn on LED" << std::endl; }
    virtual void playAudio() { std::cout << "Implement playing audio" << std::endl; }
    virtual void sendEventToAnotherSm(MyEventID)
    {
        std::cout << "Send event to another state machine in your system" << std::endl;
        // anotherStateMachine.react(e);
    }
    virtual ~MyApi() {}
};

class MyStateMachine : public virtual StateMachine<MyStateID, MyEventID>
{
public:
    MyStateMachine(MyApi &api)
        : m_api(api)
    {
        init();
    }

    void init() override
    {
        addInitialState(MyStateID::Startup, "Startup");
        addReaction(MyStateID::Startup, MyEventID::Off, [&]() {
            std::cout << "Hey a Off-Event in Startup" << std::endl;
            if (m_api.checkBatteryFull()) { // this is the easy way to call the rest of your system
                return true;                // must return true to do corresponding transition
            }
            return false;
        });
        addTransition(MyStateID::Startup,
                      MyEventID::Off,
                      MyStateID::Running); // this one will be forbidden by reaction above
        addTransition(MyStateID::Startup, MyEventID::On, MyStateID::Running);

        addState(MyStateID::Running, "Running");
        addEntry(MyStateID::Running, [&]() {
            std::cout << "Entry() of Running" << std::endl;
            startTimer(42, 500); // start timer 42 with 500 ms
            m_api.turnOnLed();   // this is the easy way to call the rest of your system
            return true;         // return what ever you want - doesnt matter
        });
        addExit(MyStateID::Running, [&]() {
            std::cout << "Exit() of Running" << std::endl;
            stopTimer(42); // timer 42 is now stopped
            return false;  // return what ever you want - doesnt matter
        });
        addTransition(MyStateID::Running, MyEventID::Off, MyStateID::Startup);
        addTransition(MyStateID::Running, MyEventID::TimerExpired, MyStateID::Startup);
        addReaction(MyStateID::Running, MyEventID::TimerExpired, [&]() {
            if (isTimerExpired(42)) {
                std::cout << "Timer 42 has expired" << std::endl;
                return true;
            }
            return false;
        });

        addInitialState(MyStateID::Lampe1An, "Lampe1An", MyStateID::Running);
        addTransition(MyStateID::Lampe1An, MyEventID::Toggle, MyStateID::Lampe1Aus);

        addState(MyStateID::Lampe1Aus, "Lampe1Aus", MyStateID::Running);
        addTransition(MyStateID::Lampe1Aus, MyEventID::Toggle, MyStateID::Lampe1An);
        addEntry(MyStateID::Lampe1Aus, []() {
            std::cout << "Entry() of Lampe1Aus" << std::endl;
            return true; // return what ever you want - doesnt matter
        });
        addExit(MyStateID::Lampe1Aus, []() {
            std::cout << "Exit() of Lampe1Aus" << std::endl;
            return false; // return what ever you want - doesnt matter
        });

        addState(MyStateID::Lampe2An, "Lampe2An", MyStateID::Running);
        addTransition(MyStateID::Lampe2An, MyEventID::Toggle, MyStateID::Lampe2Aus);
        addTransition(MyStateID::Lampe2An, MyEventID::Off, MyStateID::Startup);
        addReaction(MyStateID::Lampe2An, MyEventID::Off, []() {
            std::cout << "Hey a Off-Event in Lampe2An" << std::endl;
            return false; // returns false so the transition is skipped
        });

        addInitialState(MyStateID::Lampe2Aus, "Lampe2Aus", MyStateID::Running);
        addTransition(MyStateID::Lampe2Aus, MyEventID::Toggle, MyStateID::Lampe2An);

        addEventName(MyEventID::On, "On");
        addEventName(MyEventID::Off, "Off");
        addEventName(MyEventID::Toggle, "Toggle");
    }

private:
    MyApi &m_api;
};

int main()
{
    MyApi myApi;
    MyStateMachine m(myApi);
#ifdef MEASURE_TIME
    auto start = std::chrono::steady_clock::now();
#endif
    m.react(MyEventID::Off);
    m.react(MyEventID::On);
    m.react(MyEventID::Toggle);
    m.react(MyEventID::Toggle);
    m.react(MyEventID::Toggle);
    m.react(MyEventID::Toggle);
    m.react(MyEventID::Off);
    m.react(MyEventID::Off);
    m.react(MyEventID::On);
    m.react(MyEventID::Toggle);
    m.react(MyEventID::Toggle);
    m.react(MyEventID::Toggle);
    m.react(MyEventID::Toggle);
    m.react(MyEventID::Off);
    m.react(MyEventID::On);
    m.react(MyEventID::Off);
    m.react(MyEventID::On);

#ifdef MEASURE_TIME
    std::cout << "Elapsed(us)="
              << std::chrono::duration_cast<std::chrono::microseconds>(
                     std::chrono::steady_clock::now() - start)
                     .count()
              << std::endl;
#endif

    for (int ms = 0; ms < 1000; ms++) {
        // of course you should call this in your SysTick-Handler
        m.handleTimerTick();
    }
}

Du siehst schon – Templates brauchst du nur in einer Zeile um deine eigenen Listen für States und Events einbinden zu können.

Die Anbindung an dein restliches System hab ich nur skizziert.

Ich hoffe der Code ist ansonsten selbsterklärend – Viel Spaß damit

Ach ja, noch die Ausgabe des Programms:


onOff()
Hey a Off-Event in Startup
Implement battery check
onOn()
onEntry() of Running
Entry() of Running
Implement turn on LED
Current States: Running Lampe1An Lampe2Aus 
onToggle()
onEntry() of Lampe1Aus
Entry() of Lampe1Aus
Current States: Running Lampe1Aus Lampe2An 
onToggle()
onExit() of Lampe1Aus
Exit() of Lampe1Aus
Current States: Running Lampe1An Lampe2Aus 
onToggle()
onEntry() of Lampe1Aus
Entry() of Lampe1Aus
Current States: Running Lampe1Aus Lampe2An 
onToggle()
onExit() of Lampe1Aus
Exit() of Lampe1Aus
Current States: Running Lampe1An Lampe2Aus 
onOff()
Exit() of Lampe1Aus
onExit() of Running
Exit() of Running
Current States: Startup 
onOff()
Hey a Off-Event in Startup
Implement battery check
onOn()
onEntry() of Running
Entry() of Running
Implement turn on LED
Current States: Running Lampe1An Lampe2Aus 
onToggle()
onEntry() of Lampe1Aus
Entry() of Lampe1Aus
Current States: Running Lampe1Aus Lampe2An 
onToggle()
onExit() of Lampe1Aus
Exit() of Lampe1Aus
Current States: Running Lampe1An Lampe2Aus 
onToggle()
onEntry() of Lampe1Aus
Entry() of Lampe1Aus
Current States: Running Lampe1Aus Lampe2An 
onToggle()
onExit() of Lampe1Aus
Exit() of Lampe1Aus
Current States: Running Lampe1An Lampe2Aus 
onOff()
Exit() of Lampe1Aus
onExit() of Running
Exit() of Running
Current States: Startup 
onOn()
onEntry() of Running
Entry() of Running
Implement turn on LED
Current States: Running Lampe1An Lampe2Aus 
onOff()
Exit() of Lampe1Aus
onExit() of Running
Exit() of Running
Current States: Startup 
onOn()
onEntry() of Running
Entry() of Running
Implement turn on LED
Current States: Running Lampe1An Lampe2Aus 
Elapsed(us)=123
Timer 42 has expired
Exit() of Lampe1Aus
onExit() of Running
Exit() of Running
Current States: Startup 

svg2qml war kein guter Name – gab’s schon 🙂 Wir nennens jetzt mal InkBridge4Qml

Einfaches Tool zum generieren von QML aus SVG-Bildern. Funktionalität ist ähnlich zur kostenpflichtigen Qt Bridge for Adobe Photoshop.

Happy animating 🙂

Continue reading