Free Code Day: September 2023 - Die Ampel

Am Free Code Day kann sich jeder Entwickler bei der Surnet eigenen Software-Entwicklungsthemen widmen.

Daniel Müller

Daniel Müller

01. September 2023 · 13 min

Free Code Day

Wir haben eine Überwachung um jederzeit den Überblick zu haben, ob bei den Umgebungen unserer Kunden alles reibungslos läuft. Bei Statusveränderungen werden wir über diverse Kanäle informiert. Jedoch wäre es cool im Büro gleich auch eine visuelle Info zu haben, ob alles im grünen Bereich ist.

Die Idee

Von grossen Industriemaschinen kenne ich eine Ampel, welche Auskunft über den aktuellen Zustand einer Maschine gibt. Etwas ähnliches für unser Büro wäre also cool, um unser Äquivalent einer solchen Maschine zu überwachen.

Wir brauchen sicherlich ein Programm, welches auf einem PC läuft und die von uns verwendete Monitoring Lösung AWS CloudWatch anspricht. Dieses kann von den verschiedenen Alarmen welche wir konfiguriert haben, den aktuellen Zustand abfragen.

Ich möchte jedoch auch informiert werden, wenn das Programm auf dem Computer mal nicht laufen sollte. Daher soll das Programm welches die Ampel anspricht unabhängig vom Computer auf einem Microcontroller laufen und erwarten, dass es regelmässig einen Zustand gemeldet bekommt. Falls die Zustandsmeldung ausfällt soll ein Warnmodus dargestellt werden.

Für den Microcontroller habe ich mich für ein Teensy 3.2 entschieden, weil dies bei mir noch in einer Schublade rumlag und auf einen Einsatz wartete. Nach etwas Recherche habe ich einen passenden LED Signal Tower gefunden und diesen bestellt. Die restlichen Artikel (Netzteil, Spannungsregulator, Wiederstände, Transistoren, Board, etc) habe ich bereits gehabt.

Elektronik

Sofort ging es an das Prototyping auf einem Steckbrett. Der Microcontroller, die Wiederstände, Transistoren und der LED Signal Tower zusammengekabelt und im ersten Versuch mal eine LED via Controller geschaltet.

Foto des Prototyps

Das Programm wurde um diverse Funktionen erweitert, wie die Überwachung ob in den letzten 30 Sekunden auch ein Wert empfangen wurde. Ansonsten wechselt die LED Abfolge in einen Interval um diesen Zustand zu kennzeichnen.

Insgesamt gibt es aktuell 8 Zustände welche mit dem Programm abgebildet werden können:

  • INIT: Wechselt zwischen den Farben hin und her (zeigt wenn keine Daten empfangen wurden)
  • NONE: Alle LED ausgeschaltet (Für die Nacht)
  • ALARM: Rote LED blinkt und Buzzer pipst 2 mal
  • ALERT: Rote LED blinkt
  • ERR: Rote LED leuchtet konstant
  • CAUTION: Orange LED blinkt
  • WARN: Orange LED leuchtet konstant
  • OKAY: Grüne LED leuchtet

Nachfolgend der Code welcher dies ermöglicht:

#include <Arduino.h>
#include <TeensyThreads.h>

// Output PINs
const int redLedPin = 13;
const int orangeLedPin = 16;
const int greenLedPin = 19;
const int buzzerPin = 22;

// Reset Time
const int resetAfter = 30000;

// Possible states
enum States { NONE=0, ALARM=1, ALERT=2, ERR=3, CAUTION=5, WARN=7, OKAY=9, INIT=10 };

// State of the system and serialData
volatile States state = INIT;
volatile int serialData = 0;
volatile unsigned long lastDataReceived = millis();

