arduino-sketches – Blame information for rev 19

Subversion Repositories:
Rev:
Rev Author Line No. Line
18 office 1 /*************************************************************************/
2 /* Copyright (C) 2023 Wizardry and Steamworks - License: GNU GPLv3 */
3 /*************************************************************************/
4 // Project URL: //
19 office 5 // http://grimore.org/iot/reading_victron_energy_serial_data //
18 office 6 // //
7 // The following Arduino sketch implements a reader for all devices //
8 // created by Victron Energy by following the VE.Direct Protocol //
9 // description. The sketch captures VE.Direct frames, computes the //
10 // checksum of each frame and if the frame is valid then a JSON payload //
11 // is generated and published to an MQTT broker. //
12 // //
13 // Connecting to a Victron Energy devices involves creating a cable that //
14 // is specific to the device to be interfaced with. However, the //
15 // interface will always be an RS232 serial port interface such that the //
16 // serial ports on an ESP board can be used. //
17 // //
18 // For example, a Victron Energy Power Inverter has a port on the upper //
19 // side that consists in three pins, typically, from left-to right in //
20 // order carrying the meaning: GND, RX, TX, 5V with the RX and TX having //
21 // to be inverted (crossed-over) in order to interface with the device. //
22 // //
23 // For simplicity's sake, the sketch just uses the default serial port //
24 // which is the same one that is used typically to connect to the ESP //
25 // board using an USB cable. By consequence, serial port functions such //
26 // as Serial.println() should be avoided because they will end up //
27 // sending data to the device instead of through the USB cable. //
28 ///////////////////////////////////////////////////////////////////////////
29  
30 // Removing comment for debugging over the first serial port.
31 //#define DEBUG 1
32 // The AP to connect to via Wifi.
33 #define STA_SSID ""
34 // The AP Wifi password.
35 #define STA_PSK ""
36 // The MQTT broker to connect to.
37 #define MQTT_HOST ""
38 // The MQTT broker username.
39 #define MQTT_USERNAME ""
40 // The MQTT broker password.
41 #define MQTT_PASSWORD ""
42 // The MQTT broker port.
43 #define MQTT_PORT 1883
44 // The default MQTT client ID is "esp-CHIPID" where CHIPID is the ESP8266
45 // or ESP32 chip identifier.
46 #define MQTT_CLIENT_ID() String("esp-" + String(GET_CHIP_ID(), HEX))
47 // The authentication password to use for OTA updates.
48 #define OTA_PASSWORD ""
49 // The OTA port on which updates take place.
50 #define OTA_PORT 8266
51 // The default topic that the sketch subscribes to is "esp/CHIPID" where
52 // CHIPID is the ESP8266 or ESP32 chip identifier.
53 #define MQTT_TOPIC() String("esp/" + String(GET_CHIP_ID(), HEX))
54  
55 // Platform specific defines.
56 #if defined(ARDUINO_ARCH_ESP8266)
57 #define GET_CHIP_ID() (ESP.getChipId())
58 #elif defined(ARDUINO_ARCH_ESP32)
59 #define GET_CHIP_ID() ((uint16_t)(ESP.getEfuseMac() >> 32))
60 #endif
61  
62 // Miscellaneous defines.
63 //#define CHIP_ID_HEX (String(GET_CHIP_ID()).c_str())
64 #define HOSTNAME() String("esp-" + String(GET_CHIP_ID(), HEX))
65  
66 // https://www.victronenergy.com/upload/documents/VE.Direct-Protocol-3.33.pdf
67 // matches a message from a VE Direct frame
68 #define VE_DIRECT_MESSAGE_REGEX "\r\n([a-zA-Z0-9_]+)\t([^\r\n]+)"
69  
70 // Platform specific libraries.
71 #if defined(ARDUINO_ARCH_ESP8266)
72 #include <ESP8266WiFi.h>
73 #include <ESP8266mDNS.h>
74 #elif defined(ARDUINO_ARCH_ESP32)
75 #include <WiFi.h>
76 #include <ESPmDNS.h>
77 #endif
78 // General libraries.
79 #include <WiFiUdp.h>
80 #include <ArduinoOTA.h>
81 #include <PubSubClient.h>
82 #include <ArduinoJson.h>
83 #include <Regexp.h>
84 #if defined(ARDUINO_ARCH_ESP32)
85 #include <FS.h>
86 #include <SPIFFS.h>
87 #endif
88  
89 WiFiClient espClient;
90 PubSubClient mqttClient(espClient);
91 StaticJsonDocument<512> veFrame;
92 String veFrameBuffer = "";
93  
94 bool mqttConnect() {
95 #ifdef DEBUG
96 Serial.println("Attempting to connect to MQTT broker: " + String(MQTT_HOST));
97 #endif
98 mqttClient.setServer(MQTT_HOST, MQTT_PORT);
99  
100 StaticJsonDocument<256> msg;
101 if (mqttClient.connect(MQTT_CLIENT_ID().c_str(), MQTT_USERNAME, MQTT_PASSWORD)) {
102 #ifdef DEBUG
103 Serial.println("Established connection with MQTT broker using client ID: " + MQTT_CLIENT_ID());
104 #endif
105 msg["action"] = "connected";
106 char output[512];
107 serializeJson(msg, output, 512);
108 mqttClient.publish(MQTT_TOPIC().c_str(), output);
109 #ifdef DEBUG
110 Serial.println("Attempting to subscribe to MQTT topic: " + MQTT_TOPIC());
111 #endif
112 if (!mqttClient.subscribe(MQTT_TOPIC().c_str())) {
113 #ifdef DEBUG
114 Serial.println("Failed to subscribe to MQTT topic: " + MQTT_TOPIC());
115 #endif
116 return false;
117 }
118 #ifdef DEBUG
119 Serial.println("Subscribed to MQTT topic: " + MQTT_TOPIC());
120 #endif
121 msg.clear();
122 msg["action"] = "subscribed";
123 serializeJson(msg, output, 512);
124 mqttClient.publish(MQTT_TOPIC().c_str(), output);
125 return true;
126 }
127 #ifdef DEBUG
128 Serial.println("Connection to MQTT broker failed with MQTT client state: " + String(mqttClient.state()));
129 #endif
130 return false;
131 }
132  
133 bool programLoop() {
134 // Process OTA loop first since emergency OTA updates might be needed.
135 ArduinoOTA.handle();
136  
137 // Process MQTT client loop.
138 if (!mqttClient.connected()) {
139 // If the connection to the MQTT broker has failed then sleep before carrying on.
140 if (!mqttConnect()) {
141 return false;
142 }
143 }
144 return mqttClient.loop();
145 }
146  
147 //https://www.victronenergy.com/live/vedirect_protocol:faq#q8how_do_i_calculate_the_text_checksum
148 // computes the checksum of a VEDirect frame
149 // return true iff. the checksum of the frame is valid.
150 bool isVEDirectChecksumValid(String frame) {
151 int checksum = 0;
152 for (int i = 0; i < frame.length(); ++i) {
153 checksum = (checksum + frame[i]) & 255;
154 }
155 return checksum == 0;
156 }
157  
158 void frameRegexMatchCallback(const char *match, const unsigned int length, const MatchState &ms) {
159 // https://www.victronenergy.com/upload/documents/VE.Direct-Protocol-3.33.pdf:
160 // k -> 9 bytes
161 // v -> 33 bytes
162 char k[9];
163 ms.GetCapture(k, 0);
164 char v[33];
165 ms.GetCapture(v, 1);
166  
167 // The checksum is irrelevant since the frame has already deemed to be valid.
168 if (String(k) == "Checksum") {
169 return;
170 }
171  
172 veFrame[k] = v;
173 }
174  
175 void setup() {
176 // https://www.victronenergy.com/upload/documents/VE.Direct-Protocol-3.33.pdf
177 // baud: 19200
178 // data bits: 8
179 // parity: none
180 // stop bits: 1
181 // flow control: none
182 Serial.begin(19200);
183  
184 #ifdef DEBUG
185 Serial.println("Booted, setting up Wifi in 10s...");
186 #endif
187 delay(10000);
188  
189 WiFi.mode(WIFI_STA);
190 #if defined(ARDUINO_ARCH_ESP8266)
191 WiFi.hostname(HOSTNAME().c_str());
192 #elif defined(ARDUINO_ARCH_ESP32)
193 WiFi.setHostname(HOSTNAME().c_str());
194 #endif
195 WiFi.begin(STA_SSID, STA_PSK);
196 while (WiFi.waitForConnectResult() != WL_CONNECTED) {
197 #ifdef DEBUG
198 Serial.println("Failed to connect to Wifi, rebooting in 5s...");
199 #endif
200 delay(5000);
201 ESP.restart();
202 }
203  
204 #ifdef DEBUG
205 Serial.print("Connected to Wifi: ");
206 Serial.println(WiFi.localIP());
207 Serial.println("Setting up OTA in 10s...");
208 #endif
209 delay(10000);
210  
211 // Port defaults to 8266
212 ArduinoOTA.setPort(OTA_PORT);
213  
214 // Hostname defaults to esp-[ChipID]
215 ArduinoOTA.setHostname(HOSTNAME().c_str());
216  
217 // Set the OTA password
218 ArduinoOTA.setPassword(OTA_PASSWORD);
219  
220 ArduinoOTA.onStart([]() {
221 switch (ArduinoOTA.getCommand()) {
222 case U_FLASH: // Sketch
223 #ifdef DEBUG
224 Serial.println("OTA start updating sketch.");
225 #endif
226 break;
227 #if defined(ARDUINO_ARCH_ESP8266)
228 case U_FS:
229 #elif defined(ARDUINO_ARCH_ESP32)
230 case U_SPIFFS:
231 #endif
232 #ifdef DEBUG
233 Serial.println("OTA start updating filesystem.");
234 #endif
235 SPIFFS.end();
236 break;
237 default:
238 #ifdef DEBUG
239 Serial.println("Unknown OTA update type.");
240 #endif
241 break;
242 }
243 });
244 ArduinoOTA.onEnd([]() {
245 #ifdef DEBUG
246 Serial.println("OTA update complete.");
247 #endif
248 SPIFFS.begin();
249 #if defined(ARDUINO_ARCH_ESP8266)
250 // For what it's worth, check the filesystem on ESP8266.
251 SPIFFS.check();
252 #endif
253 ESP.restart();
254 });
255 ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
256 #ifdef DEBUG
257 Serial.printf("OTA update progress: %u%%\r", (progress / (total / 100)));
258 #endif
259 });
260 ArduinoOTA.onError([](ota_error_t error) {
261 #ifdef DEBUG
262 Serial.printf("OTA update error [%u]: ", error);
263 #endif
264 switch (error) {
265 case OTA_AUTH_ERROR:
266 #ifdef DEBUG
267 Serial.println("OTA authentication failed");
268 #endif
269 break;
270 case OTA_BEGIN_ERROR:
271 #ifdef DEBUG
272 Serial.println("OTA begin failed");
273 #endif
274 break;
275 case OTA_CONNECT_ERROR:
276 #ifdef DEBUG
277 Serial.println("OTA connect failed");
278 #endif
279 break;
280 case OTA_RECEIVE_ERROR:
281 #ifdef DEBUG
282 Serial.println("OTA receive failed");
283 #endif
284 break;
285 case OTA_END_ERROR:
286 #ifdef DEBUG
287 Serial.println("OTA end failed");
288 #endif
289 break;
290 default:
291 #ifdef DEBUG
292 Serial.println("Unknown OTA failure");
293 #endif
294 break;
295 }
296 ESP.restart();
297 });
298 ArduinoOTA.begin();
299  
300 // Set up MQTT client.
301 mqttClient.setServer(MQTT_HOST, MQTT_PORT);
302  
303 // Touchdown.
304 #ifdef DEBUG
305 Serial.println("Setup complete.");
306 #endif
307 }
308  
309 void loop() {
310 // Check the Wifi connection status.
311 int wifiStatus = WiFi.status();
312 switch (wifiStatus) {
313 case WL_CONNECTED:
314 if (!programLoop()) {
315 delay(1000);
316 break;
317 }
318 delay(1);
319 break;
320 case WL_NO_SHIELD:
321 #ifdef DEBUG
322 Serial.println("No Wifi shield present.");
323 #endif
324 goto DEFAULT_CASE;
325 break;
326 case WL_NO_SSID_AVAIL:
327 #ifdef DEBUG
328 Serial.println("Configured SSID not found.");
329 #endif
330 goto DEFAULT_CASE;
331 break;
332 // Temporary statuses indicating transitional states.
333 case WL_IDLE_STATUS:
334 case WL_SCAN_COMPLETED:
335 delay(1000);
336 break;
337 // Fatal Wifi statuses trigger a delayed ESP restart.
338 case WL_CONNECT_FAILED:
339 case WL_CONNECTION_LOST:
340 case WL_DISCONNECTED:
341 default:
342 #ifdef DEBUG
343 Serial.println("Wifi connection failed with status: " + String(wifiStatus));
344 #endif
345 DEFAULT_CASE:
346 delay(10000);
347 ESP.restart();
348 break;
349 }
350 }
351  
352 void serialEvent() {
353 while (Serial.available()) {
354 // get the new byte:
355 char c = (char)Serial.read();
356 veFrameBuffer += c;
357  
358 MatchState checksumMatchState;
359 checksumMatchState.Target((char *)veFrameBuffer.c_str());
360 char result = checksumMatchState.Match("Checksum\t.");
361 // The checksum field that marks the end of the frame has been found.
362 if (result == REGEXP_MATCHED) {
363 // Compute the checksum and see whether the frame is valid.
364 if (!isVEDirectChecksumValid(veFrameBuffer)) {
365 // If the checksum fails to compute then the frame is invalid so discard it.
366 veFrameBuffer = "";
367 return;
368 }
369  
370 // The frame is valid so match the individual messages.
371 MatchState messageMatchState((char *)veFrameBuffer.c_str());
372 messageMatchState.GlobalMatch(VE_DIRECT_MESSAGE_REGEX, frameRegexMatchCallback);
373  
374 // Publish the frame.
375 char output[512];
376 serializeJson(veFrame, output, 512);
377 veFrame.clear();
378 mqttClient.publish(MQTT_TOPIC().c_str(), output);
379  
380 // Reset the buffer.
381 veFrameBuffer = "";
382 return;
383 }
384 }
385 }