514 lines
19 KiB
C++
514 lines
19 KiB
C++
#include "lwip/ip.h"
|
|
#include "tic.h"
|
|
#include "serial.h"
|
|
#include "secret.h"
|
|
#include <Arduino.h>
|
|
#include <PubSubClient.h>
|
|
|
|
struct GroupDetail TicValues[NB_ETIQUETTE] = {};
|
|
|
|
// La lecture / ecriture des données tic s'effectue sur les variables data{1,2}.
|
|
// Pour éviter des pb, il y a un swap. ce qui permet d'avoir une variable en lecture seule, une en écriture seule.
|
|
String data1 = ""; // Variable pour stocker la trame complète
|
|
String data2 = ""; // Variable pour stocker la trame complète
|
|
|
|
int nActiveData = 1;
|
|
boolean isReceiving = false; // Indicateur pour savoir si on est dans une trame
|
|
RegistreStatus regStatus; // definition du registre status
|
|
RelaisStatus relaisStatus; // definition du relais status
|
|
Action actionJp1[11]; // actions définie pour jour +1
|
|
int nbActions;
|
|
|
|
/**
|
|
* Calculates the checksum for a given data string according to ENEDIS specifications.
|
|
* Supports standard data mode (not history mode). [well to be honest, who has interest to keep the history mode.]
|
|
* Group format in standard mode with timestamp (horodatage) - Last HT included in checksum
|
|
* LF etiquette HT horodatage HT donnee HT Chk CR
|
|
* 0A 09 09 09 0D
|
|
* \____________checkum_______________/
|
|
|
|
* Group format in standard mode without timestamp (horodatage) - Last HT included in checksum
|
|
* LF etiquette HT donnee HT Chk CR
|
|
* 0A 09 09 0D
|
|
* \_____checkum________/
|
|
*
|
|
*
|
|
* @param data The input string for which the checksum is to be calculated.
|
|
* @return The calculated checksum as an unsigned char.
|
|
*/
|
|
unsigned char calcCheckSum(const String &data) {
|
|
unsigned int sum = 0;
|
|
|
|
// Calculate the sum of ASCII values, excluding the checksum character
|
|
// The string does not contain the CR char. The char before CR is the checksum.
|
|
for (size_t i = 0; i < data.length() - 1; ++i) {
|
|
sum += data[i];
|
|
}
|
|
|
|
// Truncate the sum to 6 bits
|
|
sum &= 0x3F;
|
|
|
|
// Add 0x20 to get the final checksum
|
|
return (unsigned char)sum + 0x20;
|
|
}
|
|
|
|
static struct GroupDetail processGroup(String group) {
|
|
struct GroupDetail gd;
|
|
//gd.globale = group; // Store the entire group for reference during debug
|
|
|
|
// Calculate the checksum for the entire group
|
|
unsigned char computedChecksum = calcCheckSum(group);
|
|
|
|
// Extract the name (etiquette) from the group
|
|
int indexgrp = group.indexOf(HT);
|
|
gd.name = group.substring(0, indexgrp);
|
|
|
|
// Move to the value part
|
|
group = group.substring(indexgrp + 1);
|
|
indexgrp = group.indexOf(HT);
|
|
gd.value = group.substring(0, indexgrp);
|
|
|
|
// Move to the horodate part, if it exists
|
|
group = group.substring(indexgrp + 1);
|
|
indexgrp = group.indexOf(HT);
|
|
if (indexgrp != -1) // Check if there is an horodate part
|
|
{
|
|
gd.horodate = gd.value;
|
|
gd.value = group.substring(0, indexgrp);
|
|
group = group.substring(indexgrp + 1);
|
|
}
|
|
|
|
// Verify the checksum
|
|
gd.checkok = (group[0] == computedChecksum);
|
|
|
|
return gd;
|
|
}
|
|
|
|
static void processStge(RegistreStatus *rs, String value) {
|
|
char stge[9] = "";
|
|
// copy in the char array
|
|
strncpy(stge, value.c_str(), 8);
|
|
stge[8] = '\0';
|
|
unsigned long l = strtoul(stge, NULL, 16); // Convert hex pair to unsigned long
|
|
rs->uli = l;
|
|
}
|
|
|
|
static void processRelais(RelaisStatus *rs, String value) {
|
|
char stge[4] = "";
|
|
// copy in the char array
|
|
strncpy(stge, value.c_str(), 3);
|
|
stge[4] = '\0';
|
|
rs->ui = strtoul(stge, NULL, 16);
|
|
}
|
|
|
|
static void processActionsCalendrier(String value) {
|
|
nbActions = 0;
|
|
String s = value;
|
|
while (s.length() > 0) {
|
|
int index = s.indexOf(SP);
|
|
if (index == -1) // No space found
|
|
{
|
|
break;
|
|
} else {
|
|
char data[9] = "";
|
|
data[8] = '\0';
|
|
strncpy(data, s.substring(0, index).c_str(), 8);
|
|
if (strncmp(data, NONUTILE, 8) != 0) {
|
|
char stge[5] = "";
|
|
// copy ssss field
|
|
memcpy(stge, &data[4], 4);
|
|
actionJp1[nbActions].action.ui = strtoul(stge, NULL, 16);
|
|
// copt hhmm
|
|
memcpy(actionJp1[nbActions].startTime, &data[0], 4);
|
|
++nbActions;
|
|
}
|
|
s = s.substring(index + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Processes a data frame to extract and store relevant information.
|
|
*
|
|
* This function iterates over the input data string, extracting groups of information
|
|
* delimited by carriage return (CR) characters. Each group is processed to update
|
|
* the corresponding values in the TicValues array.
|
|
*
|
|
* @param data A reference to a String containing the data frame to be processed.
|
|
*/
|
|
static void processTrame(String &data) {
|
|
while (data.length() > 0) {
|
|
// Find the position of the next carriage return (CR) character
|
|
int index = data.indexOf(CR);
|
|
// If no CR is found, exit the loop
|
|
if (index == -1) {
|
|
break;
|
|
} else {
|
|
// Extract the group string between the start and the CR character
|
|
String group = data.substring(1, index);
|
|
// Process the group to extract detailed information
|
|
auto gd = processGroup(group);
|
|
|
|
// Check if the extracted group name matches any user-selected etiquette
|
|
int t = 0;
|
|
while ((SelectedEtiquette[t] != gd.name) && (t < NB_ETIQUETTE)) {
|
|
++t;
|
|
}
|
|
// If a match is found, update the corresponding TicValues entry if the group confirms the checksum
|
|
if (t < NB_ETIQUETTE) {
|
|
//If there is a value update....
|
|
if (TicValues[t].value.compareTo(gd.value) != 0 || TicValues[t].horodate.compareTo(gd.horodate) != 0) {
|
|
//There is some noise on instantaneous value, filter
|
|
if (SelectedEtiquette[t] == "SINSTS" || SelectedEtiquette[t] == "SINSTS1" || SelectedEtiquette[t] == "SINSTS2" || SelectedEtiquette[t] == "SINSTS3") {
|
|
int oldval = TicValues[t].value.toInt();
|
|
int newcal = (gd.value.toInt() + oldval) / 2;
|
|
//there is a significant change, so update
|
|
if (newcal < oldval * 0.92 || newcal > oldval * 1.02) {
|
|
gd.updated = true;
|
|
TicValues[t] = gd;
|
|
} else {
|
|
//the change is limited. Just record the mean to keep it fresh,
|
|
//but doesn't set the updated flag.
|
|
TicValues[t].value = String(newcal);
|
|
}
|
|
} else {
|
|
gd.updated = true;
|
|
TicValues[t] = gd;
|
|
// Depending on the group name, call the appropriate processing function
|
|
if (gd.name == "STGE") {
|
|
processStge(®Status, gd.value);
|
|
} else if (gd.name == "RELAIS") {
|
|
processRelais(&relaisStatus, gd.value);
|
|
} else if (gd.name == "PJOURF+1") {
|
|
processActionsCalendrier(gd.value);
|
|
}
|
|
}
|
|
} else {
|
|
//there is no value update, but the checksum is not ok (strange case, but...)
|
|
if (TicValues[t].checkok != gd.checkok) {
|
|
TicValues[t] = gd;
|
|
TicValues[t].updated = true;
|
|
} else {
|
|
TicValues[t].updated = false;
|
|
}
|
|
}
|
|
}
|
|
data = data.substring(index + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
static char *actionJp1AsJson() {
|
|
const int bufferSize = 1000;
|
|
static char jsonBuffer[bufferSize]; // Adjust size as needed
|
|
snprintf(jsonBuffer, bufferSize, "\"PJOURF+1\": [");
|
|
|
|
for (int i = 0; i < nbActions; i++) {
|
|
// Format each action
|
|
char actionJson[256]; // To store individual action JSON string
|
|
String relaisSec = "";
|
|
switch ((unsigned int)actionJp1[i].action.bits.relaisSec) {
|
|
case 0:
|
|
relaisSec = "no change";
|
|
break;
|
|
case 1:
|
|
relaisSec = "tempo";
|
|
break;
|
|
case 2:
|
|
relaisSec = "open";
|
|
break;
|
|
case 3:
|
|
relaisSec = "closed";
|
|
break;
|
|
default:
|
|
relaisSec = "unknown";
|
|
}
|
|
snprintf(actionJson, sizeof(actionJson),
|
|
" { \"startTime\": \"%c%c%c%c\", "
|
|
"\"relaisSec\": \"%s\", "
|
|
"\"relais7\": %u, \"relais6\": %u, \"relais5\": %u, \"relais4\": %u, "
|
|
"\"relais3\": %u, \"relais2\": %u, \"relais1\": %u, \"index\": %u }",
|
|
actionJp1[i].startTime[0], actionJp1[i].startTime[1], actionJp1[i].startTime[2], actionJp1[i].startTime[3],
|
|
relaisSec.c_str(),
|
|
actionJp1[i].action.bits.relais7, actionJp1[i].action.bits.relais6, actionJp1[i].action.bits.relais5,
|
|
actionJp1[i].action.bits.relais4, actionJp1[i].action.bits.relais3, actionJp1[i].action.bits.relais2,
|
|
actionJp1[i].action.bits.relais1, actionJp1[i].action.bits.index);
|
|
|
|
// Append the current action's JSON to the overall JSON buffer
|
|
if (i == (nbActions - 1)) { // Last item, no comma at the end
|
|
strncat(jsonBuffer, actionJson, bufferSize - strlen(jsonBuffer) - 1);
|
|
} else {
|
|
strncat(jsonBuffer, actionJson, bufferSize - strlen(jsonBuffer) - 1);
|
|
strncat(jsonBuffer, ",", bufferSize - strlen(jsonBuffer) - 1);
|
|
}
|
|
}
|
|
|
|
// End the JSON array
|
|
strncat(jsonBuffer, "]", bufferSize - strlen(jsonBuffer) - 1);
|
|
return jsonBuffer;
|
|
}
|
|
|
|
static char *relaisStatusAsJson(RelaisStatusBits *status, String rawValue) {
|
|
// Pre-allocate buffer large enough to hold the JSON string
|
|
static char response[150]; // Adjust size as needed
|
|
// Use snprintf to construct the JSON string efficiently
|
|
snprintf(response, sizeof(response),
|
|
"\"RELAIS\": "
|
|
"{"
|
|
"\"value\": \"%s\", "
|
|
"\"relaisSec\": %d, "
|
|
"\"relais1\": %d, "
|
|
"\"relais2\": %d, "
|
|
"\"relais3\": %d, "
|
|
"\"relais4\": %d, "
|
|
"\"relais5\": %d, "
|
|
"\"relais6\": %d, "
|
|
"\"relais7\": %d "
|
|
"}",
|
|
rawValue.c_str(),
|
|
status->relaisSec,
|
|
status->relais1,
|
|
status->relais2,
|
|
status->relais3,
|
|
status->relais4,
|
|
status->relais5,
|
|
status->relais6,
|
|
status->relais7);
|
|
return response;
|
|
}
|
|
|
|
static char *registreStatusAsJson(RegistreStatusBits *status, String rawValue) {
|
|
// Pre-allocate buffer large enough to hold the JSON string
|
|
static char response[1000]; // Adjust size as needed
|
|
|
|
// Use snprintf to construct the JSON string efficiently
|
|
snprintf(response, sizeof(response),
|
|
"\"STGE\": "
|
|
"{"
|
|
"\"value\": \"%s\", "
|
|
"\"contactsec\": \"%s\", "
|
|
"\"organeCoupure\": \"%s\", "
|
|
"\"cache\": \"%s\", "
|
|
"\"surtension\": \"%s\", "
|
|
"\"depassementPuissance\": \"%s\", "
|
|
"\"consoProd\": \"%s\", "
|
|
"\"senseActiveEnergy\": \"%s\", "
|
|
"\"tarifIndexConso\": %d, "
|
|
"\"tarifIndexProd\": %d, "
|
|
"\"horlogeState\": \"%s\", "
|
|
"\"ticState\": \"%s\", "
|
|
"\"comEuridis\": \"%s\", "
|
|
"\"cplState\": \"%s\", "
|
|
"\"cplSynchro\": \"%s\", "
|
|
"\"tempo\": \"%s\", "
|
|
"\"tempoNextDay\": \"%s\", "
|
|
"\"preavisPM\": \" preavis %s\", "
|
|
"\"PM\": \"%s\""
|
|
"}",
|
|
rawValue.c_str(),
|
|
kContactStatus[status->contactsec].c_str(),
|
|
kCoupure[status->organeCoupure].c_str(),
|
|
kContactStatus[status->cache].c_str(),
|
|
kOverVoltage[status->surtension].c_str(),
|
|
kOverPower[status->depassementPuissance].c_str(),
|
|
kProducer[status->consoProd].c_str(),
|
|
kActivePower[status->senseActiveEnergy].c_str(),
|
|
status->tarifIndexConso + 1,
|
|
status->tarifIndexProd + 1,
|
|
kHour[status->horlogeState].c_str(),
|
|
kTicMode[status->ticState].c_str(),
|
|
kEuridis[status->comEuridis].c_str(),
|
|
kCpl[status->cplState].c_str(),
|
|
kCplSynchro[status->cplSynchro].c_str(),
|
|
kTempoColor[status->tempo].c_str(),
|
|
kTempoColor[status->tempoNextDay].c_str(),
|
|
kPointeMobile[status->preavisPM].c_str(),
|
|
kPointeMobile[status->PM].c_str());
|
|
return response;
|
|
}
|
|
|
|
String ticValuesAsJson() {
|
|
String response = "{";
|
|
|
|
for (int i = 0; i < NB_ETIQUETTE; ++i) {
|
|
|
|
if (SelectedEtiquette[i] == "STGE") {
|
|
response += registreStatusAsJson(®Status.bits, TicValues[i].value);
|
|
} else if (SelectedEtiquette[i] == "RELAIS") {
|
|
response += relaisStatusAsJson(&relaisStatus.bits, TicValues[i].value);
|
|
} else if (SelectedEtiquette[i] == "PJOURF+1") {
|
|
response += actionJp1AsJson();
|
|
} else {
|
|
static char jres[150]; // Adjust size as needed
|
|
|
|
// Use snprintf to construct the JSON string efficiently
|
|
if (TicValues[i].horodate.isEmpty()) {
|
|
snprintf(jres, sizeof(jres),
|
|
"\"%s\": \"%s\"",
|
|
SelectedEtiquette[i].c_str(),
|
|
TicValues[i].value.c_str());
|
|
} else {
|
|
// Include horodate if it is not empty
|
|
snprintf(jres, sizeof(jres),
|
|
"\"%s\": {\"value\": \"%s\", \"horodate\": \"%s\"}",
|
|
SelectedEtiquette[i].c_str(),
|
|
TicValues[i].value.c_str(),
|
|
TicValues[i].horodate.c_str());
|
|
}
|
|
response += jres;
|
|
}
|
|
|
|
if (i < (NB_ETIQUETTE - 1)) {
|
|
response += ',';
|
|
}
|
|
}
|
|
response += "}";
|
|
return response;
|
|
}
|
|
|
|
String ticBasicValuesAsJson() {
|
|
String response = "{";
|
|
|
|
for (int i = 0; i < NB_ETIQUETTE; ++i) {
|
|
|
|
if (SelectedEtiquette[i] == "LTARF" || SelectedEtiquette[i] == "EAST" || SelectedEtiquette[i] == "EASF01" || SelectedEtiquette[i] == "EASF02" || SelectedEtiquette[i] == "EASF03" || SelectedEtiquette[i] == "EASF04" || SelectedEtiquette[i] == "EASD01" || SelectedEtiquette[i] == "EASD02" || SelectedEtiquette[i] == "EASD03" || SelectedEtiquette[i] == "EASD04" || SelectedEtiquette[i] == "EAIT" || SelectedEtiquette[i] == "ERQ1" || SelectedEtiquette[i] == "ERQ2" || SelectedEtiquette[i] == "ERQ3" || SelectedEtiquette[i] == "ERQ4" || SelectedEtiquette[i] == "IRMS1" || SelectedEtiquette[i] == "IRMS2" || SelectedEtiquette[i] == "IRMS3" || SelectedEtiquette[i] == "URMS1" || SelectedEtiquette[i] == "URMS2" || SelectedEtiquette[i] == "URMS3" || SelectedEtiquette[i] == "SINSTS" || SelectedEtiquette[i] == "SINSTSI" || SelectedEtiquette[i] == "SINSTS1" || SelectedEtiquette[i] == "SINSTS2" || SelectedEtiquette[i] == "SINSTS3" || SelectedEtiquette[i] == "SINSTSI") {
|
|
|
|
static char jres[150]; // Adjust size as needed
|
|
|
|
if (response != "{") {
|
|
response += ",";
|
|
}
|
|
|
|
// Use snprintf to construct the JSON string efficiently
|
|
snprintf(jres, sizeof(jres),
|
|
"\"%s\": \"%s\"",
|
|
SelectedEtiquette[i].c_str(),
|
|
TicValues[i].value.c_str());
|
|
response += jres;
|
|
}
|
|
}
|
|
response += "}";
|
|
return response;
|
|
}
|
|
|
|
/**
|
|
* Publishes the list of updated TIC values to the MQTT broker.
|
|
*
|
|
* This function iterates over the TicValues array and publishes the values
|
|
* that have been updated to the corresponding MQTT topics.
|
|
* After the publication, the values are not anymore identified as Updated.
|
|
* @param mqttclient Pointer to the PubSubClient instance used to publish messages.
|
|
*/
|
|
void mqttPublish(PubSubClient *mqttclient) {
|
|
for (int i = 0; i < NB_ETIQUETTE; ++i) {
|
|
String topic = MQTT_TOPIC;
|
|
topic += "/",
|
|
topic += SelectedEtiquette[i];
|
|
if (TicValues[i].updated && TicValues[i].checkok) {
|
|
mqttclient->publish(topic.c_str(), TicValues[i].value.c_str());
|
|
|
|
if (!TicValues[i].horodate.isEmpty()) {
|
|
mqttclient->publish((topic + "/date").c_str(), TicValues[i].horodate.c_str());
|
|
}
|
|
|
|
TicValues[i].updated = false;
|
|
}
|
|
|
|
if (TicValues[i].updated && !TicValues[i].checkok) {
|
|
topic += "/status";
|
|
mqttclient->publish(topic.c_str(), TicValues[i].checkok ? "Ok" : "Not Ok");
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Forces the publication of all TIC values to the MQTT broker.
|
|
*
|
|
* This function iterates over all TIC values and publishes them to their
|
|
* corresponding MQTT topics, regardless of whether they have been updated.
|
|
*
|
|
* @param mqttclient Pointer to the PubSubClient instance used to publish messages.
|
|
*/
|
|
void mqttForcePublish(PubSubClient *mqttclient) {
|
|
for (int i = 0; i < NB_ETIQUETTE; ++i) {
|
|
String topic = MQTT_TOPIC;
|
|
topic += "/",
|
|
topic += SelectedEtiquette[i];
|
|
mqttclient->publish(topic.c_str(), TicValues[i].value.c_str());
|
|
topic += "/status";
|
|
mqttclient->publish(topic.c_str(), TicValues[i].checkok ? "Ok" : "Not Ok");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reads data from the TicPort and processes it according to specific control characters.
|
|
*
|
|
* This function checks for available data on the TicPort and reads it byte by byte.
|
|
* It handles different control characters to manage the state of data reception:
|
|
* - EOT (End Of Transmission): Forces the end of transmission and rejects any ongoing data.
|
|
* - STX (Start Of Text): Indicates the start of a new data frame.
|
|
* - ETX (End Of Text): Indicates the end of the current data frame and processes the collected data.
|
|
*
|
|
* During data reception, the function appends incoming bytes to the active data buffer (data1 or data2)
|
|
* and processes the complete frame when ETX is encountered. Debug information is printed if DEBUG is defined.
|
|
*
|
|
* The built-in LED is used to indicate the state of data reception.
|
|
*/
|
|
void readTicPort() {
|
|
// Check TicPort availability
|
|
if (TicPort.available()) {
|
|
byte incomingByte = TicPort.read(); // Read a byte from the TicPort
|
|
// Check if the incoming byte is the End Of Transmission (EOT) character
|
|
if (incomingByte == EOT) {
|
|
// Force the end of transmission
|
|
// Reject everything
|
|
isReceiving = false;
|
|
digitalWrite(LED_BUILTIN, HIGH); // Turn the built-in LED to indicate the end of transmission
|
|
}
|
|
|
|
// Check if the system is currently receiving data
|
|
if (isReceiving) {
|
|
// Check if the end of the frame is reached (ETX character)
|
|
if (incomingByte == ETX) {
|
|
// Extract the useful part of the frame
|
|
if (nActiveData == 1) {
|
|
processTrame(data1); // Process the data in data1
|
|
} else {
|
|
processTrame(data2); // Process the data in data2
|
|
}
|
|
// Indicate that the data reception is complete
|
|
isReceiving = false;
|
|
digitalWrite(LED_BUILTIN, HIGH);
|
|
// Debugging information: Print the extracted data
|
|
#ifdef DEBUG
|
|
for (int i = 0; i < NB_ETIQUETTE; ++i) {
|
|
DebugPort.print(TicValues[i].name);
|
|
DebugPort.print(":");
|
|
DebugPort.println(TicValues[i].value);
|
|
}
|
|
#endif
|
|
} else {
|
|
// Add the incoming byte to the current frame
|
|
if (nActiveData == 1) {
|
|
data1 += (char)incomingByte; // Append the byte to data1
|
|
} else {
|
|
data2 += (char)incomingByte; // Append the byte to data2
|
|
}
|
|
}
|
|
} else {
|
|
// Look for the start of the frame (STX character)
|
|
if (incomingByte == STX) {
|
|
isReceiving = true;
|
|
digitalWrite(LED_BUILTIN, LOW);
|
|
if (nActiveData == 1) {
|
|
data2 = "";
|
|
nActiveData = 2;
|
|
} else {
|
|
data1 = "";
|
|
nActiveData = 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|