Rauchmelder mit Delphi auslesen

Für diese Projekt setzen wir voraus, dass eine Delphi Entwicklungsumgebung eingerichtet ist und ein grundsätzliches Verständnis der Delphi Programmiersprache vorhanden ist.

Falls dies nicht der Fall ist sollte hier begonnen werden. Informationen über die Tinkerforge API sind dann hier zu finden.

Wir setzen weiterhin voraus, dass ein passender Rauchmelder mit einem Industrial Digital In 4 Bricklet verbunden wurde wie hier beschrieben.

Ziele

Wir setzen uns folgendes Ziel für dieses Projekt:

  • Alarmstatus eines Rauchmelders auslesen
  • und auf dessen Alarmsignal reagieren.

Da dieses Projekt wahrscheinlich 24/7 laufen wird, wollen wir sicherstellen, dass das Programm möglichst robust gegen externe Einflüsse ist. Das Programm sollte weiterhin funktionieren falls

  • Bricklets ausgetauscht werden (z.B. verwenden wir keine fixen UIDs),
  • Brick Daemon läuft nicht oder wird neu gestartet,
  • WIFI Extension ist außer Reichweite oder
  • Brick wurde neu gestartet (Stromausfall oder USB getrennt).

Im Folgenden werden wir Schritt für Schritt zeigen wie diese Ziele erreicht werden können.

Schritt 1: Bricks und Bricklets dynamisch erkennen

Als Erstes legen wir fest wohin unser Programm sich verbinden soll:

const
  HOST = 'localhost';
  PORT = 4223;

Falls eine WIFI Extension verwendet wird, oder der Brick Daemon auf einem anderen PC läuft, dann muss "localhost" durch die IP Adresse oder den Hostnamen der WIFI Extension oder des anderen PCs ersetzt werden.

Nach dem Start des Programms müssen der OnEnumerate Callback und der OnConnected Callback registriert und ein erstes Enumerate ausgelöst werden:

procedure TSmokeDetector.Execute;
begin
  ipcon := TIPConnection.Create;
  ipcon.Connect(HOST, PORT);
  ipcon.OnEnumerate := {$ifdef FPC}@{$endif}EnumerateCB;
  ipcon.OnConnected := {$ifdef FPC}@{$endif}ConnectedCB;
  ipcon.Enumerate;
end;

Der Enumerate Callback wird ausgelöst wenn ein Brick per USB angeschlossen wird oder wenn die Enumerate Funktion aufgerufen wird. Dies ermöglicht es die Bricks und Bricklets im Stapel zu erkennen ohne im Voraus ihre UIDs kennen zu müssen.

Der Connected Callback wird ausgelöst wenn die Verbindung zur WIFI Extension oder zum Brick Daemon hergestellt wurde. In diesem Callback muss wiederum ein Enumerate angestoßen werden, wenn es sich um ein Auto-Reconnect handelt:

procedure TSmokeDetector.ConnectedCB(sender: TIPConnection; const connectedReason: byte);
begin
  if (connectedReason = IPCON_CONNECT_REASON_AUTO_RECONNECT) then begin
    ipcon.Enumerate;
  end;
end;

Ein Auto-Reconnect bedeutet, dass die Verbindung zur WIFI Extension oder zum Brick Daemon verloren gegangen ist und automatisch wiederhergestellt werden konnte. In diesem Fall kann es sein, dass die Bricklets ihre Konfiguration verloren haben und wir sie neu konfigurieren müssen. Da die Konfiguration beim Enumerate (siehe unten) durchgeführt wird, lösen wir einfach noch ein Enumerate aus.

Schritt 1 zusammengefügt:

const
  HOST = 'localhost';
  PORT = 4223;

type
  TSmokeDetector = class
  private
    ipcon: TIPConnection;
  public
    procedure ConnectedCB(sender: TIPConnection; const connectedReason: byte);
    procedure Execute;
  end;

