last updated: 2020-11-04
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.
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
).
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
).
Here the result with gnuplot:
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
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.
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();}
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.
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:
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.