Sistemi di controllo

Le app: dettagli

Mi riferisco qui a entrambe le app: il simulatore dei semafori, e la control room, le quali peraltro hanno svariate parti in comune, ma le porzioni di codice di seguito provengono da quest'ultima.


I colori

Per la rappresentazione dei colori, volevo qualcosa di type safe, e che mi permettesse facilmente di convertire da/a stringa, in modo da agevolare la codifica e decodifica in formato JSON, per l'invio/ricezione via MQTT.

Ho quindi optato per un enhanced enum, un tipo di rappresentazione che Dart mette a disposizione per definire un valore enumerato, arricchito con funzionalità ad hoc:

enum LightColor {
  // I valori possibili, associati alle rispettive stringhe.
  off('off'),
  red('red'),
  yellow('yellow'),
  green('green');

  // Il valore.
  const LightColor(this.value);

  // Il valore espresso come stringa.
  final String value;

  // La conversione da stringa a valore.
  static LightColor fromString(String s) {
    return LightColor.values.firstWhere(
      (color) => color.value == s,
      orElse: () => LightColor.off,
    );
  }
}

Per ottenere la stringa che rappresenza uno dei valori, è sufficiente usare la proprietà value, mentre per risalire dalla stringa al valore, si può utilizzare la funzione fromString.


Il semaforo stradale

Gli elementi che compongono un'interfaccia utente in Flutter si chiamano Widget, e ve ne sono di due tipi, secondo che abbiano o meno uno stato. Il semaforo ha uno stato, determinato da due variabili:

  • Il colore
  • Il fatto di stare o meno lampeggiando

Questo è il wrapper del widget, che definisce le due variabili in questione:

class TrafficLightWidget extends StatefulWidget {
  Le variabili di stato.
  final LightColor lightColor;
  final bool isFlashing;

  // Il widget vero e proprio.
  const TrafficLightWidget({
    super.key,
    required this.lightColor,
    this.isFlashing = false,
  });

  @override
  State<TrafficLightWidget> createState() => _TrafficLightWidgetState();
}

E questo è il widget vero e proprio; si noti quanta parte del codice è dedicata alla gestione del lampeggio:

class _TrafficLightWidgetState extends State<TrafficLightWidget> {
  // Variabili per la gestione del lampeggio.
  bool _isFlashingOn = true;
  Timer? _flashingTimer;

  // Inizializzazione.
  @override
  void initState() {
    super.initState();
    if (widget.isFlashing) {
      _startFlashing();
    }
  }

  // Gestione delle variazioni di stato.
  @override
  void didUpdateWidget(TrafficLightWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.isFlashing != oldWidget.isFlashing) {
      if (widget.isFlashing) {
        _startFlashing();
      } else {
        _stopFlashing();
      }
    }
    // ... altro codice
  }

  void _startFlashing() { /* ... Innesca il lampeggio */ }
  void _stopFlashing() { /* ... Ferma il lampeggio */ }

  // Libera le risorse.
  @override
  void dispose() {
    _flashingTimer?.cancel();
    super.dispose();
  }

  // Compone il disegno del widget.
  @override
  Widget build(BuildContext context) {
    // Queste tre funzioni determinano il colore di ciascuna luce.
    Color getRed() {
      if (widget.lightColor == LightColor.red && (!widget.isFlashing || _isFlashingOn)) {
        return Colors.red;
      }
      return Colors.black12;
    }
    Color getYellow() { /* ... simile a getRed */ }
    Color getGreen() { /* ... simile a getRed */ }

    // Il layout è una colonna, cone le tre luci intercalate da spazi.
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        _buildCircle(getRed()),
        const SizedBox(height: 16),
        _buildCircle(getYellow()),
        const SizedBox(height: 16),
        _buildCircle(getGreen()),
      ],
    );
  }

  // Funzione di utilità, per disegnare un cerchio colorato.
  Widget _buildCircle(Color color) {
    return Container(
      width: 80,
      height: 80,
      decoration: BoxDecoration(
        color: color,
        shape: BoxShape.circle,
        border: Border.all(color: Colors.black, width: 2),
      ),
    );
  }
}