procedure TSmokeDetector.ConnectedCB(sender: TIPConnection; const connectedReason: byte);
begin
  if (connectedReason = IPCON_CONNECT_REASON_AUTO_RECONNECT) then begin
    ipcon.Enumerate;
  end;
end;

procedure TSmokeDetector.Execute;
begin
  ipcon := TIPConnection.Create;
  ipcon.Connect(HOST, PORT);
  ipcon.OnEnumerate := {$ifdef FPC}@{$endif}EnumerateCB;
  ipcon.OnConnected := {$ifdef FPC}@{$endif}ConnectedCB;
  ipcon.Enumerate;
end;

Schritt 2: Bricklets beim Enumerate initialisieren

Während des Enumerierungsprozesses soll das Industrial Digital In 4 Bricklet konfiguriert werden. Dadurch ist sichergestellt, dass es neu konfiguriert wird nach einem Verbindungsabbruch oder einer Unterbrechung der Stromversorgung.

Die Konfiguration soll beim ersten Start (IPCON_ENUMERATION_TYPE_CONNECTED) durchgeführt werden und auch bei jedem extern ausgelösten Enumerate (IPCON_ENUMERATION_TYPE_AVAILABLE):

procedure TSmokeDetector.EnumerateCB(sender: TIPConnection; const uid: string;
                                     const connectedUid: string; const position: char;
                                     const hardwareVersion: TVersionNumber;
                                     const firmwareVersion: TVersionNumber;
                                     const deviceIdentifier: word; const enumerationType: byte);
begin
  if ((enumerationType = IPCON_ENUMERATION_TYPE_CONNECTED) or
      (enumerationType = IPCON_ENUMERATION_TYPE_AVAILABLE)) then begin

Das Industrial Digital In 4 Bricklet wird so eingestellt, dass es die InterruptCB Callback-Funktion aufruft wenn sich die Spannung an einem der Eingänge verändert. Die Entprellperiode wird auf 10s (10000ms) gestellt, um zu vermeiden zu viele Callback zu erhalten. Interrupt-Erkennung wird für alle Eingänge aktiviert (15 = 0b1111).

if (deviceIdentifier = BRICKLET_INDUSTRIAL_DIGITAL_IN_4_DEVICE_IDENTIFIER) then begin
  brickletIndustrialDigitalIn4 := TBrickletIndustrialDigitalIn4.Create(uid, ipcon);
  brickletIndustrialDigitalIn4.SetDebouncePeriod(10000);
  brickletIndustrialDigitalIn4.SetInterrupt(15);
  brickletIndustrialDigitalIn4.OnInterrupt := {$ifdef FPC}@{$endif}InterruptCB;
end;

Schritt 2 zusammengefügt:

procedure TSmokeDetector.EnumerateCB(sender: TIPConnection; const uid: string;
                                     const connectedUid: string; const position: char;
                                     const hardwareVersion: TVersionNumber;
                                     const firmwareVersion: TVersionNumber;
                                     const deviceIdentifier: word; const enumerationType: byte);
begin
  if ((enumerationType = IPCON_ENUMERATION_TYPE_CONNECTED) or
      (enumerationType = IPCON_ENUMERATION_TYPE_AVAILABLE)) then begin
    if (deviceIdentifier = BRICKLET_INDUSTRIAL_DIGITAL_IN_4_DEVICE_IDENTIFIER) then begin
      brickletIndustrialDigitalIn4 := TBrickletIndustrialDigitalIn4.Create(uid, ipcon);
      brickletIndustrialDigitalIn4.SetDebouncePeriod(10000);
      brickletIndustrialDigitalIn4.SetInterrupt(15);
      brickletIndustrialDigitalIn4.OnInterrupt := {$ifdef FPC}@{$endif}InterruptCB;
    end;
  end;
end;

Schritt 3: Auf Alarmsignal reagieren

Jetzt müssen wir noch auf das Alarmsignal des Rauchmelders reagieren. Es soll aber nur auf das Einschalten der LED reagiert werden, nicht auf das Ausschalten. Dazu wird valueMask auf > 0 geprüft, in diesem Fall liegt an mindesten einem Eingang eine Spannung an, sprich die LED leuchtet.

