Microcontroller projects

Creative-Lab IoT-board

last updated: 2020-11-04

IoT and MQTT

Last time we were experimenting with the MQTT protocol in our lab. MQTT is a publisher/subscriber protocol and we were astonished how simple it was to implement this protocol on two Wemos D1 mini boards (ESP8266) over Wifi.
For this it was necessary to run a message broker. We used mosquitto on a raspberry pi. One Wemos published the state of a switch. The other Wemos subscribed to this message and lit an LED accordingly.

To read the impulse LEDs of my new five smartmeters (who aren't smart at all for the moment), I needed 5 microcontroller interrupts. This was difficult with only the Wemos board, because there weren't enough interrupt pins.
So I decided to combine the Teensy 2.0 board (mega32u4) with the Wemos board.

Features of the Creative-Lab IoT-board

cl<em>iot</em>board

Wemos and Teensy can both be programmed with Arduino. Wemos is an I2C Master and Teensy the slave. So the EIA232 interface is free for projects. The pcb is one sided with only 3 bridges and it is designed in KiCad (see Downloads).

Application: Photosensors for smartmeter

The new Luxembourgian smartmeters have a blinking LED (0,06 Wh/imp). By counting these pulses during 1 minute we get the energy/minute and can calculate the power. A photo diode SFH309 with a resistor of 1MΩ in series gives proper impulses for the Teensy interrupt pin.

The housing is designed with blender and uses 4 strong magnets with 6 mm diameter (stl and blender files in Downloads).

smartmeter smartmeter2 photodiode

Here the result with gnuplot:

gnuplot

Circuit and PCB

cl<em>iot</em>board_schematic

cl<em>iot</em>board<em>pcb</em>silk

cl<em>iot</em>board<em>pcb</em>co

Software

Teensy as slave

The following Teensy program is reading 4 pin change interrupts (rising flank). Timer 1 of the mega32u4 is set to 1 second. By polling the flag register the timer interrupt is avoided. The interrupts are counted exactly during 1 minute. The data is send by I2C interrupt if requested from wemos (I2C master). The first byte is used to signal if valid data is available.

    /* read 5 photodiodes (SFH309 with R=1M) from smartmeters (interrupts) and
     * send info to wemos with I2C
     * weigu.lu */

    #include <Wire.h>

    #ifndef cbi
    #define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
    #endif

    const byte PhotoPin0 = 0;   // PB0 on Teensy (PCINT0) consum
    const byte PCINT0Pin = 0;   // PCINT0
    const byte PhotoPin4 = 13;  // PB4 on Teensy (PCINT4) solar10
    const byte PCINT4Pin = 4;   // PCINT4
    const byte PhotoPin5 = 14;  // PB5 on Teensy (PCINT5) solar8
    const byte PCINT5Pin = 5;   // PCINT5
    const byte PhotoPin6 = 15;  // PB6 on Teensy (PCINT6) solar3
    const byte PCINT6Pin = 6;   // PCINT6

    const byte teensyLED = 11; // PD6 Teensy LED
    const byte teensyI2CAddr = 0x10;

    volatile byte oldPort = 0x00;
    volatile word INT0Cnt = 0, INT4Cnt = 0, INT5Cnt = 0, INT6Cnt = 0;
    volatile word INT0CntVal = 0, INT4CntVal = 0, INT5CntVal = 0, INT6CntVal = 0;
    byte volatile Flag;
    byte txTable[20];
    byte seccounter = 0;

    void setup() {
    Flag = 0;
    pinMode(teensyLED, OUTPUT);
    Wire.begin(teensyI2CAddr);    // join I2C bus
    cbi(PORTD, 0); // no internal PU
    cbi(PORTD, 1); // external 2x1,8k
    Wire.setClock(400000L);
    delay(1);  
    Wire.onRequest(I2CTransmit); // I2C interrupt send  
    pinMode(PhotoPin0, INPUT);   // set pins to input with a pullup
    pinMode(PhotoPin4, INPUT);
    pinMode(PhotoPin5, INPUT);
    pinMode(PhotoPin6, INPUT);
    attachPinChangeInterrupt();
    noInterrupts();              // timer1 init (to get exactly 1 Minute)
    TCCR1A = 0;
    TCCR1B = 0x0C;               // WGM32 (CTC) , Prescaler = 256
    OCR1A = 62500;               // 16M/256/62500 = 1 second
    interrupts(); }

    void loop() {
    if (TIFR1 & 0x02) {        // Flag set? (Timer1 by polling)
        seccounter++;
        TIFR1 |= 0x02;           // clear Flag with 1! (data sheet)
        if (seccounter == 60) {  // send data once per minute
        seccounter = 0;
        INT0CntVal = INT0Cnt; //using only 4 interrupts of 8
        INT4CntVal = INT4Cnt;
        INT5CntVal = INT5Cnt;
        INT6CntVal = INT6Cnt;      
        INT0Cnt = 0;
        INT4Cnt = 0;
        INT5Cnt = 0;
        INT6Cnt = 0;
        Flag = 1;  }}}

    void I2CTransmit() {
    if (Flag == 0) { Wire.write(0x11);}
    else {
        txTable[0] = 0; // needed for wemos see if data ready
        txTable[1] = highByte(INT0CntVal);
        txTable[2] = lowByte(INT0CntVal);
        txTable[3] = highByte(INT4CntVal);
        txTable[4] = lowByte(INT4CntVal);
        txTable[5] = highByte(INT5CntVal);
        txTable[6] = lowByte(INT5CntVal);
        txTable[7] = highByte(INT6CntVal);
        txTable[8] = lowByte(INT6CntVal);
        txTable[9] = highByte(5); //for testing
        txTable[10] = lowByte(5);
        Wire.write(txTable,11);
        Flag = 0;}}

    void attachPinChangeInterrupt(void) {  
    oldPort = PINB;
    // pin change mask registers decide which pins are enabled as triggers
    PCMSK0 |= (1 << PCINT0Pin); //PCINT0
    PCMSK0 |= (1 << PCINT4Pin); //PCINT4
    PCMSK0 |= (1 << PCINT5Pin); //PCINT5
    PCMSK0 |= (1 << PCINT6Pin); //PCINT6
    PCICR |= (1 << PCIE0); }  // enable interrupt vector

    ISR(PCINT0_vect) {
    byte newPort = PINB;             // get the new pin states
    byte change = newPort ^ oldPort; // xor to detect a rising or falling
    // check which pins are triggered, compared with the settings
    byte rising = change & newPort;
    byte mask0 = (1 << PCINT0Pin);
    if (rising & mask0) INT0Cnt++;
    byte mask4 = (1 << PCINT4Pin);
    if (rising & mask4) INT4Cnt++;
    byte mask5 = (1 << PCINT5Pin);
    if (rising & mask5) INT5Cnt++;
    byte mask6 = (1 << PCINT6Pin);
    if (rising & mask6) INT6Cnt++;
    oldPort = newPort; }  // save the new state for next comparison

Wemos as master and publisher

The Wemos program is reading the the time from RTC DS3231. As I2C Master its getting the data from Teensy and publishing it with timestamp in json format.

mqtt<em>json</em>message

The data is also saved on an SD card using the Wemos SD card shield.

    /* Wemos D1 mini: get data from Teensy and publish with MQTT
    * (using cool lib from Nick O'Leary)
    * weigu.lu */

    #include <Wire.h>
    #include <ESP8266WiFi.h>
    #include <PubSubClient.h>
    #include <SPI.h>
    #include <SD.h>

    WiFiClient espClient;
    PubSubClient client(espClient);

    const char ssid[] = "xxx";
    const char password[] = "xxx";
    const char mqtt_server[] = "192.168.xx.xx";
    const int mqttPort = 1883;
    const char clientId[] = "smartmeters";
    const char topic[] = "/iot/home/Smartmeters";

    IPAddress wemos_ip (192,168,xx,xx); //static IP
    IPAddress dns_ip     (8,8,8,8);
    IPAddress gateway_ip (192,168,xx,xx);
    IPAddress subnet_mask(255,255,255,0);

    const int DS3231 = 0x68;
    const int Teensy2 = 0x10;
    const byte wemosSCL = D1; //D1 GPIO5
    const byte wemosSDA = D2; //D2 GPIO4
    const int chipSelect = D8; //GPIO15

    long lastMsg = 0;
    char msg[150], datetime[50], fline[100],filename[13];
    byte tsec, tmin, thour, tweekd, tday, tmon, tyear;
    volatile int counter1 = 0, counter2 = 0, counter3 = 0, counter4 = 0;
    int counterValue1 = 0, counterValue2 = 0, counterValue3 = 0, counterValue4 = 0;
    byte i2cdata[20];
    word sm[10];

    void setup() {
    pinMode(BUILTIN_LED, OUTPUT);     // Initialize the BUILTIN_LED pin as an output
    digitalWrite(LED_BUILTIN,LOW);   // On
    Serial.begin(115200);
    Serial.print("Initializing SD card...");
    if (!SD.begin(chipSelect)) {    // see if the card is present and can be initialized:
        Serial.println("Card failed, or not present");
        return; }
    Serial.println("card initialized.");  
    Wire.begin(wemosSDA,wemosSCL);               // initialise I2C communication as Master
    Wire.setClock(400000L);
    setup_wifi();
    client.setServer(mqtt_server,mqttPort); }

    void loop() {
    digitalWrite(LED_BUILTIN,HIGH);   // Off
    if (!client.connected()) { reconnect(); }
    client.loop();
    if (Wire.requestFrom(Teensy2, 11) == 11) {
        digitalWrite(LED_BUILTIN,LOW);   // On
        i2cdata[0] = Wire.read();
        if (i2cdata[0]==0){
        i2cdata[1] = Wire.read();      
        i2cdata[2] = Wire.read();      
        i2cdata[3] = Wire.read();      
        i2cdata[4] = Wire.read();      
        i2cdata[5] = Wire.read();      
        i2cdata[6] = Wire.read();      
        i2cdata[7] = Wire.read();      
        i2cdata[8] = Wire.read();      
        i2cdata[9] = Wire.read();      
        i2cdata[10] = Wire.read();      
        sm[1] = word(i2cdata[1],i2cdata[2]);
        sm[2] = word(i2cdata[3],i2cdata[4]);
        sm[3] = word(i2cdata[5],i2cdata[6]);
        sm[4] = word(i2cdata[7],i2cdata[8]);
        sm[5] = word(i2cdata[9],i2cdata[10]);
        get_time(&tsec, &tmin, &thour, &tweekd, &tday, &tmon, &tyear);      
        snprintf (msg, 150, "{\"smCounter\":\"5ximp/min\",\"time\":\"%02d%02d%02d%02d%02d%02d\""
                            ",\"sm1\":\"%ld\",\"sm2\":\"%ld\",\"sm3\":\"%ld\",\"sm4\":\"%ld\",\"sm5\":\"%ld\"}",
                            tday, tmon, tyear,thour,tmin,tsec,sm[1],sm[2],sm[3],sm[4],sm[5]);    
        snprintf (fline, 100, "%02d.%02d.%02d\t%02d:%02d:%02d\t%ld\t%ld\t%ld\t%ld\t%ld",
                            tday, tmon, tyear,thour,tmin,tsec,sm[1],sm[2],sm[3],sm[4],sm[5]);     
        snprintf (filename, 13, "%02d_%02d_%02d.txt",tday, tmon, tyear);                                                   
        Serial.println(msg);
        Serial.println(fline);
        client.publish(topic, msg);
        File dataFile = SD.open(filename, FILE_WRITE); // open file
        if (dataFile) {  // if the file is available, write to it:
            dataFile.println(fline);
            dataFile.close(); }
        else {
            Serial.println("error opening file.txt");}}}}

    void setup_wifi() {
    WiFi.softAPdisconnect(); // to eliminate Hotspot
    WiFi.disconnect();
    WiFi.mode(WIFI_STA);
    delay(100);
    WiFi.config(wemos_ip, gateway_ip, subnet_mask);
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) { delay(500); }
    randomSeed(micros()); }

    void reconnect() {
    while (!client.connected()) { // Loop until we're reconnected    
        if (client.connect(clientId)) { // Attempt to connect
        client.publish(topic, "{\"wemos\":\"connected\"}"); } // Once connected, publish an announcement...
        else { delay(5000); }}}// Wait 5 seconds before retrying

    // Convert binary coded decimal to decimal number
    byte bcdToDec(byte val) { return((val/16*10) + (val%16)); }

    // Convert decimal number to binary coded decimal
    byte decToBcd(byte val) { return((val/10*16) + (val%10)); }

    // get the time from DS3231 (I2C)
    void get_time(byte *s, byte *m, byte *h, byte *wd, byte *d, byte *mo, byte *y) {
    Wire.beginTransmission(DS3231);
    Wire.write(0); // set DS3231 register pointer to 00h
    Wire.endTransmission();
    Wire.beginTransmission(DS3231);
    Wire.requestFrom(DS3231, 7);
    if (Wire.available() == 7) {  
        *s = bcdToDec(Wire.read() & 0x7f);
        *m = bcdToDec(Wire.read());
        *h = bcdToDec(Wire.read() & 0x3f);
        *wd = bcdToDec(Wire.read());
        *d = bcdToDec(Wire.read());
        *mo = bcdToDec(Wire.read());
        *y = bcdToDec(Wire.read()); }
    Wire.endTransmission();}  

