Goed, dan krijg je ook een stoom cursus in C programmeren voor communicatie en gebruik van tekst
Ik gebruik Serial3 op een Mega. Om het op een Uno te draaien met AltSoftSerial moet je de eerste regel hieronder veranderen van //#define USE_ALTSOFTSERIAL
naar #define USE_ALTSOFTSERIAL
.
//#define USE_ALTSOFTSERIAL
#ifdef USE_ALTSOFTSERIAL
#include <AltSoftSerial.h>
AltSoftSerial altSerial;
#define P1Serial altSerial
#else
#define P1Serial Serial3
#endif
Als USE_ALTSOFTSERIAL gedefinieerd is wordt die bibliotheek gebruikt, je altSerial object aangemaakt en er wordt een "alias" P1Serial gemaakt voor altSerial.
Als USE_ALTSOFTSERIAL niet gedefinieerd is, wordt er alleen een "alias" gemaakt voor Serial3.
Door de eerste regel te veranderen kun je kiezen tussen altSerial en Serial3 en overal in het programma kun je nu P1Serial gebruiken.
Vervolgens zijn er twee globale variabelen.
// field for filter
const char fieldOfInterest[] = "1-0:2.7.0";
// P1 receive buffer
char buffer[200];
De buffer is wat groot gekozen; 50 zal waarschijnlijk groot genoeg zijn.
Vervolgens de setup() functie.
void setup()
{
Serial.begin(115200);
#ifdef USE_ALTSOFTSERIAL
P1Serial.begin(115200);
#else
P1Serial.begin(115200, SERIAL_7E1);
#endif
}
Bijna hetzelfde als die je nu gebruikt maar gebaseerd op USE_ALTSOFTSERIAL wordt Serial3 (P1Serial) geïnitialiseerd voor het 7E1 formaat.
En dan loop().
void loop()
{
// if there is data
if (P1Serial.available())
{
// read and filter
if (readP1withFilter() == true)
{
// print the received record
Serial.print("Field data received: ");
Serial.println(buffer);
// parse the data and get the float value
float value;
// on success
if (parse(buffer, value) == true)
{
// print it
Serial.print("The value in the record = ");
Serial.println(value, 3);
}
}
else
{
Serial.println("Telegram error");
}
}
}
Er wordt gekeken of er data beschikbaar is van de P1 meter. Indien dat het geval is wordt de functie readP1withFilter() aangeroepen die het eigenlijke lezen en filteren doet. Als het lezen en filteren goed gegaan is wordt de ontvange data afgedrukt en vervolgen wordt de ontvangen data verwerkt met de parse() functie. Als het verwerken goed gegaan is wordt de waarde die gevonden is afgedrukt. Behalve afdrukken kun je ook iets met die waarde doen.
/*
Read P1 and filter
Returns:
true if field of interest was found, else false
*/
bool readP1withFilter()
{
// index where to store character in buffer
static uint16_t index;
// return value
bool rb = false;
char c = P1Serial.read();
#ifdef USE_ALTSOFTSERIAL
// --- 7 bits instelling ---
c &= ~(1 << 7);
char inChar = (char)c;
#endif
if (index == 0)
{
// clear the buffer
memset(buffer, '\0', sizeof(buffer));
}
buffer[index] = c;
// get the field of interest
if (index < strlen(fieldOfInterest))
{
// if there is a partial match with the field of interest
if (buffer[index] == fieldOfInterest[index])
{
// next received character to next location in buffer
index++;
}
// if there is a mismatch
else
{
// reset the index.
index = 0;
}
//Serial.write(c);
}
// we got a matching field of interest
else
{
// next received character to next location in buffer
index++;
// prevent buffer overflow
if (index == sizeof(buffer) - 1)
{
Serial.println("Buffer overflow");
// reset the index
index = 0;
}
// if the received character is the line feed that indicates the end of the record
if (c == '\n')
{
// remove trailing CR and LF
trimRight(buffer);
// reset the index
index = 0;
// indicate that we got filtered data
rb = true;
}
}
return rb;
}
Eerst worden er twee variabelen gedeclareerd; de eerst is static hetgeen betekent dat deze de waarde behoudt als de functie eindigt. De tweede is een simpele bool die bijhoudt of er iets fout gegaan is of niet.
Vervolgens wordt één ontvangen karakter gelezen. Je zult de functie continu moeten aanroepen zolang er karakters bescjikbaar zijn. Indien nodig wordt dat karakter aangepast voor het gebruik van de AltDoftSerial bibliotheek.
Om te voorkomen dat de data in de buffer niet korrekt is wordt de buffer gewist als de index nul is.
Vervolgens wordt dat karakter in de buffer gezet.
Daarna vindt het controleren op de OBIS referentie gedaan; als het karakter in de buffer overeen komt met een karakter in dezelfde plaats in fieldOfInterest wordt er doorgegaan na het verhogen van de index, anders wordt de index weer op nul gezet en er van het begin af begonnen.
Als uiteindelijk het fieldOFInterest gevonden is wordt de rest van de data gelezen (iedere keer één karakter per keer) totdat er een LF (einde van de regel) wordt gezien of totdat er geen plaats meer is in de buffer.
Als er een LF gezien is wordt de inhoud van de buffer ontdaan van de van de mogelijke CR en LF op het eind en de waarde van rb wordt op true gezet.
rb wordt terug gegeven aan de aanroepende functie (loop()) zodat die weet wanneer er geschikte data gevonden is.
Nu loop() weet dat er geschikte data is wordt de parse() functie aangeroepen.
/*
Parse received field
In:
text to parse; will be mangled after parsing
Out:
float value of record
Returns:
false on error, else true
*/
bool parse(char *b, float &f)
{
char *key = b;
char *value;
char *units;
// return value
bool rb = false;
// check if record ends with ')'
value = strrchr(b, ')');
// check if found
if (value != nullptr)
{
// remove
*value = '\0';
// find opening '('
value = strchr(b, '(');
// check if found
if (value != nullptr)
{
// split key and value
*value = '\0';
// point to next character
value++;
// print it
units = strchr(value, '*');
if (units != nullptr)
{
*units = '\0';
units++;
Serial.print("Key = ");
Serial.println(key);
Serial.print("Value = ");
//Serial.println(value);
rb = text2float(value, f);
if (rb == true)
{
Serial.println(f, 4);
Serial.print("Units = ");
Serial.println(units);
}
}
}
}
return rb;
}
De functie heeft twee argumenten, het karakter array met de tekst die gesplits moet worden en een float waar de uiteindelijke waarde in wordt opgeslagen.
We hebben weer wat variabelen; dit zijn pointers die gebruikt gaan worden om bepaalde stukjes uit de ontvagen tekst te halen; ze wijzen naar die tekst.
Om te begrijpen hoe tekst werkt in een C programma moet je weten dat het een array van karakters is die is afgesloten met een nul karakter (nul, '\0'). Bv "1-0:2.7.0(00.203*kW)"is opgeslagen als
'1' '-' '0' ':' '2' '.' '7' '.' '0' '(' '0' '0' '.' '2' '0' '3' '*' 'k' 'W' ')' 0
De pointer key wijst simpel naar het begin van de tekst, de andere twee worden "berekend".
Eerst wordt er gecontroleerd of er een afsluitend ')' is. Indien dat het geval is wordt de ')' verwijderd door deze te vervangen door het nul-karakter dat het eind van een tekst aangeeft. Het zier er nu zo uit
'1' '-' '0' ':' '2' '.' '7' '.' '0' '(' '0' '0' '.' '2' '0' '3' '*' 'k' 'W' 0 0
^key x
De x geeft het veranderde karakter aan, ^ geeft de naam van de pointer
Vervolgens wordt er gezocht naar de openings '('; ook die wordt verwijderd (vervangen door het nul karakter) en vervolgens wordt de pointer met 1 opgehoogd zodat deze nu naar het karakter na de originale '(' wijst; dit is de pointer value. Omdat we de '(' hebben verwijderd (vervangen door '\0') ziet het er nu zo uit
'1' '-' '0' ':' '2' '.' '7' '.' '0' 0 '0' '0' '.' '2' '0' '3' '*' 'k' 'W' 0 0
^key | x
x ^value
Daarna wordt er gezocht naar de '*' en deze wordt verwijderd op dezelfde manier en de pointer wordt met 1 verhoogd zodat deze nu naar het karakter na het veranderde karakter wijst.
'1' '-' '0' ':' '2' '.' '7' '.' '0' 0 '0' '0' '.' '2' '0' '3' 0 'k' 'W' 0 0
^key | | x
x ^value |
* ^units
We hebben nu (dus) een key "1-0:2.7.0", een value "00.203" en units "kW".
De value tekst wordt vervolgens omgezet in een float met de functie text2float(). Deze functie neemt weer twee argumenten, de tekst die omgezet moet worden (bv "00.203") en de variabele waar het resultaat in moet worden opgeslagen. Als je wilt begrijpen hoe het werkt kun je bv https://cplusplus.com/reference/cstdlib/strtod/ lezen (het is in het Engels, geen idee of dat een probleem is).
/*
Convert text to float
In:
text to convert
Out:
float value
Returns:
false on error, else true
*/
bool text2float(char *txt, float &f)
{
// return value
bool rb = false;
char *endptr;
f = strtod(txt, &endptr);
if (endptr == txt || *endptr != '\0')
{
Serial.println("Not a float value");
}
else
{
rb = true;
}
return rb;
}
En als laatste de trimRight() functie; deze heeft één argument, de tekst die opgeschoond moet worden; de opgeschoonde tekst staat weer in hetzelfde argument.
Remove CR and/or LF from text
In:
text to trim; will be modified
*/
void trimRight(char *txt)
{
while (txt[strlen(txt) - 1] == '\r' || txt[strlen(txt) - 1] == '\n')
{
txt[strlen(txt) - 1] = '\0';
}
}
Deze functie kijkt simpelweg (in een while-loop) naar het laatste karakter en als dat een CR of een LF is wordt dat karakter weer vervangen door een nul-karakter.
Het volledige programma
//#define USE_ALTSOFTSERIAL
#ifdef USE_ALTSOFTSERIAL
#include <AltSoftSerial.h>
AltSoftSerial altSerial;
#define P1Serial altSerial
#else
#define P1Serial Serial3
#endif
// field for filter
const char fieldOfInterest[] = "1-0:2.7.0";
// P1 receive buffer
char buffer[200];
void setup()
{
Serial.begin(115200);
#ifdef USE_ALTSOFTSERIAL
P1Serial.begin(115200);
#else
P1Serial.begin(115200, SERIAL_7E1);
#endif
}
void loop()
{
// if there is data
if (P1Serial.available())
{
// read and filter
if (readP1withFilter() == true)
{
// print the received record
Serial.print("Field data received: ");
Serial.println(buffer);
// parse the data and get the float value
float value;
// on success
if (parse(buffer, value) == true)
{
// print it
Serial.print("The value in the record = ");
Serial.println(value, 3);
}
}
}
}
/*
Read P1 and filter
Returns:
true if field of interest was found, else false
*/
bool readP1withFilter()
{
// index where to store character in buffer
static uint16_t index;
// return value
bool rb = false;
char c = P1Serial.read();
#ifdef USE_ALTSOFTSERIAL
// --- 7 bits instelling ---
c &= ~(1 << 7);
char inChar = (char)c;
#endif
if (index == 0)
{
// clear the buffer
memset(buffer, '\0', sizeof(buffer));
}
buffer[index] = c;
// get the field of interest
if (index < strlen(fieldOfInterest))
{
// if there is a partial match with the field of interest
if (buffer[index] == fieldOfInterest[index])
{
// next received character to next location in buffer
index++;
}
// if there is a mismatch
else
{
// reset the index.
index = 0;
}
//Serial.write(c);
}
// we got a matching field of interest
else
{
// next received character to next location in buffer
index++;
// prevent buffer overflow
if (index == sizeof(buffer) - 1)
{
Serial.println("Buffer overflow");
// reset the index
index = 0;
}
// if the received character is the line feed that indicates the end of the record
if (c == '\n')
{
// remove trailing CR and LF
trimRight(buffer);
// reset the index
index = 0;
// indicate that we got filtered data
rb = true;
}
}
return rb;
}
/*
Remove CR and/or LF from text
In:
text to trim; will be modified
*/
void trimRight(char *txt)
{
while (txt[strlen(txt) - 1] == '\r' || txt[strlen(txt) - 1] == '\n')
{
txt[strlen(txt) - 1] = '\0';
}
}
/*
Parse received field
In:
text to parse; will be mangled after parsing
Out:
float value of record
Returns:
false on error, else true
*/
bool parse(char *b, float &f)
{
char *key = b;
char *value;
char *units;
// return value
bool rb = false;
// check if record ends with ')'
value = strrchr(b, ')');
// check if found
if (value != nullptr)
{
// remove
*value = '\0';
// find opening '('
value = strchr(b, '(');
// check if found
if (value != nullptr)
{
// split key and value
*value = '\0';
// point to next character
value++;
// print it
units = strchr(value, '*');
if (units != nullptr)
{
*units = '\0';
units++;
Serial.print("Key = ");
Serial.println(key);
Serial.print("Value = ");
//Serial.println(value);
rb = text2float(value, f);
if (rb == true)
{
Serial.println(f, 4);
Serial.print("Units = ");
Serial.println(units);
}
}
}
}
return rb;
}
/*
Convert text to float
In:
text to convert
Out:
float value
Returns:
false on error, else true
*/
bool text2float(char *txt, float &f)
{
// return value
bool rb = false;
char *endptr;
f = strtod(txt, &endptr);
if (endptr == txt || *endptr != '\0')
{
Serial.println("Not a float value");
}
else
{
rb = true;
}
return rb;
}
En de typisch afdruk in bv de seriële monitor
Field data received: 1-0:2.7.0(00.204*kW)
Key = 1-0:2.7.0
Value = 0.2040
Units = kW
The value in the record = 0.204
De Value wordt afgedrukt met 4 decimalen, de waarde op de laatste regel wordt afgedrukt met 3 decimalen; dit om je te laten zien hoe je het aantal decimalen kunt aanpassen. Dit heeft geen invloed op de daadwerkelijke waarde.
Opmerking
Ik gebruik een Arduino Leonardo om een P1 meter te simuleren; met een druk op de knop stuurt die een telegraam naar de Mega. Ik heb ook andere versies die data kunnen pompen maar die moet ik even terug vinden indien noodzakelijk.
Als je vragen hebt hoor ik het wel.
Je zult moeten kijken of je foutmeldingen krijgt met betrekking tot het lezen, het filteren en het omzetten naar een float. Laat het een poosje draaien; een ander terminal programma (zoals eerder vermeld) kan handig zijn zodat je het een poosje kunt laten draaien en de gegevens naar een file worden heschreven die je later kunt bekijken.
Ik zal proberen deze week een wat betere versie te schrijven die controleert of er mogelijke corruptie heeft plaats gevonden door de CRC te controleren.