procedure TSmokeDetector.InterruptCB(sender: TBrickletIndustrialDigitalIn4; const interruptMask: word; const valueMask: word);
begin
  if (valueMask > 0) then begin
    WriteLn('Fire! Fire!');
  end;
end;

Das ist es. Wenn wir diese drei Schritte zusammen in eine Datei kopieren und ausführen, dann hätten wir jetzt eine funktionierendes Programm, das den Alarmstatus eines Rauchmelders ausließt und auf dessen Alarmsignal reagiert.

In der jetzigen Form gibt das Programm nur eine Meldung aus. Dies kann auf verschiedene Weise verbessert werden. Zum Beispiel könnte das Programm jemanden per E-Mail oder SMS über den Alarm informieren.

Wie dem auch sei, wir haben noch nicht alle Ziele erreicht. Das Programm ist noch nicht robust genug. Was passiert wenn die Verbindung beim Start des Programms nicht hergestellt werden kann, oder wenn das Enumerate nach einem Auto-Reconnect nicht funktioniert?

Wir brauchen noch Fehlerbehandlung!

Schritt 4: Fehlerbehandlung und Logging

Beim Start des Programms versuchen wir solange die Verbindung herzustellen, bis es klappt:

while (true) do begin
  try
    ipcon.Connect(HOST, PORT);
    break;
  except
    on e: Exception do begin
      WriteLn('Connection Error: ' + e.Message);
      Sleep(1000);
    end;
  end;
end;

und es wird solange versucht ein Enumerate zu starten bis auch dies geklappt hat:

while (true) do begin
  try
    ipcon.Enumerate;
    break;
  except
    on e: Exception do begin
      WriteLn('Enumeration Error: ' + e.Message);
      Sleep(1000);
    end;
  end;
end;

Mit diesen Änderungen kann das Programm schon gestartet werden bevor der Master Brick angeschlossen ist.

Es müssen auch noch mögliche Fehler während des Enumerierungsprozesses behandelt werden:

if (deviceIdentifier = BRICKLET_INDUSTRIAL_DIGITAL_IN_4_DEVICE_IDENTIFIER) then begin
  try
    brickletIndustrialDigitalIn4 := TBrickletIndustrialDigitalIn4.Create(uid, ipcon);
    brickletIndustrialDigitalIn4.SetDebouncePeriod(10000);
    brickletIndustrialDigitalIn4.SetInterrupt(15);
    brickletIndustrialDigitalIn4.OnInterrupt := {$ifdef FPC}@{$endif}InterruptCB;
    WriteLn('Industrial Digital In 4 initialized');
  except
    on e: Exception do begin
      WriteLn('Industrial Digital In 4 init failed: ' + e.Message);
      brickletIndustrialDigitalIn4 := nil;
    end;
  end;
end;

Zusätzlich wollen wir noch ein paar Logausgaben einfügen. Diese ermöglichen es später herauszufinden was ein mögliches Problem ausgelöst hat.

Zum Beispiel, wenn der Master Brick über WLAN angebunden ist und häufig Auto-Reconnects auftreten, dann ist wahrscheinlich die WLAN Verbindung nicht sehr stabil.

Schritt 5: Alles zusammen

Jetzt sind alle für gesteckten Ziele für unseren gehackten Rauchmelder erreicht.

Das gesamte Programm für den gehackten Rauchmelder (download):

program SmokeDetector;

{$ifdef MSWINDOWS}{$apptype CONSOLE}{$endif}
{$ifdef FPC}{$mode OBJFPC}{$H+}{$endif}

uses
  SysUtils, IPConnection, Device, BrickletIndustrialDigitalIn4;

const
  HOST = 'localhost';
  PORT = 4223;