void ledThread() {
  while(1) {
    States tempState = state;
    // Run for each state
    switch(state) {
      case ALARM:
        digitalWrite(orangeLedPin, LOW);
        digitalWrite(greenLedPin, LOW);
        
        digitalWrite(redLedPin, HIGH);
        digitalWrite(buzzerPin, HIGH);
        threads.delay(200);
        digitalWrite(buzzerPin, LOW);
        threads.delay(200);
        digitalWrite(buzzerPin, HIGH);
        threads.delay(200);
        digitalWrite(redLedPin, LOW);
        digitalWrite(buzzerPin, LOW);
        if (tempState == state) {
          state = ALERT;
        }
        threads.delay(600);
        break;

      case ALERT:
        digitalWrite(orangeLedPin, LOW);
        digitalWrite(greenLedPin, LOW);
        digitalWrite(buzzerPin, LOW);
        
        digitalWrite(redLedPin, HIGH);
        threads.delay(600);
        digitalWrite(redLedPin, LOW);
        threads.delay(600);
        break;

      case ERR:
        digitalWrite(orangeLedPin, LOW);
        digitalWrite(greenLedPin, LOW);
        digitalWrite(buzzerPin, LOW);

        digitalWrite(redLedPin, HIGH);
        threads.delay(1000);
        break;

      case CAUTION:
        digitalWrite(redLedPin, LOW);
        digitalWrite(greenLedPin, LOW);
        digitalWrite(buzzerPin, LOW);
        
        digitalWrite(orangeLedPin, HIGH);
        threads.delay(600);
        digitalWrite(orangeLedPin, LOW);
        threads.delay(600);
        break;

      case WARN:
        digitalWrite(redLedPin, LOW);
        digitalWrite(greenLedPin, LOW);
        digitalWrite(buzzerPin, LOW);

        digitalWrite(orangeLedPin, HIGH);
        threads.delay(1000);
        break;

      case OKAY:
        digitalWrite(redLedPin, LOW);
        digitalWrite(orangeLedPin, LOW);
        digitalWrite(buzzerPin, LOW);

        digitalWrite(greenLedPin, HIGH);
        threads.delay(1000);
        break;

      case NONE:
        digitalWrite(redLedPin, LOW);
        digitalWrite(orangeLedPin, LOW);
        digitalWrite(greenLedPin, LOW);
        digitalWrite(buzzerPin, LOW);
        threads.delay(1000);
        break;

      case INIT:
        digitalWrite(orangeLedPin, LOW);
        digitalWrite(greenLedPin, LOW);
        digitalWrite(buzzerPin, LOW);

        digitalWrite(redLedPin, HIGH);
        threads.delay(600);
        digitalWrite(redLedPin, LOW);
        digitalWrite(orangeLedPin, HIGH);
        threads.delay(600);
        digitalWrite(orangeLedPin, LOW);
        digitalWrite(greenLedPin, HIGH);
        threads.delay(600);
        break;

      default:
        digitalWrite(orangeLedPin, LOW);
        digitalWrite(greenLedPin, LOW);
        digitalWrite(buzzerPin, LOW);

        digitalWrite(redLedPin, HIGH);
        threads.delay(1000);
        digitalWrite(redLedPin, LOW);
        threads.delay(1000);
        break;
    }
  }
}

void setup() {
  // Set Pins as output pins
  pinMode(redLedPin, OUTPUT);
  pinMode(orangeLedPin, OUTPUT);
  pinMode(greenLedPin, OUTPUT);
  pinMode(buzzerPin, OUTPUT);
  
  // Open serial connection
  Serial.begin(9600);

  // Start LED control thread
  threads.addThread(ledThread);
}

void loop() {
  // Read serial data
  serialData = Serial.read();

  // Update state if a single-digit number has been sent
  if (serialData >= '0' && serialData <= '9' ) {
    // Convert from character to number
    state = (States)(serialData - '0');
    // Set last data received time
    lastDataReceived = millis();
  }

  // Reset if no data has been delivered since a certain time
  if (abs(millis() - lastDataReceived) > resetAfter) {
    state = INIT;
  }

  delay(200);
}