MQTT.FX

For testing and debugging you can use the cool MQTT.FX software. It's a JavaFX based MQTT Client based on Eclipse Paho an very comfortable for testing purpose. Downloads on http://www.mqttfx.org.

mqttfx

Subscribe in Python

My beaglebone server is running the following Python script at reboot. In Python we need the paho.mqtt.client (use apt or pip) to subscribe to a topic. The data is saved in a file and a graphic is generated with gnuplot and send by email (full code in Downloads).

    #!/usr/bin/python3

    import sys
    import os
    import shutil
    import shlex
    from time import gmtime, strftime, localtime, sleep
    from datetime import datetime, timedelta, time
    import paho.mqtt.client as mqtt
    import json
    import subprocess
    ...
    imp = 0.06  #Wh/imp
    clientID = "getsmartmeters"
    brokerIP = "192.168.xx.xx"
    brokerPort = 1883
    topic  = "smartmeters"
    DEBUG = 0

    # Callback that is executed when the client receives a CONNACK response from the server.
    def onConnect(client, userdata, flags, rc):
    print("Connected with result code " + str(rc))

    # Callback that is executed when subscribing to a topic
    def onSubscribe(client, userdata, mid, granted_qos):
    if DEBUG: print('Subscribed on topic.')

    # Callback that is executed when a message is received.
    def onMessage(client, userdata, message):
        print('message received')
        ftime = strftime("%Y_%m_%d", localtime())
        ftime3 = strftime("%d.%m.%y %H:%M:%S", localtime())
        io=message.payload.decode("utf-8");
        print(io)
        try:
            ioj=json.loads(io)
        except:
            ioj={"time":"error!!!!!!","sm1":"0","sm2":"0","sm3":"0","sm4":"0","sm5":"0"}
        consum_e = 0
        solar10_e = 0
        solar8_e = 0
        solar3_e = 0
        solarall_e = 0
        temp = ioj["time"]
        if temp[0]!='e':
            iodate = '.'.join((temp[0:2],temp[2:4],temp[4:6]))
            iotime = ':'.join((temp[6:8],temp[8:10],temp[10:12]))    
            sm1 = str(round(float(ioj["sm1"])*imp*60,2))
            sm2 = str(round(float(ioj["sm2"])*imp*60,2))
            sm3 = str(round(float(ioj["sm3"])*imp*60,2))
            sm4 = str(round(float(ioj["sm4"])*imp*60,2))
            solarall = str(round((float(sm2)+float(sm3)+float(sm4)),2))
            try:    
                f = open (sm_datafile1, 'r')
            except IOError:
                print("error reading file "+sm_datafile1)
            lineList = f.readlines()            #read all lines        
            f.close()
            try:    
                f = open (sm_datafile1, 'a')
            except IOError:
                print ("Cannot create or find file: " + sm_datafile1)
            try:    
                f2 = open (sm_datafile2+ftime+'.min', 'a')
            except IOError:
                print ("Cannot create or find file: " + sm_datafile2)
            if (len(lineList)) == 1:
                sm_data = ' '.join((ftime3,sm1,sm2,sm3,sm4,solarall,'0','0',
                                    '0','0','0',iodate,iotime))
                sm_data = sm_data + '\n'                                
            else:                        
                line = lineList[len(lineList)-1]    #get the last line
                lline =shlex.split(line)            #convert string (space seperated items) to list
                consum_e = str(round(float(sm1)/60+float(lline[7]),2)) #Wh
                solar10_e = str(round(float(sm2)/60+float(lline[8]),2))
                solar8_e = str(round(float(sm3)/60+float(lline[9]),2))
                solar3_e = str(round(float(sm4)/60+float(lline[10]),2))
                solarall_e = str(round(float(solarall)/60+float(lline[11]),2))
                sm_data = ' '.join((ftime3,sm1,sm2,sm3,sm4,solarall,consum_e,
                          solar10_e,solar8_e,solar3_e,solarall_e,iodate,iotime))
                sm_data = sm_data + '\n'
            print (sm_data)
            f.write(sm_data)
            f2.write(sm_data)
            f.close()
            f2.close()

    # Callback that is executed when we disconnect from the broker.
    def onDisconnect(client, userdata, message):
    print("Disconnected from the broker.")
    ...
    #-----------------------------------------------------------------------------
    # Main
    #-----------------------------------------------------------------------------
    ...
    mqttc = mqtt.Client(client_id=clientID, clean_session=True) # create client
    mqttc.on_connect    = onConnect   # define the callback functions
    mqttc.on_subscribe  = onSubscribe
    mqttc.on_message    = onMessage
    mqttc.on_disconnect = onDisconnect
    mqttc.connect(brokerIP, brokerPort, keepalive=60, bind_address="") # connect to the broker
    mqttc.loop_start()
    Cnt = 0
    try:
        while True:                   # looping, asking every 2 seconds.
            mqttc.subscribe(topic, 0)  # Subscribe to the topic (topic name, QoS)
            sleep(2)
            mqttc.unsubscribe(topic)   # Unsubscribe from topic
            Cnt=Cnt+1
            #print(Cnt)
            if Cnt==30:
                ftime4 = strftime("%H:%M", localtime())
                ftime = strftime("%Y_%m_%d", localtime())
                try:
                    sm_create_gp_file()
                except:
                    print ("cannot create sm_gp file")               
                try:
                    p = subprocess.Popen(["/usr/bin/gnuplot", sm_gnupfile2],
                    stdin= subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                    stdout, stderr = p.communicate('\r\n\r\n'.encode(), timeout=10)
                    print ("gnuplot used for sm")
                except:
                    print ("Error plotting file sm (gnuplot)")
                try:
                    shutil.copy(png_dir+'sm_'+ftime+'.png', wpath+'sm_daily.png')
                except:
                    print ("Cannot copy sm file")
                Cnt = 0
            ...        
    except KeyboardInterrupt:
        print("Keyboard interrupt by user")
        mqttc.loop_stop() # clean up
        mqttc.disconnect()

Here the result with gnuplot:

gnuplot

Add a cron job

If you want to start the Python script automatically at reboot, add the following line to your /etc/crontab file.

@reboot root python3 /home/myname/teensylogger/bbb_smartmeter_led.py >> /home/myname/smartmeter_led__log.txt 2>&1

The output of the Python script is redirected to a text-file, for debugging.

Downloads