type
  TSmokeDetector = class
  private
    ipcon: TIPConnection;
    brickletIndustrialDigitalIn4: TBrickletIndustrialDigitalIn4;
  public
    constructor Create;
    destructor Destroy; override;
    procedure ConnectedCB(sender: TIPConnection; const connectedReason: byte);
    procedure EnumerateCB(sender: TIPConnection; const uid: string;
                          const connectedUid: string; const position: char;
                          const hardwareVersion: TVersionNumber;
                          const firmwareVersion: TVersionNumber;
                          const deviceIdentifier: word; const enumerationType: byte);
    procedure InterruptCB(sender: TBrickletIndustrialDigitalIn4; const interruptMask: word; const valueMask: word);
    procedure Execute;
  end;

var
  sd: TSmokeDetector;

constructor TSmokeDetector.Create;
begin
  ipcon := nil;
  brickletIndustrialDigitalIn4 := nil;
end;

destructor TSmokeDetector.Destroy;
begin
  if (brickletIndustrialDigitalIn4 <> nil) then brickletIndustrialDigitalIn4.Destroy;
  if (ipcon <> nil) then ipcon.Destroy;
  inherited Destroy;
end;

procedure TSmokeDetector.ConnectedCB(sender: TIPConnection; const connectedReason: byte);
begin
  if (connectedReason = IPCON_CONNECT_REASON_AUTO_RECONNECT) then begin
    WriteLn('Auto Reconnect');
    while (true) do begin
      try
        ipcon.Enumerate;
        break;
      except
        on e: Exception do begin
          WriteLn('Enumeration Error: ' + e.Message);
          Sleep(1000);
        end;
      end;
    end;
  end;
end;

procedure TSmokeDetector.EnumerateCB(sender: TIPConnection; const uid: string;
                                     const connectedUid: string; const position: char;
                                     const hardwareVersion: TVersionNumber;
                                     const firmwareVersion: TVersionNumber;
                                     const deviceIdentifier: word; const enumerationType: byte);
begin
  if ((enumerationType = IPCON_ENUMERATION_TYPE_CONNECTED) or
      (enumerationType = IPCON_ENUMERATION_TYPE_AVAILABLE)) then begin
    if (deviceIdentifier = BRICKLET_INDUSTRIAL_DIGITAL_IN_4_DEVICE_IDENTIFIER) then begin
      try
        brickletIndustrialDigitalIn4 := TBrickletIndustrialDigitalIn4.Create(uid, ipcon);
        brickletIndustrialDigitalIn4.SetDebouncePeriod(10000);
        brickletIndustrialDigitalIn4.SetInterrupt(15);
        brickletIndustrialDigitalIn4.OnInterrupt := {$ifdef FPC}@{$endif}InterruptCB;
        WriteLn('Industrial Digital In 4 initialized');
      except
        on e: Exception do begin
          WriteLn('Industrial Digital In 4 init failed: ' + e.Message);
          brickletIndustrialDigitalIn4 := nil;
        end;
      end;
    end;
  end;
end;

procedure TSmokeDetector.InterruptCB(sender: TBrickletIndustrialDigitalIn4; const interruptMask: word; const valueMask: word);
begin
  if (valueMask > 0) then begin
    WriteLn('Fire! Fire!');
  end;
end;

procedure TSmokeDetector.Execute;
begin
  ipcon := TIPConnection.Create;
  while (true) do begin
    try
      ipcon.Connect(HOST, PORT);
      break;
    except
      on e: Exception do begin
        WriteLn('Connection Error: ' + e.Message);
        Sleep(1000);
      end;
    end;
  end;
  ipcon.OnEnumerate := {$ifdef FPC}@{$endif}EnumerateCB;
  ipcon.OnConnected := {$ifdef FPC}@{$endif}ConnectedCB;
  while (true) do begin
    try
      ipcon.Enumerate;
      break;
    except
      on e: Exception do begin
        WriteLn('Enumeration Error: ' + e.Message);
        Sleep(1000);
      end;
    end;
  end;
  WriteLn('Press key to exit');
  ReadLn;
end;

begin
  sd := TSmokeDetector.Create;
  sd.Execute;
  sd.Destroy;
end.