#include "lwip/ip.h" #include "tic.h" #include "serial.h" #include "secret.h" #include #include // #define DEBUG 1 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; static struct GroupDetail processGroup(String group) { struct GroupDetail gd; gd.globale = group; unsigned char computedChecksum = calcCheckSum(group); int indexgrp = group.indexOf(HT); gd.name = group.substring(0, indexgrp); group = group.substring(indexgrp + 1); indexgrp = group.indexOf(HT); gd.value = group.substring(0, indexgrp); group = group.substring(indexgrp + 1); indexgrp = group.indexOf(HT); String key = ""; if (indexgrp != -1) // some parameters may have hour recording. { gd.horodate = gd.value; gd.value = group.substring(0, indexgrp); group = group.substring(indexgrp + 1); } //gd.checksum = group; if (group[0]==computedChecksum) { gd.checkok = true; } else { gd.checkok = false; } 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 && gd.checkok){ //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; if (newcal < oldval * 0.92 || newcal > oldval * 1.02) { gd.updated = true; TicValues[t] = gd; } else { 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 { 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 snprintf(jres, sizeof(jres), "\"%s\": \"%s\"", SelectedEtiquette[i].c_str(), TicValues[i].value.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; } void mqttPublish(PubSubClient *mqttclient) { for (int i = 0; i < NB_ETIQUETTE; ++i) { if (TicValues[i].updated) { String topic = MQTT_TOPIC; topic += "/", topic += SelectedEtiquette[i]; mqttclient->publish(topic.c_str(), TicValues[i].value.c_str()); TicValues[i].updated = false; } } } 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()); } } /** * 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; } } } } } // Function to calculate the checksum unsigned char calcCheckSum(const String &data) { unsigned int sum = 0; // Calculate the sum of ASCII values 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 unsigned char checksum = sum + 0x20; return checksum; } unsigned char calcCheckSum2(GroupDetail *data) { char c; uint sum = 2 * HT; if (data->name.length() && data->value.length()) { for (char &c : data->name) { if (c >= 0x20 && c <= 0x7E) { sum += c; } else { return 0; } } for (char &c : data->value) { if (c >= 0x20 && c <= 0x7E) { sum += c; } else { return 0; } } return (sum & 0x3f) + 0x20; } return 0; }