Sistemi di controllo

La macchina a stati: dettagli

Per la realizzazione, mi sono avvalso del linguaggio C#, che per me aveva due vantaggi:

  • Il supporto al multithreading;
  • La disponibilità di una libreria già sviluppata e collaudata per la realizzazione di macchine a stati; nei diagrammi dell'articolo precedente sono descritte le classi coinvolte.

La macchina a stati:

  • È composta da un certo numero di stati;
  • Scoda eventi dalla sua coda degli eventi, e li utilizza per svolgere la sua logica di controllo;
  • Contiene le variabili di stato extra, rendendole disponibili agli stati;
  • Svolge il compito di delegato nei confronti degli stati per quanto riguarda le azioni da svolgere.

Macchina a stati

public class CrossingSm : EuSm<CrossingSm>
{
    // Variabili di stato extra.
    public EuTimer Timer { get; private set; }
    public int PedestrianCounter { get; private set; } = 0;

    // Il client MQTT.
    public readonly MqttClient _MqttClient;

    // Costruzione della macchina a stati.
    public CrossingSm() : base("CrossingControl")
    {
        // Includi tutti gli stati, e lo pseudo-stato.
        WithStates(new EuState<CrossingSm>[]
        {
                new GreenState(),
                new LongGreenState(),
                new PendingState(),
                new YellowState(),
                new RedState(),
                new UrgingState(),
                new InactiveState()
        });
        WithAnyState(new AnyState());

        // Inizializza il timer e il client MQTT.
        Timer = new EuTimer("CrossingTimer").WithEventQueue(EventQueue);
        _MqttClient = new MqttClient("CrossingControlClient", EventQueue)
            .WithBrokerHost("myhost")
            .WithCredentials(new Tuple<string, string>("myuser", "mypass"));
    }

    // Inizializzazione della macchina a stati.
    protected override void OnEntry()
    {
        // Avvia il client MQTT.
        _MqttClient.StartTask();
    }

    // Deinizializzazione della macchina a stati.
    protected override void OnExit()
    {
        // Ferma il timer e il client MQTT.
        Timer.StopTimer();
        _MqttClient.StopTask();
        _MqttClient.Dispose();
    }

    // Funzione delegata per il riallineamento.
    public void Refresh() 
    {
        // ... altro codice, per pubblicare tutte le info rilevanti.
    }

    // Funzione delegata per eseguire la (dis)attivazione del sistema.
    public void SetSystemActive(bool isActive)
    {
        _MqttClient.PublishSystemActive(isActive);
    }

    // Funzione delegata per il cambio di colore del semaforo stradale.
    public void SetTrafficColor(LightColor color, bool isFlashing = false)
    {
        _MqttClient.PublishTrafficColor(color, isFlashing);
    }

    // ... altro codice

    // Funzione delegata per l'impostazione del conteggio sul display pedonale.
    public void SetPedestrianCounter(int seconds)
    {
        PedestrianCounter = seconds;
        _MqttClient.PublishPedestrianCounter(PedestrianCounter);
    }

    // Funzione delegata per il decremento del conteggio sul display pedonale.
    public void DecrementPedestrianCounter()
    {
        if (PedestrianCounter > 0)
            PedestrianCounter--;
        _MqttClient.PublishPedestrianCounter(PedestrianCounter);
    }

}

Il codice è auto-esplicativo; le variabili Timer e PedestrianCounter, nonché la serie di funzioni da Refresh a DecrementPedestrianCounter, sono quelle per le quali la macchina a stati funge da delegato per gli stati.


Stati: alcuni esempi

La macchina a stati, se è impostato uno stato di tipo AnyState, lo gestisce prima di gestire lo stato corrente, e a prescindere da quale sia lo stato corrente.

Gli pseudo-stati Any state e Any state except Inactive, con un semplice artificio, vengono gestiti entrambi dalla medesima classe AnyState:

  • L'evento di richiesta refresh viene gestito a prescindere
  • L'evento di disattivazione del sistema viene gestito solo se lo stato corrente non è InactiveState.

public class AnyState : EuState<CrossingSm>
{
    // Gestione degli eventi di riallineamento e di riattivazione del sistema.
    public override void OnEvent(EuEvent smEvent)
    {
        if (smEvent is RefreshEvent)
            _sm.Refresh();
        else if (_sm.CurrentNode is not InactiveState &&
                smEvent is SystemActiveEvent activeEvent && !activeEvent.IsActive)
            ToState<InactiveState>();
        base.OnEvent(smEvent);
    }
}

In generale, per implementare gli stati, è sufficiente seguire pedissequamente il diagramma a stati. Ormai d'abitudine, se devo fare una modifica, la faccio prima nel diagramma, poi la riporto nel codice, al fine di averli sempre ben allineati.

Lo stato GreenState implementa in modo specifico sia la funzione OnEntry che la OnEvent, nel modo seguente:

public class GreenState : EuState<CrossingSm>
{
    // Esecuzione delle azioni all'ingresso dello stato.
    public override void OnEntry()
    {
        _sm.SetTrafficColor(LightColor.Green);
        _sm.SetPedestrianColor(LightColor.Red);
        _sm._Timer.StartTimer(CrossingSm.GreenTime);
    }

    // Gestione degli eventi, in particolare lo scadere
    // del timer e la richiesta attraversamento.
    public override void OnEvent(EuEvent smEvent)
    {
        if (smEvent is EuTimer.ElapsedEvent)
            ToState<LongGreenState>();
        else if (smEvent is PedestrianRequestEvent)
            ToState<PendingState>();
    }
}

Come si vede, fa riferimento alla macchina a stati sia per le variabili extra che per eseguire le azioni.

Lo stato LongGreenState implementa in modo specifico solo la funzione OnEvent, poiché non prevede azioni all'ingresso dello stato:

public class LongGreenState : EuState<CrossingSm>
{
    public override void OnEvent(EuEvent smEvent)
    {
        if (smEvent is PedestrianRequestEvent)
        {
            int pedestrianCounter = (int)CrossingSm.YellowTime.TotalSeconds;
            _sm.SetPedestrianCounter(pedestrianCounter);
            ToState<YellowState>();
        }
    }
}

Le transizioni di stato, ove previste, vengono eseguite utilizzando la funzione ToState, che è parametrizzata con la classe dello stato, non la sua istanza; si tratta di una decisione di progetto a livello della libreria, per garantire la type safety.


Giorgio Barchiesi
Albo degli Ingegneri Sez. A, N. 4027 della Prov. di Trento
P.IVA 02370260222, C.F. BRC GRG 58L26 C794R

Copyright © 2015-2024 Giorgio Barchiesi - Tutti i diritti riservati