diff --git a/examples/more/Ublox_HiveMQCloud_TLS_DirectLink/Ublox_HiveMQCloud_TLS_DirectLink.ino b/examples/more/Ublox_HiveMQCloud_TLS_DirectLink/Ublox_HiveMQCloud_TLS_DirectLink.ino new file mode 100644 index 00000000..860a993f --- /dev/null +++ b/examples/more/Ublox_HiveMQCloud_TLS_DirectLink/Ublox_HiveMQCloud_TLS_DirectLink.ino @@ -0,0 +1,305 @@ +/* Tested on ESP32-S2 with Ublox Lara R280 modem*/ + +#define TINY_GSM_MODEM_UBLOX + +#include +#include +#include +#include "certificates.h" + +const char* broker = "your-mqtt-broker-hostname"; +const uint16_t port = 8883; +const char* user = "your-mqtt-username"; +const char* pass = "your-mqtt-password"; +const char* topicLed = "GsmClientTest/led/tl"; +const char* topicInit = "GsmClientTest/init/tl"; + +const char* apn = "your-apn"; +const char* gprsUser = "your-gprs-username"; +const char* gprsPass = "your-gprs-password"; + +HardwareSerial SerialAT(1); +TinyGsm modem(SerialAT); +TinyGsmClientSecure client(modem); +PubSubClient mqtt(client); + +uint32_t lastReconnectAttempt = 0; +uint32_t start_publish = millis(); +uint32_t end_publish = millis(); +uint32_t timer = millis(); + +uint8_t data[900]; + +void mqttCallback(char* topic, byte* payload, unsigned int len) { + Serial.print("Message arrived ["); + Serial.print(topic); + Serial.print("]: "); + Serial.write(payload, len); + Serial.println(); +} + +boolean mqttConnect() { + Serial.print("Connecting to "); + Serial.print(broker); + + boolean status = mqtt.connect("tinygsmclienttest", user, pass); + + if (status == false) { + Serial.printf(" fail, rc = %d \n", mqtt.state()); + return false; + } + Serial.println(" success"); + + mqtt.subscribe(topicLed); + return mqtt.connected(); +} + +// Add server CA cert to ublox modem memory +void addCACert() { + // Search if server CA exists in modem memory + Serial.println("Searching for hiveMQCA in modem CA storage..."); + modem.sendAT(GF("+USECMNG=3")); + if (modem.waitResponse(GF("hiveMQCA")) == 1) { + Serial.println("hiveMQCA found..."); + return; + } + + // Server CA not found in memory, import now + Serial.println("hiveMQCA not found..."); + Serial.println("Importing hiveMQCA..."); + + int cert_size = sizeof(cert) - 1; + modem.sendAT(GF("+USECMNG=0,0,\"hiveMQCA\","), cert_size); + if (modem.waitResponse(GF(">")) != 1) { + Serial.println("No response..."); + return; + } + Serial.print("Writing CA to modem... "); + Serial.println(cert_size); + for (int i = 0; i < cert_size; i++) { + char c = pgm_read_byte(&cert[i]); + modem.stream.write(c); + } + modem.stream.flush(); + if (modem.waitResponse(GF("OK")) != 1) { + Serial.println("CA cert upload failed..."); + return; + } + Serial.println("CA cert upload completed..."); +} + +void setSSLProfile() { + Serial.print("Reset SSL profile 0..."); + modem.sendAT(GF("+USECPRF=0")); + if (modem.waitResponse(GF("OK")) != 1) { + Serial.println(" fail"); + return; + } + Serial.println(" success"); + + Serial.print("Set profile 0 certification to level 1..."); + modem.sendAT(GF("+USECPRF=0,0,1")); + if (modem.waitResponse(GF("OK")) != 1) { + Serial.println(" fail"); + return; + } + Serial.println(" success"); + + Serial.print("Set profile 0 to use any validation TLS version..."); + modem.sendAT(GF("+USECPRF=0,1,0")); + if (modem.waitResponse(GF("OK")) != 1) { + Serial.println(" fail"); + return; + } + Serial.println(" success"); + + Serial.print("Set profile 0 SNI to host..."); + modem.sendAT(GF("+USECPRF=0,10,\""), broker, "\""); + if (modem.waitResponse(GF("OK")) != 1) { + Serial.println(" fail"); + return; + } + Serial.println(" success"); + + Serial.print("Set profile 0 root CA to hiveMQCA..."); + modem.sendAT(GF("+USECPRF=0,3,\"hiveMQCA\"")); + if (modem.waitResponse(GF("OK")) != 1) { + Serial.println(" fail"); + return; + } + Serial.println(" success"); +} + +void setup() { + Serial.begin(115200); + delay(10); + + SerialAT.begin(115200); + SerialAT.setPins(10, 9, 11, 12); + SerialAT.setHwFlowCtrlMode(); // Required for direct link mode to prevent loss of data, as recommended by Ublox + delay(10); + + Serial.println("Initializing modem..."); + if (!modem.init()) { + Serial.println(" fail"); + delay(10000); + return; + } + + String modemInfo = modem.getModemInfo(); + Serial.print("Modem Info: "); + Serial.println(modemInfo); + + Serial.print("Waiting for network..."); + if (!modem.waitForNetwork()) { + Serial.println(" fail"); + delay(10000); + return; + } + Serial.println(" success"); + + if (modem.isNetworkConnected()) { Serial.println("Network connected"); } + + Serial.print(F("Connecting to ")); + Serial.print(apn); + if (!modem.gprsConnect(apn, gprsUser, gprsPass)) { + Serial.println(" fail"); + delay(10000); + return; + } + Serial.println(" success"); + + if (modem.isGprsConnected()) { Serial.println("GPRS connected"); } + + addCACert(); + setSSLProfile(); + modem.setSSLProfileID(0); // set secure connection to use SSL profile 0 + + mqtt.setServer(broker, port); + mqtt.setCallback(mqttCallback); + mqtt.setBufferSize(1024); + + mqttConnect(); + + timer = millis(); + while (millis() - timer <= 3000) { + mqtt.loop(); + } + + // some random data to be published to broker + for (int j=0; j<9; j++) { + for (int i=0; i<100; i++) { + data[100 * j + i] = i; + } + } +} + +void loop() { + // Direct Link mode + Serial.print("Enable direct link mode..."); + Serial.println(modem.enableDL()); + Serial.println("Publishing 36 kB data to broker..."); // Maximum 65535 bytes per transmission due to modem limitations? + start_publish = millis(); + mqtt.beginPublish(topicInit, 36000, false); + for (int i=0; i<40; i++) { + mqtt.write((uint8_t*)&data, sizeof(data)); + } + mqtt.endPublish(); + mqtt.flush(); + end_publish = millis(); + Serial.println("Publish ended..."); + Serial.print("Publish time used in Direct Link mode (ms): "); + Serial.println(end_publish - start_publish); + + timer = millis(); + while (millis() - timer <= 10000) { + // Network and GPRS connection check requires AT command mode, does not work in Direct Link mode + + // To check for host connection in Direct Link mode, client.read() must be called to catch the disconnection result code. + // Modem will put back to AT command mode automatically when disconnected from host in Direct Link mode. + + // host connection check for other applications in Direct Link mode + /* + if (client.available()) { + client.read(); + if (!client.connected()) { + // client disconnected from host + } + } + */ + + // host connection check for mqtt broker in Direct Link mode using pubsubclient library + if (!mqtt.connected()) { + Serial.println("=== MQTT NOT CONNECTED ==="); + uint32_t t = millis(); + if (t - lastReconnectAttempt > 1000L) { + lastReconnectAttempt = t; + if (mqttConnect()) { lastReconnectAttempt = 0; } + } + delay(100); + return; + } + mqtt.loop(); + } + + Serial.print("Disable direct link mode..."); + Serial.println(modem.disableDL()); + Serial.println(); + + // AT command mode + Serial.println("AT command mode"); + Serial.println("Publishing 36 kB data to broker..."); + start_publish = millis(); + mqtt.beginPublish(topicInit, 36000, false); + for (int i=0; i<40; i++) { + mqtt.write((uint8_t*)&data, sizeof(data)); // Maximum 1024 bytes packet per serial write with 50 ms delay per packet due AT command limitations + } + mqtt.endPublish(); + mqtt.flush(); + end_publish = millis(); + Serial.println("Publish ended..."); + Serial.print("Publish time used in AT command mode (ms): "); + Serial.println(end_publish - start_publish); + + timer = millis(); + while (millis() - timer <= 10000) { + // Network and GPRS check requires AT command mode, does not work in Direct Link mode + + if (!modem.isNetworkConnected()) { + Serial.println("Network disconnected"); + if (!modem.waitForNetwork(180000L, true)) { + Serial.println(" fail"); + delay(10000); + return; + } + if (modem.isNetworkConnected()) { + Serial.println("Network re-connected"); + } + + if (!modem.isGprsConnected()) { + Serial.println("GPRS disconnected!"); + Serial.print(F("Connecting to ")); + Serial.print(apn); + if (!modem.gprsConnect(apn, gprsUser, gprsPass)) { + Serial.println(" fail"); + delay(10000); + return; + } + if (modem.isGprsConnected()) { Serial.println("GPRS reconnected"); } + } + } + + if (!mqtt.connected()) { + Serial.println("=== MQTT NOT CONNECTED ==="); + uint32_t t = millis(); + if (t - lastReconnectAttempt > 1000L) { + lastReconnectAttempt = t; + if (mqttConnect()) { lastReconnectAttempt = 0; } + } + delay(100); + return; + } + mqtt.loop(); + } + Serial.println(); +} diff --git a/examples/more/Ublox_HiveMQCloud_TLS_DirectLink/certificates.h b/examples/more/Ublox_HiveMQCloud_TLS_DirectLink/certificates.h new file mode 100644 index 00000000..cb38781c --- /dev/null +++ b/examples/more/Ublox_HiveMQCloud_TLS_DirectLink/certificates.h @@ -0,0 +1,33 @@ +// HiveMQ Cloud server CA cert, can be downloaded from https://community.hivemq.com/t/frequently-asked-questions/514 +const char cert[] PROGMEM = +"-----BEGIN CERTIFICATE-----\r\n" +"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\r\n" +"-----END CERTIFICATE-----\r\n"; diff --git a/src/TinyGsmClientUBLOX.h b/src/TinyGsmClientUBLOX.h index cf600402..5540cd23 100644 --- a/src/TinyGsmClientUBLOX.h +++ b/src/TinyGsmClientUBLOX.h @@ -85,6 +85,7 @@ class TinyGsmUBLOX : public TinyGsmModem, prev_check = 0; sock_connected = false; got_data = false; + direct_link = false; if (mux < TINY_GSM_MUX_COUNT) { this->mux = mux; @@ -626,6 +627,58 @@ class TinyGsmUBLOX : public TinyGsmModem, /* * Client related functions */ + +private: + int _ssl_profile = -1; + uint8_t _sock_id = 0; + const char* _dl_dc = "\r\nDISCONNECT\r\n\r\nOK\r\n\r\n+UUSOCL:"; + uint8_t _dl_dc_seq = 0; + bool _dl_connected = false; + +public: + // set USECMNG profile id + // must be called BEFORE client modemConnect to host + void setSSLProfileID(int id) { + _ssl_profile = id; + } + + // enable direct link mode + // must be called AFTER client modemConnect to host + bool enableDL() { + if (sockets[_sock_id]->direct_link) { + return false; + } + sendAT(GF("+USODL="), _sock_id); + if (waitResponse(GF("CONNECT")) != 1) { + return false; + } + while (stream.available()) { + stream.read(); + } + sockets[_sock_id]->sock_available = 0; + sockets[_sock_id]->rx.clear(); + sockets[_sock_id]->direct_link = true; + _dl_dc_seq = 0; + _dl_connected = true; + return true; + } + + // disable direct link mode + bool disableDL() { + if (!sockets[_sock_id]->direct_link) { + return false; + } + delay(2500); // must wait at least 2 sec after transmission to socket before ending DL + stream.print("+++"); + stream.flush(); + sockets[_sock_id]->sock_available = 0; + sockets[_sock_id]->rx.clear(); + sockets[_sock_id]->direct_link = false; + _dl_dc_seq = 0; + _dl_connected = false; + return waitResponse(10000) == 1; + } + protected: bool modemConnect(const char* host, uint16_t port, uint8_t* mux, bool ssl = false, int timeout_s = 120) { @@ -638,10 +691,19 @@ class TinyGsmUBLOX : public TinyGsmModem, if (waitResponse(GF(GSM_NL "+USOCR:")) != 1) { return false; } *mux = streamGetIntBefore('\n'); waitResponse(); + + _sock_id = *mux; + sockets[_sock_id]->sock_available = 0; + sockets[_sock_id]->direct_link = false; if (ssl) { - sendAT(GF("+USOSEC="), *mux, ",1"); - waitResponse(); + if (_ssl_profile == -1) { + sendAT(GF("+USOSEC="), *mux, ",1"); + } + else { + sendAT(GF("+USOSEC="), *mux, ",1,", _ssl_profile); + } + waitResponse(); } // Enable NODELAY @@ -662,7 +724,13 @@ class TinyGsmUBLOX : public TinyGsmModem, return (1 == rsp); } - int16_t modemSend(const void* buff, size_t len, uint8_t mux) { + size_t modemSend(const void* buff, size_t len, uint8_t mux) { + if (sockets[mux]->direct_link) { + size_t sent = stream.write(reinterpret_cast(buff), len); + stream.flush(); + return sent; + } + sendAT(GF("+USOWR="), mux, ',', (uint16_t)len); if (waitResponse(GF("@")) != 1) { return 0; } // 50ms delay, see AT manual section 25.10.4 @@ -678,6 +746,43 @@ class TinyGsmUBLOX : public TinyGsmModem, size_t modemRead(size_t size, uint8_t mux) { if (!sockets[mux]) return 0; + + if (sockets[mux]->direct_link) { + size_t len = stream.available(); + if (len > size) { + len = size; + } + for (int i = 0; i < len; i++) { + + // Handle disconnection from host in Direct Link mode. + // The modem will automatically put back to AT command mode if disconnected from host. + // Does not work on Ublox LEON-G100-03S / LEON-G200-03S and previous versions + // as "DISCONNECT" result code is not supported on these modems. + if (char(stream.peek()) == _dl_dc[_dl_dc_seq]) { + _dl_dc_seq++; + } + else { + _dl_dc_seq = 0; + } + if (_dl_dc_seq == (sizeof(_dl_dc) - 1)) { + while (stream.available()) { + stream.read(); + } + sockets[mux]->sock_connected = false; + sockets[mux]->sock_available = 0; + sockets[mux]->rx.clear(); + sockets[mux]->direct_link = false; + _dl_dc_seq = 0; + _dl_connected = false; + return 0; + } + + moveCharFromStreamToFifo(mux); + } + sockets[mux]->sock_available = modemGetAvailable(mux); + return len; + } + sendAT(GF("+USORD="), mux, ',', (uint16_t)size); if (waitResponse(GF(GSM_NL "+USORD:")) != 1) { return 0; } streamSkipUntil(','); // Skip mux @@ -694,6 +799,16 @@ class TinyGsmUBLOX : public TinyGsmModem, size_t modemGetAvailable(uint8_t mux) { if (!sockets[mux]) return 0; + + if (sockets[mux]->direct_link) { + size_t result = 0; + result = stream.available(); + if (!result) { + sockets[mux]->sock_connected = modemGetConnected(mux); + } + return result; + } + // NOTE: Querying a closed socket gives an error "operation not allowed" sendAT(GF("+USORD="), mux, ",0"); size_t result = 0; @@ -712,6 +827,10 @@ class TinyGsmUBLOX : public TinyGsmModem, } bool modemGetConnected(uint8_t mux) { + if (sockets[mux]->direct_link) { + return _dl_connected; + } + // NOTE: Querying a closed socket gives an error "operation not allowed" sendAT(GF("+USOCTL="), mux, ",10"); uint8_t res = waitResponse(GF(GSM_NL "+USOCTL:")); diff --git a/src/TinyGsmTCP.tpp b/src/TinyGsmTCP.tpp index 391d1fe8..06942f86 100644 --- a/src/TinyGsmTCP.tpp +++ b/src/TinyGsmTCP.tpp @@ -313,13 +313,27 @@ class TinyGsmTCP { bool sock_connected; bool got_data; RxFifo rx; + bool direct_link; }; /* * Basic functions */ protected: + + bool _direct_link = false; + void maintainImpl() { + _direct_link = false; +#if defined TINY_GSM_MODEM_UBLOX + for (int mux = 0; mux < muxCount; mux++) { + GsmClient* sock = thisModem().sockets[mux]; + if (sock && sock->direct_link) { + _direct_link = true; + break; + } + } +#endif #if defined TINY_GSM_BUFFER_READ_AND_CHECK_SIZE // Keep listening for modem URC's and proactively iterate through // sockets asking if any data is avaiable @@ -330,13 +344,17 @@ class TinyGsmTCP { sock->sock_available = thisModem().modemGetAvailable(mux); } } - while (thisModem().stream.available()) { - thisModem().waitResponse(15, NULL, NULL); - } + if (!_direct_link) { + while (thisModem().stream.available()) { + thisModem().waitResponse(15, NULL, NULL); + } + } #elif defined TINY_GSM_NO_MODEM_BUFFER || defined TINY_GSM_BUFFER_READ_NO_CHECK // Just listen for any URC's - thisModem().waitResponse(100, NULL, NULL); + if (!_direct_link) { + thisModem().waitResponse(100, NULL, NULL); + } #else #error Modem client has been incorrectly created