Als ich mit den Funktionen zufrieden war ging es darum die Komponenten final zusammenzulöten. Ich habe mich dazu entschieden die Komponenten auf einer Lochrasterplatine anzuordnen und diese von Hand anzulöten.

Foto des Layouts vor dem Löten

Das gelötete Board bekam noch ein 3D gedrucktes Gehäuse, um alles sicher zu verpacken.

Programm

Das Abrufen des aktuellen AWS CloudWatch Alarm Zustands und das Übertragen des Wertes an den Microcontroller gelang via USB Serial ziemlich einfach. Alle 25 Sekunden wird der aktuelle Stand geladen und dann der schlimmste Zustand (Kritisch > Warnung) dargestellt.

Ich habe auf den CloudWatch Alarmen einen neuen Tag "LedTower" hinzugefügt, mit welchem eingestellt werden kann welche der Visualisierungen gewählt werden soll. So können wir für kritische Überwachungen den Modus ALARM mit Buzzer konfigurieren und für Performanceüberwachungen einen Warnzustand anwenden.

Folgender Code macht dies möglich:

import { CloudWatchClient, DescribeAlarmsCommand, ListTagsForResourceCommand } from '@aws-sdk/client-cloudwatch';
import { DateTime, DurationLike } from 'luxon';
import { SerialPort } from 'serialport';

// Beeping alarm interval (only alert every 25 minutes using a beep)
const alarmInterval: DurationLike = { minutes: 25 };
let lastAlarm: DateTime = DateTime.now().minus(alarmInterval);

// Alert if needed variable is missing
if (!process.env.SERIALPATH) {
  console.error('You need to specify a serial port e.g. `SERIALPATH=/dev/tty.usbmodem83511301 npm start`');
  process.exit(1);
}

// AWS Init
const client = new CloudWatchClient({ region: 'eu-west-1' });
const getAlarms = new DescribeAlarmsCommand({
  StateValue: 'ALARM'
});

// Serial Port init
const port = new SerialPort({
  path: process.env.SERIALPATH,
  baudRate: 9600
}, (err) => {
  if (err) {
    console.error(`An error occured while opening port to ${process.env.SERIALPATH}`, err);
    process.exit(1);
  }
});

// Enum corresponding to the configured enums on the microcontroller
enum LEDState {
  NONE = 0,
  ALARM = 1,
  ALERT = 2,
  ERR = 3,
  CAUTION = 5,
  WARN = 7,
  OKAY = 9
}
function stringToEnum(value?: string): LEDState {
  if (!value) {
    return LEDState.WARN;
  }
  return LEDState[value as keyof typeof LEDState] ?? LEDState.WARN;
}

async function main() {
  let newState: LEDState = LEDState.OKAY;

  try {
    const states: Array<LEDState> = [];
    // Get CloudWatch Alerts
    const data = await client.send(getAlarms);
    for (const metric of data.MetricAlarms ?? []) {
      if (metric.ActionsEnabled) {
        // Decide per alert which state it should trigger
        const getTags = new ListTagsForResourceCommand({
          ResourceARN: metric.AlarmArn
        });
        const tags = await client.send(getTags);
        const tag = tags.Tags?.find(tag => tag.Key === 'LedTower')?.Value;
        states.push(stringToEnum(tag));
      }
    }
    // Use the most severe error as our new state
    if (states.length > 0) {
      newState = Math.min(...states);
    }
  } catch (err) {
    console.error(err);
    newState = LEDState.CAUTION;
  } finally {
    if (newState === LEDState.ALARM) {
      if (lastAlarm < DateTime.now().minus(alarmInterval)) {
        lastAlarm = DateTime.now();
      } else {
        newState = LEDState.ALERT;
      }
    }
    // Send alert to the microcontroller
    port.write(newState.toString());
  }
}

// Start application
main().catch(console.error);
setInterval(() => {
  main().catch(console.error);
}, 25_000);

Ergebnis

Nun haben wir in unserem Büro eine Ampel welche uns auf einen Blick verrät ob alles in Ordnung ist.

Foto der Ampel