From 6e3e8c333015e50a4841ceca4fd88a07244bbf4a Mon Sep 17 00:00:00 2001 From: Merijn <19893132+mreijnde@users.noreply.github.com> Date: Wed, 25 Jan 2023 23:43:53 +0100 Subject: [PATCH] [RS232] add JSON parsing and hardware serial support to RS232 gateway (#1409) * add JSON parse option to RS232 gateway * add hardware UART support to RS232 gateway * changed default RS232toMQTTmode to raw (instead of JSON) * removed reference to Arduino Mega in RS232 gateway config * update RS232 gateway doc --- docs/use/rs232.md | 33 +++++++++ main/ZgatewayRS232.ino | 150 +++++++++++++++++++++++++++++++++++++---- main/config_RS232.h | 114 ++++++++++++++++++++++++++----- 3 files changed, 266 insertions(+), 31 deletions(-) diff --git a/docs/use/rs232.md b/docs/use/rs232.md index 6175f15..efdaf27 100644 --- a/docs/use/rs232.md +++ b/docs/use/rs232.md @@ -1,5 +1,7 @@ # RS232 gateway +The RS232 gateway can be used to send and receive data from the serial connection to and from MQTT. Both softwareSerial as hardwareSerial are supported. HardwareSerial can be used for higher baud rates, but is limited to specific pins on most platforms. + ## Sending an RS232 message Simply publish the message you wish to transmit, minus the prefix and postfix. For example, to send the "Turn On" signal for a Mitsubishi XD221U projector, the code is simply '!' so you would use the command @@ -8,7 +10,12 @@ Simply publish the message you wish to transmit, minus the prefix and postfix. F It will automatically add the prefix and postfix you set in [config_RS232.h](https://github.com/1technophile/OpenMQTTGateway/blob/master/main/config_RS232.h). + ## Receiving an RS232 message + +Two modes are available for receiving RS232 messages. + +### Single MQTT message mode (default) To receive a message, subscribe to all with `mosquitto_sub -t +/# -v` and perform an action that should get a response from the device. For example, If I were to send the "Turn On" signal from earlier, I would receive back @@ -21,3 +28,29 @@ Because this projector echoes back a received command to acknowledge. Some devic ```json home/OpenMQTTGateway/RStoMQTT {"value":"!:N"} ``` + +### JSON mode +This mode can be used if the received message on the serial link is JSON. The JSON keys are used as separate MQTT sub-topics. For nested JSON this will be repeated for sub-keys up to the specified nesting level. + +For example: + +input received at serial link: +```json +{temperature: {sens1: 22, sens2: 23}, humidity: {sens1: 80, sens2: 60}} +``` + + +output in case of max nesting level 1: +```json +home/OpenMQTTGateway/RS232toMQTT/temperature "{sens1: 22, sens2: 23}" +home/OpenMQTTGateway/RS232toMQTT/humidity "{sens1: 80, sens2: 60}" +``` + +output in case of max nesting level 2 (or higher): +```json +home/OpenMQTTGateway/RS232toMQTT/temperature/sens1 22 +home/OpenMQTTGateway/RS232toMQTT/temperature/sens2 23 +home/OpenMQTTGateway/RS232toMQTT/humidity/sens1 80 +home/OpenMQTTGateway/RS232toMQTT/humidity/sens2 60 +``` + diff --git a/main/ZgatewayRS232.ino b/main/ZgatewayRS232.ino index c0cb730..f6da25e 100644 --- a/main/ZgatewayRS232.ino +++ b/main/ZgatewayRS232.ino @@ -29,33 +29,89 @@ #ifdef ZgatewayRS232 -# include +# ifndef RS232_UART // software serial mode +# include +SoftwareSerial RS232SoftSerial(RS232_RX_GPIO, RS232_TX_GPIO); // RX, TX +# endif -SoftwareSerial RS232Serial(RS232_RX_GPIO, RS232_TX_GPIO); // RX, TX +// use pointer to stream class for serial communication to make code +// compatible with both softwareSerial as hardwareSerial. +Stream* RS232Stream = NULL; void setupRS232() { +//Initalize serial port +# ifdef RS232_UART // Hardware serial +# if RS232_UART == 0 // init UART0 + Serial.end(); // stop if already initialized +# ifdef ESP32 + Serial.begin(RS232Baud, SERIAL_8N1, RS232_RX_GPIO, RS232_TX_GPIO); +# else + Serial.begin(RS232Baud, SERIAL_8N1); +# endif +# if defined(ESP8266) && defined(RS232_UART0_SWAP) + Serial.swap(); // swap UART0 ports from (GPIO1,GPIO3) to (GPIO15,GPIO13) +# endif + RS232Stream = &Serial; + Log.notice(F("RS232 HW UART0" CR)); + +# elif RS232_UART == 1 // init UART1 + Serial1.end(); // stop if already initialized +# ifdef ESP32 + Serial1.begin(RS232Baud, SERIAL_8N1, RS232_RX_GPIO, RS232_TX_GPIO); +# else + Serial1.begin(RS232Baud, SERIAL_8N1); +# endif + RS232Stream = &Serial1; + Log.notice(F("RS232 HW UART1" CR)); + +# elif RS232_UART == 2 // init UART2 + Serial2.end(); // stop if already initialized +# ifdef ESP32 + Serial2.begin(RS232Baud, SERIAL_8N1, RS232_RX_GPIO, RS232_TX_GPIO); +# else + Serial2.begin(RS232Baud, SERIAL_8N1); +# endif + RS232Stream = &Serial2; + Log.notice(F("RS232 HW UART2" CR)); + +# elif RS232_UART == 3 // init UART3 + Serial3.end(); // stop if already initialized + Serial3.begin(RS232Baud, SERIAL_8N1); + RS232Stream = &Serial3; + Log.notice(F("RS232 HW UART3" CR)); +# endif + +# else // Software serial // define pin modes for RX, TX: pinMode(RS232_RX_GPIO, INPUT); pinMode(RS232_TX_GPIO, OUTPUT); - // set the data rate for the SoftwareSerial port - RS232Serial.begin(RS232Baud); + RS232SoftSerial.begin(RS232Baud); + RS232Stream = &RS232SoftSerial; // get stream of serial - Log.notice(F("RS232_RX_GPIO: %d " CR), RS232_RX_GPIO); - Log.notice(F("RS232_TX_GPIO: %d " CR), RS232_TX_GPIO); - Log.notice(F("RS232Baud: %d " CR), RS232Baud); - Log.trace(F("ZgatewayRS232 setup done " CR)); + Log.notice(F("RS232_RX_GPIO: %d" CR), RS232_RX_GPIO); + Log.notice(F("RS232_TX_GPIO: %d" CR), RS232_TX_GPIO); +# endif + + // Flush all bytes in the "link" serial port buffer + while (RS232Stream->available() > 0) + RS232Stream->read(); + + Log.notice(F("RS232Baud: %d" CR), RS232Baud); + Log.trace(F("ZgatewayRS232 setup done" CR)); } +# if RS232toMQTTmode == 0 // Convert received data to single MQTT topic void RS232toMQTT() { + // Send all RS232 output (up to RS232InPost char revieved) as MQTT message //This function is Blocking, but there should only ever be a few bytes, usually an ACK or a NACK. - if (RS232Serial.available()) { + if (RS232Stream->available()) { Log.trace(F("RS232toMQTT" CR)); static char RS232data[MAX_INPUT]; static unsigned int input_pos = 0; static char inChar; do { - if (RS232Serial.available()) { - inChar = RS232Serial.read(); + if (RS232Stream->available()) { + inChar = RS232Stream->read(); RS232data[input_pos] = inChar; input_pos++; Log.trace(F("Received %c" CR), inChar); @@ -69,6 +125,72 @@ void RS232toMQTT() { } } +# elif RS232toMQTTmode == 1 // Convert recievee JSON data to multiple MQTT topics based (nested) keys +void RS232toMQTT() { + // Assumes valid JSON data at RS232 interface. Use (nested) keys to split JSON data in separate + // sub-MQTT-topics up to the defined nesting level. + if (RS232Stream->available()) { + Log.trace(F("RS232toMQTT" CR)); + + // Allocate the JSON document + StaticJsonDocument doc; + + // Read the JSON document from the "link" serial port + DeserializationError err = deserializeJson(doc, *RS232Stream); + + if (err == DeserializationError::Ok) { + // JSON received, send as MQTT topics + char topic[mqtt_topic_max_size + 1] = subjectRS232toMQTT; + sendMQTTfromNestedJson(doc.as(), topic, 0, RS232maxJSONlevel); + } else { + // Print error to serial log + Log.error(F("Error in RS232JSONtoMQTT, deserializeJson() returned %s"), err.c_str()); + + // Flush all bytes in the "link" serial port buffer + while (RS232Stream->available() > 0) + RS232Stream->read(); + } + } +} + +void sendMQTTfromNestedJson(JsonVariant obj, char* topic, int level, int maxLevel) { + // recursively step through JSON data and send MQTT messages + if (level < maxLevel && obj.is()) { + int topicLength = strlen(topic); + // loop over fields + for (JsonPair pair : obj.as()) { + // check if new key still fits in topic cstring + const char* key = pair.key().c_str(); + Log.trace(F("level=%d, key='%s'" CR), level, pair.key().c_str()); + if (topicLength + 2 + strlen(key) <= mqtt_topic_max_size) { + // add new level to existing topic cstring + topic[topicLength] = '/'; // add slash + topic[topicLength + 1] = '\0'; // terminate + strncat(topic + topicLength, key, mqtt_topic_max_size - topicLength - 2); + + // step recursively into next level + sendMQTTfromNestedJson(pair.value(), topic, level + 1, maxLevel); + + // restore topic + topic[topicLength] = '\0'; + } else { + Log.error(F("Nested key '%s' at level %d does not fit within max topic length of %d, skipping"), + key, level, mqtt_topic_max_size); + } + } + + } else { + // output value at current json level + char output[MAX_INPUT + 1]; + serializeJson(obj, output, MAX_INPUT); + Log.notice(F("level=%d, topic=%s, value: %s\n"), level, topic, output); + + // send MQTT message + pub(topic, &output[0]); + } +} +# endif + void MQTTtoRS232(char* topicOri, JsonObject& RS232data) { Log.trace(F("json" CR)); if (cmpToMainTopic(topicOri, subjectMQTTtoRS232)) { @@ -79,9 +201,9 @@ void MQTTtoRS232(char* topicOri, JsonObject& RS232data) { Log.trace(F("Value set: %s" CR), data); Log.trace(F("Prefix set: %s" CR), prefix); Log.trace(F("Postfix set: %s" CR), postfix); - RS232Serial.print(prefix); - RS232Serial.print(data); - RS232Serial.print(postfix); + RS232Stream->print(prefix); + RS232Stream->print(data); + RS232Stream->print(postfix); } } #endif \ No newline at end of file diff --git a/main/config_RS232.h b/main/config_RS232.h index 68b2d80..a91cbe1 100644 --- a/main/config_RS232.h +++ b/main/config_RS232.h @@ -30,27 +30,106 @@ extern void setupRS232(); extern void RS232toMQTT(); extern void MQTTtoRS232(char* topicOri, JsonObject& RS232data); -/*-------------------IR topics & parameters----------------------*/ -//RS232 MQTT Subjects -#define subjectGTWRS232toMQTT "/RS232toMQTT" -#define subjectRS232toMQTT "/RS232toMQTT" -#define subjectMQTTtoRS232 "/commands/MQTTtoRS232" -#define subjectForwardMQTTtoRS232 "home/gateway2/commands/MQTTtoRS232" +/*-------------------RS232 topics & parameters----------------------*/ + +// Settings RS232 to MQTT topic +#define subjectRS232toMQTT "/RS232toMQTT" + +// setting to specify mode used for sending MQTT messages: +// 0: RAW: all recieved input at RS232 interface is collected in single MQTT message +// defined by RS232toMQTTsubject +// +// 1: JSON: Assumes input at RS232 interface to be valid JSON. JSON keys are used to +// split the JSON data in separate MQTT sub-topics. This will be repeated for +// sub-keys up to the specified nesting level (RS232maxJSONlevel). +// +// EXAMPLE: "{temperature: {sens1: 22, sens2: 23}, humidity: {sens1: 80, sens2: 60}}" +// - with RS232maxJSONlevel=1: +// ./RS232toMQTT/temperature ==> "{sens1: 22, sens2: 23}" +// ./RS232toMQTT/humidity ==> "{sens1: 80, sens2: 60}" +// +// - with RS232maxJSONlevel=2 (or higher): +// ./RS232toMQTT/temperature/sens1 ==> 22 +// ./RS232toMQTT/temperature/sens2 ==> 23 +// ./RS232toMQTT/humidity/sens1 ==> 80 +// ./RS232toMQTT/humidity/sens2 ==> 60 +// +#ifndef RS232toMQTTmode +# define RS232toMQTTmode 0 +#endif + +// settings for RS232TopicMode 0 (RAW) +#define RS232InPost '\n' // Hacky way to get last character of postfix for incoming +#define MAX_INPUT 200 // how much serial data we expect + +// settings for RS232TopicMode 1 (JSON) +#define RS232maxJSONlevel 2 // Max nested level in which JSON keys are converted to seperate sub-topics +#define RS232JSONDocSize 1024 // bytes to reserve for the JSON doc + +// settings for MQTT to RS232 +#define subjectMQTTtoRS232 "/commands/MQTTtoRS232" +#define RS232Pre "00" // The prefix for the RS232 message +#define RS232Post "\r" // The postfix for the RS232 message //Setup for RS232 -#define MAX_INPUT 50 // how much serial data we expect -#define RS232Baud 9600 // The serial connection Baud -#define RS232Pre "00" // The prefix for the RS232 message -#define RS232Post "\r" // The postfix for the RS232 message -#define RS232InPost '\r' // Hacky way to get last character of postfix for incoming +#ifndef RS232Baud +# define RS232Baud 9600 // The serial connection Baud +#endif /*-------------------PIN DEFINITIONS----------------------*/ +#ifndef RS232_UART +// set hardware serial UART to be used for device communication or +// use software emulaton if not defined +// +// VALUES: +// - not defined: use software emulation (pins set by RS232_RX_GPIO & RS232_TX_GPIO) +// +// - 0: use HW UART0 (serial), warning: default used for logging & usb +// ESP8266: (TX0 GPIO1, RX0 GPIO3) +// (TX0 GPIO15, RX0 GPIO13) with UART0 swap enabled +// ESP32: (TX0 by RS232_TX_GPIO, RX0 by RS232_RX_GPIO) +// ATmega2560: (TX0 pin1, RX0 pin0) +// Arduino Uno: (TX0 pin1, RX0 pin0) +// +// - 1: use HW UART1 (serial1) +// ESP8266: (TX1 GPIO2, RX none) only transmit available +// ESP32: (TX1 by RS232_TX_GPIO, RX1 by RS232_RX_GPIO) +// ATmega2560: (TX1 pin18, RX1 pin19) +// Arduino Uno: N/A +// +// - 2: use HW UART2 (serial2) +// ESP8266: N/A +// ESP32: (TX2 by RS232_TX_GPIO, RX2 by RS232_RX_GPIO) +// ATmega2560: (TX2 pin16, RX1 pin17) +// Arduino Uno: N/A +// +// - 3: use HW UART3 (serial3) +// ESP8266: N/A +// ESP32: N/A +// ATmega2560: (TX2 pin14, RX1 pin15) +// Arduino Uno: N/A +// +// defaults +# ifdef ESP32 +# define RS232_UART 1 // use HW UART ESP32 +# else +# undef RS232_UART // default use software serial +//# define RS232_UART 1 // define to use HW UART +# endif +#endif + +#ifndef RS232_UART0_SWAP +// option for ESP8266 only to swap UART0 ports from (GPIO1,GPIO3) to (GPIO15,GPIO13) +# define RS232_UART0_SWAP +#endif + #ifndef RS232_RX_GPIO -# ifdef ESP8266 +// define receive pin (for software serial or ESP32 with configurable HW UART) +# if defined(ESP8266) && !defined(RS232_UART) # define RS232_RX_GPIO 4 //D2 -# elif ESP32 +# elif defined(ESP32) # define RS232_RX_GPIO 26 -# elif __AVR_ATmega2560__ +# elif defined(__AVR_ATmega2560__) && !defined(RS232_UART) # define RS232_RX_GPIO 2 // 2 = D2 on arduino mega # else # define RS232_RX_GPIO 0 // 0 = D2 on arduino UNO @@ -58,15 +137,16 @@ extern void MQTTtoRS232(char* topicOri, JsonObject& RS232data); #endif #ifndef RS232_TX_GPIO -# ifdef ESP8266 +// define transmit pin (for software serial and/or ESP32) +# if defined(ESP8266) && !defined(RS232_UART) # define RS232_TX_GPIO 2 //D4 # elif ESP32 # define RS232_TX_GPIO 14 -# elif __AVR_ATmega2560__ +# elif defined(__AVR_ATmega2560__) && !defined(RS232_UART) # define RS232_TX_GPIO 9 # else # define RS232_TX_GPIO 9 # endif #endif -#endif +#endif \ No newline at end of file