Statemachine in C

@rew: Bijna goed.

Jouw voorbeeld gaat uit van een StartTijd in de toekomst, dus wanneer je de aktie wilt uitvoeren, en dat levert inderdaad een negatief getal op als het nog geen tijd is. Maar dat is lastig testen want millis() is unsigned en is dus altijd positief.

Mijn voorbeeld gaat uit van een StartTime in het verleden, dus wanneer je begint met de delay.


   if ((millis() - StartTime) > Delay)
   {  StartTime += Delay;

StartTime is een moment in het verleden. En millis() loopt steeds door. Dus (millis() - StartTime) is altijd positief. En je start de aktie (en zet een nieuwe StartTijd) als het verschil groot genoeg is. De getallen hoeven niet signed te zijn. De enige eis is dat alle getallen even groot zijn, want als millis() bijv een 16-bit getal geeft en StartTime is 32-bit dan gaat het mis

Veel mensen zetten StartTime dmv millis()


  if ((millis() - StartTime) > Delay)
  {  StartTime - millis();

Dat werkt ook, maar geeft extra vertraging als je de test te laat uitvoert. Bijv als je systeem zo traag is dat je de controle maar 1 keer per meerdere milliseconden kunt uitvoeren. Dat is soms een probleem, niet altijd.

Rekenvoorbeeld:

Stel StartTime is unsigned long (32 bit). En de waarde is 0xFFFFFFFE (dus tegen overflow aan.
En je wilt een actie starten telkens na 5 milliseconden (Delay = 5).


Millis()      StartTime    Millis() - StartTime   
0xFFFFFFFE    0xFFFFFFFE   0x00000000             Nog niet
0xFFFFFFFF    0xFFFFFFFE   0x00000001             Nog niet
0x00000000    0xFFFFFFFE   0x00000002             Nog niet
0x00000001    0xFFFFFFFE   0x00000003             Nog niet
0x00000002    0xFFFFFFFE   0x00000004             Nog niet
0x00000003    0xFFFFFFFE   0x00000005             Nu wel
Zet nieuwe StartTime (StartTijd + Delay)
0x00000003    0x00000003   0x00000000             Nog niet
0x00000004    0x00000003   0x00000001             Nog niet
enz ...

PS Dat het goed blijft gaan ook igv overflow is 'by design'. Dat is inherent aan het gebruik van "2's complement" binaire notatie.

Mijn ervaring is dat wanneer je over zoiets nadenkt en dat uitwerkt met de diverse functies dat het resultaat wat complex wordt en dat anderen juist weer afschrikt. Vaak geeft dit een hoop gezeik of gezijk ;)

Ik wil hem nog wel een stukje complexer maken.

Ik werk veel met een statemachine in een SQL database. Dat is voor embedded misschien wat overdreven, maar ipv in "usercode" zoals hierboven zou je dus states in een array kunnen definieren en zo kunnen aanpassen zonder opnieuw programmeren. Bijvoorbeeld over-the-air of met een SD kaartje.

Ik doe een poging, in onderstaande array kan dus vrij makkelijk 'in memory' wijzingen aanbrengen zoals het overslaan van de betaling of bepaalde producten alleen op bepaalde tijden toestaan:


{
  "states": {
    "state": [
      {
        "id": "WaitPayment",
        "OnSuccessNextState": "ProductSelection",
        "OnFailNextState": "WaitPayment",
        "prereq": "time>8:00 and time < 18:00"
        "action": "read_payment_terminal"
      },
      {
        "id": "ProductSelection",
        "OnSuccessNextState": "MakeProduct",
        "OnFailNextState": "ProductSelection",
        "action": "$product=read_selection_terminal"
      },
      {
        "id": "MakeProduct",
        "OnSuccessNextState": "WaitForDrops",
        "OnFailNextState": "WaitPayment",
        "action": "start_product($product)"
      },
      {
        "id": "WaitForDrops",
        "OnSuccessNextState": "DoneOnDisplay",
        "OnFailNextState": "WaitForDrops",
        "action": "checkTimer($timer1,3s)"
      },   .....................
    ]
  }
}

edit: ik heb die prereq met tijden van een ander systeem, maar ik bedenk me dat je net zo goed een state "checkTime" zou kunnen maken.

Op die manier kan je de software zeer flexibel maken via configuratie. Ik heb ook zoon tool gemaakt voor het werk die op zoon soort manier te configureren is. Wel een nadeel, je kan het ook 'kapot' configureren. Daar moet je dan in de software weer rekening mee houden. Het configureren is ook niet voor iedereen weggelegd. Andere optie is iets als lua ondersteunen, maar dat is allemaal weer afhankelijk van de toepassing.

Voor zij die het leuk vinden, dit idee is hier ontstaan. Op deze site staan veel meer leuke design patterns.
https://refactoring.guru/design-patterns

PE2BAS

Op dinsdag 12 november 2024 11:43:49 schreef deKees:

Rekenvoorbeeld:
....
PS Dat het goed blijft gaan ook igv overflow is 'by design'. Dat is inherent aan het gebruik van "2's complement" binaire notatie.

Nu snap ik het!
In principe komt dit ook overeen met @rew's eerdere uitleg.
@rew en @deKees, dank voor de uitleg! _/-\o_

Over die timeout. De timer value moet wel unsigned zijn waarmee je begint. Het beste is een macro gebruiken hiervoor zoals in de linux kernel waar de wrapping weggewerkt wordt waardoor dit altijd goed gaat mits de timeout < 1/2 * resolutie van de timer variable.
Zie deze discussie: https://stackoverflow.com/questions/8206762/how-does-linux-handle-over…

1-st law of Henri: De wet van behoud van ellende. 2-nd law of Henri: Ellende komt nooit alleen.

In mijn beleving komt het weinig voor dat verschillende state machines gemeenschappelijke behoeften hebben. Meestal draaien de state machines volledig onafhankelijk van elkaar.

Bijv:


void  loop()
{  Knipperlicht.Run();
   ADC.Run();
   TempControl.Run();
   SerialPort.Run();
}

Dus voor iedere state machine maak ik dan een object zodat je de bijbehorende data mee kunt nemen. En het enige dat de machines gemeenschappelijk hebben is dat ze allemaal een Run() functie hebben. De implementatie is dan voor elke functie verschillend. Het Knipperlicht is bijv timer gestuurd, de ADC reageert op het ADC status register, de SerialPort reageert op inkomende data.

Op dinsdag 12 november 2024 19:55:57 schreef henri62:
Over die timeout. De timer value moet wel unsigned zijn waarmee je begint. Het beste is een macro gebruiken hiervoor zoals in de linux kernel waar de wrapping weggewerkt wordt waardoor dit altijd goed gaat mits de timeout < 1/2 * resolutie van de timer variable.
Zie deze discussie: https://stackoverflow.com/questions/8206762/how-does-linux-handle-over…

Strict genomen klopt dit niet. De timer value hoeft niet signed of unsigned te zijn. Feitelijk maakt dat geen verschil. Waar het om gaat is dat alle getallen bij dezelfde waarde over de kop gaan.

Je berekent eerst het verschil tussen 'nu' en de 'StartTime' en dat levert altijd een klein positief getal op. En de af te tellen delay is ook een klein positief getal. Of het type dan al dan niet signed is maakt geen verschil. Behalve als je een timeout value gaat zetten van meer dan 25 dagen. Het verschil wordt dan ooit groter dan 0x8000000 en dan maakt signed/unsigned weer wel verschil.

Die Macro van linux forceert een timeout berekening door eerst de verschiltijd te berekenen. Zonder die macro loop je het risico om het fout te doen.