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, 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. È buona norma, se si deve fare una modifica, aggiornare prima il diagramma, e solo in seguito il 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, lo stato 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