A parte tutte le funzioni in override, che fanno parte della gestione generale di un widget, abbiamo alcune funzioni di utilità:

  • _startFlashing e _stopFlashing, per innescare o fermare il lampeggio;
  • _buildCircle, per creare un cerchio colorato.

La funzione build è quella che effettivamente compone il semaforo, e si svolge in due passi:

  • In base allo stato, dato da widget.lightColor e widget.isFlashing, determina con quale colore va dipinta ciascuna delle tre luci del semaforo; Colors.black12 è una tonalità grigio chiaro, che rappresenta una luce spenta;
  • Prepara un layout a colonna, con le tre luci alternate da spazi.

La sala di controllo

La sala di controllo, a sua volta, è un widget che contiene un semaforo stradale e un semaforo pedonale. Di seguito evidenzio solo le parti di interazione via MQTT:

  Future<void> _connectMqtt() async {
    // ... altro codice di predisposizione del client
    _client.onConnected = () => widget.onLog('[MQTT] Connected');
    _client.onDisconnected = () => widget.onLog('[MQTT] Disconnected');
    _client.onSubscribed = (topic) => widget.onLog('[MQTT] Subscribed to $topic');

    try {
      // Connetti al broker.
      await _client.connect();

      // Esegui le subscribe.
      _client.subscribe('traffic/color', MqttQos.atMostOnce);
      _client.subscribe('pedestrian/color', MqttQos.atMostOnce);
      _client.subscribe('pedestrian/counter', MqttQos.atMostOnce);
      _client.subscribe('system/active', MqttQos.atMostOnce);

      // Prepara la ricezione dei messaggi.
      _client.updates!.listen((List<MqttReceivedMessage<MqttMessage>> updates) {
        final topic = updates[0].topic;
        final payload = updates[0].payload as MqttPublishMessage;
        final payloadMessage = MqttPublishPayload
            .bytesToStringAsString(payload.payload.message);
        setState(() {
          if (topic == 'traffic/color') {
            _handleTrafficColorMessage(payloadMessage);
          } else if (topic == 'pedestrian/color') {
            _handlePedestrianColorMessage(payloadMessage);
          } else if (topic == 'pedestrian/counter') {
            _handlePedestrianCounterMessage(payloadMessage);
          } else if (topic == 'system/active') {
            _handleSystemActiveMessage(payloadMessage);
          }
        });
      });


      // Richiedi riallineamento.
      _requestRefresh();
    } catch (e) {
      widget.onLog('[MQTT] Connection failed: $e');
    }
  }

  // Gestisce il messaggio di cambio colore del semaforo stradale.
  void _handleTrafficColorMessage(String message) {
    widget.onLog('[MQTT] Received traffic color: $message');
    setState(() {
      try {
        
        final data = jsonDecode(message);
        final colorStr = data['color'] as String? ?? 'off';
        final isFlashing = data['isFlashing'] as bool? ?? false;
        _trafficLightColor = LightColor.fromString(colorStr);
        _isTrafficFlashing = isFlashing;
      } catch (e) {
        widget.onLog('[MQTT] Failed parsing message; error = $e');
      }
    });
  }

  // ... altro codice di gestione dei messaggi

  // Pubblica la richiesta di riallineamento.
  void _requestRefresh() {
    final builder = MqttClientPayloadBuilder();
    _client.publishMessage('refresh', MqttQos.atMostOnce, builder.payload!);
    widget.onLog('[MQTT] Sent refresh request');
  }

Si possono notare in particolare le parti in cui:

  • Il client viene configurato con delle callback per ricevere aggiornamenti di stato  MQTT;
  • Viene tentata la connessione;
  • Vengono eseguite le subscribe;
  • Vengono intercettati e gestiti i messaggi in arrivo;
  • Viene chiesto il refresh.

Il main

L'app, definita nel main, è a sua volta un widget, il cui scopo principale è costruire il layout complessivo, con la sala di controllo e con la finestra di log. Inoltre dà il necessario supporto al logging da parte dei widget in essa contenuti.


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