Read out Smoke Detectors using C

For this project we are assuming, that you have a C development environment set up and that you have a rudimentary understanding of the C language.

If you are totally new to C itself you should start here. If you are new to the Tinkerforge API, you should start here.

We are also assuming that you have a smoke detector connected to an Industrial Digital In 4 Bricklet as described here.

Goals

We are setting the following goal for this project:

  • Read out the alarm status of a smoke detector
  • and react on its alarm signal.

Since this project will likely run 24/7, we will also make sure that the application is as robust towards external influences as possible. The application should still work when

  • Bricklets are exchanged (i.e. we don't rely on UIDs),
  • Brick Daemon isn't running or is restarted,
  • WIFI Extension is out of range or
  • Brick is restarted (power loss or accidental USB removal).

In the following we will show step-by-step how this can be achieved.

Step 1: Discover Bricks and Bricklets

To start off, we need to define where our program should connect to:

#define HOST "localhost"
#define PORT 4223

If the WIFI Extension is used or if the Brick Daemon is running on a different PC, you have to exchange "localhost" with the IP address or hostname of the WIFI Extension or PC.

When the program is started, we need to register the IPCON_CALLBACK_ENUMERATE callback and the IPCON_CALLBACK_CONNECTED callback and trigger a first enumerate:

typedef struct {
    IPConnection ipcon;
} SmokeDetector;

int main() {
    SmokeDetector sd;
    ipcon_create(&sd.ipcon);
    ipcon_connect(&sd.ipcon, HOST, PORT);

    ipcon_register_callback(&sd.ipcon,
                            IPCON_CALLBACK_ENUMERATE,
                            (void *)cb_enumerate,
                            (void *)&sd);
    ipcon_register_callback(&sd.ipcon,
                            IPCON_CALLBACK_CONNECTED,
                            (void *)cb_connected,
                            (void *)&sd);

    ipcon_enumerate(&sd.ipcon);
    return 0;
}

The enumerate callback is triggered if a Brick gets connected over USB or if the ipcon_enumerate() function is called. This allows to discover the Bricks and Bricklets in a stack without knowing their types or UIDs beforehand.

The connected callback is triggered if the connection to the WIFI Extension or to the Brick Daemon got established. In this callback we need to trigger the enumerate again, if the reason is an auto reconnect:

void cb_connected(uint8_t connected_reason, void *user_data) {
    SmokeDetector *sd = (SmokeDetector *)user_data;

    if(connected_reason == IPCON_CONNECT_REASON_AUTO_RECONNECT) {
        ipcon_enumerate(&sd->ipcon);
    }
}

An auto reconnect means, that the connection to the WIFI Extension or to the Brick Daemon was lost and could subsequently be established again. In this case the Bricklets may have lost their configurations and we have to reconfigure them. Since the configuration is done during the enumeration process (see below), we have to trigger another enumeration.

Step 1 put together:

typedef struct {
    IPConnection ipcon;
} SmokeDetector;

void cb_connected(uint8_t connected_reason, void *user_data) {
    SmokeDetector *sd = (SmokeDetector *)user_data;

    if(connected_reason == IPCON_CONNECT_REASON_AUTO_RECONNECT) {
        ipcon_enumerate(&sd->ipcon);
    }
}

int main() {
    SmokeDetector sd;
    ipcon_create(&sd.ipcon);
    ipcon_connect(&sd.ipcon, HOST, PORT);

    ipcon_register_callback(&sd.ipcon,
                            IPCON_CALLBACK_ENUMERATE,
                            (void *)cb_enumerate,
                            (void *)&sd);
    ipcon_register_callback(&sd.ipcon,
                            IPCON_CALLBACK_CONNECTED,
                            (void *)cb_connected,
                            (void *)&sd);

    ipcon_enumerate(&sd.ipcon);
    return 0;
}

Step 2: Initialize Bricklet on Enumeration

During the enumeration we want to configure the Industrial Digital In 4 Bricklet. Doing this during the enumeration ensures that the Bricklet gets reconfigured if the Brick was disconnected or there was a power loss.

The configurations should be performed on first startup (IPCON_ENUMERATION_TYPE_CONNECTED) as well as whenever the enumeration is triggered externally by us (IPCON_ENUMERATION_TYPE_AVAILABLE):

void cb_enumerate(const char *uid, const char *connected_uid,
                  char position, uint8_t hardware_version[3],
                  uint8_t firmware_version[3], uint16_t device_identifier,
                  uint8_t enumeration_type, void *user_data) {
    SmokeDetector *sd = (SmokeDetector*)user_data;

    if(enumeration_type == IPCON_ENUMERATION_TYPE_CONNECTED ||
       enumeration_type == IPCON_ENUMERATION_TYPE_AVAILABLE) {

We configure the Industrial Digital In 4 Bricklet to call the cb_interrupt callback if a change of the voltage level on any input pin is detected. The debounce period is set to 10s (10000ms) to avoid being spammed with callbacks. Interrupt detection is enabled for all inputs (15 = 0b1111).

if(device_identifier == INDUSTRIAL_DIGITAL_IN_4_DEVICE_IDENTIFIER) {
    industrial_digital_in_4_create(&sd->idi4, uid, &sd->ipcon);
    industrial_digital_in_4_set_debounce_period(&sd->idi4, 10000);
    industrial_digital_in_4_register_callback(&sd->idi4,
                                              INDUSTRIAL_DIGITAL_IN_4_CALLBACK_INTERRUPT,
                                              (void *)cb_interrupt,
                                              (void *)sd);
    industrial_digital_in_4_set_interrupt(&sd->idi4, 15);
}

Step 2 put together:

void cb_enumerate(const char *uid, const char *connected_uid,
                  char position, uint8_t hardware_version[3],
                  uint8_t firmware_version[3], uint16_t device_identifier,
                  uint8_t enumeration_type, void *user_data) {
    SmokeDetector *sd = (SmokeDetector*)user_data;

    if(enumeration_type == IPCON_ENUMERATION_TYPE_CONNECTED ||
       enumeration_type == IPCON_ENUMERATION_TYPE_AVAILABLE) {
        if(device_identifier == INDUSTRIAL_DIGITAL_IN_4_DEVICE_IDENTIFIER) {
            industrial_digital_in_4_create(&sd->idi4, uid, &sd->ipcon);
            industrial_digital_in_4_set_debounce_period(&sd->idi4, 10000);
            industrial_digital_in_4_register_callback(&sd->idi4,
                                                      INDUSTRIAL_DIGITAL_IN_4_CALLBACK_INTERRUPT,
                                                      (void *)cb_interrupt,
                                                      (void *)sd);
            industrial_digital_in_4_set_interrupt(&sd->idi4, 15);
        }
    }
}

Step 3: Handle the alarm signal

Now we need to react on the alarm signal of the smoke detector. But we want to react only if the LED is turned on, not if it is turn off. This is done by checking value_mask for being > 0. In that case there is a voltage applied to at least one input, therefore, the LED is on.

void cb_interrupt(uint16_t interrupt_mask, uint16_t value_mask, void *user_data) {
    if(value_mask > 0) {
        printf("Fire! Fire!\n");
    }
}

That's it. If we would copy these three steps together in one file and execute it, we would have a working program that reads the alarm status of a hacked smoke detector and reacts on its alarm signal!

Currently the program just outputs a warning. There are several ways to extend this. For example, the program could send an email or a text message to notify someone about the alarm.

However, we do not meet all of our goals yet. The program is not yet robust enough. What happens if it can't connect on startup? What happens if the enumerate after an auto reconnect doesn't work?

What we need is error handling!

Step 4: Error handling and Logging

On startup, we need to try to connect until the connection works:

while(true) {
    int rc = ipcon_connect(&sd.ipcon, HOST, PORT);
    if(rc < 0) {
        fprintf(stderr, "Could not connect to brickd: %d\n", rc);
        // TODO: sleep 1s
        continue;
    }
    break;
}

and we need to try enumerating until the message goes through:

while(true) {
    int rc = ipcon_enumerate(&sd.ipcon);
    if(rc < 0) {
        fprintf(stderr, "Could not enumerate: %d\n", rc);
        // TODO: sleep 1s
        continue;
    }
    break;
}

There is no portable sleep function in C. On Windows windows.h declares a Sleep function that takes the duration in milliseconds. On POSIX systems such as Linux and macOS there is a sleep function declared in unistd.h that takes the duration in seconds.

With these changes it is now possible to first start the program and connect the Master Brick afterwards.

We also have to deal with errors during the initialization:

if(device_identifier == INDUSTRIAL_DIGITAL_IN_4_DEVICE_IDENTIFIER) {
    industrial_digital_in_4_create(&sd->idi4, uid, &sd->ipcon);
    industrial_digital_in_4_set_debounce_period(&sd->idi4, 10000);
    industrial_digital_in_4_register_callback(&sd->idi4,
                                              INDUSTRIAL_DIGITAL_IN_4_CALLBACK_INTERRUPT,
                                              (void *)cb_interrupt,
                                              (void *)sd);

    int rc = industrial_digital_in_4_set_interrupt(&sd->idi4, 15);
    if(rc < 0) {
        fprintf(stderr, "Industrial Digital In 4 init failed: %d\n", rc);
    } else {
        printf("Industrial Digital In 4 initialized\n");
    }
}

Additionally we added some logging. With the logging we can later find out what exactly caused a potential problem.

For example, if we connect to the Master Brick via Wi-Fi and we have regular auto reconnects, it likely means that the Wi-Fi connection is not very stable.

Step 5: Everything put together

That's it! We are already done with our hacked smoke detector and all of the goals should be met.

Now all of the above put together (download):

#include <stdio.h>

#include "ip_connection.h"
#include "bricklet_industrial_digital_in_4.h"

#define HOST "localhost"
#define PORT 4223

typedef struct {
    IPConnection ipcon;
    IndustrialDigitalIn4 idi4;
} SmokeDetector;

void cb_interrupt(uint16_t interrupt_mask, uint16_t value_mask, void *user_data) {
    // avoid unused parameter warning
    (void)interrupt_mask; (void)user_data;

    if(value_mask > 0) {
        printf("Fire! Fire!\n");
    }
}

void cb_connected(uint8_t connected_reason, void *user_data) {
    SmokeDetector *sd = (SmokeDetector *)user_data;

    if(connected_reason == IPCON_CONNECT_REASON_AUTO_RECONNECT) {
        printf("Auto Reconnect\n");

        while(true) {
            int rc = ipcon_enumerate(&sd->ipcon);
            if(rc < 0) {
                fprintf(stderr, "Could not enumerate: %d\n", rc);
                // TODO: sleep 1s
                continue;
            }
            break;
        }
    }
}

void cb_enumerate(const char *uid, const char *connected_uid,
                  char position, uint8_t hardware_version[3],
                  uint8_t firmware_version[3], uint16_t device_identifier,
                  uint8_t enumeration_type, void *user_data) {
    SmokeDetector *sd = (SmokeDetector *)user_data;

    // avoid unused parameter warning
    (void)connected_uid; (void)position; (void)hardware_version; (void)firmware_version;

    if(enumeration_type == IPCON_ENUMERATION_TYPE_CONNECTED ||
       enumeration_type == IPCON_ENUMERATION_TYPE_AVAILABLE) {
        if(device_identifier == INDUSTRIAL_DIGITAL_IN_4_DEVICE_IDENTIFIER) {
            industrial_digital_in_4_create(&sd->idi4, uid, &sd->ipcon);
            industrial_digital_in_4_set_debounce_period(&sd->idi4, 10000);
            industrial_digital_in_4_register_callback(&sd->idi4,
                                                      INDUSTRIAL_DIGITAL_IN_4_CALLBACK_INTERRUPT,
                                                      (void (*)(void))cb_interrupt,
                                                      (void *)sd);

            int rc = industrial_digital_in_4_set_interrupt(&sd->idi4, 15);
            if(rc < 0) {
                fprintf(stderr, "Industrial Digital In 4 init failed: %d\n", rc);
            } else {
                printf("Industrial Digital In 4 initialized\n");
            }
        }
    }
}

int main(void) {
    SmokeDetector sd;

    ipcon_create(&sd.ipcon);

    while(true) {
        int rc = ipcon_connect(&sd.ipcon, HOST, PORT);
        if(rc < 0) {
            fprintf(stderr, "Could not connect to brickd: %d\n", rc);
            // TODO: sleep 1s
            continue;
        }
        break;
    }

    ipcon_register_callback(&sd.ipcon,
                            IPCON_CALLBACK_ENUMERATE,
                            (void (*)(void))cb_enumerate,
                            (void *)&sd);

    ipcon_register_callback(&sd.ipcon,
                            IPCON_CALLBACK_CONNECTED,
                            (void (*)(void))cb_connected,
                            (void *)&sd);

    while(true) {
        int rc = ipcon_enumerate(&sd.ipcon);
        if(rc < 0) {
            fprintf(stderr, "Could not enumerate: %d\n", rc);
            // TODO: sleep 1s
            continue;
        }
        break;
    }

    printf("Press key to exit\n");
    getchar();
    ipcon_destroy(&sd.ipcon);
    return 0;
}