diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/.rak11200.test.skip b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/.rak11200.test.skip new file mode 100644 index 0000000..e69de29 diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/.rak11300.test.skip b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/.rak11300.test.skip new file mode 100644 index 0000000..e69de29 diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/Arduino/Blues-Hummingbird-Gateway/.rak11200.test.skip b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/Arduino/Blues-Hummingbird-Gateway/.rak11200.test.skip new file mode 100644 index 0000000..e69de29 diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/Arduino/Blues-Hummingbird-Gateway/.rak11300.test.skip b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/Arduino/Blues-Hummingbird-Gateway/.rak11300.test.skip new file mode 100644 index 0000000..e69de29 diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/Arduino/Blues-Hummingbird-Gateway/Blues-Hummingbird-Gateway.ino b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/Arduino/Blues-Hummingbird-Gateway/Blues-Hummingbird-Gateway.ino new file mode 100644 index 0000000..4429cca --- /dev/null +++ b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/Arduino/Blues-Hummingbird-Gateway/Blues-Hummingbird-Gateway.ino @@ -0,0 +1,322 @@ +/** + * @file main.cpp + * @author Bernd Giesecke (bernd@giesecke.tk) + * @brief App event handlers + * @version 0.1 + * @date 2023-04-25 + * + * @copyright Copyright (c) 2023 + * + */ + +#include "main.h" + +/** LoRaWAN packet */ +WisCayenne g_solution_data(255); + +/** Received package for parsing */ +uint8_t rcvd_data[256]; +/** Length of received package */ +uint16_t rcvd_data_len = 0; + +/** Send Fail counter **/ +uint8_t send_fail = 0; + +/** Set the device name, max length is 10 characters */ +char g_ble_dev_name[10] = "RAK"; + +/** Flag for RAK1906 sensor */ +bool has_rak1906 = false; + +/** Flag is Blues Notecard was found */ +bool has_blues = false; + +/** + * @brief Initial setup of the application (before LoRaWAN and BLE setup) + * + */ +void setup_app(void) +{ + Serial.begin(115200); + time_t serial_timeout = millis(); + // On nRF52840 the USB serial is not available immediately + while (!Serial) + { + if ((millis() - serial_timeout) < 5000) + { + delay(100); + digitalWrite(LED_GREEN, !digitalRead(LED_GREEN)); + } + else + { + break; + } + } + digitalWrite(LED_GREEN, LOW); + + // Set firmware version + api_set_version(SW_VERSION_1, SW_VERSION_2, SW_VERSION_3); +} + +/** + * @brief Final setup of application (after LoRaWAN and BLE setup) + * + * @return true + * @return false + */ +bool init_app(void) +{ + Serial.printf("init_app\n"); + + Serial.printf("++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n"); + Serial.printf("WisBlock Hummingbird Blues Sensor\n"); + Serial.printf("++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n"); + + // Initialize User AT commands + init_user_at(); + + // Check if RAK1906 is available + has_rak1906 = init_rak1906(); + if (has_rak1906) + { + Serial.printf("+EVT:RAK1906\n"); + } + + // Initialize Blues Notecard + has_blues = init_blues(); + if (!has_blues) + { + Serial.printf("+EVT:CELLULAR_ERROR\n"); + } + else + { + Serial.printf("+EVT:RAK13102\n"); + Serial.printf("Start P2P RX\n"); + // Set to permanent listen + g_lora_p2p_rx_mode = RX_MODE_RX; + Radio.Rx(0); + } + + pinMode(WB_IO2, OUTPUT); + digitalWrite(WB_IO2, LOW); + return true; +} + +/** + * @brief Handle events + * Events can be + * - timer (setup with AT+SENDINT=xxx) + * - interrupt events + * - wake-up signals from other tasks + */ +void app_event_handler(void) +{ + // Timer triggered event + if ((g_task_event_type & STATUS) == STATUS) + { + g_task_event_type &= N_STATUS; + Serial.printf("Timer wakeup\n"); + if (g_lpwan_has_joined) + { + // Reset the packet + g_solution_data.reset(); + + // Get battery level + float batt_level_f = read_batt(); + g_solution_data.addVoltage(LPP_CHANNEL_BATT, batt_level_f / 1000.0); + + // Read sensors and battery + if (has_rak1906) + { + read_rak1906(); + } + + if (!has_blues) + { + if (g_lorawan_settings.lorawan_enable) + { + lmh_error_status result = send_lora_packet(g_solution_data.getBuffer(), g_solution_data.getSize()); + switch (result) + { + case LMH_SUCCESS: + Serial.printf("Packet enqueued\n"); + break; + case LMH_BUSY: + Serial.printf("LoRa transceiver is busy\n"); + Serial.printf("+EVT:BUSY\n\n"); + break; + case LMH_ERROR: + Serial.printf("+EVT:SIZE_ERROR\n\n"); + Serial.printf("Packet error, too big to send with current DR\n"); + break; + } + } + else + { + // Add unique identifier in front of the P2P packet, here we use the DevEUI + g_solution_data.addDevID(LPP_CHANNEL_DEVID, &g_lorawan_settings.node_device_eui[4]); + // uint8_t packet_buffer[g_solution_data.getSize() + 8]; + // memcpy(packet_buffer, g_lorawan_settings.node_device_eui, 8); + // memcpy(&packet_buffer[8], g_solution_data.getBuffer(), g_solution_data.getSize()); + + // Send packet over LoRa + // if (send_p2p_packet(packet_buffer, g_solution_data.getSize() + 8)) + if (send_p2p_packet(g_solution_data.getBuffer(), g_solution_data.getSize())) + { + Serial.printf("Packet enqueued\n"); + } + else + { + Serial.printf("+EVT:SIZE_ERROR\n"); + Serial.printf("Packet too big\n"); + } + } + } + else + { + Serial.printf("Get hub sync status:\n"); + blues_hub_status(); + + g_solution_data.addDevID(0, &g_lorawan_settings.node_device_eui[4]); + blues_parse_send(g_solution_data.getBuffer(), g_solution_data.getSize()); + } + // Reset the packet + g_solution_data.reset(); + } + else + { + Serial.printf("Network not joined, skip sending\n"); + } + } + + // Parse request event + if ((g_task_event_type & PARSE) == PARSE) + { + g_task_event_type &= N_PARSE; + + if (has_blues) + { + if (!blues_parse_send(rcvd_data, rcvd_data_len)) + { + Serial.printf("Parsing or sending failed\n"); + + Serial.printf("**********************************************\n"); + Serial.printf("Get hub sync status:\n"); + // {“req”:”hub.sync.status”} + blues_start_req("hub.sync.status\n"); + blues_send_req(); + + Serial.printf("**********************************************\n"); + delay(2000); + + Serial.printf("Get note card status:\n"); + // {“req”:”card.wireless”} + blues_start_req("card.wireless\n"); + blues_send_req(); + + Serial.printf("**********************************************\n"); + delay(2000); + } + } + else + { + Serial.printf("Got PARSE request, but no Blues Notecard detected\n"); + } + } +} + +/** + * @brief Handle BLE events + * + */ +void ble_data_handler(void) +{ + if (g_enable_ble) + { + if ((g_task_event_type & BLE_DATA) == BLE_DATA) + { + Serial.printf("RECEIVED BLE\n"); + // BLE UART data arrived + g_task_event_type &= N_BLE_DATA; + + while (g_ble_uart.available() > 0) + { + at_serial_input(uint8_t(g_ble_uart.read())); + delay(5); + } + at_serial_input(uint8_t('\n')); + } + } +} + +/** + * @brief Handle LoRa events + * + */ +void lora_data_handler(void) +{ + // LoRa Join finished handling + if ((g_task_event_type & LORA_JOIN_FIN) == LORA_JOIN_FIN) + { + g_task_event_type &= N_LORA_JOIN_FIN; + if (g_join_result) + { + Serial.printf("Successfully joined network\n"); + } + else + { + Serial.printf("Join network failed\n"); + /// \todo here join could be restarted. + // lmh_join(); + } + } + + // LoRa data handling + if ((g_task_event_type & LORA_DATA) == LORA_DATA) + { + g_task_event_type &= N_LORA_DATA; + Serial.printf("Received package over LoRa\n"); + char log_buff[g_rx_data_len * 3] = {0}; + uint8_t log_idx = 0; + for (int idx = 0; idx < g_rx_data_len; idx++) + { + sprintf(&log_buff[log_idx], "%02X ", g_rx_lora_data[idx]); + log_idx += 3; + } + Serial.printf("%s", log_buff); + +#if MY_DEBUG > 0 + CayenneLPP lpp(g_rx_data_len - 8); + memcpy(lpp.getBuffer(), &g_rx_lora_data[8], g_rx_data_len - 8); + DynamicJsonDocument jsonBuffer(4096); + JsonObject root = jsonBuffer.to(); + lpp.decodeTTN(lpp.getBuffer(), g_rx_data_len - 8, root); + serializeJsonPretty(root, Serial); + Serial.println(); +#endif + memcpy(rcvd_data, g_rx_lora_data, g_rx_data_len); + rcvd_data_len = g_rx_data_len; + api_wake_loop(PARSE); + } + + // LoRa TX finished handling + if ((g_task_event_type & LORA_TX_FIN) == LORA_TX_FIN) + { + g_task_event_type &= N_LORA_TX_FIN; + + Serial.printf("LPWAN TX cycle %s", g_rx_fin_result ? "finished ACK" : "failed NAK\n"); + + if (!g_rx_fin_result) + { + // Increase fail send counter + send_fail++; + + if (send_fail == 10) + { + // Too many failed sendings, reset node and try to rejoin + delay(100); + api_reset(); + } + } + } +} diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/Arduino/Blues-Hummingbird-Gateway/RAK1906_env.cpp b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/Arduino/Blues-Hummingbird-Gateway/RAK1906_env.cpp new file mode 100644 index 0000000..8544c6f --- /dev/null +++ b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/Arduino/Blues-Hummingbird-Gateway/RAK1906_env.cpp @@ -0,0 +1,111 @@ +/** + * @file RAK1906_env.cpp + * @author Bernd Giesecke (bernd.giesecke@rakwireless.com) + * @brief BME680 sensor functions + * @version 0.1 + * @date 2021-05-29 + * + * @copyright Copyright (c) 2021 + * + */ + +#include "main.h" +#include +#include + +/** BME680 instance for Wire */ +Adafruit_BME680 bme(&Wire); + +/** Last temperature read */ +float _last_temp_rak1906 = 0; +/** Last humidity read */ +float _last_humid_rak1906 = 0; +/** Last pressure read */ +float _last_pressure_rak1906 = 0; + +/** + * @brief Initialize the BME680 sensor + * + * @return true if sensor was found + * @return false if sensor was not found + */ +bool init_rak1906(void) +{ + Wire.begin(); + + if (!bme.begin(0x76)) + { + Serial.printf("Could not find a valid BME680 sensor, check wiring!\n"); + return false; + } + + // Set up oversampling and filter initialization + bme.setTemperatureOversampling(BME680_OS_8X); + bme.setHumidityOversampling(BME680_OS_2X); + bme.setPressureOversampling(BME680_OS_4X); + bme.setIIRFilterSize(BME680_FILTER_SIZE_3); + // bme.setGasHeater(320, 150); // 320*C for 150 ms + // As we do not use the BSEC library here, the gas value is useless and just consumes battery. Better to switch it off + bme.setGasHeater(0, 0); // switch off + + return true; +} + +/** + * @brief Read environment data from BME680 + * Data is added to Cayenne LPP payload as channels + * LPP_CHANNEL_HUMID_2, LPP_CHANNEL_TEMP_2, + * LPP_CHANNEL_PRESS_2 and LPP_CHANNEL_GAS_2 + * + * + * @return true if reading was successful + * @return false if reading failed + */ +bool read_rak1906() +{ + Serial.printf("Start BME reading\n"); + bme.beginReading(); + time_t wait_start = millis(); + bool read_success = false; + while ((millis() - wait_start) < 5000) + { + if (bme.endReading()) + { + read_success = true; + break; + } + } + + if (!read_success) + { + Serial.printf("BME timeout\n"); + return false; + } + + _last_temp_rak1906 = bme.temperature; + _last_humid_rak1906 = bme.humidity; + _last_pressure_rak1906 = (float)(bme.pressure) / 100.0; + + g_solution_data.addRelativeHumidity(LPP_CHANNEL_HUMID_2, _last_humid_rak1906); + g_solution_data.addTemperature(LPP_CHANNEL_TEMP_2, _last_temp_rak1906); + g_solution_data.addBarometricPressure(LPP_CHANNEL_PRESS_2, _last_pressure_rak1906); + +#if MY_DEBUG > 0 + Serial.printf("RH= %.2f T= %.2f P= %.3f\n", bme.humidity, bme.temperature, (float)(bme.pressure) / 100.0); +#endif + + return true; +} + +/** + * @brief Returns the latest values from the sensor + * or starts a new reading + * + * @param values array for temperature [0], humidity [1] and pressure [2] + */ +void get_rak1906_values(float *values) +{ + values[0] = _last_temp_rak1906; + values[1] = _last_humid_rak1906; + values[2] = _last_pressure_rak1906; +} diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/Arduino/Blues-Hummingbird-Gateway/RAK1906_env.h b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/Arduino/Blues-Hummingbird-Gateway/RAK1906_env.h new file mode 100644 index 0000000..036ec19 --- /dev/null +++ b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/Arduino/Blues-Hummingbird-Gateway/RAK1906_env.h @@ -0,0 +1,26 @@ +/** + * @file RAK1906_env.h + * @author Bernd Giesecke (bernd@giesecke.tk) + * @brief Global definitions and forward declarations + * @version 0.1 + * @date 2022-09-23 + * + * @copyright Copyright (c) 2022 + * + */ +#ifndef RAK1906_H +#define RAK1906_H +#include + +// Function declarations +bool init_rak1906(void); +bool read_rak1906(void); +void get_rak1906_values(float *values); + +// Cayenne LPP Channel numbers per sensor value +#define LPP_CHANNEL_HUMID_2 6 // RAK1906 +#define LPP_CHANNEL_TEMP_2 7 // RAK1906 +#define LPP_CHANNEL_PRESS_2 8 // RAK1906 +#define LPP_CHANNEL_GAS_2 9 // RAK1906 + +#endif // RAK1906_H \ No newline at end of file diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/Arduino/Blues-Hummingbird-Gateway/blues.cpp b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/Arduino/Blues-Hummingbird-Gateway/blues.cpp new file mode 100644 index 0000000..c22b84a --- /dev/null +++ b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/Arduino/Blues-Hummingbird-Gateway/blues.cpp @@ -0,0 +1,235 @@ +/** + * @file blues.cpp + * @author Bernd Giesecke (bernd@giesecke.tk) + * @brief Blues.IO NoteCard handler + * @version 0.1 + * @date 2023-04-27 + * + * @copyright Copyright (c) 2023 + * + */ +#include "main.h" +#include "product_uid.h" + +#ifndef PRODUCT_UID +#define PRODUCT_UID "com.my-company.my-name:my-project" +#pragma message "PRODUCT_UID is not defined in this example. Please ensure your Notecard has a product identifier set before running this example or define it in code here. More details at https://dev.blues.io/tools-and-sdks/samples/product-uid" +#endif +#define myProductID PRODUCT_UID + +Notecard notecard; + +J *req; + +bool init_blues(void) +{ + Wire.begin(); + notecard.begin(); + + // Get the ProductUID from the saved settings + // If no settings are found, use NoteCard internal settings! + if (read_blues_settings()) + { + Serial.printf("Found saved settings, override NoteCard internal settings!"); + if (memcmp(g_blues_settings.product_uid, "com.my-company.my-name", 22) == 0) + { + Serial.printf("No Product ID saved\n"); + AT_PRINTF(":EVT NO PUID"); + memcpy(g_blues_settings.product_uid, PRODUCT_UID, 33); + } + + Serial.printf("Set Product ID and connection mode\n"); + if (blues_start_req("hub.set")) + { + JAddStringToObject(req, "product", g_blues_settings.product_uid); + if (g_blues_settings.conn_continous) + { + JAddStringToObject(req, "mode", "continuous"); + } + else + { + JAddStringToObject(req, "mode", "minimum"); + } + // Set sync time to 20 times the sensor read time + JAddNumberToObject(req, "seconds", (g_lorawan_settings.send_repeat_time * 20 / 1000)); + JAddBoolToObject(req, "heartbeat", true); + + if (!blues_send_req()) + { + Serial.printf("hub.set request failed\n"); + return false; + } + } + else + { + Serial.printf("hub.set request failed\n"); + return false; + } + +#if USE_GNSS == 1 + Serial.printf("Set location mode\n"); + if (blues_start_req("card.location.mode")) + { + // Continous GNSS mode + // JAddStringToObject(req, "mode", "continous"); + + // Periodic GNSS mode + JAddStringToObject(req, "mode", "periodic"); + + // Set location acquisition time to the sensor read time + JAddNumberToObject(req, "seconds", (g_lorawan_settings.send_repeat_time / 2000)); + JAddBoolToObject(req, "heartbeat", true); + if (!blues_send_req()) + { + Serial.printf("card.location.mode request failed\n"); + return false; + } + } + else + { + Serial.printf("card.location.mode request failed\n"); + return false; + } +#else + Serial.printf("Stop location mode\n"); + if (blues_start_req("card.location.mode")) + { + // GNSS mode off + JAddStringToObject(req, "mode", "off"); + if (!blues_send_req()) + { + Serial.printf("card.location.mode request failed\n"); + return false; + } + } + else + { + Serial.printf("card.location.mode request failed\n"); + return false; + } +#endif + + /// \todo reset attn signal needs rework + // pinMode(WB_IO5, INPUT); + // if (g_blues_settings.motion_trigger) + // { + // if (blues_start_req("card.attn")) + // { + // JAddStringToObject(req, "mode", "disarm"); + // if (!blues_send_req()) + // { + // Serial.printf("card.attn request failed"); + // } + + // if (!blues_enable_attn()) + // { + // return false; + // } + // } + // } + // else + // { + // Serial.printf("card.attn request failed\n"); + // return false; + // } + + Serial.printf("Set APN\n"); + // {“req”:”card.wireless”} + if (blues_start_req("card.wireless")) + { + JAddStringToObject(req, "mode", "auto"); + + if (g_blues_settings.use_ext_sim) + { + // USING EXTERNAL SIM CARD + JAddStringToObject(req, "apn", g_blues_settings.ext_sim_apn); + JAddStringToObject(req, "method", "dual-secondary-primary"); + } + else + { + // USING BLUES eSIM CARD + JAddStringToObject(req, "method", "primary"); + } + if (!blues_send_req()) + { + Serial.printf("card.wireless request failed\n"); + return false; + } + } + else + { + Serial.printf("card.wireless request failed\n"); + return false; + } + +#if IS_V2 == 1 + // Only for V2 cards, setup the WiFi network + Serial.printf("Set WiFi\n"); + if (blues_start_req("card.wifi")) + { + JAddStringToObject(req, "ssid", "-"); + JAddStringToObject(req, "password", "-"); + JAddStringToObject(req, "name", "RAK-"); + JAddStringToObject(req, "org", "RAK-PH"); + JAddBoolToObject(req, "start", false); + + if (!blues_send_req()) + { + Serial.printf("card.wifi request failed"); + } + } + else + { + Serial.printf("card.wifi request failed\n"); + return false; + } +#endif + } + + // {"req": "card.version"} + if (blues_start_req("card.version")) + { + if (!blues_send_req()) + { + Serial.printf("card.version request failed\n"); + } + } + return true; +} + +bool blues_start_req(String request_name) +{ + req = notecard.newRequest(request_name.c_str()); + if (req != NULL) + { + return true; + } + return false; +} + +bool blues_send_req(void) +{ + char *json = JPrintUnformatted(req); + Serial.printf("Card request = %s\n", json); + + J *rsp; + rsp = notecard.requestAndResponse(req); + if (rsp == NULL) + { + return false; + } + json = JPrintUnformatted(rsp); + Serial.printf("Card response = %s\n", json); + notecard.deleteResponse(rsp); + + return true; +} + +void blues_hub_status(void) +{ + blues_start_req("hub.status"); + if (!blues_send_req()) + { + Serial.printf("hub.status request failed\n"); + } +} diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/Arduino/Blues-Hummingbird-Gateway/blues_parse_send.cpp b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/Arduino/Blues-Hummingbird-Gateway/blues_parse_send.cpp new file mode 100644 index 0000000..8616c6e --- /dev/null +++ b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/Arduino/Blues-Hummingbird-Gateway/blues_parse_send.cpp @@ -0,0 +1,221 @@ +/** + * @file blues_parse_send.cpp + * @author Bernd Giesecke (bernd@giesecke.tk) + * @brief Parse received LoRa packet and create Blues NoteCard note + * @version 0.1 + * @date 2023-04-25 + * + * @copyright Copyright (c) 2023 + * + */ +#include "main.h" + +/** Number of defined sensor types */ +#define NUM_DEFINED_SENSOR_TYPES 38 + +uint8_t value_id[NUM_DEFINED_SENSOR_TYPES] = {0, 1, 2, 3, 100, 101, 102, 103, + 104, 112, 113, 115, 116, 117, 118, 120, + 121, 125, 128, 130, 131, 132, 133, 134, + 135, 136, 137, 138, 142, 188, 190, 191, + 192, 193, 194, 195, 203, 255}; + +uint8_t value_size[NUM_DEFINED_SENSOR_TYPES] = {1, 1, 2, 2, 4, 2, 1, 2, + 1, 2, 6, 2, 2, 2, 4, 1, + 2, 2, 2, 4, 4, 2, 4, 6, + 3, 9, 11, 2, 1, 2, 2, 2, + 2, 2, 2, 2, 1, 4}; + +String value_name[NUM_DEFINED_SENSOR_TYPES] = {"digital_in", "digital_out", "analog_in", "analog_out", "generic", "illuminance", "presence", "temperature", + "humidity", "humidity_prec", "accelerometer", "barometer", "voltage", "current", "frequency", "percentage", + "altitude", "concentration", "power", "distance", "energy", "direction", "time", "gyrometer", + "colour", "gps", "gps", "voc", "switch", "soil_moist", "wind_speed", "wind_direction", + "soil_ec", "soil_ph_h", "soil_ph_l", "pyranometer", "light", "node_id"}; + +uint32_t value_divider[NUM_DEFINED_SENSOR_TYPES] = {1, 1, 100, 100, 1, 1, 1, 10, + 2, 10, 1000, 10, 100, 1000, 1, 1, + 1, 1, 1, 1000, 1000, 1, 1, 100, + 1, 10000, 1000000, 1, 1, 10, 100, 1, + 1000, 100, 10, 1, 1, 1}; + +// {136;9;"gps";true; [ 10000, 10000, 100 ]}, +// {137;11;"gps";true;[ 1000000, 1000000, 100 ]}, + +bool blues_parse_send(uint8_t *data, uint16_t data_len) +{ + blues_start_req("note.add"); + + JAddStringToObject(req, "file", "sensors.qo"); + // JAddBoolToObject(req, "sync", true); + J *body = JCreateObject(); + if (body != NULL) + { + uint16_t byte_idx = 0; + uint8_t sens_num = 0; + float float_val1 = 0.0; + float float_val2 = 0.0; + float float_val3 = 0.0; + uint32_t unsigned_val1 = 0; + uint32_t unsigned_val2 = 0; + uint32_t unsigned_val3 = 0; + int32_t signed_val1 = 0; + String sens_full_name = ""; + char node_id_str[9]; + J *sec_lvl; + char rounding[40]; + + // JAddStringToObject(body, "node_id", sensor_id.c_str()); + + while (byte_idx < data_len) + { + uint16_t current_byte_idx = byte_idx; + uint16_t sens_idx = 256; + sens_num = data[current_byte_idx++]; + Serial.printf("Sensor Number %d\n", sens_num); + // find matching index + for (int idx = 0; idx < NUM_DEFINED_SENSOR_TYPES; idx++) + { + if (value_id[idx] == data[current_byte_idx]) + { + sens_idx = idx; + break; + } + } + if (sens_idx == 256) + { + // Wrong sensor ID + Serial.printf("Unknown Sensor %d\n", data[current_byte_idx]); + JAddStringToObject(body, "error", "Invalid LPP ID"); + JAddItemToObject(req, "body", body); + if (!blues_send_req()) + { + Serial.printf("Failed to send error packet\n"); + } + return false; + } + Serial.printf("Found Sensor %d\n", data[current_byte_idx]); + current_byte_idx++; + switch (value_id[sens_idx]) + { + case 113: + case 134: + Serial.printf("Found accelerometer or gyrometer\n"); + sens_full_name = value_name[sens_idx] + "_" + String(sens_num); + sec_lvl = JAddObjectToObject(body, sens_full_name.c_str()); + + float_val1 = (float)((int16_t)data[current_byte_idx + 1] << 8 | (int16_t)data[current_byte_idx]) / value_divider[sens_idx]; + current_byte_idx += 2; + JAddNumberToObject(sec_lvl, "X", float_val1); + float_val2 = (float)((int16_t)data[current_byte_idx + 1] << 8 | (int16_t)data[current_byte_idx]) / value_divider[sens_idx]; + current_byte_idx += 2; + JAddNumberToObject(sec_lvl, "Y", float_val2); + float_val3 = (float)((int16_t)data[current_byte_idx + 1] << 8 | (int16_t)data[current_byte_idx]) / value_divider[sens_idx]; + current_byte_idx += 2; + JAddNumberToObject(sec_lvl, "Z", float_val3); + Serial.printf("x %.4f y %.4f z %.4f\n", float_val1, float_val2, float_val3); + break; + case 136: + Serial.printf("Found GPS 4 digit\n"); + sens_full_name = value_name[sens_idx] + "_" + String(sens_num); + sec_lvl = JAddObjectToObject(body, sens_full_name.c_str()); + + float_val1 = (float)((int16_t)data[current_byte_idx + 2] << 16 | (int16_t)data[current_byte_idx + 1] << 8 | (int16_t)data[current_byte_idx]) / 10000.0; + current_byte_idx += 3; + JAddNumberToObject(sec_lvl, "Lat", float_val1); + float_val2 = (float)((int16_t)data[current_byte_idx + 2] << 16 | (int16_t)data[current_byte_idx + 1] << 8 | (int16_t)data[current_byte_idx]) / 10000.0; + current_byte_idx += 3; + JAddNumberToObject(sec_lvl, "Lng", float_val2); + float_val3 = (float)((int16_t)data[current_byte_idx + 2] << 16 | (int16_t)data[current_byte_idx + 1] << 8 | (int16_t)data[current_byte_idx]) / 100.0; + current_byte_idx += 3; + JAddNumberToObject(sec_lvl, "Alt", float_val3); + Serial.printf("lat %.4f lng %.4f alt %.4f\n", float_val1, float_val2, float_val3); + break; + case 137: + Serial.printf("Found GPS 6 digit\n"); + sens_full_name = value_name[sens_idx] + "_" + String(sens_num); + sec_lvl = JAddObjectToObject(body, sens_full_name.c_str()); + + float_val1 = (float)((int16_t)data[current_byte_idx + 3] << 16 | (int16_t)data[current_byte_idx + 2] << 16 | (int16_t)data[current_byte_idx + 1] << 8 | (int16_t)data[current_byte_idx]) / 1000000.0; + current_byte_idx += 4; + JAddNumberToObject(sec_lvl, "Lat", float_val1); + float_val2 = (float)((int16_t)data[current_byte_idx + 3] << 16 | (int16_t)data[current_byte_idx + 2] << 16 | (int16_t)data[current_byte_idx + 1] << 8 | (int16_t)data[current_byte_idx]) / 1000000.0; + current_byte_idx += 4; + JAddNumberToObject(sec_lvl, "Lng", float_val2); + float_val3 = (float)((int16_t)data[current_byte_idx + 2] << 16 | (int16_t)data[current_byte_idx + 1] << 8 | (int16_t)data[current_byte_idx]) / 100.0; + current_byte_idx += 3; + JAddNumberToObject(sec_lvl, "Alt", float_val3); + Serial.printf("lat %.4f lng %.4f alt %.4f\n", float_val1, float_val2, float_val3); + break; + case 135: + Serial.printf("Found Color\n"); + sens_full_name = value_name[sens_idx] + "_" + String(sens_num); + sec_lvl = JAddObjectToObject(body, sens_full_name.c_str()); + + unsigned_val1 = (int16_t)data[current_byte_idx]; + current_byte_idx += 4; + JAddNumberToObject(sec_lvl, "Red", unsigned_val1); + unsigned_val2 = (int16_t)data[current_byte_idx]; + current_byte_idx += 4; + JAddNumberToObject(sec_lvl, "Green", unsigned_val1); + unsigned_val3 = (int16_t)data[current_byte_idx]; + current_byte_idx += 3; + JAddNumberToObject(sec_lvl, "Blue", unsigned_val1); + Serial.printf("r %ld g %ld b %ld\n", unsigned_val1, unsigned_val2, unsigned_val3); + break; + case 255: + unsigned_val1 = 0; + for (int cnt = 0; cnt < value_size[sens_idx]; cnt++) + { + unsigned_val1 = (unsigned_val1 << 8) | data[current_byte_idx]; + current_byte_idx++; + } + sprintf(node_id_str,"%08LX",(uint64_t)unsigned_val1); + Serial.printf("unsigned_val1 %s\n", node_id_str); + + sens_full_name = value_name[sens_idx] + "_" + String(sens_num); + JAddStringToObject(body, value_name[sens_idx].c_str(), node_id_str); + + Serial.printf("Added %s %08LX\n", sens_full_name.c_str(), (uint64_t)unsigned_val1); + + break; + default: + signed_val1 = 0; + for (int cnt = 0; cnt < value_size[sens_idx]; cnt++) + { + signed_val1 = (signed_val1 << 8) | data[current_byte_idx]; + current_byte_idx++; + } + float_val1 = (float)signed_val1 / value_divider[sens_idx]; + + // Limit to 2 decimals + sprintf(rounding, "%.2f", float_val1); + sscanf(rounding, "%f", &float_val1); + + sens_full_name = value_name[sens_idx] + "_" + String(sens_num); + JAddNumberToObject(body, sens_full_name.c_str(), float_val1); + Serial.printf("Added %s %.2f\n", sens_full_name.c_str(), float_val1); + + break; + } + byte_idx = byte_idx + value_size[sens_idx] + 2; + // Serial.printf("Data size %d Position %d", data_len, byte_idx); + Serial.printf(">>>>><<<<<\n"); + } + JAddItemToObject(req, "body", body); + + JAddBinaryToObject(req, "payload", data, data_len); + + Serial.printf("Finished parsing\n"); + if (!blues_send_req()) + { + Serial.printf("Send request failed\n"); + return false; + } + return true; + } + else + { + Serial.printf("Error creating body\n"); + } + + return false; +} diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/Arduino/Blues-Hummingbird-Gateway/main.h b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/Arduino/Blues-Hummingbird-Gateway/main.h new file mode 100644 index 0000000..ec62d15 --- /dev/null +++ b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/Arduino/Blues-Hummingbird-Gateway/main.h @@ -0,0 +1,87 @@ +/** + * @file main.h + * @author Bernd Giesecke (bernd@giesecke.tk) + * @brief Includes, defines and globals + * @version 0.1 + * @date 2023-04-25 + * + * @copyright Copyright (c) 2023 + * + */ + +#ifndef _MAIN_H_ +#define _MAIN_H_ + +// Application definitions +// major version increase on API change / not backwards compatible +#define SW_VERSION_1 1 +// minor version increase on API change / backward compatible +#define SW_VERSION_2 0 +// patch version increase on bugfix, no affect on API +#define SW_VERSION_3 0 +// 0 = Notecard V1 version, 1 = Notecard V2 version +#define IS_V2 1 + +#include +#include // Click to install library: http://librarymanager/All#WisBlock-API-V2 +#include // Click to install library: http://librarymanager/All#Blues-Wireless-Notecard +#include "RAK1906_env.h" + +/** Define the version of your SW */ +#ifndef SW_VERSION_1 +#define SW_VERSION_1 1 // major version increase on API change / not backwards compatible +#endif +#ifndef SW_VERSION_2 +#define SW_VERSION_2 0 // minor version increase on API change / backward compatible +#endif +#ifndef SW_VERSION_3 +#define SW_VERSION_3 0 // patch version increase on bugfix, no affect on API +#endif + +/** Application function definitions */ +void setup_app(void); +bool init_app(void); +void app_event_handler(void); +void ble_data_handler(void) __attribute__((weak)); +void lora_data_handler(void); + +// Wakeup flags +#define PARSE 0b1000000000000000 +#define N_PARSE 0b0111111111111111 + +// Cayenne LPP Channel numbers per sensor value +#define LPP_CHANNEL_BATT 1 // Base Board + +// Globals +extern WisCayenne g_solution_data; + +// Parser +bool blues_parse_send(uint8_t *data, uint16_t data_len); + +// Blues.io +struct s_blues_settings +{ + uint16_t valid_mark = 0xAA55; // Validity marker + char product_uid[256] = "com.my-company.my-name:my-project"; // Blues Product UID + bool conn_continous = false; // Use periodic connection + bool use_ext_sim = false; // Use external SIM + char ext_sim_apn[256] = "internet"; // APN to be used with external SIM + bool motion_trigger = true; // Send data on motion trigger +}; + +bool init_blues(void); +bool blues_send_req(void); +void blues_hub_status(void); +bool blues_start_req(String request_name); +bool blues_send_req(void); +bool blues_send_payload(uint8_t *data, uint16_t data_len); + +extern J *req; +extern s_blues_settings g_blues_settings; + +// User AT commands +void init_user_at(void); +bool read_blues_settings(void); +void save_blues_settings(void); + +#endif // _MAIN_H_ diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/Arduino/Blues-Hummingbird-Gateway/product_uid.h b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/Arduino/Blues-Hummingbird-Gateway/product_uid.h new file mode 100644 index 0000000..dc9bb78 --- /dev/null +++ b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/Arduino/Blues-Hummingbird-Gateway/product_uid.h @@ -0,0 +1,3 @@ +#ifndef PRODUCT_UID +#define PRODUCT_UID "com.my-company.my-name:my-project" // "com.my-company.my-name:my-project" +#endif diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/Arduino/Blues-Hummingbird-Gateway/user_at_cmd.cpp b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/Arduino/Blues-Hummingbird-Gateway/user_at_cmd.cpp new file mode 100644 index 0000000..ad070e4 --- /dev/null +++ b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/Arduino/Blues-Hummingbird-Gateway/user_at_cmd.cpp @@ -0,0 +1,385 @@ +/** + * @file user_at_cmd.cpp + * @author Bernd Giesecke (bernd@giesecke.tk) + * @brief User AT commands + * @version 0.1 + * @date 2023-08-18 + * + * @copyright Copyright (c) 2023 + * + */ +#include "main.h" + +#include +#include +using namespace Adafruit_LittleFS_Namespace; + +/** Filename to save Blues settings */ +static const char blues_file_name[] = "BLUES"; + +/** File to save battery check status */ +File this_file(InternalFS); + +/** Structure for saved Blues Notecard settings */ +s_blues_settings g_blues_settings; + +/** + * @brief Set Blues Product UID + * + * @param str Product UID as Hex String + * @return int AT_SUCCESS if ok, AT_ERRNO_PARA_FAIL if invalid value + */ +int at_set_blues_prod_uid(char *str) +{ + if (strlen(str) < 25) + { + return AT_ERRNO_PARA_NUM; + } + + for (int i = 0; str[i] != '\0'; i++) + { + if (str[i] >= 'A' && str[i] <= 'Z') // checking for uppercase characters + str[i] = str[i] + 32; // converting uppercase to lowercase + } + + char new_uid[256] = {0}; + snprintf(new_uid, 255, str); + + Serial.printf("Received new Blues Product UID %s\n", new_uid); + + bool need_save = strcmp(new_uid, g_blues_settings.product_uid) == 0 ? false : true; + + if (need_save) + { + snprintf(g_blues_settings.product_uid, 256, new_uid); + } + + // Save new master node address if changed + if (need_save) + { + save_blues_settings(); + } + return AT_SUCCESS; +} + +/** + * @brief Get Blues Product UID + * + * @return int AT_SUCCESS + */ +int at_query_blues_prod_uid(void) +{ + snprintf(g_at_query_buf, ATQUERY_SIZE, "%s", g_blues_settings.product_uid); + return AT_SUCCESS; +} + +/** + * @brief Set usage of eSIM or external SIM and APN + * + * @param str params as string, format 0 or 1:APN_NAME + * @return int + * AT_SUCCESS is params are set correct + * AT_ERRNO_PARA_NUM if params error + */ +int at_set_blues_ext_sim(char *str) +{ + char *param; + bool new_use_ext_sim; + char new_ext_sim_apn[256]; + + // Get string up to first : + param = strtok(str, ":"); + if (param != NULL) + { + if (param[0] == '0') + { + Serial.printf("Enable eSIM\n"); + new_use_ext_sim = false; + } + else if (param[0] == '1') + { + Serial.printf("Enable external SIM\n"); + new_use_ext_sim = true; + param = strtok(NULL, ":"); + if (param != NULL) + { + for (int i = 0; param[i] != '\0'; i++) + { + if (param[i] >= 'A' && param[i] <= 'Z') // checking for uppercase characters + param[i] = param[i] + 32; // converting uppercase to lowercase + } + snprintf(new_ext_sim_apn, 256, "%s", param); + } + else + { + Serial.printf("Missing external SIM APN\n"); + return AT_ERRNO_PARA_NUM; + } + } + else + { + Serial.printf("Invalid SIM flag %d\n", param[0]); + return AT_ERRNO_PARA_NUM; + } + } + + bool need_save = false; + if (new_use_ext_sim != g_blues_settings.use_ext_sim) + { + g_blues_settings.use_ext_sim = new_use_ext_sim; + need_save = true; + } + if (strcmp(new_ext_sim_apn, g_blues_settings.product_uid) != 0) + { + snprintf(g_blues_settings.ext_sim_apn, 256, new_ext_sim_apn); + need_save = true; + } + + if (need_save) + { + save_blues_settings(); + } + return AT_SUCCESS; +} + +/** + * @brief Get Blues SIM settings + * + * @return int AT_SUCCESS + */ +int at_query_blues_ext_sim(void) +{ + if (g_blues_settings.use_ext_sim) + { + snprintf(g_at_query_buf, ATQUERY_SIZE, "1:%s", g_blues_settings.ext_sim_apn); + Serial.printf("Using external SIM with APN = %s\n", g_blues_settings.ext_sim_apn); + } + else + { + snprintf(g_at_query_buf, ATQUERY_SIZE, "0"); + Serial.printf("Using eSIM\n"); + } + return AT_SUCCESS; +} + +/** + * @brief Set Blues NoteCard mode + * /// \todo work in progress + * + * @param str params as string, format 0 or 1 + * @return int + * AT_SUCCESS is params are set correct + * AT_ERRNO_PARA_NUM if params error + */ +int at_set_blues_mode(char *str) +{ + bool new_connection_mode; + + if (str[0] == '0') + { + Serial.printf("Set minimum connection mode\n"); + new_connection_mode = false; + } + else if (str[0] == '1') + { + Serial.printf("Set continuous connection mode\n"); + new_connection_mode = true; + } + else + { + Serial.printf("Invalid motion trigger flag %d\n", str[0]); + return AT_ERRNO_PARA_NUM; + } + + bool need_save = false; + if (new_connection_mode != g_blues_settings.conn_continous) + { + g_blues_settings.conn_continous = new_connection_mode; + need_save = true; + } + + if (need_save) + { + save_blues_settings(); + } + return AT_SUCCESS; +} + +/** + * @brief Get Blues mode settings + * + * @return int AT_SUCCESS + */ +int at_query_blues_mode(void) +{ + snprintf(g_at_query_buf, ATQUERY_SIZE, "%s", g_blues_settings.conn_continous ? "1" : "0"); + Serial.printf("Using %s connection\n", g_blues_settings.conn_continous ? "continous" : "periodic"); + return AT_SUCCESS; +} + +// /** +// * @brief Enable/disable the motion trigger +// * +// * @param str params as string, format 0 or 1 +// * @return int +// * AT_SUCCESS is params are set correct +// * AT_ERRNO_PARA_NUM if params error +// */ +// int at_set_blues_trigger(char *str) +// { +// bool new_motion_trigger; + +// if (str[0] == '0') +// { +// Serial.printf("Disable motion trigger"); +// new_motion_trigger = false; +// blues_disable_attn(); +// } +// else if (str[0] == '1') +// { +// Serial.printf("Enable motion trigger"); +// new_motion_trigger = true; +// blues_enable_attn(); +// } +// else +// { +// Serial.printf("Invalid motion trigger flag %d", str[0]); +// return AT_ERRNO_PARA_NUM; +// } + +// bool need_save = false; +// if (new_motion_trigger != g_blues_settings.motion_trigger) +// { +// g_blues_settings.motion_trigger = new_motion_trigger; +// need_save = true; +// } + +// if (need_save) +// { +// save_blues_settings(); +// } +// return AT_SUCCESS; +// } + +// /** +// * @brief Get Blues motion trigger settings +// * +// * @return int AT_SUCCESS +// */ +// int at_query_blues_trigger(void) +// { +// snprintf(g_at_query_buf, ATQUERY_SIZE, "%s", g_blues_settings.motion_trigger ? "1" : "0"); +// Serial.printf("Motion trigger is %s", g_blues_settings.motion_trigger ? "enabled" : "disabled"); +// return AT_SUCCESS; +// } + +static int at_reset_blues_settings(void) +{ + if (InternalFS.exists(blues_file_name)) + { + InternalFS.remove(blues_file_name); + } + return AT_SUCCESS; +} + +/** + * @brief Read saved Blues Product ID + * + */ +bool read_blues_settings(void) +{ + bool structure_valid = false; + if (InternalFS.exists(blues_file_name)) + { + this_file.open(blues_file_name, FILE_O_READ); + this_file.read((void *)&g_blues_settings.valid_mark, sizeof(s_blues_settings)); + this_file.close(); + + // Check for valid data + if (g_blues_settings.valid_mark == 0xAA55) + { + structure_valid = true; + Serial.printf("Valid Blues settings found, Blues Product UID = %s\n", g_blues_settings.product_uid); + if (g_blues_settings.use_ext_sim) + { + Serial.printf("Using external SIM with APN = %s\n", g_blues_settings.ext_sim_apn); + } + else + { + Serial.printf("Using eSIM\n"); + } + } + else + { + Serial.printf("No valid Blues settings found\n"); + } + } + + if (!structure_valid) + { + return false; + + // No settings file found optional to set defaults (ommitted!) + // g_blues_settings.valid_mark = 0xAA55; // Validity marker + // sprintf(g_blues_settings.product_uid, "com.my-company.my-name:my-project"); // Blues Product UID + // g_blues_settings.conn_continous = false; // Use periodic connection + // g_blues_settings.use_ext_sim = false; // Use external SIM + // sprintf(g_blues_settings.ext_sim_apn, "-"); // APN to be used with external SIM + // g_blues_settings.motion_trigger = true; // Send data on motion trigger + // save_blues_settings(); + } + + return true; +} + +/** + * @brief Save the Blues Product ID + * + */ +void save_blues_settings(void) +{ + if (InternalFS.exists(blues_file_name)) + { + InternalFS.remove(blues_file_name); + } + + g_blues_settings.valid_mark = 0xAA55; + this_file.open(blues_file_name, FILE_O_WRITE); + this_file.write((const char *)&g_blues_settings.valid_mark, sizeof(s_blues_settings)); + this_file.close(); + Serial.printf("Saved Blues Settings\n"); +} + +/** + * @brief List of all available commands with short help and pointer to functions + * + */ +atcmd_t g_user_at_cmd_new_list[] = { + /*| CMD | AT+CMD? | AT+CMD=? | AT+CMD=value | AT+CMD | Permissions |*/ + // Module commands + {"+BUID", "Set/get the Blues product UID", at_query_blues_prod_uid, at_set_blues_prod_uid, NULL, "RW"}, + {"+BSIM", "Set/get Blues SIM settings", at_query_blues_ext_sim, at_set_blues_ext_sim, NULL, "RW"}, + {"+BMOD", "Set/get Blues NoteCard connection modes", at_query_blues_mode, at_set_blues_mode, NULL, "RW"}, + // {"+BTRIG", "Set/get Blues send trigger", at_query_blues_trigger, at_set_blues_trigger, NULL, "RW"}, + {"+BR", "Remove all Blues Settings", NULL, NULL, at_reset_blues_settings, "RW"}, +}; + +/** Number of user defined AT commands */ +uint8_t g_user_at_cmd_num = 0; + +/** Pointer to the combined user AT command structure */ +atcmd_t *g_user_at_cmd_list; + +/** + * @brief Initialize the user defined AT command list + * + */ +void init_user_at(void) +{ + // Assign custom AT command list to pointer used by WisBlock API + g_user_at_cmd_list = g_user_at_cmd_new_list; + + // Add AT commands to structure + g_user_at_cmd_num += sizeof(g_user_at_cmd_new_list) / sizeof(atcmd_t); + Serial.printf("Added %d User AT commands\n", g_user_at_cmd_num); +} diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/Decoder.js b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/Decoder.js new file mode 100644 index 0000000..fa41d99 --- /dev/null +++ b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/Decoder.js @@ -0,0 +1,189 @@ +function get_device_name(device, value) { + return device + "_" + value; +} + +function Decoder(request) { + + var data = JSON.parse(request.body); + + var device = data.device; + console.log(device); + var decoded = {}; + + decoded.sensor = data.body.node_id; + + if (typeof data.body.voltage_1 != 'undefined') { + decoded[get_device_name(data.body.node_id, "voltage")] = data.body.voltage_1; + } + if (typeof data.body.humidity_2 != 'undefined') { + decoded[get_device_name(data.body.node_id, "humidity")] = data.body.humidity_2; + } + if (typeof data.body.temperature_3 != 'undefined') { + decoded[get_device_name(data.body.node_id, "temperature")] = data.body.temperature_3; + } + if (typeof data.body.barometer_4 != 'undefined') { + decoded[get_device_name(data.body.node_id, "pressure")] = data.body.barometer_4; + } + if (typeof data.body.illuminance_5 != 'undefined') { + decoded[get_device_name(data.body.node_id, "illuminance")] = data.body.illuminance_5; + } + if (typeof data.body.humidity_6 != 'undefined') { + decoded[get_device_name(data.body.node_id, "humidity")] = data.body.humidity_6; + } + if (typeof data.body.temperature_7 != 'undefined') { + decoded[get_device_name(data.body.node_id, "temperature")] = data.body.temperature_7; + } + if (typeof data.body.barometer_8 != 'undefined') { + decoded[get_device_name(data.body.node_id, "pressure")] = data.body.barometer_8; + } + if (typeof data.body.analog_9 != 'undefined') { + decoded[get_device_name(data.body.node_id, "voc")] = data.body.analog_9; + } + if (typeof data.body.gps_10 != 'undefined') { + decoded[get_device_name(data.body.node_id, "location")] = data.body.gps_10; + } + if (typeof data.body.gps_10 != 'undefined') { + decoded[get_device_name(data.body.node_id, "location")] = data.body.gps_10; + } + if (typeof data.body.temperature_11 != 'undefined') { + decoded[get_device_name(data.body.node_id, "soil_temp")] = data.body.temperature_11; + } + if (typeof data.body.humidity_12 != 'undefined') { + decoded[get_device_name(data.body.node_id, "soil_humid")] = data.body.humidity_12; + } + if (typeof data.body.illuminance_15 != 'undefined') { + decoded[get_device_name(data.body.node_id, "illuminance")] = data.body.illuminance_15; + } + if (typeof data.body.voc_16 != 'undefined') { + decoded[get_device_name(data.body.node_id, "voc")] = data.body.voc_16; + } + if (typeof data.body.analog_in_17 != 'undefined') { + decoded[get_device_name(data.body.node_id, "mq2_gas")] = data.body.analog_in_17; + } + if (typeof data.body.percentage_18 != 'undefined') { + decoded[get_device_name(data.body.node_id, "mq2_gas_perc")] = data.body.percentage_18; + } + if (typeof data.body.analog_in_19 != 'undefined') { + decoded[get_device_name(data.body.node_id, "co2_gas")] = data.body.analog_in_19; + } + if (typeof data.body.percentage_20 != 'undefined') { + decoded[get_device_name(data.body.node_id, "co2_gas_perc")] = data.body.percentage_20; + } + if (typeof data.body.analog_in_21 != 'undefined') { + decoded[get_device_name(data.body.node_id, "mq3_gas")] = data.body.analog_in_21; + } + if (typeof data.body.percentage_22 != 'undefined') { + decoded[get_device_name(data.body.node_id, "mq3_gas_perc")] = data.body.percentage_22; + } + if (typeof data.body.analog_in_23 != 'undefined') { + decoded[get_device_name(data.body.node_id, "tof_distance")] = data.body.analog_in_23; + } + if (typeof data.body.presence_24 != 'undefined') { + decoded[get_device_name(data.body.node_id, "tof_valid")] = data.body.presence_24; + } + if (typeof data.body.gyrometer_25 != 'undefined') { + decoded[get_device_name(data.body.node_id, "gyro")] = data.body.gyrometer_25; + } + if (typeof data.body.digital_in_26 != 'undefined') { + decoded[get_device_name(data.body.node_id, "gesture")] = data.body.digital_in_26; + } + if (typeof data.body.analog_in_27 != 'undefined') { + decoded[get_device_name(data.body.node_id, "uvi")] = data.body.analog_in_27; + } + if (typeof data.body.illuminance_28 != 'undefined') { + decoded[get_device_name(data.body.node_id, "uvs")] = data.body.illuminance_28; + } + if (typeof data.body.analog_29 != 'undefined') { + decoded[get_device_name(data.body.node_id, "ina219_current")] = data.body.analog_29; + } + if (typeof data.body.analog_30 != 'undefined') { + decoded[get_device_name(data.body.node_id, "ina219_voltage")] = data.body.analog_30; + } + if (typeof data.body.analog_31 != 'undefined') { + decoded[get_device_name(data.body.node_id, "ina219_voltage")] = data.body.analog_31; + } + if (typeof data.body.presence_32 != 'undefined') { + decoded[get_device_name(data.body.node_id, "tp_left")] = data.body.presence_32; + } + if (typeof data.body.presence_33 != 'undefined') { + decoded[get_device_name(data.body.node_id, "tp_center")] = data.body.presence_33; + } + if (typeof data.body.presence_34 != 'undefined') { + decoded[get_device_name(data.body.node_id, "tp_right")] = data.body.presence_34; + } + if (typeof data.body.concentration_35 != 'undefined') { + decoded[get_device_name(data.body.node_id, "co2")] = data.body.concentration_35; + } + if (typeof data.body.temperature_39 != 'undefined') { + decoded[get_device_name(data.body.node_id, "mlx_temperature")] = data.body.temperature_39; + } + if (typeof data.body.voc_40 != 'undefined') { + decoded[get_device_name(data.body.node_id, "pm1_0")] = data.body.voc_40; + } + if (typeof data.body.voc_41 != 'undefined') { + decoded[get_device_name(data.body.node_id, "pm2_5")] = data.body.voc_41; + } + if (typeof data.body.voc_42 != 'undefined') { + decoded[get_device_name(data.body.node_id, "pm10")] = data.body.voc_42; + } + if (typeof data.body.presence_43 != 'undefined') { + decoded[get_device_name(data.body.node_id, "earthqake_alert")] = data.body.presence_43; + } + if (typeof data.body.analog_44 != 'undefined') { + decoded[get_device_name(data.body.node_id, "earthquake_si")] = data.body.analog_44; + } + if (typeof data.body.analog_45 != 'undefined') { + decoded[get_device_name(data.body.node_id, "earthquake_pga")] = data.body.analog_45; + } + if (typeof data.body.presence_46 != 'undefined') { + decoded[get_device_name(data.body.node_id, "earthquake_shutoff")] = data.body.presence_46; + } + if (typeof data.body.presence_47 != 'undefined') { + decoded[get_device_name(data.body.node_id, "earthquake_collapse")] = data.body.presence_47; + } + if (typeof data.body.presence_48 != 'undefined') { + decoded[get_device_name(data.body.node_id, "switch")] = data.body.presence_48; + } + if (typeof data.body.wind_speed_49 != 'undefined') { + decoded[get_device_name(data.body.node_id, "wind_speed")] = data.body.wind_speed_49; + } + if (typeof data.body.wind_direction_50 != 'undefined') { + decoded[get_device_name(data.body.node_id, "wind_direction")] = data.body.wind_direction_50; + } + if (typeof data.body.analog_61 != 'undefined') { + decoded[get_device_name(data.body.node_id, "water_level")] = data.body.analog_61; + } + if (typeof data.body.presence_62 != 'undefined') { + decoded[get_device_name(data.body.node_id, "low_level_alert")] = data.body.presence_62; + } + if (typeof data.body.presence_63 != 'undefined') { + decoded[get_device_name(data.body.node_id, "overflow_alert")] = data.body.presence_63; + } + + if (("tower_lat" in data) && ("tower_lon" in data)) { + decoded.tower_location = "(" + data.tower_lat + "," + data.tower_lon + ")"; + } + if (("where_lat" in data) && ("where_lon" in data)) { + decoded.device_location = "(" + data.where_lat + "," + data.where_lon + ")"; + } + + decoded.rssi = data.rssi; + decoded.bars = data.bars; + decoded.temp = data.temp; + decoded.orientation = data.orientation; + decoded.card_temperature = data.body.temperature; + + // Array where we store the fields that are being sent to Datacake + var datacakeFields = [] + + // take each field from decodedElsysFields and convert them to Datacake format + for (var key in decoded) { + if (decoded.hasOwnProperty(key)) { + datacakeFields.push({ field: key.toUpperCase(), value: decoded[key], device: device }) + } + } + + // forward data to Datacake + return datacakeFields; + +} \ No newline at end of file diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/PlatformIO/.gitignore b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/PlatformIO/.gitignore new file mode 100644 index 0000000..993d834 --- /dev/null +++ b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/PlatformIO/.gitignore @@ -0,0 +1,9 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch +Netlify.txt +src/product_uid.h +Debug-Build +Build diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/PlatformIO/.pio/libdeps/rak4631-debug/Adafruit Unified Sensor/.gitignore b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/PlatformIO/.pio/libdeps/rak4631-debug/Adafruit Unified Sensor/.gitignore new file mode 100644 index 0000000..d04184a --- /dev/null +++ b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/PlatformIO/.pio/libdeps/rak4631-debug/Adafruit Unified Sensor/.gitignore @@ -0,0 +1,108 @@ +# +# NOTE! Don't add files that are generated in specific +# subdirectories here. Add them in the ".gitignore" file +# in that subdirectory instead. +# +# NOTE! Please use 'git ls-files -i --exclude-standard' +# command after changing this file, to see if there are +# any tracked files which get ignored after the change. +# +# Normal rules +# +.* +*.o +*.o.* +*.a +*.s +*.ko +*.so +*.so.dbg +*.mod.c +*.i +*.lst +*.symtypes +*.order +modules.builtin +*.elf +*.bin +*.gz +*.bz2 +*.lzma +*.patch +*.gcno + +# +# Top-level generic files +# +/tags +/TAGS +/linux +/vmlinux +/vmlinuz +/System.map +/Module.markers +/Module.symvers + +# +# git files that we don't want to ignore even it they are dot-files +# +!.gitignore +!.mailmap + +# +# Generated include files +# +include/config +include/linux/version.h +include/generated + +# stgit generated dirs +patches-* + +# quilt's files +patches +series + +# cscope files +cscope.* +ncscope.* + +# gnu global files +GPATH +GRTAGS +GSYMS +GTAGS + +# QT-Creator files +Makefile.am.user +*.config +*.creator +*.creator.user +*.files +*.includes + +*.orig +*~ +\#*# +*.lo +*.la +Makefile +Makefile.in +aclocal.m4 +autoconfig.h +autoconfig.h.in +autom4te.cache/ +build-aux/ +config.log +config.status +configure +libtool +libupnp.pc +m4/libtool.m4 +m4/ltoptions.m4 +m4/ltsugar.m4 +m4/ltversion.m4 +m4/lt~obsolete.m4 +stamp-h1 +docs/doxygen + diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/PlatformIO/.vscode/extensions.json b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/PlatformIO/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/PlatformIO/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/PlatformIO/create_uf2.py b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/PlatformIO/create_uf2.py new file mode 100644 index 0000000..2b673d9 --- /dev/null +++ b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/PlatformIO/create_uf2.py @@ -0,0 +1,105 @@ +import sys +import struct + +Import("env") + +# Parse input and create UF2 file +def create_uf2(source, target, env): + # source_hex = target[0].get_abspath() + source_hex = target[0].get_string(False) + source_hex = '.\\'+source_hex + print("#########################################################") + print("Create UF2 from "+source_hex) + print("#########################################################") + # print("Source: " + source_hex) + target = source_hex.replace(".hex", "") + target = target + ".uf2" + # print("Target: " + target) + + with open(source_hex, mode='rb') as f: + inpbuf = f.read() + + outbuf = convert_from_hex_to_uf2(inpbuf.decode("utf-8")) + + write_file(target, outbuf) + print("#########################################################") + print(target + " is ready to flash to target device") + print("#########################################################") + + +# Add callback after .hex file was created +env.AddPostAction("$BUILD_DIR/${PROGNAME}.hex", create_uf2) + +# UF2 creation taken from uf2conv.py +UF2_MAGIC_START0 = 0x0A324655 # "UF2\n" +UF2_MAGIC_START1 = 0x9E5D5157 # Randomly selected +UF2_MAGIC_END = 0x0AB16F30 # Ditto + +familyid = 0xADA52840 + + +class Block: + def __init__(self, addr): + self.addr = addr + self.bytes = bytearray(256) + + def encode(self, blockno, numblocks): + global familyid + flags = 0x0 + if familyid: + flags |= 0x2000 + hd = struct.pack(" +#include + +/** BME680 instance for Wire */ +Adafruit_BME680 bme(&Wire); + +/** Last temperature read */ +float _last_temp_rak1906 = 0; +/** Last humidity read */ +float _last_humid_rak1906 = 0; +/** Last pressure read */ +float _last_pressure_rak1906 = 0; + +/** + * @brief Initialize the BME680 sensor + * + * @return true if sensor was found + * @return false if sensor was not found + */ +bool init_rak1906(void) +{ + Wire.begin(); + + if (!bme.begin(0x76)) + { + MYLOG("BME", "Could not find a valid BME680 sensor, check wiring!"); + return false; + } + + // Set up oversampling and filter initialization + bme.setTemperatureOversampling(BME680_OS_8X); + bme.setHumidityOversampling(BME680_OS_2X); + bme.setPressureOversampling(BME680_OS_4X); + bme.setIIRFilterSize(BME680_FILTER_SIZE_3); + // bme.setGasHeater(320, 150); // 320*C for 150 ms + // As we do not use the BSEC library here, the gas value is useless and just consumes battery. Better to switch it off + bme.setGasHeater(0, 0); // switch off + + return true; +} + +/** + * @brief Read environment data from BME680 + * Data is added to Cayenne LPP payload as channels + * LPP_CHANNEL_HUMID_2, LPP_CHANNEL_TEMP_2, + * LPP_CHANNEL_PRESS_2 and LPP_CHANNEL_GAS_2 + * + * + * @return true if reading was successful + * @return false if reading failed + */ +bool read_rak1906() +{ + MYLOG("BME", "Start BME reading"); + bme.beginReading(); + time_t wait_start = millis(); + bool read_success = false; + while ((millis() - wait_start) < 5000) + { + if (bme.endReading()) + { + read_success = true; + break; + } + } + + if (!read_success) + { + MYLOG("BME", "BME timeout"); + return false; + } + + _last_temp_rak1906 = bme.temperature; + _last_humid_rak1906 = bme.humidity; + _last_pressure_rak1906 = (float)(bme.pressure) / 100.0; + + g_solution_data.addRelativeHumidity(LPP_CHANNEL_HUMID_2, _last_humid_rak1906); + g_solution_data.addTemperature(LPP_CHANNEL_TEMP_2, _last_temp_rak1906); + g_solution_data.addBarometricPressure(LPP_CHANNEL_PRESS_2, _last_pressure_rak1906); + +#if MY_DEBUG > 0 + MYLOG("BME", "RH= %.2f T= %.2f P= %.3f", bme.humidity, bme.temperature, (float)(bme.pressure) / 100.0); +#endif + + return true; +} + +/** + * @brief Returns the latest values from the sensor + * or starts a new reading + * + * @param values array for temperature [0], humidity [1] and pressure [2] + */ +void get_rak1906_values(float *values) +{ + values[0] = _last_temp_rak1906; + values[1] = _last_humid_rak1906; + values[2] = _last_pressure_rak1906; +} diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/PlatformIO/src/RAK1906_env.h b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/PlatformIO/src/RAK1906_env.h new file mode 100644 index 0000000..036ec19 --- /dev/null +++ b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/PlatformIO/src/RAK1906_env.h @@ -0,0 +1,26 @@ +/** + * @file RAK1906_env.h + * @author Bernd Giesecke (bernd@giesecke.tk) + * @brief Global definitions and forward declarations + * @version 0.1 + * @date 2022-09-23 + * + * @copyright Copyright (c) 2022 + * + */ +#ifndef RAK1906_H +#define RAK1906_H +#include + +// Function declarations +bool init_rak1906(void); +bool read_rak1906(void); +void get_rak1906_values(float *values); + +// Cayenne LPP Channel numbers per sensor value +#define LPP_CHANNEL_HUMID_2 6 // RAK1906 +#define LPP_CHANNEL_TEMP_2 7 // RAK1906 +#define LPP_CHANNEL_PRESS_2 8 // RAK1906 +#define LPP_CHANNEL_GAS_2 9 // RAK1906 + +#endif // RAK1906_H \ No newline at end of file diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/PlatformIO/src/blues.cpp b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/PlatformIO/src/blues.cpp new file mode 100644 index 0000000..18ea170 --- /dev/null +++ b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/PlatformIO/src/blues.cpp @@ -0,0 +1,235 @@ +/** + * @file blues.cpp + * @author Bernd Giesecke (bernd@giesecke.tk) + * @brief Blues.IO NoteCard handler + * @version 0.1 + * @date 2023-04-27 + * + * @copyright Copyright (c) 2023 + * + */ +#include "main.h" +#include "product_uid.h" + +#ifndef PRODUCT_UID +#define PRODUCT_UID "com.my-company.my-name:my-project" +#pragma message "PRODUCT_UID is not defined in this example. Please ensure your Notecard has a product identifier set before running this example or define it in code here. More details at https://dev.blues.io/tools-and-sdks/samples/product-uid" +#endif +#define myProductID PRODUCT_UID + +Notecard notecard; + +J *req; + +bool init_blues(void) +{ + Wire.begin(); + notecard.begin(); + + // Get the ProductUID from the saved settings + // If no settings are found, use NoteCard internal settings! + if (read_blues_settings()) + { + MYLOG("BLUES", "Found saved settings, override NoteCard internal settings!"); + if (memcmp(g_blues_settings.product_uid, "com.my-company.my-name", 22) == 0) + { + MYLOG("BLUES", "No Product ID saved"); + AT_PRINTF(":EVT NO PUID"); + memcpy(g_blues_settings.product_uid, PRODUCT_UID, 33); + } + + MYLOG("BLUES", "Set Product ID and connection mode"); + if (blues_start_req("hub.set")) + { + JAddStringToObject(req, "product", g_blues_settings.product_uid); + if (g_blues_settings.conn_continous) + { + JAddStringToObject(req, "mode", "continuous"); + } + else + { + JAddStringToObject(req, "mode", "minimum"); + } + // Set sync time to 20 times the sensor read time + JAddNumberToObject(req, "seconds", (g_lorawan_settings.send_repeat_time * 20 / 1000)); + JAddBoolToObject(req, "heartbeat", true); + + if (!blues_send_req()) + { + MYLOG("BLUES", "hub.set request failed"); + return false; + } + } + else + { + MYLOG("BLUES", "hub.set request failed"); + return false; + } + +#if USE_GNSS == 1 + MYLOG("BLUES", "Set location mode"); + if (blues_start_req("card.location.mode")) + { + // Continous GNSS mode + // JAddStringToObject(req, "mode", "continous"); + + // Periodic GNSS mode + JAddStringToObject(req, "mode", "periodic"); + + // Set location acquisition time to the sensor read time + JAddNumberToObject(req, "seconds", (g_lorawan_settings.send_repeat_time / 2000)); + JAddBoolToObject(req, "heartbeat", true); + if (!blues_send_req()) + { + MYLOG("BLUES", "card.location.mode request failed"); + return false; + } + } + else + { + MYLOG("BLUES", "card.location.mode request failed"); + return false; + } +#else + MYLOG("BLUES", "Stop location mode"); + if (blues_start_req("card.location.mode")) + { + // GNSS mode off + JAddStringToObject(req, "mode", "off"); + if (!blues_send_req()) + { + MYLOG("BLUES", "card.location.mode request failed"); + return false; + } + } + else + { + MYLOG("BLUES", "card.location.mode request failed"); + return false; + } +#endif + + /// \todo reset attn signal needs rework + // pinMode(WB_IO5, INPUT); + // if (g_blues_settings.motion_trigger) + // { + // if (blues_start_req("card.attn")) + // { + // JAddStringToObject(req, "mode", "disarm"); + // if (!blues_send_req()) + // { + // MYLOG("BLUES", "card.attn request failed"); + // } + + // if (!blues_enable_attn()) + // { + // return false; + // } + // } + // } + // else + // { + // MYLOG("BLUES", "card.attn request failed"); + // return false; + // } + + MYLOG("BLUES", "Set APN"); + // {“req”:”card.wireless”} + if (blues_start_req("card.wireless")) + { + JAddStringToObject(req, "mode", "auto"); + + if (g_blues_settings.use_ext_sim) + { + // USING EXTERNAL SIM CARD + JAddStringToObject(req, "apn", g_blues_settings.ext_sim_apn); + JAddStringToObject(req, "method", "dual-secondary-primary"); + } + else + { + // USING BLUES eSIM CARD + JAddStringToObject(req, "method", "primary"); + } + if (!blues_send_req()) + { + MYLOG("BLUES", "card.wireless request failed"); + return false; + } + } + else + { + MYLOG("BLUES", "card.wireless request failed"); + return false; + } + +#if IS_V2 == 1 + // Only for V2 cards, setup the WiFi network + MYLOG("BLUES", "Set WiFi"); + if (blues_start_req("card.wifi")) + { + JAddStringToObject(req, "ssid", "-"); + JAddStringToObject(req, "password", "-"); + JAddStringToObject(req, "name", "RAK-"); + JAddStringToObject(req, "org", "RAK-PH"); + JAddBoolToObject(req, "start", false); + + if (!blues_send_req()) + { + MYLOG("BLUES", "card.wifi request failed"); + } + } + else + { + MYLOG("BLUES", "card.wifi request failed"); + return false; + } +#endif + } + + // {"req": "card.version"} + if (blues_start_req("card.version")) + { + if (!blues_send_req()) + { + MYLOG("BLUES", "card.version request failed"); + } + } + return true; +} + +bool blues_start_req(String request_name) +{ + req = notecard.newRequest(request_name.c_str()); + if (req != NULL) + { + return true; + } + return false; +} + +bool blues_send_req(void) +{ + char *json = JPrintUnformatted(req); + MYLOG("BLUES", "Card request = %s", json); + + J *rsp; + rsp = notecard.requestAndResponse(req); + if (rsp == NULL) + { + return false; + } + json = JPrintUnformatted(rsp); + MYLOG("BLUES", "Card response = %s", json); + notecard.deleteResponse(rsp); + + return true; +} + +void blues_hub_status(void) +{ + blues_start_req("hub.status"); + if (!blues_send_req()) + { + MYLOG("BLUES", "hub.status request failed"); + } +} \ No newline at end of file diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/PlatformIO/src/blues_parse_send.cpp b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/PlatformIO/src/blues_parse_send.cpp new file mode 100644 index 0000000..f0cb727 --- /dev/null +++ b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/PlatformIO/src/blues_parse_send.cpp @@ -0,0 +1,221 @@ +/** + * @file blues_parse_send.cpp + * @author Bernd Giesecke (bernd@giesecke.tk) + * @brief Parse received LoRa packet and create Blues NoteCard note + * @version 0.1 + * @date 2023-04-25 + * + * @copyright Copyright (c) 2023 + * + */ +#include "main.h" + +/** Number of defined sensor types */ +#define NUM_DEFINED_SENSOR_TYPES 38 + +uint8_t value_id[NUM_DEFINED_SENSOR_TYPES] = {0, 1, 2, 3, 100, 101, 102, 103, + 104, 112, 113, 115, 116, 117, 118, 120, + 121, 125, 128, 130, 131, 132, 133, 134, + 135, 136, 137, 138, 142, 188, 190, 191, + 192, 193, 194, 195, 203, 255}; + +uint8_t value_size[NUM_DEFINED_SENSOR_TYPES] = {1, 1, 2, 2, 4, 2, 1, 2, + 1, 2, 6, 2, 2, 2, 4, 1, + 2, 2, 2, 4, 4, 2, 4, 6, + 3, 9, 11, 2, 1, 2, 2, 2, + 2, 2, 2, 2, 1, 4}; + +String value_name[NUM_DEFINED_SENSOR_TYPES] = {"digital_in", "digital_out", "analog_in", "analog_out", "generic", "illuminance", "presence", "temperature", + "humidity", "humidity_prec", "accelerometer", "barometer", "voltage", "current", "frequency", "percentage", + "altitude", "concentration", "power", "distance", "energy", "direction", "time", "gyrometer", + "colour", "gps", "gps", "voc", "switch", "soil_moist", "wind_speed", "wind_direction", + "soil_ec", "soil_ph_h", "soil_ph_l", "pyranometer", "light", "node_id"}; + +uint32_t value_divider[NUM_DEFINED_SENSOR_TYPES] = {1, 1, 100, 100, 1, 1, 1, 10, + 2, 10, 1000, 10, 100, 1000, 1, 1, + 1, 1, 1, 1000, 1000, 1, 1, 100, + 1, 10000, 1000000, 1, 1, 10, 100, 1, + 1000, 100, 10, 1, 1, 1}; + +// {136;9;"gps";true; [ 10000, 10000, 100 ]}, +// {137;11;"gps";true;[ 1000000, 1000000, 100 ]}, + +bool blues_parse_send(uint8_t *data, uint16_t data_len) +{ + blues_start_req("note.add"); + + JAddStringToObject(req, "file", "sensors.qo"); + // JAddBoolToObject(req, "sync", true); + J *body = JCreateObject(); + if (body != NULL) + { + uint16_t byte_idx = 0; + uint8_t sens_num = 0; + float float_val1 = 0.0; + float float_val2 = 0.0; + float float_val3 = 0.0; + uint32_t unsigned_val1 = 0; + uint32_t unsigned_val2 = 0; + uint32_t unsigned_val3 = 0; + int32_t signed_val1 = 0; + String sens_full_name = ""; + char node_id_str[9]; + J *sec_lvl; + char rounding[40]; + + // JAddStringToObject(body, "node_id", sensor_id.c_str()); + + while (byte_idx < data_len) + { + uint16_t current_byte_idx = byte_idx; + uint16_t sens_idx = 256; + sens_num = data[current_byte_idx++]; + MYLOG("PARSE", "Sensor Number %d", sens_num); + // find matching index + for (int idx = 0; idx < NUM_DEFINED_SENSOR_TYPES; idx++) + { + if (value_id[idx] == data[current_byte_idx]) + { + sens_idx = idx; + break; + } + } + if (sens_idx == 256) + { + // Wrong sensor ID + MYLOG("PARSE", "Unknown Sensor %d", data[current_byte_idx]); + JAddStringToObject(body, "error", "Invalid LPP ID"); + JAddItemToObject(req, "body", body); + if (!blues_send_req()) + { + MYLOG("PARSE", "Failed to send error packet"); + } + return false; + } + MYLOG("PARSE", "Found Sensor %d", data[current_byte_idx]); + current_byte_idx++; + switch (value_id[sens_idx]) + { + case 113: + case 134: + MYLOG("PARSE", "Found accelerometer or gyrometer"); + sens_full_name = value_name[sens_idx] + "_" + String(sens_num); + sec_lvl = JAddObjectToObject(body, sens_full_name.c_str()); + + float_val1 = (float)((int16_t)data[current_byte_idx + 1] << 8 | (int16_t)data[current_byte_idx]) / value_divider[sens_idx]; + current_byte_idx += 2; + JAddNumberToObject(sec_lvl, "X", float_val1); + float_val2 = (float)((int16_t)data[current_byte_idx + 1] << 8 | (int16_t)data[current_byte_idx]) / value_divider[sens_idx]; + current_byte_idx += 2; + JAddNumberToObject(sec_lvl, "Y", float_val2); + float_val3 = (float)((int16_t)data[current_byte_idx + 1] << 8 | (int16_t)data[current_byte_idx]) / value_divider[sens_idx]; + current_byte_idx += 2; + JAddNumberToObject(sec_lvl, "Z", float_val3); + MYLOG("PARSE", "x %.4f y %.4f z %.4f", float_val1, float_val2, float_val3); + break; + case 136: + MYLOG("PARSE", "Found GPS 4 digit"); + sens_full_name = value_name[sens_idx] + "_" + String(sens_num); + sec_lvl = JAddObjectToObject(body, sens_full_name.c_str()); + + float_val1 = (float)((int16_t)data[current_byte_idx + 2] << 16 | (int16_t)data[current_byte_idx + 1] << 8 | (int16_t)data[current_byte_idx]) / 10000.0; + current_byte_idx += 3; + JAddNumberToObject(sec_lvl, "Lat", float_val1); + float_val2 = (float)((int16_t)data[current_byte_idx + 2] << 16 | (int16_t)data[current_byte_idx + 1] << 8 | (int16_t)data[current_byte_idx]) / 10000.0; + current_byte_idx += 3; + JAddNumberToObject(sec_lvl, "Lng", float_val2); + float_val3 = (float)((int16_t)data[current_byte_idx + 2] << 16 | (int16_t)data[current_byte_idx + 1] << 8 | (int16_t)data[current_byte_idx]) / 100.0; + current_byte_idx += 3; + JAddNumberToObject(sec_lvl, "Alt", float_val3); + MYLOG("PARSE", "lat %.4f lng %.4f alt %.4f", float_val1, float_val2, float_val3); + break; + case 137: + MYLOG("PARSE", "Found GPS 6 digit"); + sens_full_name = value_name[sens_idx] + "_" + String(sens_num); + sec_lvl = JAddObjectToObject(body, sens_full_name.c_str()); + + float_val1 = (float)((int16_t)data[current_byte_idx + 3] << 16 | (int16_t)data[current_byte_idx + 2] << 16 | (int16_t)data[current_byte_idx + 1] << 8 | (int16_t)data[current_byte_idx]) / 1000000.0; + current_byte_idx += 4; + JAddNumberToObject(sec_lvl, "Lat", float_val1); + float_val2 = (float)((int16_t)data[current_byte_idx + 3] << 16 | (int16_t)data[current_byte_idx + 2] << 16 | (int16_t)data[current_byte_idx + 1] << 8 | (int16_t)data[current_byte_idx]) / 1000000.0; + current_byte_idx += 4; + JAddNumberToObject(sec_lvl, "Lng", float_val2); + float_val3 = (float)((int16_t)data[current_byte_idx + 2] << 16 | (int16_t)data[current_byte_idx + 1] << 8 | (int16_t)data[current_byte_idx]) / 100.0; + current_byte_idx += 3; + JAddNumberToObject(sec_lvl, "Alt", float_val3); + MYLOG("PARSE", "lat %.4f lng %.4f alt %.4f", float_val1, float_val2, float_val3); + break; + case 135: + MYLOG("PARSE", "Found Color"); + sens_full_name = value_name[sens_idx] + "_" + String(sens_num); + sec_lvl = JAddObjectToObject(body, sens_full_name.c_str()); + + unsigned_val1 = (int16_t)data[current_byte_idx]; + current_byte_idx += 4; + JAddNumberToObject(sec_lvl, "Red", unsigned_val1); + unsigned_val2 = (int16_t)data[current_byte_idx]; + current_byte_idx += 4; + JAddNumberToObject(sec_lvl, "Green", unsigned_val1); + unsigned_val3 = (int16_t)data[current_byte_idx]; + current_byte_idx += 3; + JAddNumberToObject(sec_lvl, "Blue", unsigned_val1); + MYLOG("PARSE", "r %ld g %ld b %ld", unsigned_val1, unsigned_val2, unsigned_val3); + break; + case 255: + unsigned_val1 = 0; + for (int cnt = 0; cnt < value_size[sens_idx]; cnt++) + { + unsigned_val1 = (unsigned_val1 << 8) | data[current_byte_idx]; + current_byte_idx++; + } + sprintf(node_id_str,"%08LX",(uint64_t)unsigned_val1); + MYLOG("PARSE", "unsigned_val1 %s", node_id_str); + + sens_full_name = value_name[sens_idx] + "_" + String(sens_num); + JAddStringToObject(body, value_name[sens_idx].c_str(), node_id_str); + + MYLOG("PARSE", "Added %s %08LX", sens_full_name.c_str(), (uint64_t)unsigned_val1); + + break; + default: + signed_val1 = 0; + for (int cnt = 0; cnt < value_size[sens_idx]; cnt++) + { + signed_val1 = (signed_val1 << 8) | data[current_byte_idx]; + current_byte_idx++; + } + float_val1 = (float)signed_val1 / value_divider[sens_idx]; + + // Limit to 2 decimals + sprintf(rounding, "%.2f", float_val1); + sscanf(rounding, "%f", &float_val1); + + sens_full_name = value_name[sens_idx] + "_" + String(sens_num); + JAddNumberToObject(body, sens_full_name.c_str(), float_val1); + MYLOG("PARSE", "Added %s %.2f", sens_full_name.c_str(), float_val1); + + break; + } + byte_idx = byte_idx + value_size[sens_idx] + 2; + // MYLOG("PARSE", "Data size %d Position %d", data_len, byte_idx); + MYLOG("PARSE", ">>>>><<<<<"); + } + JAddItemToObject(req, "body", body); + + JAddBinaryToObject(req, "payload", data, data_len); + + MYLOG("PARSE", "Finished parsing"); + if (!blues_send_req()) + { + MYLOG("PARSE", "Send request failed"); + return false; + } + return true; + } + else + { + MYLOG("PARSE", "Error creating body"); + } + + return false; +} diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/PlatformIO/src/main.cpp b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/PlatformIO/src/main.cpp new file mode 100644 index 0000000..4de8112 --- /dev/null +++ b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/PlatformIO/src/main.cpp @@ -0,0 +1,322 @@ +/** + * @file main.cpp + * @author Bernd Giesecke (bernd@giesecke.tk) + * @brief App event handlers + * @version 0.1 + * @date 2023-04-25 + * + * @copyright Copyright (c) 2023 + * + */ + +#include "main.h" + +/** LoRaWAN packet */ +WisCayenne g_solution_data(255); + +/** Received package for parsing */ +uint8_t rcvd_data[256]; +/** Length of received package */ +uint16_t rcvd_data_len = 0; + +/** Send Fail counter **/ +uint8_t send_fail = 0; + +/** Set the device name, max length is 10 characters */ +char g_ble_dev_name[10] = "RAK"; + +/** Flag for RAK1906 sensor */ +bool has_rak1906 = false; + +/** Flag is Blues Notecard was found */ +bool has_blues = false; + +/** + * @brief Initial setup of the application (before LoRaWAN and BLE setup) + * + */ +void setup_app(void) +{ + Serial.begin(115200); + time_t serial_timeout = millis(); + // On nRF52840 the USB serial is not available immediately + while (!Serial) + { + if ((millis() - serial_timeout) < 5000) + { + delay(100); + digitalWrite(LED_GREEN, !digitalRead(LED_GREEN)); + } + else + { + break; + } + } + digitalWrite(LED_GREEN, LOW); + + // Set firmware version + api_set_version(SW_VERSION_1, SW_VERSION_2, SW_VERSION_3); +} + +/** + * @brief Final setup of application (after LoRaWAN and BLE setup) + * + * @return true + * @return false + */ +bool init_app(void) +{ + MYLOG("APP", "init_app"); + + MYLOG("INI", "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++"); + MYLOG("INI", "WisBlock Hummingbird Blues Sensor"); + MYLOG("INI", "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++"); + + // Initialize User AT commands + init_user_at(); + + // Check if RAK1906 is available + has_rak1906 = init_rak1906(); + if (has_rak1906) + { + AT_PRINTF("+EVT:RAK1906"); + } + + // Initialize Blues Notecard + has_blues = init_blues(); + if (!has_blues) + { + AT_PRINTF("+EVT:CELLULAR_ERROR"); + } + else + { + AT_PRINTF("+EVT:RAK13102"); + MYLOG("APP", "Start P2P RX"); + // Set to permanent listen + g_lora_p2p_rx_mode = RX_MODE_RX; + Radio.Rx(0); + } + + pinMode(WB_IO2, OUTPUT); + digitalWrite(WB_IO2, LOW); + return true; +} + +/** + * @brief Handle events + * Events can be + * - timer (setup with AT+SENDINT=xxx) + * - interrupt events + * - wake-up signals from other tasks + */ +void app_event_handler(void) +{ + // Timer triggered event + if ((g_task_event_type & STATUS) == STATUS) + { + g_task_event_type &= N_STATUS; + MYLOG("APP", "Timer wakeup"); + if (g_lpwan_has_joined) + { + // Reset the packet + g_solution_data.reset(); + + // Get battery level + float batt_level_f = read_batt(); + g_solution_data.addVoltage(LPP_CHANNEL_BATT, batt_level_f / 1000.0); + + // Read sensors and battery + if (has_rak1906) + { + read_rak1906(); + } + + if (!has_blues) + { + if (g_lorawan_settings.lorawan_enable) + { + lmh_error_status result = send_lora_packet(g_solution_data.getBuffer(), g_solution_data.getSize()); + switch (result) + { + case LMH_SUCCESS: + MYLOG("APP", "Packet enqueued"); + break; + case LMH_BUSY: + MYLOG("APP", "LoRa transceiver is busy"); + AT_PRINTF("+EVT:BUSY\n"); + break; + case LMH_ERROR: + AT_PRINTF("+EVT:SIZE_ERROR\n"); + MYLOG("APP", "Packet error, too big to send with current DR"); + break; + } + } + else + { + // Add unique identifier in front of the P2P packet, here we use the DevEUI + g_solution_data.addDevID(LPP_CHANNEL_DEVID, &g_lorawan_settings.node_device_eui[4]); + // uint8_t packet_buffer[g_solution_data.getSize() + 8]; + // memcpy(packet_buffer, g_lorawan_settings.node_device_eui, 8); + // memcpy(&packet_buffer[8], g_solution_data.getBuffer(), g_solution_data.getSize()); + + // Send packet over LoRa + // if (send_p2p_packet(packet_buffer, g_solution_data.getSize() + 8)) + if (send_p2p_packet(g_solution_data.getBuffer(), g_solution_data.getSize())) + { + MYLOG("APP", "Packet enqueued"); + } + else + { + AT_PRINTF("+EVT:SIZE_ERROR\n"); + MYLOG("APP", "Packet too big"); + } + } + } + else + { + MYLOG("APP", "Get hub sync status:"); + blues_hub_status(); + + g_solution_data.addDevID(0, &g_lorawan_settings.node_device_eui[4]); + blues_parse_send(g_solution_data.getBuffer(), g_solution_data.getSize()); + } + // Reset the packet + g_solution_data.reset(); + } + else + { + MYLOG("APP", "Network not joined, skip sending"); + } + } + + // Parse request event + if ((g_task_event_type & PARSE) == PARSE) + { + g_task_event_type &= N_PARSE; + + if (has_blues) + { + if (!blues_parse_send(rcvd_data, rcvd_data_len)) + { + MYLOG("APP", "Parsing or sending failed"); + + MYLOG("APP", "**********************************************"); + MYLOG("APP", "Get hub sync status:"); + // {“req”:”hub.sync.status”} + blues_start_req("hub.sync.status"); + blues_send_req(); + + MYLOG("APP", "**********************************************"); + delay(2000); + + MYLOG("APP", "Get note card status:"); + // {“req”:”card.wireless”} + blues_start_req("card.wireless"); + blues_send_req(); + + MYLOG("APP", "**********************************************"); + delay(2000); + } + } + else + { + MYLOG("APP", "Got PARSE request, but no Blues Notecard detected"); + } + } +} + +/** + * @brief Handle BLE events + * + */ +void ble_data_handler(void) +{ + if (g_enable_ble) + { + if ((g_task_event_type & BLE_DATA) == BLE_DATA) + { + MYLOG("AT", "RECEIVED BLE"); + // BLE UART data arrived + g_task_event_type &= N_BLE_DATA; + + while (g_ble_uart.available() > 0) + { + at_serial_input(uint8_t(g_ble_uart.read())); + delay(5); + } + at_serial_input(uint8_t('\n')); + } + } +} + +/** + * @brief Handle LoRa events + * + */ +void lora_data_handler(void) +{ + // LoRa Join finished handling + if ((g_task_event_type & LORA_JOIN_FIN) == LORA_JOIN_FIN) + { + g_task_event_type &= N_LORA_JOIN_FIN; + if (g_join_result) + { + MYLOG("APP", "Successfully joined network"); + } + else + { + MYLOG("APP", "Join network failed"); + /// \todo here join could be restarted. + // lmh_join(); + } + } + + // LoRa data handling + if ((g_task_event_type & LORA_DATA) == LORA_DATA) + { + g_task_event_type &= N_LORA_DATA; + MYLOG("APP", "Received package over LoRa"); + char log_buff[g_rx_data_len * 3] = {0}; + uint8_t log_idx = 0; + for (int idx = 0; idx < g_rx_data_len; idx++) + { + sprintf(&log_buff[log_idx], "%02X ", g_rx_lora_data[idx]); + log_idx += 3; + } + MYLOG("APP", "%s", log_buff); + +#if MY_DEBUG > 0 + CayenneLPP lpp(g_rx_data_len - 8); + memcpy(lpp.getBuffer(), &g_rx_lora_data[8], g_rx_data_len - 8); + DynamicJsonDocument jsonBuffer(4096); + JsonObject root = jsonBuffer.to(); + lpp.decodeTTN(lpp.getBuffer(), g_rx_data_len - 8, root); + serializeJsonPretty(root, Serial); + Serial.println(); +#endif + memcpy(rcvd_data, g_rx_lora_data, g_rx_data_len); + rcvd_data_len = g_rx_data_len; + api_wake_loop(PARSE); + } + + // LoRa TX finished handling + if ((g_task_event_type & LORA_TX_FIN) == LORA_TX_FIN) + { + g_task_event_type &= N_LORA_TX_FIN; + + MYLOG("APP", "LPWAN TX cycle %s", g_rx_fin_result ? "finished ACK" : "failed NAK"); + + if (!g_rx_fin_result) + { + // Increase fail send counter + send_fail++; + + if (send_fail == 10) + { + // Too many failed sendings, reset node and try to rejoin + delay(100); + api_reset(); + } + } + } +} diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/PlatformIO/src/main.h b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/PlatformIO/src/main.h new file mode 100644 index 0000000..d30dad8 --- /dev/null +++ b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/PlatformIO/src/main.h @@ -0,0 +1,101 @@ +/** + * @file main.h + * @author Bernd Giesecke (bernd@giesecke.tk) + * @brief Includes, defines and globals + * @version 0.1 + * @date 2023-04-25 + * + * @copyright Copyright (c) 2023 + * + */ + +#ifndef _MAIN_H_ +#define _MAIN_H_ + +#include +#include +#include +#include "RAK1906_env.h" + +// Debug output set to 0 to disable app debug output +#ifndef MY_DEBUG +#define MY_DEBUG 1 +#endif + +#if MY_DEBUG > 0 +#define MYLOG(tag, ...) \ + do \ + { \ + if (tag) \ + PRINTF("[%s] ", tag); \ + PRINTF(__VA_ARGS__); \ + PRINTF("\n"); \ + Serial.flush(); \ + if (g_ble_uart_is_connected) \ + { \ + g_ble_uart.printf(__VA_ARGS__); \ + g_ble_uart.printf("\n"); \ + } \ + } while (0) +#else +#define MYLOG(...) +#endif + +/** Define the version of your SW */ +#ifndef SW_VERSION_1 +#define SW_VERSION_1 1 // major version increase on API change / not backwards compatible +#endif +#ifndef SW_VERSION_2 +#define SW_VERSION_2 0 // minor version increase on API change / backward compatible +#endif +#ifndef SW_VERSION_3 +#define SW_VERSION_3 0 // patch version increase on bugfix, no affect on API +#endif + +/** Application function definitions */ +void setup_app(void); +bool init_app(void); +void app_event_handler(void); +void ble_data_handler(void) __attribute__((weak)); +void lora_data_handler(void); + +// Wakeup flags +#define PARSE 0b1000000000000000 +#define N_PARSE 0b0111111111111111 + +// Cayenne LPP Channel numbers per sensor value +#define LPP_CHANNEL_BATT 1 // Base Board + +// Globals +extern WisCayenne g_solution_data; + +// Parser +bool blues_parse_send(uint8_t *data, uint16_t data_len); + +// Blues.io +struct s_blues_settings +{ + uint16_t valid_mark = 0xAA55; // Validity marker + char product_uid[256] = "com.my-company.my-name:my-project"; // Blues Product UID + bool conn_continous = false; // Use periodic connection + bool use_ext_sim = false; // Use external SIM + char ext_sim_apn[256] = "internet"; // APN to be used with external SIM + bool motion_trigger = true; // Send data on motion trigger +}; + +bool init_blues(void); +bool blues_send_req(void); +void blues_hub_status(void); +bool blues_start_req(String request_name); +bool blues_send_req(void); +bool blues_send_payload(uint8_t *data, uint16_t data_len); + +extern J *req; +extern s_blues_settings g_blues_settings; + +// User AT commands +void init_user_at(void); +bool read_blues_settings(void); +void save_blues_settings(void); + +#endif // _MAIN_H_ \ No newline at end of file diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/PlatformIO/src/user_at_cmd.cpp b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/PlatformIO/src/user_at_cmd.cpp new file mode 100644 index 0000000..4a9365f --- /dev/null +++ b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/PlatformIO/src/user_at_cmd.cpp @@ -0,0 +1,385 @@ +/** + * @file user_at_cmd.cpp + * @author Bernd Giesecke (bernd@giesecke.tk) + * @brief User AT commands + * @version 0.1 + * @date 2023-08-18 + * + * @copyright Copyright (c) 2023 + * + */ +#include "main.h" + +#include +#include +using namespace Adafruit_LittleFS_Namespace; + +/** Filename to save Blues settings */ +static const char blues_file_name[] = "BLUES"; + +/** File to save battery check status */ +File this_file(InternalFS); + +/** Structure for saved Blues Notecard settings */ +s_blues_settings g_blues_settings; + +/** + * @brief Set Blues Product UID + * + * @param str Product UID as Hex String + * @return int AT_SUCCESS if ok, AT_ERRNO_PARA_FAIL if invalid value + */ +int at_set_blues_prod_uid(char *str) +{ + if (strlen(str) < 25) + { + return AT_ERRNO_PARA_NUM; + } + + for (int i = 0; str[i] != '\0'; i++) + { + if (str[i] >= 'A' && str[i] <= 'Z') // checking for uppercase characters + str[i] = str[i] + 32; // converting uppercase to lowercase + } + + char new_uid[256] = {0}; + snprintf(new_uid, 255, str); + + MYLOG("USR_AT", "Received new Blues Product UID %s", new_uid); + + bool need_save = strcmp(new_uid, g_blues_settings.product_uid) == 0 ? false : true; + + if (need_save) + { + snprintf(g_blues_settings.product_uid, 256, new_uid); + } + + // Save new master node address if changed + if (need_save) + { + save_blues_settings(); + } + return AT_SUCCESS; +} + +/** + * @brief Get Blues Product UID + * + * @return int AT_SUCCESS + */ +int at_query_blues_prod_uid(void) +{ + snprintf(g_at_query_buf, ATQUERY_SIZE, "%s", g_blues_settings.product_uid); + return AT_SUCCESS; +} + +/** + * @brief Set usage of eSIM or external SIM and APN + * + * @param str params as string, format 0 or 1:APN_NAME + * @return int + * AT_SUCCESS is params are set correct + * AT_ERRNO_PARA_NUM if params error + */ +int at_set_blues_ext_sim(char *str) +{ + char *param; + bool new_use_ext_sim; + char new_ext_sim_apn[256]; + + // Get string up to first : + param = strtok(str, ":"); + if (param != NULL) + { + if (param[0] == '0') + { + MYLOG("USR_AT", "Enable eSIM"); + new_use_ext_sim = false; + } + else if (param[0] == '1') + { + MYLOG("USR_AT", "Enable external SIM"); + new_use_ext_sim = true; + param = strtok(NULL, ":"); + if (param != NULL) + { + for (int i = 0; param[i] != '\0'; i++) + { + if (param[i] >= 'A' && param[i] <= 'Z') // checking for uppercase characters + param[i] = param[i] + 32; // converting uppercase to lowercase + } + snprintf(new_ext_sim_apn, 256, "%s", param); + } + else + { + MYLOG("USR_AT", "Missing external SIM APN"); + return AT_ERRNO_PARA_NUM; + } + } + else + { + MYLOG("USR_AT", "Invalid SIM flag %d", param[0]); + return AT_ERRNO_PARA_NUM; + } + } + + bool need_save = false; + if (new_use_ext_sim != g_blues_settings.use_ext_sim) + { + g_blues_settings.use_ext_sim = new_use_ext_sim; + need_save = true; + } + if (strcmp(new_ext_sim_apn, g_blues_settings.product_uid) != 0) + { + snprintf(g_blues_settings.ext_sim_apn, 256, new_ext_sim_apn); + need_save = true; + } + + if (need_save) + { + save_blues_settings(); + } + return AT_SUCCESS; +} + +/** + * @brief Get Blues SIM settings + * + * @return int AT_SUCCESS + */ +int at_query_blues_ext_sim(void) +{ + if (g_blues_settings.use_ext_sim) + { + snprintf(g_at_query_buf, ATQUERY_SIZE, "1:%s", g_blues_settings.ext_sim_apn); + MYLOG("USR_AT", "Using external SIM with APN = %s", g_blues_settings.ext_sim_apn); + } + else + { + snprintf(g_at_query_buf, ATQUERY_SIZE, "0"); + MYLOG("USR_AT", "Using eSIM"); + } + return AT_SUCCESS; +} + +/** + * @brief Set Blues NoteCard mode + * /// \todo work in progress + * + * @param str params as string, format 0 or 1 + * @return int + * AT_SUCCESS is params are set correct + * AT_ERRNO_PARA_NUM if params error + */ +int at_set_blues_mode(char *str) +{ + bool new_connection_mode; + + if (str[0] == '0') + { + MYLOG("USR_AT", "Set minimum connection mode"); + new_connection_mode = false; + } + else if (str[0] == '1') + { + MYLOG("USR_AT", "Set continuous connection mode"); + new_connection_mode = true; + } + else + { + MYLOG("USR_AT", "Invalid motion trigger flag %d", str[0]); + return AT_ERRNO_PARA_NUM; + } + + bool need_save = false; + if (new_connection_mode != g_blues_settings.conn_continous) + { + g_blues_settings.conn_continous = new_connection_mode; + need_save = true; + } + + if (need_save) + { + save_blues_settings(); + } + return AT_SUCCESS; +} + +/** + * @brief Get Blues mode settings + * + * @return int AT_SUCCESS + */ +int at_query_blues_mode(void) +{ + snprintf(g_at_query_buf, ATQUERY_SIZE, "%s", g_blues_settings.conn_continous ? "1" : "0"); + MYLOG("USR_AT", "Using %s connection", g_blues_settings.conn_continous ? "continous" : "periodic"); + return AT_SUCCESS; +} + +// /** +// * @brief Enable/disable the motion trigger +// * +// * @param str params as string, format 0 or 1 +// * @return int +// * AT_SUCCESS is params are set correct +// * AT_ERRNO_PARA_NUM if params error +// */ +// int at_set_blues_trigger(char *str) +// { +// bool new_motion_trigger; + +// if (str[0] == '0') +// { +// MYLOG("USR_AT", "Disable motion trigger"); +// new_motion_trigger = false; +// blues_disable_attn(); +// } +// else if (str[0] == '1') +// { +// MYLOG("USR_AT", "Enable motion trigger"); +// new_motion_trigger = true; +// blues_enable_attn(); +// } +// else +// { +// MYLOG("USR_AT", "Invalid motion trigger flag %d", str[0]); +// return AT_ERRNO_PARA_NUM; +// } + +// bool need_save = false; +// if (new_motion_trigger != g_blues_settings.motion_trigger) +// { +// g_blues_settings.motion_trigger = new_motion_trigger; +// need_save = true; +// } + +// if (need_save) +// { +// save_blues_settings(); +// } +// return AT_SUCCESS; +// } + +// /** +// * @brief Get Blues motion trigger settings +// * +// * @return int AT_SUCCESS +// */ +// int at_query_blues_trigger(void) +// { +// snprintf(g_at_query_buf, ATQUERY_SIZE, "%s", g_blues_settings.motion_trigger ? "1" : "0"); +// MYLOG("USR_AT", "Motion trigger is %s", g_blues_settings.motion_trigger ? "enabled" : "disabled"); +// return AT_SUCCESS; +// } + +static int at_reset_blues_settings(void) +{ + if (InternalFS.exists(blues_file_name)) + { + InternalFS.remove(blues_file_name); + } + return AT_SUCCESS; +} + +/** + * @brief Read saved Blues Product ID + * + */ +bool read_blues_settings(void) +{ + bool structure_valid = false; + if (InternalFS.exists(blues_file_name)) + { + this_file.open(blues_file_name, FILE_O_READ); + this_file.read((void *)&g_blues_settings.valid_mark, sizeof(s_blues_settings)); + this_file.close(); + + // Check for valid data + if (g_blues_settings.valid_mark == 0xAA55) + { + structure_valid = true; + MYLOG("USR_AT", "Valid Blues settings found, Blues Product UID = %s", g_blues_settings.product_uid); + if (g_blues_settings.use_ext_sim) + { + MYLOG("USR_AT", "Using external SIM with APN = %s", g_blues_settings.ext_sim_apn); + } + else + { + MYLOG("USR_AT", "Using eSIM"); + } + } + else + { + MYLOG("USR_AT", "No valid Blues settings found"); + } + } + + if (!structure_valid) + { + return false; + + // No settings file found optional to set defaults (ommitted!) + // g_blues_settings.valid_mark = 0xAA55; // Validity marker + // sprintf(g_blues_settings.product_uid, "com.my-company.my-name:my-project"); // Blues Product UID + // g_blues_settings.conn_continous = false; // Use periodic connection + // g_blues_settings.use_ext_sim = false; // Use external SIM + // sprintf(g_blues_settings.ext_sim_apn, "-"); // APN to be used with external SIM + // g_blues_settings.motion_trigger = true; // Send data on motion trigger + // save_blues_settings(); + } + + return true; +} + +/** + * @brief Save the Blues Product ID + * + */ +void save_blues_settings(void) +{ + if (InternalFS.exists(blues_file_name)) + { + InternalFS.remove(blues_file_name); + } + + g_blues_settings.valid_mark = 0xAA55; + this_file.open(blues_file_name, FILE_O_WRITE); + this_file.write((const char *)&g_blues_settings.valid_mark, sizeof(s_blues_settings)); + this_file.close(); + MYLOG("USR_AT", "Saved Blues Settings"); +} + +/** + * @brief List of all available commands with short help and pointer to functions + * + */ +atcmd_t g_user_at_cmd_new_list[] = { + /*| CMD | AT+CMD? | AT+CMD=? | AT+CMD=value | AT+CMD | Permissions |*/ + // Module commands + {"+BUID", "Set/get the Blues product UID", at_query_blues_prod_uid, at_set_blues_prod_uid, NULL, "RW"}, + {"+BSIM", "Set/get Blues SIM settings", at_query_blues_ext_sim, at_set_blues_ext_sim, NULL, "RW"}, + {"+BMOD", "Set/get Blues NoteCard connection modes", at_query_blues_mode, at_set_blues_mode, NULL, "RW"}, + // {"+BTRIG", "Set/get Blues send trigger", at_query_blues_trigger, at_set_blues_trigger, NULL, "RW"}, + {"+BR", "Remove all Blues Settings", NULL, NULL, at_reset_blues_settings, "RW"}, +}; + +/** Number of user defined AT commands */ +uint8_t g_user_at_cmd_num = 0; + +/** Pointer to the combined user AT command structure */ +atcmd_t *g_user_at_cmd_list; + +/** + * @brief Initialize the user defined AT command list + * + */ +void init_user_at(void) +{ + // Assign custom AT command list to pointer used by WisBlock API + g_user_at_cmd_list = g_user_at_cmd_new_list; + + // Add AT commands to structure + g_user_at_cmd_num += sizeof(g_user_at_cmd_new_list) / sizeof(atcmd_t); + MYLOG("USR_AT", "Added %d User AT commands", g_user_at_cmd_num); +} diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/README.md b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/README.md new file mode 100644 index 0000000..1ba9ade --- /dev/null +++ b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/README.md @@ -0,0 +1,250 @@ +# WisBlock Goes Blues +| RAKWireless | RAKstar | Blues | +| :-: | :-: | :-: | + +---- + +While WisBlock is usually associated with _**LoRa**_ and _**LoRaWAN**_, this time we are diving into the cellular data transmission using the Blues.IO Notecard. To make it more interesting, we are mixing LoRa P2P communication and cellular communication into one project. + +# Overview +When I got a [Blues Notecard](https://blues.io/products/notecard/) for some testing, the first thing was of course to connect it to the WisBlock modules. After some initial testing like connecting the Notecard to my cellular provider and sending some sensor data, I was hungry for more. + +Browsing through the Blues website I found their very interesting product [Sparrow Development Kit](https://blues.io/products/sparrow-iot-sensor-clusters-over-lora/). What caught my interest was the fact that it combines LoRa and Cellular communication in a simple way. + +Inspired by _**Sparrow**_ and after browsing through the open source code of it on [Github](https://github.com/blues/sparrow-reference-firmware) I decided to build a similar system with WisBlock sensors. + +---- + +# Principles of Blues Sparrow +Sparrow connects multiple sensors nodes over LoRa P2P to a "gateway" sensor node that is equipped with a Blues Notecard. The sensor nodes register themselves on the gateway and send their sensor data in assigned timeslots to the gateway. The gateway then forwards the sensor data to the Blues Notehub. From there you can send the data either to the Sparrow example web application (you have to setup this by yourself) or to another visualization platform. +This is a very interesting combination of LoRa and Cellular communication. + +---- + +# Principles of the WisBlock Hummingbird Concept of Proof +The WisBlock Hummingbird CoP transfers the idea of Blues Sparrow to the WisBlock world. It uses WisBlock modules for both the sensor nodes and the gateway that is connected to the Blues Notecard. The sensor nodes are sending their data over LoRa P2P to the gateway node which then forwards the data to the Blues Notehub. + +---- + +# Differences between Blues Sparrow and WisBlock Hummingbird +I looked into the protocol and data flow of Sparrow and (of course) found some things that I wanted to change to make it easier to integrate my existing sensors to the sensor network. +1) Most of my WisBlock examples can be switched between LoRaWAN and LoRa P2P communication, but Sparrow uses their "Notes" format (data in JSON format) to transmit the sensor data. This would require a change in the source codes of my existing sensor applications. I prefer to keep my existing data format, which is Cayenne LPP. This allows me to basically connect any of my exising sensor devices to the Hummingbird gateway without any code changes. The only thing that has to be done is to switch the LoRa protocol from LoRaWAN to LoRa P2P and setup the same parameters as on the gateway. +2) Sparrow requires that the sensor nodes are registering themselves on the gateway and submit their sensor data format as a template to the gateway. As I am sending data from the nodes to the gateway in Cayenne LPP format, this registration with a template is not required. Instead I implemented in the gateway firmware a parser for the data that can basically understand the data sent from any sensor that uses Cayenne LPP format. +3) Sparrow assignes a timeslot to each sensor where they are allowed to send their sensor data. I skipped this time slot assignment, as I want to connect as well sensors that want to send a data packet on an event, e.g. a door status switched (house alarm system) or an alarm for water leaking. Instead of the timeslots, I implemented Semtech SX1262's CAD feature. CAD (channel activity detection) helps to prevent collision of data sent from different devices on the same frequency at the same time. It does check for activity on the frequency before it starts sending the data packet. This is not 100% preventing packet collision in the air, but it works quite well for me. +4) Sparrow uses different _**Note**_ definitions for the different devices, like _**motion.qo**_ for motion sensors, _**sensors.qo**_ for environment sensors. In this PoC I used only one _**Note**_ type, the _**sensors.qo**_ and the differentiation between the different sensors is done in the end.point. +5) Blues offers an open source [reference web application](https://sparrowstarter.netlify.app/) to visualize the data of the Sparrow sensor nodes. They have as well easy to follow [guides](https://github.com/blues/sparrow-reference-web-app#cloud-deployment) how to deploy the reference web application in the cloud with e.g. [Netlify](https://www.netlify.com/) or [Vercel](https://vercel.com). But as I don't have a Sparrow system and (despite the very good documentation) I struggled to get the reference web app to life, I instead switched for the visualization to my favorite [Datacake](https://datacake.co/). + +---- + +# How to use WisBlock Hummingbird + +---- + +## Hummingbird Gateway +The only thing that requires some work is to setup the WisBlock system with the Blues Notecard. At the moment there is no WisBlock IO module available to plug-in the Notecard, but luckily, with a [Notecarrier-A](https://blues.io/products/notecarrier/notecarrier-a/) or [Notecarrier-B](https://blues.io/products/notecarrier/notecarrier-b/) only 3 wires (I2C bus + GND) are required to connect my WisBlock Core with the Notecard on the Notecarrier. Of course this requires that both the Notecarrier and the WisBlock Base Board are supplied by separate power supplies. This is on the to-do-list for the next improvements. +
Hardware Setup
+The code in this repository is for the Hummingbird Gateway and supports beside of the communication to the Blues Notecard a RAK1906 environment sensor. The code can be used as well for a simple sensor node with a RAK1906 sensor without the Blues Notecard. + +### ⚠️ _IMPORTANT 1_ ⚠️ +You have to setup your Notecard at Blues.IO before it can be used. This setup is not scope of this README, please follow the very good [Quickstart](https://dev.blues.io/quickstart/) guides provided by Blues. + +### ⚠️ _IMPORTANT 2_ ⚠️ +To connect the Blues Notecard, a _**Product UID**_ is required. This product UID is created while you setup your Notecard in the Notehub following the above mentioned Quickstart. Of course I am not sharing my Product UID here. The Product UID is defined in a file named _**`product_uid.h`**_ that you have to create in the src file of the project. The content of this file is like this: +```cpp +#ifndef PRODUCT_UID +#define PRODUCT_UID "" // "com.my-company.my-name:my-project" +#pragma message "PRODUCT_UID is not defined in this example. Please ensure your Notecard has a product identifier set before running this example or define it in code here. More details at https://dev.blues.io/tools-and-sdks/samples/product-uid" +#endif +``` +You have to replace _**``**_ with your own product UID. + +### ⚠️ _IMPORTANT 3_ ⚠️ +In the file _**blues.cpp**_ the firmware is setting up the APN and the connection mode. +1) if using the eSIM card from Blues.IO, there is no need to do this and this code part can be removed. +2) if using an external SIM card, this needs to be done only _**ONCE**_ and it is usually done in the initial setup of the Notecard and you can remove this code part. + +**Code part to be removed:** ==> [blues.cpp](https://github.com/beegee-tokyo/Hummingbird-Blues-Gateway/blob/44b5093bf170faf65016ae071a5598281e6a899b/src/blues.cpp#L49) +```cpp + /*************************************************/ + /* If the Notecard is properly setup, there is */ + /* need to setup the APN and card mode on every */ + /* restart! It will reuse the APN and mode that */ + /* was originally setup. */ + /*************************************************/ + /* If using the built-in eSIM card from Blues.IO */ + /* These code lines should be complete removed! */ + /*************************************************/ + MYLOG("BLUES", "Set APN"); + // {“req”:”card.wireless”} + req = notecard.newRequest("card.wireless"); + // For SMART + // JAddStringToObject(req, "apn", "internet"); + // JAddStringToObject(req, "mode", "a"); + // For Monogoto + JAddStringToObject(req, "apn", "data.mono"); + JAddStringToObject(req, "mode", "a"); + // if (!notecard.sendRequest(req)) + if (!blues_send_req()) + { + MYLOG("BLUES", "card.wireless request failed"); + return false; + } +``` +---- + +## Hummingbird Sensor + +As I am using the "standard" data format of my WisBlock examples, many of my existing WisBlock example application can be used without any changes: + +- [WisBlock-Seismic-Sensor](https://github.com/beegee-tokyo/WisBlock-Seismic-Sensor) +- [WisBlock-Seismic-Sensor](https://github.com/beegee-tokyo/WisBlock-Seismic-Sensor/tree/main/PIO-Arduino-Seismic-Sensor) (only the Arduino version of the application, the RUI3 version needs some changes) +- [RUI3 door/window status](https://github.com/beegee-tokyo/RUI3-RAK13011) +- [RAK4631-Kit-4-RAK1906](https://github.com/beegee-tokyo/RAK4631-Kit-4-RAK1906) +- [RAK4631-Kit-1-RAK1901-RAK1902-RAK1903](https://github.com/beegee-tokyo/RAK4631-Kit-1-RAK1901-RAK1902-RAK1903) +- [WisBlock Indoor Air Quality Sensor](https://github.com/beegee-tokyo/WisBlock-IAQ-PM-CO2-VOC-EPD) + +For other (older) example codes, it is required to add the LoRa P2P send functionality and/or extend the CayenneLPP data packet with the device identifier. I use the DevEUI of the device as unique device identifier, as every WisBlock Core has the unique DevEUI printed on its label. +In my examples, I use the [CayenneLPP library from ElectronicCats](https://github.com/ElectronicCats/CayenneLPP) with my own class extension. When using this library, the data packet is generated in **`WisCayenne g_solution_data(255);`**. The unique device identifier (the DevEUI) is added at the start of the existing data packet using this few lines of code: + +---- + +### ⚠️ Using WisBlock-API-V2: ⚠️ +```cpp +// Add unique identifier in front of the P2P packet, here we use the DevEUI +uint8_t p2p_buffer[g_solution_data.getSize() + 8]; +memcpy(p2p_buffer, g_lorawan_settings.node_device_eui, 8); +// Add the packet data +memcpy(&p2p_buffer[8], g_solution_data.getBuffer(), g_solution_data.getSize()); +``` +before sending the packet with +```cpp +send_p2p_packet(p2p_buffer, g_solution_data.getSize() + 8); +``` +---- +### ⚠️ Using RUI3: ⚠️ +```cpp +uint8_t packet_buffer[g_solution_data.getSize() + 8]; +if (!api.lorawan.deui.get(packet_buffer, 8)) +{ + MYLOG("UPLINK", "Could not get DevEUI"); +} + +memcpy(&packet_buffer[8], g_solution_data.getBuffer(), g_solution_data.getSize()); + +for (int idx = 0; idx < g_solution_data.getSize() + 8; idx++) +{ + Serial.printf("%02X", packet_buffer[idx]); +} +Serial.println(""); +``` +before sending the packet with +```cpp +api.lorawan.psend(g_solution_data.getSize() + 8, packet_buffer); +``` +---- + +# Hummingbird in Action + +---- + +## Hummingbird Gateway + +After doing the hardware setup, flashing the firmware and following Blues Quickstart guides to setup my Notehub and the Notecard, my existing sensors are able to send data to the Hummingbird Gateway. + +Here is an example log output with the result of the CayenneLPP data parsing then the packet sent from the gateway over the Notecard: +
Gateway Log
+ +---- + +## Blues Notehub +The notes send to the Blues Notehub can be seen in the _**Events**_ listing of the Nothub +
Notehub Events Log
+ +---- + +To forward the messages to Datacake, a _**Route**_ has to be defined in the Notehub. There are many easy to follow tutorials available in the Blues documentation, in this case I used of course the [Datacake Tutorial](https://dev.blues.io/guides-and-tutorials/routing-data-to-cloud/datacake/) to setup the routing. + +Once the route has been setup, the Notefiles used in this route have to been selected. As all sensor nodes data are sent as _**sensor.qo**_, this _**Note**_ has to be enabled. + +### ⚠️ INFO ⚠️ +Different to the tutorial steps, I did not use the Transform Data option !!!! + +
Notehub Route Setup
+ +---- + +Now the routing events are shown in the Routes log view: +
Notehub Routed Log
+ +---- + +When opening one of these events, the sensor data can be seen in the Body view: +
Notehub Routed Data
+ +---- + +## Datacake + +To visualize the data in Datacake a matching device has to be defined, as it is described in the [Datacake Tutorial](https://dev.blues.io/guides-and-tutorials/routing-data-to-cloud/datacake/). + +### ⚠️ INFO ⚠️ +As Hummingbird is sending sensor data from different sensor nodes to one end-point, a different payload decoder is required !!!! + +I wrote a payload decoder that separates the incoming sensor data depending on the sensor node ID into different fields. This is required to distinguish between the data of the sensor nodes. The Datacake decoder for this task is the file [Decoder.js](./Decoder.js) in this repository. +The content of this file has to be copied into the _**HTTP Payload Decoder**_ of Datacake: +
Payload Decoder
+ +---- + +Then the matching fields for the sensor data have to been created. The easiest way to do this is to wait for incoming data from the sensors. If no matching field is existing, the data will be shown in the _**Suggested Fields**_ list in the configuration: +
Suggested Fields
+ +The sensor data can be easily assigned to fields using the _**Create Field**_ button. + +---- + +Once all the sensor data is assigned to fields, we can start with the visualization of the data. +
Create Fields
+As you can see, there are multiple fields for battery, temperature, humidity, ..., but each field has a leading device ID! + +---- + +Datacake has two options, the first one is the _**Device Dashboard**_, but as we expect a lot of data from different devices, it would be quite crowded and difficult to distinguish between the different devices. To make it easier to view the data per devices, I instead created a device independent _**Dashboard**_ that allows me to create tabs to separate the data from the different sensors. Such Dashboards can be created with _**Add Dashboard**_ on the left side of the Datacake panel: +
Create Dashboard
+ +---- + +After creating the _**Dashboard**_ I clicked on the button on the right side to enable editing, then on _**Edit Dashboard Meta**_. +
Enable editing Dashboard
+In the opening window I added a tab for each of my Hummingbird sensor devices: +
Add tabs
+ +This allows me to sort the data from the different sensor nodes into these tabs. + +---- + +I will not go into details how to create visualization widgets in Datacake, this step is handled in other tutorials already. + +---- + +The final result for the two sensors and the sensor gateway that are sending sensor data looks like this: + +_**Sensor Device 1 is a particulate matter sensor**_ +
PM Sensor
+ +_**Sensor Device 2 is a barometric pressure sensor**_ +
Barometer Sensor
+ +_**Sensor Device 3 is the Hummingbird gateway that I enquipped with an environment sensor**_ +
Sensor Gateway
+ +---- +---- + +# LoRa® is a registered trademark or service mark of Semtech Corporation or its affiliates. + + +# LoRaWAN® is a licensed mark. + +---- +---- \ No newline at end of file diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Blues-Io-Logo-Bloack-150px.png b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Blues-Io-Logo-Bloack-150px.png new file mode 100644 index 0000000..f552645 Binary files /dev/null and b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Blues-Io-Logo-Bloack-150px.png differ diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Datacake-Create-Tabs-1.png b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Datacake-Create-Tabs-1.png new file mode 100644 index 0000000..57ce8f1 Binary files /dev/null and b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Datacake-Create-Tabs-1.png differ diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Datacake-Create-Tabs-2.png b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Datacake-Create-Tabs-2.png new file mode 100644 index 0000000..140412a Binary files /dev/null and b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Datacake-Create-Tabs-2.png differ diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Datacake-Created-Fields.png b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Datacake-Created-Fields.png new file mode 100644 index 0000000..010ae5b Binary files /dev/null and b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Datacake-Created-Fields.png differ diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Datacake-Dashboard-Create.png b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Datacake-Dashboard-Create.png new file mode 100644 index 0000000..56dc4b8 Binary files /dev/null and b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Datacake-Dashboard-Create.png differ diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Datacake-Payload-Decoder.png b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Datacake-Payload-Decoder.png new file mode 100644 index 0000000..52299a6 Binary files /dev/null and b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Datacake-Payload-Decoder.png differ diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Datacake-Sensor-Tabs-1.png b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Datacake-Sensor-Tabs-1.png new file mode 100644 index 0000000..cb99385 Binary files /dev/null and b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Datacake-Sensor-Tabs-1.png differ diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Datacake-Sensor-Tabs-2.png b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Datacake-Sensor-Tabs-2.png new file mode 100644 index 0000000..ffbaa7e Binary files /dev/null and b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Datacake-Sensor-Tabs-2.png differ diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Datacake-Sensor-Tabs-3.png b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Datacake-Sensor-Tabs-3.png new file mode 100644 index 0000000..c85278e Binary files /dev/null and b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Datacake-Sensor-Tabs-3.png differ diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Datacake-Suggested-Fields.png b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Datacake-Suggested-Fields.png new file mode 100644 index 0000000..72e696f Binary files /dev/null and b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Datacake-Suggested-Fields.png differ diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Notehub-Event-Log.png b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Notehub-Event-Log.png new file mode 100644 index 0000000..06b569b Binary files /dev/null and b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Notehub-Event-Log.png differ diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Notehub-Routes-Event.png b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Notehub-Routes-Event.png new file mode 100644 index 0000000..748c5fc Binary files /dev/null and b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Notehub-Routes-Event.png differ diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Notehub-Routes-Log.png b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Notehub-Routes-Log.png new file mode 100644 index 0000000..3c5647c Binary files /dev/null and b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Notehub-Routes-Log.png differ diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Notehub-Routes-Setup.png b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Notehub-Routes-Setup.png new file mode 100644 index 0000000..0b98ca3 Binary files /dev/null and b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/Notehub-Routes-Setup.png differ diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/RAK-Whirls.png b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/RAK-Whirls.png new file mode 100644 index 0000000..e0def36 Binary files /dev/null and b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/RAK-Whirls.png differ diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/hardware.jpg b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/hardware.jpg new file mode 100644 index 0000000..5afe5fe Binary files /dev/null and b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/hardware.jpg differ diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/log_gateway.png b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/log_gateway.png new file mode 100644 index 0000000..a8d81e6 Binary files /dev/null and b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/log_gateway.png differ diff --git a/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/rakstar.jpg b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/rakstar.jpg new file mode 100644 index 0000000..f4ebb59 Binary files /dev/null and b/examples/RAK4630/solutions/Blues-Hummingbird-Gateway/assets/rakstar.jpg differ diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/.rak11200.test.skip b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/.rak11200.test.skip new file mode 100644 index 0000000..e69de29 diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/.rak11300.test.skip b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/.rak11300.test.skip new file mode 100644 index 0000000..e69de29 diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/Arduino/Blues-WisBlock-Tracker/.rak11200.test.skip b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/Arduino/Blues-WisBlock-Tracker/.rak11200.test.skip new file mode 100644 index 0000000..e69de29 diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/Arduino/Blues-WisBlock-Tracker/.rak11300.test.skip b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/Arduino/Blues-WisBlock-Tracker/.rak11300.test.skip new file mode 100644 index 0000000..e69de29 diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/Arduino/Blues-WisBlock-Tracker/Blues-WisBlock-Tracker.ino b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/Arduino/Blues-WisBlock-Tracker/Blues-WisBlock-Tracker.ino new file mode 100644 index 0000000..9399400 --- /dev/null +++ b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/Arduino/Blues-WisBlock-Tracker/Blues-WisBlock-Tracker.ino @@ -0,0 +1,380 @@ +/** + * @file Blues-WisBlock-Tracker.ino + * @author Bernd Giesecke (bernd@giesecke.tk) + * @brief App event handlers + * @version 0.1 + * @date 2023-04-25 + * + * @copyright Copyright (c) 2023 + * + */ + +#include "main.h" + +/** LoRaWAN packet */ +WisCayenne g_solution_data(255); + +/** Received package for parsing */ +uint8_t rcvd_data[256]; +/** Length of received package */ +uint16_t rcvd_data_len = 0; + +/** Send Fail counter **/ +uint8_t send_fail = 0; + +/** Set the device name, max length is 10 characters */ +char g_ble_dev_name[10] = "RAK"; + +/** Flag for RAK1906 sensor */ +bool has_rak1906 = false; + +/** Flag is Blues Notecard was found */ +bool has_blues = false; + +SoftwareTimer delayed_sending; +void delayed_cellular(TimerHandle_t unused); + +/** + * @brief Initial setup of the application (before LoRaWAN and BLE setup) + * + */ +void setup_app(void) +{ + Serial.begin(115200); + time_t serial_timeout = millis(); + // On nRF52840 the USB serial is not available immediately + while (!Serial) + { + if ((millis() - serial_timeout) < 5000) + { + delay(100); + digitalWrite(LED_GREEN, !digitalRead(LED_GREEN)); + } + else + { + break; + } + } + digitalWrite(LED_GREEN, LOW); + + // Set firmware version + api_set_version(SW_VERSION_1, SW_VERSION_2, SW_VERSION_3); + g_enable_ble = true; +} + +/** + * @brief Final setup of application (after LoRaWAN and BLE setup) + * + * @return true + * @return false + */ +bool init_app(void) +{ + Serial.printf("init_app\n"); + + Serial.printf("++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n"); + Serial.printf("WisBlock Blues Tracker\n"); + Serial.printf("++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n"); + + // Initialize User AT commands + init_user_at(); + + // Check if RAK1906 is available + has_rak1906 = init_rak1906(); + if (has_rak1906) + { + Serial.printf("+EVT:RAK1906\n"); + } + + // Initialize Blues Notecard + has_blues = init_blues(); + if (!has_blues) + { + Serial.printf("+EVT:CELLULAR_ERROR\n"); + } + + pinMode(WB_IO2, OUTPUT); + digitalWrite(WB_IO2, LOW); + + restart_advertising(30); + + delayed_sending.begin(15000, delayed_cellular, NULL, false); + + // Start the send interval timer and send a first message + if (!g_lorawan_settings.auto_join) + { + Serial.printf("Initialize LoRaWAN stack, but do not join\n"); + if (g_lorawan_settings.lorawan_enable) + { + Serial.printf("Auto join is enabled, start LoRaWAN and join\n"); + init_lorawan(); + } + api_timer_start(); + api_wake_loop(STATUS); + } + return true; +} + +/** + * @brief Handle events + * Events can be + * - timer (setup with AT+SENDINT=xxx) + * - interrupt events + * - wake-up signals from other tasks + */ +void app_event_handler(void) +{ + // Timer triggered event + if ((g_task_event_type & STATUS) == STATUS) + { + g_task_event_type &= N_STATUS; + + Serial.printf("Timer wakeup\n"); + + // Reset the packet + g_solution_data.reset(); + + if (!blues_get_location()) + { + Serial.printf("Failed to get location\n"); + } + + // Get battery level + float batt_level_f = read_batt(); + g_solution_data.addVoltage(LPP_CHANNEL_BATT, batt_level_f / 1000.0); + + // Read sensors and battery + if (has_rak1906) + { + read_rak1906(); + } + + bool check_rejoin = false; + + if (g_lpwan_has_joined) + { + /*************************************************************************************/ + /* */ + /* If the device is setup for LoRaWAN, try first to send the data as confirmed */ + /* packet. If the sending fails, retry over cellular modem */ + /* */ + /* If the device is setup for LoRa P2P, send always as P2P packet AND over the */ + /* cellular modem */ + /* */ + /*************************************************************************************/ + if (g_lorawan_settings.lorawan_enable) + { + lmh_error_status result = send_lora_packet(g_solution_data.getBuffer(), g_solution_data.getSize()); + switch (result) + { + case LMH_SUCCESS: + Serial.printf("Packet enqueued\n"); + break; + case LMH_BUSY: + re_init_lorawan(); + result = send_lora_packet(g_solution_data.getBuffer(), g_solution_data.getSize()); + if (result != LMH_SUCCESS) + { + // Send over cellular connection + delayed_sending.start(); + check_rejoin = true; + send_fail++; + Serial.printf("LoRa transceiver is busy\n"); + Serial.printf("+EVT:BUSY\n"); + } + break; + case LMH_ERROR: + re_init_lorawan(); + result = send_lora_packet(g_solution_data.getBuffer(), g_solution_data.getSize()); + if (result != LMH_SUCCESS) + { + // Send over cellular connection + delayed_sending.start(); + check_rejoin = true; + send_fail++; + Serial.printf("+EVT:SIZE_ERROR\n"); + Serial.printf("Packet error, too big to send with current DR\n"); + } + break; + } + } + else + { + // Add unique identifier in front of the P2P packet, here we use the DevEUI + g_solution_data.addDevID(LPP_CHANNEL_DEVID, &g_lorawan_settings.node_device_eui[4]); + + // Send packet over LoRa + // if (send_p2p_packet(packet_buffer, g_solution_data.getSize() + 8)) + if (send_p2p_packet(g_solution_data.getBuffer(), g_solution_data.getSize())) + { + Serial.printf("Packet enqueued\n"); + } + else + { + Serial.printf("+EVT:SIZE_ERROR\n"); + Serial.printf("Packet too big\n"); + } + + // Send as well over cellular connection + delayed_sending.start(); + } + } + else + { + // delayed_sending.start(); + g_task_event_type |= USE_CELLULAR; + if (g_lorawan_settings.lorawan_enable) + { + check_rejoin = true; + send_fail++; + } + Serial.printf("Network not joined, skip sending over LoRaWAN\n"); + } + + if (check_rejoin) + { + // Check how many times we send over LoRaWAN failed and retry to join LNS after 10 times failing + if (send_fail >= 10) + { + // Too many failed sendings, try to rejoin + Serial.printf("Retry to join LNS\n"); + send_fail = 0; + // int8_t init_result = re_init_lorawan(); + lmh_join(); + } + } + } + + // Send over Blues event + if ((g_task_event_type & USE_CELLULAR) == USE_CELLULAR) + { + g_task_event_type &= N_USE_CELLULAR; + // Send over cellular connection + Serial.printf("Get hub sync status:\n"); + blues_hub_status(); + + g_solution_data.addDevID(0, &g_lorawan_settings.node_device_eui[4]); + blues_send_payload(g_solution_data.getBuffer(), g_solution_data.getSize()); + + // Request sync with NoteHub + blues_start_req("hub.sync"); + blues_send_req(); + } + + // Blues ATTN event + if ((g_task_event_type & BLUES_ATTN) == BLUES_ATTN) + { + g_task_event_type &= N_BLUES_ATTN; + // Send over cellular connection + Serial.printf("Blues ATTN event\n"); + + blues_start_req("card.attn"); + blues_send_req(); + + blues_start_req("card.time"); + blues_send_req(); + + // req = notecard.newRequest("card.attn"); + // if (!blues_send_req()) + // { + // Serial.printf("card.attn request failed\n"); + // return false; + // } + + blues_enable_attn(); + } +} + +/** + * @brief Handle BLE events + * + */ +void ble_data_handler(void) +{ + if (g_enable_ble) + { + if ((g_task_event_type & BLE_DATA) == BLE_DATA) + { + Serial.printf("RECEIVED BLE\n"); + // BLE UART data arrived + g_task_event_type &= N_BLE_DATA; + + while (g_ble_uart.available() > 0) + { + at_serial_input(uint8_t(g_ble_uart.read())); + delay(5); + } + at_serial_input(uint8_t('\n')); + } + } +} + +/** + * @brief Handle LoRa events + * + */ +void lora_data_handler(void) +{ + // LoRa Join finished handling + if ((g_task_event_type & LORA_JOIN_FIN) == LORA_JOIN_FIN) + { + g_task_event_type &= N_LORA_JOIN_FIN; + if (g_join_result) + { + Serial.printf("Successfully joined network\n"); + send_fail = 0; + } + else + { + Serial.printf("Join network failed\n"); + } + } + + // LoRa data handling + if ((g_task_event_type & LORA_DATA) == LORA_DATA) + { + g_task_event_type &= N_LORA_DATA; + Serial.printf("Received package over LoRa\n"); + char log_buff[g_rx_data_len * 3] = {0}; + uint8_t log_idx = 0; + for (int idx = 0; idx < g_rx_data_len; idx++) + { + sprintf(&log_buff[log_idx], "%02X ", g_rx_lora_data[idx]); + log_idx += 3; + } + Serial.printf("%s\n", log_buff); + } + + // LoRa TX finished handling + if ((g_task_event_type & LORA_TX_FIN) == LORA_TX_FIN) + { + g_task_event_type &= N_LORA_TX_FIN; + + Serial.printf("LPWAN TX cycle %s\n", g_rx_fin_result ? "finished ACK" : "failed NAK"); + + if (!g_rx_fin_result) + { + if (g_lorawan_settings.lorawan_enable) + { + delayed_sending.start(); + } + + // Increase fail send counter + send_fail++; + } + else + { + send_fail = 0; + } + } +} + +/** + * @brief Timer callback to decouple the LoRaWAN sending and the cellular sending + * + * @param unused + */ +void delayed_cellular(TimerHandle_t unused) +{ + api_wake_loop(USE_CELLULAR); +} diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/Arduino/Blues-WisBlock-Tracker/RAK1906_env.cpp b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/Arduino/Blues-WisBlock-Tracker/RAK1906_env.cpp new file mode 100644 index 0000000..feebe6a --- /dev/null +++ b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/Arduino/Blues-WisBlock-Tracker/RAK1906_env.cpp @@ -0,0 +1,109 @@ +/** + @file RAK1906_env.cpp + @author Bernd Giesecke (bernd.giesecke@rakwireless.com) + @brief BME680 sensor functions + @version 0.1 + @date 2021-05-29 + + @copyright Copyright (c) 2021 + +*/ + +#include "main.h" +#include // Click to install library: http://librarymanager/All#Adafruit +#include // Click to install library: http://librarymanager/All#Adafruit-BME680 + +/** BME680 instance for Wire */ +Adafruit_BME680 bme(&Wire); + +/** Last temperature read */ +float _last_temp_rak1906 = 0; +/** Last humidity read */ +float _last_humid_rak1906 = 0; +/** Last pressure read */ +float _last_pressure_rak1906 = 0; + +/** + @brief Initialize the BME680 sensor + + @return true if sensor was found + @return false if sensor was not found +*/ +bool init_rak1906(void) +{ + Wire.begin(); + + if (!bme.begin(0x76)) + { + Serial.printf("Could not find a valid BME680 sensor, check wiring!\n"); + return false; + } + + // Set up oversampling and filter initialization + bme.setTemperatureOversampling(BME680_OS_8X); + bme.setHumidityOversampling(BME680_OS_2X); + bme.setPressureOversampling(BME680_OS_4X); + bme.setIIRFilterSize(BME680_FILTER_SIZE_3); + // bme.setGasHeater(320, 150); // 320*C for 150 ms + // As we do not use the BSEC library here, the gas value is useless and just consumes battery. Better to switch it off + bme.setGasHeater(0, 0); // switch off + + return true; +} + +/** + @brief Read environment data from BME680 + Data is added to Cayenne LPP payload as channels + LPP_CHANNEL_HUMID_2, LPP_CHANNEL_TEMP_2, + LPP_CHANNEL_PRESS_2 and LPP_CHANNEL_GAS_2 + + + @return true if reading was successful + @return false if reading failed +*/ +bool read_rak1906() +{ + Serial.printf("Start BME reading\n"); + bme.beginReading(); + time_t wait_start = millis(); + bool read_success = false; + while ((millis() - wait_start) < 5000) + { + if (bme.endReading()) + { + read_success = true; + break; + } + } + + if (!read_success) + { + Serial.printf("BME timeout\n"); + return false; + } + + _last_temp_rak1906 = bme.temperature; + _last_humid_rak1906 = bme.humidity; + _last_pressure_rak1906 = (float)(bme.pressure) / 100.0; + + g_solution_data.addRelativeHumidity(LPP_CHANNEL_HUMID_2, _last_humid_rak1906); + g_solution_data.addTemperature(LPP_CHANNEL_TEMP_2, _last_temp_rak1906); + g_solution_data.addBarometricPressure(LPP_CHANNEL_PRESS_2, _last_pressure_rak1906); + + Serial.printf("RH= %.2f T= %.2f P= %.3f\n", bme.humidity, bme.temperature, (float)(bme.pressure) / 100.0); + + return true; +} + +/** + @brief Returns the latest values from the sensor + or starts a new reading + + @param values array for temperature [0], humidity [1] and pressure [2] +*/ +void get_rak1906_values(float *values) +{ + values[0] = _last_temp_rak1906; + values[1] = _last_humid_rak1906; + values[2] = _last_pressure_rak1906; +} diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/Arduino/Blues-WisBlock-Tracker/RAK1906_env.h b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/Arduino/Blues-WisBlock-Tracker/RAK1906_env.h new file mode 100644 index 0000000..34fe01b --- /dev/null +++ b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/Arduino/Blues-WisBlock-Tracker/RAK1906_env.h @@ -0,0 +1,20 @@ +/** + * @file RAK1906_env.h + * @author Bernd Giesecke (bernd@giesecke.tk) + * @brief Global definitions and forward declarations + * @version 0.1 + * @date 2022-09-23 + * + * @copyright Copyright (c) 2022 + * + */ +#ifndef RAK1906_H +#define RAK1906_H +#include + +// Function declarations +bool init_rak1906(void); +bool read_rak1906(void); +void get_rak1906_values(float *values); + +#endif // RAK1906_H \ No newline at end of file diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/Arduino/Blues-WisBlock-Tracker/blues.cpp b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/Arduino/Blues-WisBlock-Tracker/blues.cpp new file mode 100644 index 0000000..295eb9f --- /dev/null +++ b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/Arduino/Blues-WisBlock-Tracker/blues.cpp @@ -0,0 +1,506 @@ +/** + * @file blues.cpp + * @author Bernd Giesecke (bernd@giesecke.tk) + * @brief Blues.IO NoteCard handler + * @version 0.1 + * @date 2023-04-27 + * + * @copyright Copyright (c) 2023 + * + */ +#include "main.h" + +#ifndef PRODUCT_UID +#define PRODUCT_UID "com.my-company.my-name:my-project" +#endif +#define myProductID PRODUCT_UID + +Notecard notecard; + +void blues_attn_cb(void); + +J *req; + +/** + * @brief Initialize Blues NoteCard + * + * @return true if NoteCard was found and setup was successful + * @return false if NoteCard was not found or the setup failed + */ +bool init_blues(void) +{ + Wire.begin(); + notecard.begin(); + + // Get the ProductUID from the saved settings + // If no settings are found, use NoteCard internal settings! + if (read_blues_settings()) + { + Serial.printf("Found saved settings, override NoteCard internal settings!\n"); + if (memcmp(g_blues_settings.product_uid, "com.my-company.my-name", 22) == 0) + { + Serial.printf("No Product ID saved\n"); + AT_PRINTF(":EVT NO PUID"); + memcpy(g_blues_settings.product_uid, PRODUCT_UID, 33); + } + + Serial.printf("Set Product ID and connection mode\n"); + if (blues_start_req("hub.set")) + { + JAddStringToObject(req, "product", g_blues_settings.product_uid); + if (g_blues_settings.conn_continous) + { + JAddStringToObject(req, "mode", "continuous"); + } + else + { + JAddStringToObject(req, "mode", "minimum"); + } + // Set sync time to 20 times the sensor read time + JAddNumberToObject(req, "seconds", (g_lorawan_settings.send_repeat_time * 20 / 1000)); + JAddBoolToObject(req, "heartbeat", true); + + if (!blues_send_req()) + { + Serial.printf("hub.set request failed\n"); + return false; + } + } + else + { + Serial.printf("hub.set request failed\n"); + return false; + } + +#if USE_GNSS == 1 + Serial.printf("Set location mode\n"); + if (blues_start_req("card.location.mode")) + { + // Continous GNSS mode + // JAddStringToObject(req, "mode", "continous"); + + // Periodic GNSS mode + JAddStringToObject(req, "mode", "periodic"); + + // Set location acquisition time to the sensor read time + JAddNumberToObject(req, "seconds", (g_lorawan_settings.send_repeat_time / 2000)); + JAddBoolToObject(req, "heartbeat", true); + if (!blues_send_req()) + { + Serial.printf("card.location.mode request failed\n"); + return false; + } + } + else + { + Serial.printf("card.location.mode request failed\n"); + return false; + } +#else + Serial.printf("Stop location mode\n"); + if (blues_start_req("card.location.mode")) + { + // GNSS mode off + JAddStringToObject(req, "mode", "off"); + if (!blues_send_req()) + { + Serial.printf("card.location.mode request failed\n"); + return false; + } + } + else + { + Serial.printf("card.location.mode request failed\n"); + return false; + } +#endif + + /// \todo reset attn signal needs rework + // pinMode(WB_IO5, INPUT); + // if (g_blues_settings.motion_trigger) + // { + // if (blues_start_req("card.attn")) + // { + // JAddStringToObject(req, "mode", "disarm"); + // if (!blues_send_req()) + // { + // Serial.printf("card.attn request failed\n"); + // } + + // if (!blues_enable_attn()) + // { + // return false; + // } + // } + // } + // else + // { + // Serial.printf("card.attn request failed\n"); + // return false; + // } + + Serial.printf("Set APN\n"); + // {“req”:”card.wireless”} + if (blues_start_req("card.wireless")) + { + JAddStringToObject(req, "mode", "auto"); + + if (g_blues_settings.use_ext_sim) + { + // USING EXTERNAL SIM CARD + JAddStringToObject(req, "apn", g_blues_settings.ext_sim_apn); + JAddStringToObject(req, "method", "dual-secondary-primary"); + } + else + { + // USING BLUES eSIM CARD + JAddStringToObject(req, "method", "primary"); + } + if (!blues_send_req()) + { + Serial.printf("card.wireless request failed\n"); + return false; + } + } + else + { + Serial.printf("card.wireless request failed\n"); + return false; + } + +#if IS_V2 == 1 + // Only for V2 cards, setup the WiFi network + Serial.printf("Set WiFi\n"); + if (blues_start_req("card.wifi")) + { + JAddStringToObject(req, "ssid", "-"); + JAddStringToObject(req, "password", "-"); + JAddStringToObject(req, "name", "RAK-"); + JAddStringToObject(req, "org", "RAK-PH"); + JAddBoolToObject(req, "start", false); + + if (!blues_send_req()) + { + Serial.printf("card.wifi request failed\n"); + } + } + else + { + Serial.printf("card.wifi request failed\n"); + return false; + } +#endif + } + + // {"req": "card.version"} + if (blues_start_req("card.version")) + { + if (!blues_send_req()) + { + Serial.printf("card.version request failed\n"); + } + } + return true; +} + +/** + * @brief Send a data packet to NoteHub.IO + * + * @param data Payload as byte array (CayenneLPP formatted) + * @param data_len Length of payload + * @return true if note could be sent to NoteCard + * @return false if note send failed + */ +bool blues_send_payload(uint8_t *data, uint16_t data_len) +{ + if (blues_start_req("note.add")) + { + JAddStringToObject(req, "file", "data.qo"); + JAddBoolToObject(req, "sync", true); + J *body = JCreateObject(); + if (body != NULL) + { + char node_id[24]; + sprintf(node_id, "%02x%02x%02x%02x%02x%02x%02x%02x", + g_lorawan_settings.node_device_eui[0], g_lorawan_settings.node_device_eui[1], + g_lorawan_settings.node_device_eui[2], g_lorawan_settings.node_device_eui[3], + g_lorawan_settings.node_device_eui[4], g_lorawan_settings.node_device_eui[5], + g_lorawan_settings.node_device_eui[6], g_lorawan_settings.node_device_eui[7]); + JAddStringToObject(body, "dev_eui", node_id); + + JAddItemToObject(req, "body", body); + + JAddBinaryToObject(req, "payload", data, data_len); + + Serial.printf("Finished parsing\n"); + if (!blues_send_req()) + { + Serial.printf("Send request failed\n"); + return false; + } + return true; + } + else + { + Serial.printf("Error creating body\n"); + } + } + return false; +} + +/** + * @brief Create a request structure to be sent to the NoteCard + * + * @param request_name name of request, e.g. card.wireless + * @return true if request could be created + * @return false if request could not be created + */ +bool blues_start_req(String request_name) +{ + req = notecard.newRequest(request_name.c_str()); + if (req != NULL) + { + return true; + } + return false; +} + +/** + * @brief Send a completed request to the NoteCard. + * + * @return true if request could be sent and the response does not have "err" + * @return false if request could not be sent or the response did have "err" + */ +bool blues_send_req(void) +{ + char *json = JPrintUnformatted(req); + Serial.printf("Card request = %s\n", json); + + J *rsp; + rsp = notecard.requestAndResponse(req); + if (rsp == NULL) + { + return false; + } + json = JPrintUnformatted(rsp); + if (JIsPresent(rsp, "err")) + { + Serial.printf("Card error response = %s\n", json); + notecard.deleteResponse(rsp); + return false; + } + Serial.printf("Card response = %s\n", json); + notecard.deleteResponse(rsp); + + return true; +} + +/** + * @brief Request NoteHub status, mainly for debug purposes + * + */ +void blues_hub_status(void) +{ + blues_start_req("hub.status"); + if (!blues_send_req()) + { + Serial.printf("hub.status request failed\n"); + } +} + +/** + * @brief Get the location information from the NoteCard + * + * @return true if a location could be acquired + * @return false if request failed or no location is available + */ +bool blues_get_location(void) +{ + bool result = false; + if (blues_start_req("card.location")) + { + J *rsp; + rsp = notecard.requestAndResponse(req); + if (rsp == NULL) + { + Serial.printf("card.location failed, report no location\n"); + return false; + } + char *json = JPrintUnformatted(rsp); + Serial.printf("Card response = %s\n", json); + + if (JHasObjectItem(rsp, "lat") && JHasObjectItem(rsp, "lat")) + { + float blues_latitude = JGetNumber(rsp, "lat"); + float blues_longitude = JGetNumber(rsp, "lon"); + float blues_altitude = 0; + + if ((blues_latitude == 0.0) && (blues_longitude == 0.0)) + { + Serial.printf("No valid GPS data, report no location\n"); + } + else + { + Serial.printf("Got location Lat %.6f Long %0.6f\n", blues_latitude, blues_longitude); + g_solution_data.addGNSS_6(LPP_CHANNEL_GPS, (uint32_t)(blues_latitude * 10000000), (uint32_t)(blues_longitude * 10000000), blues_altitude); + result = true; + } + } + + notecard.deleteResponse(rsp); + } + + if (!result) + { + // No GPS coordinates, get last tower location + if (blues_start_req("card.time")) + { + J *rsp; + rsp = notecard.requestAndResponse(req); + if (rsp == NULL) + { + Serial.printf("card.time failed, report no location\n"); + return false; + } + char *json = JPrintUnformatted(rsp); + Serial.printf("Card response = %s\n", json); + + if (JHasObjectItem(rsp, "lat") && JHasObjectItem(rsp, "lat")) + { + float blues_latitude = JGetNumber(rsp, "lat"); + float blues_longitude = JGetNumber(rsp, "lon"); + float blues_altitude = 0; + + if ((blues_latitude == 0.0) && (blues_longitude == 0.0)) + { + Serial.printf("No valid GPS data, report no location\n"); + } + else + { + Serial.printf("Got tower location Lat %.6f Long %0.6f\n", blues_latitude, blues_longitude); + g_solution_data.addGNSS_6(LPP_CHANNEL_GPS, (uint32_t)(blues_latitude * 10000000), (uint32_t)(blues_longitude * 10000000), blues_altitude); + result = true; + } + } + + notecard.deleteResponse(rsp); + } + } + + // Clear last GPS location + if (blues_start_req("card.location.mode")) + { + JAddBoolToObject(req, "delete", true); + J *rsp; + rsp = notecard.requestAndResponse(req); + if (rsp == NULL) + { + Serial.printf("card.location.mode\n"); + } + char *json = JPrintUnformatted(rsp); + Serial.printf("Card response = %s\n", json); + notecard.deleteResponse(rsp); + } + return result; +} + +/** + * @brief Enable ATTN interrupt + * At the moment enables only the alarm on motion + * + * @return true if ATTN could be enabled + * @return false if ATTN could not be enabled + */ +bool blues_enable_attn(void) +{ + Serial.printf("Enable ATTN on motion\n"); + if (blues_start_req("card.attn")) + { + JAddStringToObject(req, "mode", "motion"); + if (!blues_send_req()) + { + Serial.printf("card.attn request failed\n"); + return false; + } + } + else + { + Serial.printf("Request creation failed\n"); + } + attachInterrupt(WB_IO5, blues_attn_cb, RISING); + + Serial.printf("Arm ATTN on motion\n"); + if (blues_start_req("card.attn")) + { + JAddStringToObject(req, "mode", "arm"); + if (!blues_send_req()) + { + Serial.printf("card.attn request failed\n"); + return false; + } + } + else + { + Serial.printf("Request creation failed\n"); + } + return true; +} + +/** + * @brief Disable ATTN interrupt + * + * @return true if ATTN could be disabled + * @return false if ATTN could not be disabled + */ +bool blues_disable_attn(void) +{ + Serial.printf("Disable ATTN on motion\n"); + detachInterrupt(WB_IO5); + + if (blues_start_req("card.attn")) + { + JAddStringToObject(req, "mode", "disarm"); + if (!blues_send_req()) + { + Serial.printf("card.attn request failed\n"); + } + } + else + { + Serial.printf("Request creation failed\n"); + } + if (blues_start_req("card.attn")) + { + JAddStringToObject(req, "mode", "-motion"); + if (!blues_send_req()) + { + Serial.printf("card.attn request failed\n"); + } + } + else + { + Serial.printf("Request creation failed\n"); + } + + return true; +} + +/** + * @brief Get the reason for the ATTN interrup + * /// \todo work in progress + * @return String reason /// \todo return value not final yet + */ +String blues_attn_reason(void) +{ + return ""; +} + +/** + * @brief Callback for ATTN interrupt + * Wakes up the app_handler with an BLUES_ATTN event + * + */ +void blues_attn_cb(void) +{ + api_wake_loop(BLUES_ATTN); +} diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/Arduino/Blues-WisBlock-Tracker/main.h b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/Arduino/Blues-WisBlock-Tracker/main.h new file mode 100644 index 0000000..1ee5b8d --- /dev/null +++ b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/Arduino/Blues-WisBlock-Tracker/main.h @@ -0,0 +1,94 @@ +/** + @file main.h + @author Bernd Giesecke (bernd@giesecke.tk) + @brief Includes, defines and globals + @version 0.1 + @date 2023-04-25 + + @copyright Copyright (c) 2023 + +*/ + +#ifndef _MAIN_H_ +#define _MAIN_H_ + +// Defines for compile settings +// major version increase on API change / not backwards compatible +#define SW_VERSION_1 1 +// minor version increase on API change / backward compatible +#define SW_VERSION_2 0 +// patch version increase on bugfix, no affect on API +#define SW_VERSION_3 0 +// 0 = Notecard V1 version, 1 = Notecard V2 version +#define IS_V2 1 +// 0 No GNSS location, 1 = activate GNSS location +#define USE_GNSS 1 + +#include +#include // Click to install library: http://librarymanager/All#WisBlock-API-V2 +#include // Click to install library: http://librarymanager/All#Blues-Wireless-Notecard +#include "RAK1906_env.h" + +/** Define the version of your SW */ +#ifndef SW_VERSION_1 +#define SW_VERSION_1 1 // major version increase on API change / not backwards compatible +#endif +#ifndef SW_VERSION_2 +#define SW_VERSION_2 0 // minor version increase on API change / backward compatible +#endif +#ifndef SW_VERSION_3 +#define SW_VERSION_3 0 // patch version increase on bugfix, no affect on API +#endif + +/** Application function definitions */ +void setup_app(void); +bool init_app(void); +void app_event_handler(void); +void ble_data_handler(void) __attribute__((weak)); +void lora_data_handler(void); + +// Wakeup flags +#define USE_CELLULAR 0b1000000000000000 +#define N_USE_CELLULAR 0b0111111111111111 +#define BLUES_ATTN 0b0100000000000000 +#define N_BLUES_ATTN 0b1011111111111111 + +// Cayenne LPP Channel numbers per sensor value +#define LPP_CHANNEL_BATT 1 // Base Board +#define LPP_CHANNEL_HUMID_2 6 // RAK1906 +#define LPP_CHANNEL_TEMP_2 7 // RAK1906 +#define LPP_CHANNEL_PRESS_2 8 // RAK1906 +#define LPP_CHANNEL_GAS_2 9 // RAK1906 +#define LPP_CHANNEL_GPS 10 // RAK1910/RAK12500 + +// Globals +extern WisCayenne g_solution_data; + +// Blues.io +struct s_blues_settings +{ + uint16_t valid_mark = 0xAA55; // Validity marker + char product_uid[256] = "com.my-company.my-name:my-project"; // Blues Product UID + bool conn_continous = false; // Use periodic connection + bool use_ext_sim = false; // Use external SIM + char ext_sim_apn[256] = "internet"; // APN to be used with external SIM + bool motion_trigger = true; // Send data on motion trigger +}; + +bool init_blues(void); +bool blues_start_req(String request_name); +bool blues_send_req(void); +void blues_hub_status(void); +bool blues_get_location(void); +bool blues_enable_attn(void); +bool blues_disable_attn(void); +bool blues_send_payload(uint8_t *data, uint16_t data_len); +extern J *req; +extern s_blues_settings g_blues_settings; + +// User AT commands +void init_user_at(void); +bool read_blues_settings(void); +void save_blues_settings(void); + +#endif // _MAIN_H_ diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/Arduino/Blues-WisBlock-Tracker/user_at_cmd.cpp b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/Arduino/Blues-WisBlock-Tracker/user_at_cmd.cpp new file mode 100644 index 0000000..c73382a --- /dev/null +++ b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/Arduino/Blues-WisBlock-Tracker/user_at_cmd.cpp @@ -0,0 +1,387 @@ +/** + * @file user_at_cmd.cpp + * @author Bernd Giesecke (bernd@giesecke.tk) + * @brief User AT commands + * @version 0.1 + * @date 2023-08-18 + * + * @copyright Copyright (c) 2023 + * + */ +#include "main.h" + +#include +#include +using namespace Adafruit_LittleFS_Namespace; + +/** Filename to save Blues settings */ +static const char blues_file_name[] = "BLUES"; + +/** File to save battery check status */ +File this_file(InternalFS); + +/** Structure for saved Blues Notecard settings */ +s_blues_settings g_blues_settings; + +/** + * @brief Set Blues Product UID + * + * @param str Product UID as Hex String + * @return int AT_SUCCESS if ok, AT_ERRNO_PARA_FAIL if invalid value + */ +int at_set_blues_prod_uid(char *str) +{ + if (strlen(str) < 25) + { + return AT_ERRNO_PARA_NUM; + } + + for (int i = 0; str[i] != '\0'; i++) + { + if (str[i] >= 'A' && str[i] <= 'Z') // checking for uppercase characters + str[i] = str[i] + 32; // converting uppercase to lowercase + } + + char new_uid[256] = {0}; + snprintf(new_uid, 255, str); + + Serial.printf("Received new Blues Product UID %s\n", new_uid); + + bool need_save = strcmp(new_uid, g_blues_settings.product_uid) == 0 ? false : true; + + if (need_save) + { + snprintf(g_blues_settings.product_uid, 256, new_uid); + } + + // Save new master node address if changed + if (need_save) + { + save_blues_settings(); + } + return AT_SUCCESS; +} + +/** + * @brief Get Blues Product UID + * + * @return int AT_SUCCESS + */ +int at_query_blues_prod_uid(void) +{ + snprintf(g_at_query_buf, ATQUERY_SIZE, "%s", g_blues_settings.product_uid); + return AT_SUCCESS; +} + +/** + * @brief Set usage of eSIM or external SIM and APN + * + * @param str params as string, format 0 or 1:APN_NAME + * @return int + * AT_SUCCESS is params are set correct + * AT_ERRNO_PARA_NUM if params error + */ +int at_set_blues_ext_sim(char *str) +{ + char *param; + bool new_use_ext_sim; + char new_ext_sim_apn[256]; + + // Get string up to first : + param = strtok(str, ":"); + if (param != NULL) + { + if (param[0] == '0') + { + Serial.printf("Enable eSIM\n"); + new_use_ext_sim = false; + } + else if (param[0] == '1') + { + Serial.printf("Enable external SIM\n"); + new_use_ext_sim = true; + param = strtok(NULL, ":"); + if (param != NULL) + { + for (int i = 0; param[i] != '\0'; i++) + { + if (param[i] >= 'A' && param[i] <= 'Z') // checking for uppercase characters + param[i] = param[i] + 32; // converting uppercase to lowercase + } + snprintf(new_ext_sim_apn, 256, "%s", param); + } + else + { + Serial.printf("Missing external SIM APN\n"); + return AT_ERRNO_PARA_NUM; + } + } + else + { + Serial.printf("Invalid SIM flag %d\n", param[0]); + return AT_ERRNO_PARA_NUM; + } + } + + bool need_save = false; + if (new_use_ext_sim != g_blues_settings.use_ext_sim) + { + g_blues_settings.use_ext_sim = new_use_ext_sim; + need_save = true; + } + if (strcmp(new_ext_sim_apn, g_blues_settings.product_uid) != 0) + { + snprintf(g_blues_settings.ext_sim_apn, 256, new_ext_sim_apn); + need_save = true; + } + + if (need_save) + { + save_blues_settings(); + } + return AT_SUCCESS; +} + +/** + * @brief Get Blues SIM settings + * + * @return int AT_SUCCESS + */ +int at_query_blues_ext_sim(void) +{ + if (g_blues_settings.use_ext_sim) + { + snprintf(g_at_query_buf, ATQUERY_SIZE, "1:%s", g_blues_settings.ext_sim_apn); + Serial.printf("Using external SIM with APN = %s\n", g_blues_settings.ext_sim_apn); + } + else + { + snprintf(g_at_query_buf, ATQUERY_SIZE, "0"); + Serial.printf("Using eSIM\n"); + } + return AT_SUCCESS; +} + +/** + * @brief Set Blues NoteCard mode + * /// \todo work in progress + * + * @param str params as string, format 0 or 1 + * @return int + * AT_SUCCESS is params are set correct + * AT_ERRNO_PARA_NUM if params error + */ +int at_set_blues_mode(char *str) +{ + bool new_connection_mode; + + if (str[0] == '0') + { + Serial.printf("Set minimum connection mode\n"); + new_connection_mode = false; + blues_disable_attn(); + } + else if (str[0] == '1') + { + Serial.printf("Set continuous connection mode\n"); + new_connection_mode = true; + blues_enable_attn(); + } + else + { + Serial.printf("Invalid motion trigger flag %d\n", str[0]); + return AT_ERRNO_PARA_NUM; + } + + bool need_save = false; + if (new_connection_mode != g_blues_settings.conn_continous) + { + g_blues_settings.conn_continous = new_connection_mode; + need_save = true; + } + + if (need_save) + { + save_blues_settings(); + } + return AT_SUCCESS; +} + +/** + * @brief Get Blues mode settings + * + * @return int AT_SUCCESS + */ +int at_query_blues_mode(void) +{ + snprintf(g_at_query_buf, ATQUERY_SIZE, "%s", g_blues_settings.conn_continous ? "1" : "0"); + Serial.printf("Using %s connection\n", g_blues_settings.conn_continous ? "continous" : "periodic"); + return AT_SUCCESS; +} + +/** + * @brief Enable/disable the motion trigger + * + * @param str params as string, format 0 or 1 + * @return int + * AT_SUCCESS is params are set correct + * AT_ERRNO_PARA_NUM if params error + */ +int at_set_blues_trigger(char *str) +{ + bool new_motion_trigger; + + if (str[0] == '0') + { + Serial.printf("Disable motion trigger\n"); + new_motion_trigger = false; + blues_disable_attn(); + } + else if (str[0] == '1') + { + Serial.printf("Enable motion trigger\n"); + new_motion_trigger = true; + blues_enable_attn(); + } + else + { + Serial.printf("Invalid motion trigger flag %d\n", str[0]); + return AT_ERRNO_PARA_NUM; + } + + bool need_save = false; + if (new_motion_trigger != g_blues_settings.motion_trigger) + { + g_blues_settings.motion_trigger = new_motion_trigger; + need_save = true; + } + + if (need_save) + { + save_blues_settings(); + } + return AT_SUCCESS; +} + +/** + * @brief Get Blues motion trigger settings + * + * @return int AT_SUCCESS + */ +int at_query_blues_trigger(void) +{ + snprintf(g_at_query_buf, ATQUERY_SIZE, "%s", g_blues_settings.motion_trigger ? "1" : "0"); + Serial.printf("Motion trigger is %s\n", g_blues_settings.motion_trigger ? "enabled" : "disabled"); + return AT_SUCCESS; +} + +static int at_reset_blues_settings(void) +{ + if (InternalFS.exists(blues_file_name)) + { + InternalFS.remove(blues_file_name); + } + return AT_SUCCESS; +} + +/** + * @brief Read saved Blues Product ID + * + */ +bool read_blues_settings(void) +{ + bool structure_valid = false; + if (InternalFS.exists(blues_file_name)) + { + this_file.open(blues_file_name, FILE_O_READ); + this_file.read((void *)&g_blues_settings.valid_mark, sizeof(s_blues_settings)); + this_file.close(); + + // Check for valid data + if (g_blues_settings.valid_mark == 0xAA55) + { + structure_valid = true; + Serial.printf("Valid Blues settings found, Blues Product UID = %s\n", g_blues_settings.product_uid); + if (g_blues_settings.use_ext_sim) + { + Serial.printf("Using external SIM with APN = %s\n", g_blues_settings.ext_sim_apn); + } + else + { + Serial.printf("Using eSIM\n"); + } + } + else + { + Serial.printf("No valid Blues settings found\n"); + } + } + + if (!structure_valid) + { + return false; + + // No settings file found optional to set defaults (ommitted!) + // g_blues_settings.valid_mark = 0xAA55; // Validity marker + // sprintf(g_blues_settings.product_uid, "com.my-company.my-name:my-project"); // Blues Product UID + // g_blues_settings.conn_continous = false; // Use periodic connection + // g_blues_settings.use_ext_sim = false; // Use external SIM + // sprintf(g_blues_settings.ext_sim_apn, "-"); // APN to be used with external SIM + // g_blues_settings.motion_trigger = true; // Send data on motion trigger + // save_blues_settings(); + } + + return true; +} + +/** + * @brief Save the Blues Product ID + * + */ +void save_blues_settings(void) +{ + if (InternalFS.exists(blues_file_name)) + { + InternalFS.remove(blues_file_name); + } + + g_blues_settings.valid_mark = 0xAA55; + this_file.open(blues_file_name, FILE_O_WRITE); + this_file.write((const char *)&g_blues_settings.valid_mark, sizeof(s_blues_settings)); + this_file.close(); + Serial.printf("Saved Blues Settings\n"); +} + +/** + * @brief List of all available commands with short help and pointer to functions + * + */ +atcmd_t g_user_at_cmd_new_list[] = { + /*| CMD | AT+CMD? | AT+CMD=? | AT+CMD=value | AT+CMD | Permissions |*/ + // Module commands + {"+BUID", "Set/get the Blues product UID", at_query_blues_prod_uid, at_set_blues_prod_uid, NULL, "RW"}, + {"+BSIM", "Set/get Blues SIM settings", at_query_blues_ext_sim, at_set_blues_ext_sim, NULL, "RW"}, + {"+BMOD", "Set/get Blues NoteCard connection modes", at_query_blues_mode, at_set_blues_mode, NULL, "RW"}, + {"+BTRIG", "Set/get Blues send trigger", at_query_blues_trigger, at_set_blues_trigger, NULL, "RW"}, + {"+BR", "Remove all Blues Settings", NULL, NULL, at_reset_blues_settings, "RW"}, +}; + +/** Number of user defined AT commands */ +uint8_t g_user_at_cmd_num = 0; + +/** Pointer to the combined user AT command structure */ +atcmd_t *g_user_at_cmd_list; + +/** + * @brief Initialize the user defined AT command list + * + */ +void init_user_at(void) +{ + // Assign custom AT command list to pointer used by WisBlock API + g_user_at_cmd_list = g_user_at_cmd_new_list; + + // Add AT commands to structure + g_user_at_cmd_num += sizeof(g_user_at_cmd_new_list) / sizeof(atcmd_t); + Serial.printf("Added %d User AT commands\n", g_user_at_cmd_num); +} diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/.gitignore b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/.gitignore new file mode 100644 index 0000000..993d834 --- /dev/null +++ b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/.gitignore @@ -0,0 +1,9 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch +Netlify.txt +src/product_uid.h +Debug-Build +Build diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/.pio/libdeps/rak4631-debug/Adafruit Unified Sensor/.gitignore b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/.pio/libdeps/rak4631-debug/Adafruit Unified Sensor/.gitignore new file mode 100644 index 0000000..d04184a --- /dev/null +++ b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/.pio/libdeps/rak4631-debug/Adafruit Unified Sensor/.gitignore @@ -0,0 +1,108 @@ +# +# NOTE! Don't add files that are generated in specific +# subdirectories here. Add them in the ".gitignore" file +# in that subdirectory instead. +# +# NOTE! Please use 'git ls-files -i --exclude-standard' +# command after changing this file, to see if there are +# any tracked files which get ignored after the change. +# +# Normal rules +# +.* +*.o +*.o.* +*.a +*.s +*.ko +*.so +*.so.dbg +*.mod.c +*.i +*.lst +*.symtypes +*.order +modules.builtin +*.elf +*.bin +*.gz +*.bz2 +*.lzma +*.patch +*.gcno + +# +# Top-level generic files +# +/tags +/TAGS +/linux +/vmlinux +/vmlinuz +/System.map +/Module.markers +/Module.symvers + +# +# git files that we don't want to ignore even it they are dot-files +# +!.gitignore +!.mailmap + +# +# Generated include files +# +include/config +include/linux/version.h +include/generated + +# stgit generated dirs +patches-* + +# quilt's files +patches +series + +# cscope files +cscope.* +ncscope.* + +# gnu global files +GPATH +GRTAGS +GSYMS +GTAGS + +# QT-Creator files +Makefile.am.user +*.config +*.creator +*.creator.user +*.files +*.includes + +*.orig +*~ +\#*# +*.lo +*.la +Makefile +Makefile.in +aclocal.m4 +autoconfig.h +autoconfig.h.in +autom4te.cache/ +build-aux/ +config.log +config.status +configure +libtool +libupnp.pc +m4/libtool.m4 +m4/ltoptions.m4 +m4/ltsugar.m4 +m4/ltversion.m4 +m4/lt~obsolete.m4 +stamp-h1 +docs/doxygen + diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/.vscode/extensions.json b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/Decoder.js b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/Decoder.js new file mode 100644 index 0000000..3234e90 --- /dev/null +++ b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/Decoder.js @@ -0,0 +1,238 @@ +/** + * @reference https://github.com/myDevicesIoT/cayenne-docs/blob/master/docs/LORA.md + * @reference http://openmobilealliance.org/wp/OMNA/LwM2M/LwM2MRegistry.html#extlabel + * + * Adapted for lora-app-server from https://gist.github.com/iPAS/e24970a91463a4a8177f9806d1ef14b8 + * + * Type IPSO LPP Hex Data Size Data Resolution per bit + * Digital Input 3200 0 0 1 1 + * Digital Output 3201 1 1 1 1 + * Analog Input 3202 2 2 2 0.01 Signed + * Analog Output 3203 3 3 2 0.01 Signed + * Illuminance Sensor 3301 101 65 2 1 Lux Unsigned MSB + * Presence Sensor 3302 102 66 1 1 + * Temperature Sensor 3303 103 67 2 0.1 °C Signed MSB + * Humidity Sensor 3304 104 68 1 0.5 % Unsigned + * Accelerometer 3313 113 71 6 0.001 G Signed MSB per axis + * Barometer 3315 115 73 2 0.1 hPa Unsigned MSB + * Time 3333 133 85 4 Unix time MSB + * Gyrometer 3334 134 86 6 0.01 °/s Signed MSB per axis + * GPS Location 3336 136 88 9 Latitude : 0.0001 ° Signed MSB + * Longitude : 0.0001 ° Signed MSB + * Altitude : 0.01 meter Signed MSB + * + * Additional types + * Generic Sensor 3300 100 64 4 Unsigned integer MSB + * Voltage 3316 116 74 2 0.01 V Unsigned MSB + * Current 3317 117 75 2 0.001 A Unsigned MSB + * Frequency 3318 118 76 4 1 Hz Unsigned MSB + * Percentage 3320 120 78 1 1% Unsigned + * Altitude 3321 121 79 2 1m Signed MSB + * Concentration 3325 125 7D 2 1 PPM unsigned : 1pmm = 1 * 10 ^-6 = 0.000 001 + * Power 3328 128 80 2 1 W Unsigned MSB + * Distance 3330 130 82 4 0.001m Unsigned MSB + * Energy 3331 131 83 4 0.001kWh Unsigned MSB + * Colour 3335 135 87 3 R: 255 G: 255 B: 255 + * Direction 3332 132 84 2 1º Unsigned MSB + * Switch 3342 142 8E 1 0/1 + * + * RAKwireless specific types + * GPS Location 3337 137 89 11 Higher precision location information + * Latitude : 0.000001 ° Signed MSB + * Longitude : 0.000001 ° Signed MSB + * Altitude : 0.01 meter Signed MSB + * VOC index 3338 138 8A 1 VOC index + * Wind Speed 3390 190 BE 2 Wind speed 0.01 m/s + * Wind Direction 3391 191 BF 2 Wind direction 1º Unsigned MSB + * Light Level 3403 203 CB 1 0 0-5 lux, 1 6-50 lux, 2 51-100 lux, 3 101-500 lux, 4 501-2000 lux, 6 >2000 lux + * Soil Moisture 3388 188 BC 2 0.1 % in 0~100% (m3/m3) + * Soil EC 3392 192 C0 2 0.001, mS/cm + * Soil pH high prec. 3393 193 C1 2 0.01 pH + * Soil pH low prec. 3394 194 C2 2 0.1 pH + * Pyranometer 3395 195 C3 2 1 unsigned MSB (W/m2) + * Precise Humidity 3312 112 70 2 0.1 %RH + * Device ID 3555 255 FF 4 Number + * + */ + +// lppDecode decodes an array of bytes into an array of ojects, +// each one with the channel, the data type and the value. +function lppDecode(bytes) { + + var sensor_types = { + 0: { 'size': 1, 'name': 'digital_in', 'signed': false, 'divisor': 1 }, + 1: { 'size': 1, 'name': 'digital_out', 'signed': false, 'divisor': 1 }, + 2: { 'size': 2, 'name': 'analog_in', 'signed': true, 'divisor': 100 }, + 3: { 'size': 2, 'name': 'analog_out', 'signed': true, 'divisor': 100 }, + 100: { 'size': 4, 'name': 'generic', 'signed': false, 'divisor': 1 }, + 101: { 'size': 2, 'name': 'illuminance', 'signed': false, 'divisor': 1 }, + 102: { 'size': 1, 'name': 'presence', 'signed': false, 'divisor': 1 }, + 103: { 'size': 2, 'name': 'temperature', 'signed': true, 'divisor': 10 }, + 104: { 'size': 1, 'name': 'humidity', 'signed': false, 'divisor': 2 }, + 112: { 'size': 2, 'name': 'humidity_prec', 'signed': true, 'divisor': 10 }, + 113: { 'size': 6, 'name': 'accelerometer', 'signed': true, 'divisor': 1000 }, + 115: { 'size': 2, 'name': 'barometer', 'signed': false, 'divisor': 10 }, + 116: { 'size': 2, 'name': 'voltage', 'signed': false, 'divisor': 100 }, + 117: { 'size': 2, 'name': 'current', 'signed': false, 'divisor': 1000 }, + 118: { 'size': 4, 'name': 'frequency', 'signed': false, 'divisor': 1 }, + 120: { 'size': 1, 'name': 'percentage', 'signed': false, 'divisor': 1 }, + 121: { 'size': 2, 'name': 'altitude', 'signed': true, 'divisor': 1 }, + 125: { 'size': 2, 'name': 'concentration', 'signed': false, 'divisor': 1 }, + 128: { 'size': 2, 'name': 'power', 'signed': false, 'divisor': 1 }, + 130: { 'size': 4, 'name': 'distance', 'signed': false, 'divisor': 1000 }, + 131: { 'size': 4, 'name': 'energy', 'signed': false, 'divisor': 1000 }, + 132: { 'size': 2, 'name': 'direction', 'signed': false, 'divisor': 1 }, + 133: { 'size': 4, 'name': 'time', 'signed': false, 'divisor': 1 }, + 134: { 'size': 6, 'name': 'gyrometer', 'signed': true, 'divisor': 100 }, + 135: { 'size': 3, 'name': 'colour', 'signed': false, 'divisor': 1 }, + 136: { 'size': 9, 'name': 'gps', 'signed': true, 'divisor': [10000, 10000, 100] }, + 137: { 'size': 11, 'name': 'gps', 'signed': true, 'divisor': [1000000, 1000000, 100] }, + 138: { 'size': 2, 'name': 'voc', 'signed': false, 'divisor': 1 }, + 142: { 'size': 1, 'name': 'switch', 'signed': false, 'divisor': 1 }, + 188: { 'size': 2, 'name': 'soil_moist', 'signed': false, 'divisor': 10 }, + 190: { 'size': 2, 'name': 'wind_speed', 'signed': false, 'divisor': 100 }, + 191: { 'size': 2, 'name': 'wind_direction', 'signed': false, 'divisor': 1 }, + 192: { 'size': 2, 'name': 'soil_ec', 'signed': false, 'divisor': 1000 }, + 193: { 'size': 2, 'name': 'soil_ph_h', 'signed': false, 'divisor': 100 }, + 194: { 'size': 2, 'name': 'soil_ph_l', 'signed': false, 'divisor': 10 }, + 195: { 'size': 2, 'name': 'pyranometer', 'signed': false, 'divisor': 1 }, + 203: { 'size': 1, 'name': 'light', 'signed': false, 'divisor': 1 }, + 255: { 'size': 4, 'name': 'dev_id', 'unsigned': false, 'divisor': 1 }, + }; + + function arrayToDecimal(stream, is_signed, divisor) { + + var value = 0; + for (var i = 0; i < stream.length; i++) { + if (stream[i] > 0xFF) + throw 'Byte value overflow!'; + value = (value << 8) | stream[i]; + } + + if (is_signed) { + var edge = 1 << (stream.length) * 8; // 0x1000.. + var max = (edge - 1) >> 1; // 0x0FFF.. >> 1 + value = (value > max) ? value - edge : value; + } + + value /= divisor; + + return value; + + } + + var sensors = []; + var i = 0; + while (i < bytes.length) { + + var s_no = bytes[i++]; + var s_type = bytes[i++]; + if (typeof sensor_types[s_type] == 'undefined') { + throw 'Sensor type error!: ' + s_type; + } + + var s_value = 0; + var type = sensor_types[s_type]; + switch (s_type) { + + case 113: // Accelerometer + case 134: // Gyrometer + s_value = { + 'x': arrayToDecimal(bytes.slice(i + 0, i + 2), type.signed, type.divisor), + 'y': arrayToDecimal(bytes.slice(i + 2, i + 4), type.signed, type.divisor), + 'z': arrayToDecimal(bytes.slice(i + 4, i + 6), type.signed, type.divisor) + }; + break; + case 136: // GPS Location + s_value = { + 'latitude': arrayToDecimal(bytes.slice(i + 0, i + 3), type.signed, type.divisor[0]), + 'longitude': arrayToDecimal(bytes.slice(i + 3, i + 6), type.signed, type.divisor[1]), + 'altitude': arrayToDecimal(bytes.slice(i + 6, i + 9), type.signed, type.divisor[2]) + }; + break; + case 137: // Precise GPS Location + s_value = { + 'latitude': arrayToDecimal(bytes.slice(i + 0, i + 4), type.signed, type.divisor[0]), + 'longitude': arrayToDecimal(bytes.slice(i + 4, i + 8), type.signed, type.divisor[1]), + 'altitude': arrayToDecimal(bytes.slice(i + 8, i + 11), type.signed, type.divisor[2]) + }; + sensors.push({ + 'channel': s_no, + 'type': s_type, + 'name': 'location', + 'value': "(" + s_value.latitude + "," + s_value.longitude + ")" + }); + sensors.push({ + 'channel': s_no, + 'type': s_type, + 'name': 'altitude', + 'value': s_value.altitude + }); + break; + case 135: // Colour + s_value = { + 'r': arrayToDecimal(bytes.slice(i + 0, i + 1), type.signed, type.divisor), + 'g': arrayToDecimal(bytes.slice(i + 1, i + 2), type.signed, type.divisor), + 'b': arrayToDecimal(bytes.slice(i + 2, i + 3), type.signed, type.divisor) + }; + break; + + default: // All the rest + s_value = arrayToDecimal(bytes.slice(i, i + type.size), type.signed, type.divisor); + break; + } + + sensors.push({ + 'channel': s_no, + 'type': s_type, + 'name': type.name, + 'value': s_value + }); + + i += type.size; + + } + + return sensors; + +} + +function Decoder(request, fPort) { + + var decoded = {}; + + console.log('Found LoRaWAN object'); + + if (fPort === 6) { + decoded.isLoRaWAN = false; + decoded.source = 'Cellular'; + } else { + decoded.isLoRaWAN = true; + decoded.source = 'LoRaWAN'; + } + + // Decode from LoRaWAN payload + lppDecode(request, 1).forEach(function (field) { + if ((field['type'] == 101) || (field['type'] == 103) || (field['type'] == 104) || (field['type'] == 115)) { + decoded[field['name']] = field['value']; + decoded[field['name'] + '_' + field['channel']] = field['value']; + } + else { + decoded[field['name'] + '_' + field['channel']] = field['value']; + } + }); + + // Array where we store the fields that are being sent to Datacake + var datacakeFields = [] + + // take each field from decoded and convert them to Datacake format + for (var key in decoded) { + if (decoded.hasOwnProperty(key)) { + datacakeFields.push({ field: key.toUpperCase(), value: decoded[key] }) + } + } + + // forward data to Datacake + return datacakeFields; + +} \ No newline at end of file diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/Dual-Path-To-Datacake.pptx b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/Dual-Path-To-Datacake.pptx new file mode 100644 index 0000000..ba9a5f8 Binary files /dev/null and b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/Dual-Path-To-Datacake.pptx differ diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/LICENSE b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/LICENSE new file mode 100644 index 0000000..fbb844b --- /dev/null +++ b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Bernd Giesecke + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/create_uf2.py b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/create_uf2.py new file mode 100644 index 0000000..2b673d9 --- /dev/null +++ b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/create_uf2.py @@ -0,0 +1,105 @@ +import sys +import struct + +Import("env") + +# Parse input and create UF2 file +def create_uf2(source, target, env): + # source_hex = target[0].get_abspath() + source_hex = target[0].get_string(False) + source_hex = '.\\'+source_hex + print("#########################################################") + print("Create UF2 from "+source_hex) + print("#########################################################") + # print("Source: " + source_hex) + target = source_hex.replace(".hex", "") + target = target + ".uf2" + # print("Target: " + target) + + with open(source_hex, mode='rb') as f: + inpbuf = f.read() + + outbuf = convert_from_hex_to_uf2(inpbuf.decode("utf-8")) + + write_file(target, outbuf) + print("#########################################################") + print(target + " is ready to flash to target device") + print("#########################################################") + + +# Add callback after .hex file was created +env.AddPostAction("$BUILD_DIR/${PROGNAME}.hex", create_uf2) + +# UF2 creation taken from uf2conv.py +UF2_MAGIC_START0 = 0x0A324655 # "UF2\n" +UF2_MAGIC_START1 = 0x9E5D5157 # Randomly selected +UF2_MAGIC_END = 0x0AB16F30 # Ditto + +familyid = 0xADA52840 + + +class Block: + def __init__(self, addr): + self.addr = addr + self.bytes = bytearray(256) + + def encode(self, blockno, numblocks): + global familyid + flags = 0x0 + if familyid: + flags |= 0x2000 + hd = struct.pack(" +#include + +/** BME680 instance for Wire */ +Adafruit_BME680 bme(&Wire); + +/** Last temperature read */ +float _last_temp_rak1906 = 0; +/** Last humidity read */ +float _last_humid_rak1906 = 0; +/** Last pressure read */ +float _last_pressure_rak1906 = 0; + +/** + * @brief Initialize the BME680 sensor + * + * @return true if sensor was found + * @return false if sensor was not found + */ +bool init_rak1906(void) +{ + Wire.begin(); + + if (!bme.begin(0x76)) + { + MYLOG("BME", "Could not find a valid BME680 sensor, check wiring!"); + return false; + } + + // Set up oversampling and filter initialization + bme.setTemperatureOversampling(BME680_OS_8X); + bme.setHumidityOversampling(BME680_OS_2X); + bme.setPressureOversampling(BME680_OS_4X); + bme.setIIRFilterSize(BME680_FILTER_SIZE_3); + // bme.setGasHeater(320, 150); // 320*C for 150 ms + // As we do not use the BSEC library here, the gas value is useless and just consumes battery. Better to switch it off + bme.setGasHeater(0, 0); // switch off + + return true; +} + +/** + * @brief Read environment data from BME680 + * Data is added to Cayenne LPP payload as channels + * LPP_CHANNEL_HUMID_2, LPP_CHANNEL_TEMP_2, + * LPP_CHANNEL_PRESS_2 and LPP_CHANNEL_GAS_2 + * + * + * @return true if reading was successful + * @return false if reading failed + */ +bool read_rak1906() +{ + MYLOG("BME", "Start BME reading"); + bme.beginReading(); + time_t wait_start = millis(); + bool read_success = false; + while ((millis() - wait_start) < 5000) + { + if (bme.endReading()) + { + read_success = true; + break; + } + } + + if (!read_success) + { + MYLOG("BME", "BME timeout"); + return false; + } + + _last_temp_rak1906 = bme.temperature; + _last_humid_rak1906 = bme.humidity; + _last_pressure_rak1906 = (float)(bme.pressure) / 100.0; + + g_solution_data.addRelativeHumidity(LPP_CHANNEL_HUMID_2, _last_humid_rak1906); + g_solution_data.addTemperature(LPP_CHANNEL_TEMP_2, _last_temp_rak1906); + g_solution_data.addBarometricPressure(LPP_CHANNEL_PRESS_2, _last_pressure_rak1906); + +#if MY_DEBUG > 0 + MYLOG("BME", "RH= %.2f T= %.2f P= %.3f", bme.humidity, bme.temperature, (float)(bme.pressure) / 100.0); +#endif + + return true; +} + +/** + * @brief Returns the latest values from the sensor + * or starts a new reading + * + * @param values array for temperature [0], humidity [1] and pressure [2] + */ +void get_rak1906_values(float *values) +{ + values[0] = _last_temp_rak1906; + values[1] = _last_humid_rak1906; + values[2] = _last_pressure_rak1906; +} diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/src/RAK1906_env.h b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/src/RAK1906_env.h new file mode 100644 index 0000000..34fe01b --- /dev/null +++ b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/src/RAK1906_env.h @@ -0,0 +1,20 @@ +/** + * @file RAK1906_env.h + * @author Bernd Giesecke (bernd@giesecke.tk) + * @brief Global definitions and forward declarations + * @version 0.1 + * @date 2022-09-23 + * + * @copyright Copyright (c) 2022 + * + */ +#ifndef RAK1906_H +#define RAK1906_H +#include + +// Function declarations +bool init_rak1906(void); +bool read_rak1906(void); +void get_rak1906_values(float *values); + +#endif // RAK1906_H \ No newline at end of file diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/src/blues.cpp b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/src/blues.cpp new file mode 100644 index 0000000..5cb8998 --- /dev/null +++ b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/src/blues.cpp @@ -0,0 +1,506 @@ +/** + * @file blues.cpp + * @author Bernd Giesecke (bernd@giesecke.tk) + * @brief Blues.IO NoteCard handler + * @version 0.1 + * @date 2023-04-27 + * + * @copyright Copyright (c) 2023 + * + */ +#include "main.h" + +#ifndef PRODUCT_UID +#define PRODUCT_UID "com.my-company.my-name:my-project" +#endif +#define myProductID PRODUCT_UID + +Notecard notecard; + +void blues_attn_cb(void); + +J *req; + +/** + * @brief Initialize Blues NoteCard + * + * @return true if NoteCard was found and setup was successful + * @return false if NoteCard was not found or the setup failed + */ +bool init_blues(void) +{ + Wire.begin(); + notecard.begin(); + + // Get the ProductUID from the saved settings + // If no settings are found, use NoteCard internal settings! + if (read_blues_settings()) + { + MYLOG("BLUES", "Found saved settings, override NoteCard internal settings!"); + if (memcmp(g_blues_settings.product_uid, "com.my-company.my-name", 22) == 0) + { + MYLOG("BLUES", "No Product ID saved"); + AT_PRINTF(":EVT NO PUID"); + memcpy(g_blues_settings.product_uid, PRODUCT_UID, 33); + } + + MYLOG("BLUES", "Set Product ID and connection mode"); + if (blues_start_req("hub.set")) + { + JAddStringToObject(req, "product", g_blues_settings.product_uid); + if (g_blues_settings.conn_continous) + { + JAddStringToObject(req, "mode", "continuous"); + } + else + { + JAddStringToObject(req, "mode", "minimum"); + } + // Set sync time to 20 times the sensor read time + JAddNumberToObject(req, "seconds", (g_lorawan_settings.send_repeat_time * 20 / 1000)); + JAddBoolToObject(req, "heartbeat", true); + + if (!blues_send_req()) + { + MYLOG("BLUES", "hub.set request failed"); + return false; + } + } + else + { + MYLOG("BLUES", "hub.set request failed"); + return false; + } + +#if USE_GNSS == 1 + MYLOG("BLUES", "Set location mode"); + if (blues_start_req("card.location.mode")) + { + // Continous GNSS mode + // JAddStringToObject(req, "mode", "continous"); + + // Periodic GNSS mode + JAddStringToObject(req, "mode", "periodic"); + + // Set location acquisition time to the sensor read time + JAddNumberToObject(req, "seconds", (g_lorawan_settings.send_repeat_time / 2000)); + JAddBoolToObject(req, "heartbeat", true); + if (!blues_send_req()) + { + MYLOG("BLUES", "card.location.mode request failed"); + return false; + } + } + else + { + MYLOG("BLUES", "card.location.mode request failed"); + return false; + } +#else + MYLOG("BLUES", "Stop location mode"); + if (blues_start_req("card.location.mode")) + { + // GNSS mode off + JAddStringToObject(req, "mode", "off"); + if (!blues_send_req()) + { + MYLOG("BLUES", "card.location.mode request failed"); + return false; + } + } + else + { + MYLOG("BLUES", "card.location.mode request failed"); + return false; + } +#endif + + /// \todo reset attn signal needs rework + // pinMode(WB_IO5, INPUT); + // if (g_blues_settings.motion_trigger) + // { + // if (blues_start_req("card.attn")) + // { + // JAddStringToObject(req, "mode", "disarm"); + // if (!blues_send_req()) + // { + // MYLOG("BLUES", "card.attn request failed"); + // } + + // if (!blues_enable_attn()) + // { + // return false; + // } + // } + // } + // else + // { + // MYLOG("BLUES", "card.attn request failed"); + // return false; + // } + + MYLOG("BLUES", "Set APN"); + // {“req”:”card.wireless”} + if (blues_start_req("card.wireless")) + { + JAddStringToObject(req, "mode", "auto"); + + if (g_blues_settings.use_ext_sim) + { + // USING EXTERNAL SIM CARD + JAddStringToObject(req, "apn", g_blues_settings.ext_sim_apn); + JAddStringToObject(req, "method", "dual-secondary-primary"); + } + else + { + // USING BLUES eSIM CARD + JAddStringToObject(req, "method", "primary"); + } + if (!blues_send_req()) + { + MYLOG("BLUES", "card.wireless request failed"); + return false; + } + } + else + { + MYLOG("BLUES", "card.wireless request failed"); + return false; + } + +#if IS_V2 == 1 + // Only for V2 cards, setup the WiFi network + MYLOG("BLUES", "Set WiFi"); + if (blues_start_req("card.wifi")) + { + JAddStringToObject(req, "ssid", "-"); + JAddStringToObject(req, "password", "-"); + JAddStringToObject(req, "name", "RAK-"); + JAddStringToObject(req, "org", "RAK-PH"); + JAddBoolToObject(req, "start", false); + + if (!blues_send_req()) + { + MYLOG("BLUES", "card.wifi request failed"); + } + } + else + { + MYLOG("BLUES", "card.wifi request failed"); + return false; + } +#endif + } + + // {"req": "card.version"} + if (blues_start_req("card.version")) + { + if (!blues_send_req()) + { + MYLOG("BLUES", "card.version request failed"); + } + } + return true; +} + +/** + * @brief Send a data packet to NoteHub.IO + * + * @param data Payload as byte array (CayenneLPP formatted) + * @param data_len Length of payload + * @return true if note could be sent to NoteCard + * @return false if note send failed + */ +bool blues_send_payload(uint8_t *data, uint16_t data_len) +{ + if (blues_start_req("note.add")) + { + JAddStringToObject(req, "file", "data.qo"); + JAddBoolToObject(req, "sync", true); + J *body = JCreateObject(); + if (body != NULL) + { + char node_id[24]; + sprintf(node_id, "%02x%02x%02x%02x%02x%02x%02x%02x", + g_lorawan_settings.node_device_eui[0], g_lorawan_settings.node_device_eui[1], + g_lorawan_settings.node_device_eui[2], g_lorawan_settings.node_device_eui[3], + g_lorawan_settings.node_device_eui[4], g_lorawan_settings.node_device_eui[5], + g_lorawan_settings.node_device_eui[6], g_lorawan_settings.node_device_eui[7]); + JAddStringToObject(body, "dev_eui", node_id); + + JAddItemToObject(req, "body", body); + + JAddBinaryToObject(req, "payload", data, data_len); + + MYLOG("PARSE", "Finished parsing"); + if (!blues_send_req()) + { + MYLOG("PARSE", "Send request failed"); + return false; + } + return true; + } + else + { + MYLOG("PARSE", "Error creating body"); + } + } + return false; +} + +/** + * @brief Create a request structure to be sent to the NoteCard + * + * @param request_name name of request, e.g. card.wireless + * @return true if request could be created + * @return false if request could not be created + */ +bool blues_start_req(String request_name) +{ + req = notecard.newRequest(request_name.c_str()); + if (req != NULL) + { + return true; + } + return false; +} + +/** + * @brief Send a completed request to the NoteCard. + * + * @return true if request could be sent and the response does not have "err" + * @return false if request could not be sent or the response did have "err" + */ +bool blues_send_req(void) +{ + char *json = JPrintUnformatted(req); + MYLOG("BLUES", "Card request = %s", json); + + J *rsp; + rsp = notecard.requestAndResponse(req); + if (rsp == NULL) + { + return false; + } + json = JPrintUnformatted(rsp); + if (JIsPresent(rsp, "err")) + { + MYLOG("BLUES", "Card error response = %s", json); + notecard.deleteResponse(rsp); + return false; + } + MYLOG("BLUES", "Card response = %s", json); + notecard.deleteResponse(rsp); + + return true; +} + +/** + * @brief Request NoteHub status, mainly for debug purposes + * + */ +void blues_hub_status(void) +{ + blues_start_req("hub.status"); + if (!blues_send_req()) + { + MYLOG("BLUES", "hub.status request failed"); + } +} + +/** + * @brief Get the location information from the NoteCard + * + * @return true if a location could be acquired + * @return false if request failed or no location is available + */ +bool blues_get_location(void) +{ + bool result = false; + if (blues_start_req("card.location")) + { + J *rsp; + rsp = notecard.requestAndResponse(req); + if (rsp == NULL) + { + MYLOG("BLUES", "card.location failed, report no location"); + return false; + } + char *json = JPrintUnformatted(rsp); + MYLOG("BLUES", "Card response = %s", json); + + if (JHasObjectItem(rsp, "lat") && JHasObjectItem(rsp, "lat")) + { + float blues_latitude = JGetNumber(rsp, "lat"); + float blues_longitude = JGetNumber(rsp, "lon"); + float blues_altitude = 0; + + if ((blues_latitude == 0.0) && (blues_longitude == 0.0)) + { + MYLOG("BLUES", "No valid GPS data, report no location"); + } + else + { + MYLOG("BLUES", "Got location Lat %.6f Long %0.6f", blues_latitude, blues_longitude); + g_solution_data.addGNSS_6(LPP_CHANNEL_GPS, (uint32_t)(blues_latitude * 10000000), (uint32_t)(blues_longitude * 10000000), blues_altitude); + result = true; + } + } + + notecard.deleteResponse(rsp); + } + + if (!result) + { + // No GPS coordinates, get last tower location + if (blues_start_req("card.time")) + { + J *rsp; + rsp = notecard.requestAndResponse(req); + if (rsp == NULL) + { + MYLOG("BLUES", "card.time failed, report no location"); + return false; + } + char *json = JPrintUnformatted(rsp); + MYLOG("BLUES", "Card response = %s", json); + + if (JHasObjectItem(rsp, "lat") && JHasObjectItem(rsp, "lat")) + { + float blues_latitude = JGetNumber(rsp, "lat"); + float blues_longitude = JGetNumber(rsp, "lon"); + float blues_altitude = 0; + + if ((blues_latitude == 0.0) && (blues_longitude == 0.0)) + { + MYLOG("BLUES", "No valid GPS data, report no location"); + } + else + { + MYLOG("BLUES", "Got tower location Lat %.6f Long %0.6f", blues_latitude, blues_longitude); + g_solution_data.addGNSS_6(LPP_CHANNEL_GPS, (uint32_t)(blues_latitude * 10000000), (uint32_t)(blues_longitude * 10000000), blues_altitude); + result = true; + } + } + + notecard.deleteResponse(rsp); + } + } + + // Clear last GPS location + if (blues_start_req("card.location.mode")) + { + JAddBoolToObject(req, "delete", true); + J *rsp; + rsp = notecard.requestAndResponse(req); + if (rsp == NULL) + { + MYLOG("BLUES", "card.location.mode"); + } + char *json = JPrintUnformatted(rsp); + MYLOG("BLUES", "Card response = %s", json); + notecard.deleteResponse(rsp); + } + return result; +} + +/** + * @brief Enable ATTN interrupt + * At the moment enables only the alarm on motion + * + * @return true if ATTN could be enabled + * @return false if ATTN could not be enabled + */ +bool blues_enable_attn(void) +{ + MYLOG("BLUES", "Enable ATTN on motion"); + if (blues_start_req("card.attn")) + { + JAddStringToObject(req, "mode", "motion"); + if (!blues_send_req()) + { + MYLOG("BLUES", "card.attn request failed"); + return false; + } + } + else + { + MYLOG("BLUES", "Request creation failed"); + } + attachInterrupt(WB_IO5, blues_attn_cb, RISING); + + MYLOG("BLUES", "Arm ATTN on motion"); + if (blues_start_req("card.attn")) + { + JAddStringToObject(req, "mode", "arm"); + if (!blues_send_req()) + { + MYLOG("BLUES", "card.attn request failed"); + return false; + } + } + else + { + MYLOG("BLUES", "Request creation failed"); + } + return true; +} + +/** + * @brief Disable ATTN interrupt + * + * @return true if ATTN could be disabled + * @return false if ATTN could not be disabled + */ +bool blues_disable_attn(void) +{ + MYLOG("BLUES", "Disable ATTN on motion"); + detachInterrupt(WB_IO5); + + if (blues_start_req("card.attn")) + { + JAddStringToObject(req, "mode", "disarm"); + if (!blues_send_req()) + { + MYLOG("BLUES", "card.attn request failed"); + } + } + else + { + MYLOG("BLUES", "Request creation failed"); + } + if (blues_start_req("card.attn")) + { + JAddStringToObject(req, "mode", "-motion"); + if (!blues_send_req()) + { + MYLOG("BLUES", "card.attn request failed"); + } + } + else + { + MYLOG("BLUES", "Request creation failed"); + } + + return true; +} + +/** + * @brief Get the reason for the ATTN interrup + * /// \todo work in progress + * @return String reason /// \todo return value not final yet + */ +String blues_attn_reason(void) +{ + return ""; +} + +/** + * @brief Callback for ATTN interrupt + * Wakes up the app_handler with an BLUES_ATTN event + * + */ +void blues_attn_cb(void) +{ + api_wake_loop(BLUES_ATTN); +} \ No newline at end of file diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/src/main.cpp b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/src/main.cpp new file mode 100644 index 0000000..c9bbf69 --- /dev/null +++ b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/src/main.cpp @@ -0,0 +1,380 @@ +/** + * @file main.cpp + * @author Bernd Giesecke (bernd@giesecke.tk) + * @brief App event handlers + * @version 0.1 + * @date 2023-04-25 + * + * @copyright Copyright (c) 2023 + * + */ + +#include "main.h" + +/** LoRaWAN packet */ +WisCayenne g_solution_data(255); + +/** Received package for parsing */ +uint8_t rcvd_data[256]; +/** Length of received package */ +uint16_t rcvd_data_len = 0; + +/** Send Fail counter **/ +uint8_t send_fail = 0; + +/** Set the device name, max length is 10 characters */ +char g_ble_dev_name[10] = "RAK"; + +/** Flag for RAK1906 sensor */ +bool has_rak1906 = false; + +/** Flag is Blues Notecard was found */ +bool has_blues = false; + +SoftwareTimer delayed_sending; +void delayed_cellular(TimerHandle_t unused); + +/** + * @brief Initial setup of the application (before LoRaWAN and BLE setup) + * + */ +void setup_app(void) +{ + Serial.begin(115200); + time_t serial_timeout = millis(); + // On nRF52840 the USB serial is not available immediately + while (!Serial) + { + if ((millis() - serial_timeout) < 5000) + { + delay(100); + digitalWrite(LED_GREEN, !digitalRead(LED_GREEN)); + } + else + { + break; + } + } + digitalWrite(LED_GREEN, LOW); + + // Set firmware version + api_set_version(SW_VERSION_1, SW_VERSION_2, SW_VERSION_3); + g_enable_ble = true; +} + +/** + * @brief Final setup of application (after LoRaWAN and BLE setup) + * + * @return true + * @return false + */ +bool init_app(void) +{ + MYLOG("APP", "init_app"); + + MYLOG("INI", "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++"); + MYLOG("INI", "WisBlock Blues Tracker"); + MYLOG("INI", "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++"); + + // Initialize User AT commands + init_user_at(); + + // Check if RAK1906 is available + has_rak1906 = init_rak1906(); + if (has_rak1906) + { + AT_PRINTF("+EVT:RAK1906"); + } + + // Initialize Blues Notecard + has_blues = init_blues(); + if (!has_blues) + { + AT_PRINTF("+EVT:CELLULAR_ERROR"); + } + + pinMode(WB_IO2, OUTPUT); + digitalWrite(WB_IO2, LOW); + + restart_advertising(30); + + delayed_sending.begin(15000, delayed_cellular, NULL, false); + + // Start the send interval timer and send a first message + if (!g_lorawan_settings.auto_join) + { + MYLOG("APP", "Initialize LoRaWAN stack, but do not join"); + if (g_lorawan_settings.lorawan_enable) + { + API_LOG("API", "Auto join is enabled, start LoRaWAN and join"); + init_lorawan(); + } + api_timer_start(); + api_wake_loop(STATUS); + } + return true; +} + +/** + * @brief Handle events + * Events can be + * - timer (setup with AT+SENDINT=xxx) + * - interrupt events + * - wake-up signals from other tasks + */ +void app_event_handler(void) +{ + // Timer triggered event + if ((g_task_event_type & STATUS) == STATUS) + { + g_task_event_type &= N_STATUS; + + MYLOG("APP", "Timer wakeup"); + + // Reset the packet + g_solution_data.reset(); + + if (!blues_get_location()) + { + MYLOG("APP", "Failed to get location"); + } + + // Get battery level + float batt_level_f = read_batt(); + g_solution_data.addVoltage(LPP_CHANNEL_BATT, batt_level_f / 1000.0); + + // Read sensors and battery + if (has_rak1906) + { + read_rak1906(); + } + + bool check_rejoin = false; + + if (g_lpwan_has_joined) + { + /*************************************************************************************/ + /* */ + /* If the device is setup for LoRaWAN, try first to send the data as confirmed */ + /* packet. If the sending fails, retry over cellular modem */ + /* */ + /* If the device is setup for LoRa P2P, send always as P2P packet AND over the */ + /* cellular modem */ + /* */ + /*************************************************************************************/ + if (g_lorawan_settings.lorawan_enable) + { + lmh_error_status result = send_lora_packet(g_solution_data.getBuffer(), g_solution_data.getSize()); + switch (result) + { + case LMH_SUCCESS: + MYLOG("APP", "Packet enqueued"); + break; + case LMH_BUSY: + re_init_lorawan(); + result = send_lora_packet(g_solution_data.getBuffer(), g_solution_data.getSize()); + if (result != LMH_SUCCESS) + { + // Send over cellular connection + delayed_sending.start(); + check_rejoin = true; + send_fail++; + MYLOG("APP", "LoRa transceiver is busy"); + AT_PRINTF("+EVT:BUSY\n"); + } + break; + case LMH_ERROR: + re_init_lorawan(); + result = send_lora_packet(g_solution_data.getBuffer(), g_solution_data.getSize()); + if (result != LMH_SUCCESS) + { + // Send over cellular connection + delayed_sending.start(); + check_rejoin = true; + send_fail++; + AT_PRINTF("+EVT:SIZE_ERROR\n"); + MYLOG("APP", "Packet error, too big to send with current DR"); + } + break; + } + } + else + { + // Add unique identifier in front of the P2P packet, here we use the DevEUI + g_solution_data.addDevID(LPP_CHANNEL_DEVID, &g_lorawan_settings.node_device_eui[4]); + + // Send packet over LoRa + // if (send_p2p_packet(packet_buffer, g_solution_data.getSize() + 8)) + if (send_p2p_packet(g_solution_data.getBuffer(), g_solution_data.getSize())) + { + MYLOG("APP", "Packet enqueued"); + } + else + { + AT_PRINTF("+EVT:SIZE_ERROR\n"); + MYLOG("APP", "Packet too big"); + } + + // Send as well over cellular connection + delayed_sending.start(); + } + } + else + { + // delayed_sending.start(); + g_task_event_type |= USE_CELLULAR; + if (g_lorawan_settings.lorawan_enable) + { + check_rejoin = true; + send_fail++; + } + MYLOG("APP", "Network not joined, skip sending over LoRaWAN"); + } + + if (check_rejoin) + { + // Check how many times we send over LoRaWAN failed and retry to join LNS after 10 times failing + if (send_fail >= 10) + { + // Too many failed sendings, try to rejoin + MYLOG("APP", "Retry to join LNS"); + send_fail = 0; + // int8_t init_result = re_init_lorawan(); + lmh_join(); + } + } + } + + // Send over Blues event + if ((g_task_event_type & USE_CELLULAR) == USE_CELLULAR) + { + g_task_event_type &= N_USE_CELLULAR; + // Send over cellular connection + MYLOG("APP", "Get hub sync status:"); + blues_hub_status(); + + g_solution_data.addDevID(0, &g_lorawan_settings.node_device_eui[4]); + blues_send_payload(g_solution_data.getBuffer(), g_solution_data.getSize()); + + // Request sync with NoteHub + blues_start_req("hub.sync"); + blues_send_req(); + } + + // Blues ATTN event + if ((g_task_event_type & BLUES_ATTN) == BLUES_ATTN) + { + g_task_event_type &= N_BLUES_ATTN; + // Send over cellular connection + MYLOG("APP", "Blues ATTN event"); + + blues_start_req("card.attn"); + blues_send_req(); + + blues_start_req("card.time"); + blues_send_req(); + + // req = notecard.newRequest("card.attn"); + // if (!blues_send_req()) + // { + // MYLOG("BLUES", "card.attn request failed"); + // return false; + // } + + blues_enable_attn(); + } +} + +/** + * @brief Handle BLE events + * + */ +void ble_data_handler(void) +{ + if (g_enable_ble) + { + if ((g_task_event_type & BLE_DATA) == BLE_DATA) + { + MYLOG("AT", "RECEIVED BLE"); + // BLE UART data arrived + g_task_event_type &= N_BLE_DATA; + + while (g_ble_uart.available() > 0) + { + at_serial_input(uint8_t(g_ble_uart.read())); + delay(5); + } + at_serial_input(uint8_t('\n')); + } + } +} + +/** + * @brief Handle LoRa events + * + */ +void lora_data_handler(void) +{ + // LoRa Join finished handling + if ((g_task_event_type & LORA_JOIN_FIN) == LORA_JOIN_FIN) + { + g_task_event_type &= N_LORA_JOIN_FIN; + if (g_join_result) + { + MYLOG("APP", "Successfully joined network"); + send_fail = 0; + } + else + { + MYLOG("APP", "Join network failed"); + } + } + + // LoRa data handling + if ((g_task_event_type & LORA_DATA) == LORA_DATA) + { + g_task_event_type &= N_LORA_DATA; + MYLOG("APP", "Received package over LoRa"); + char log_buff[g_rx_data_len * 3] = {0}; + uint8_t log_idx = 0; + for (int idx = 0; idx < g_rx_data_len; idx++) + { + sprintf(&log_buff[log_idx], "%02X ", g_rx_lora_data[idx]); + log_idx += 3; + } + MYLOG("APP", "%s", log_buff); + } + + // LoRa TX finished handling + if ((g_task_event_type & LORA_TX_FIN) == LORA_TX_FIN) + { + g_task_event_type &= N_LORA_TX_FIN; + + MYLOG("APP", "LPWAN TX cycle %s", g_rx_fin_result ? "finished ACK" : "failed NAK"); + + if (!g_rx_fin_result) + { + if (g_lorawan_settings.lorawan_enable) + { + delayed_sending.start(); + } + + // Increase fail send counter + send_fail++; + } + else + { + send_fail = 0; + } + } +} + +/** + * @brief Timer callback to decouple the LoRaWAN sending and the cellular sending + * + * @param unused + */ +void delayed_cellular(TimerHandle_t unused) +{ + api_wake_loop(USE_CELLULAR); +} \ No newline at end of file diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/src/main.h b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/src/main.h new file mode 100644 index 0000000..9943fc3 --- /dev/null +++ b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/src/main.h @@ -0,0 +1,106 @@ +/** + * @file main.h + * @author Bernd Giesecke (bernd@giesecke.tk) + * @brief Includes, defines and globals + * @version 0.1 + * @date 2023-04-25 + * + * @copyright Copyright (c) 2023 + * + */ + +#ifndef _MAIN_H_ +#define _MAIN_H_ + +#include +#include +#include +#include "RAK1906_env.h" + +// Debug output set to 0 to disable app debug output +#ifndef MY_DEBUG +#define MY_DEBUG 1 +#endif + +#if MY_DEBUG > 0 +#define MYLOG(tag, ...) \ + do \ + { \ + if (tag) \ + PRINTF("[%s] ", tag); \ + PRINTF(__VA_ARGS__); \ + PRINTF("\n"); \ + Serial.flush(); \ + if (g_ble_uart_is_connected) \ + { \ + g_ble_uart.printf(__VA_ARGS__); \ + g_ble_uart.printf("\n"); \ + } \ + } while (0) +#else +#define MYLOG(...) +#endif + +/** Define the version of your SW */ +#ifndef SW_VERSION_1 +#define SW_VERSION_1 1 // major version increase on API change / not backwards compatible +#endif +#ifndef SW_VERSION_2 +#define SW_VERSION_2 0 // minor version increase on API change / backward compatible +#endif +#ifndef SW_VERSION_3 +#define SW_VERSION_3 0 // patch version increase on bugfix, no affect on API +#endif + +/** Application function definitions */ +void setup_app(void); +bool init_app(void); +void app_event_handler(void); +void ble_data_handler(void) __attribute__((weak)); +void lora_data_handler(void); + +// Wakeup flags +#define USE_CELLULAR 0b1000000000000000 +#define N_USE_CELLULAR 0b0111111111111111 +#define BLUES_ATTN 0b0100000000000000 +#define N_BLUES_ATTN 0b1011111111111111 + +// Cayenne LPP Channel numbers per sensor value +#define LPP_CHANNEL_BATT 1 // Base Board +#define LPP_CHANNEL_HUMID_2 6 // RAK1906 +#define LPP_CHANNEL_TEMP_2 7 // RAK1906 +#define LPP_CHANNEL_PRESS_2 8 // RAK1906 +#define LPP_CHANNEL_GAS_2 9 // RAK1906 +#define LPP_CHANNEL_GPS 10 // RAK1910/RAK12500 + +// Globals +extern WisCayenne g_solution_data; + +// Blues.io +struct s_blues_settings +{ + uint16_t valid_mark = 0xAA55; // Validity marker + char product_uid[256] = "com.my-company.my-name:my-project"; // Blues Product UID + bool conn_continous = false; // Use periodic connection + bool use_ext_sim = false; // Use external SIM + char ext_sim_apn[256] = "internet"; // APN to be used with external SIM + bool motion_trigger = true; // Send data on motion trigger +}; + +bool init_blues(void); +bool blues_start_req(String request_name); +bool blues_send_req(void); +void blues_hub_status(void); +bool blues_get_location(void); +bool blues_enable_attn(void); +bool blues_disable_attn(void); +bool blues_send_payload(uint8_t *data, uint16_t data_len); +extern J *req; +extern s_blues_settings g_blues_settings; + +// User AT commands +void init_user_at(void); +bool read_blues_settings(void); +void save_blues_settings(void); + +#endif // _MAIN_H_ \ No newline at end of file diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/src/user_at_cmd.cpp b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/src/user_at_cmd.cpp new file mode 100644 index 0000000..6aad63e --- /dev/null +++ b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/PlatformIO/src/user_at_cmd.cpp @@ -0,0 +1,387 @@ +/** + * @file user_at_cmd.cpp + * @author Bernd Giesecke (bernd@giesecke.tk) + * @brief User AT commands + * @version 0.1 + * @date 2023-08-18 + * + * @copyright Copyright (c) 2023 + * + */ +#include "main.h" + +#include +#include +using namespace Adafruit_LittleFS_Namespace; + +/** Filename to save Blues settings */ +static const char blues_file_name[] = "BLUES"; + +/** File to save battery check status */ +File this_file(InternalFS); + +/** Structure for saved Blues Notecard settings */ +s_blues_settings g_blues_settings; + +/** + * @brief Set Blues Product UID + * + * @param str Product UID as Hex String + * @return int AT_SUCCESS if ok, AT_ERRNO_PARA_FAIL if invalid value + */ +int at_set_blues_prod_uid(char *str) +{ + if (strlen(str) < 25) + { + return AT_ERRNO_PARA_NUM; + } + + for (int i = 0; str[i] != '\0'; i++) + { + if (str[i] >= 'A' && str[i] <= 'Z') // checking for uppercase characters + str[i] = str[i] + 32; // converting uppercase to lowercase + } + + char new_uid[256] = {0}; + snprintf(new_uid, 255, str); + + MYLOG("USR_AT", "Received new Blues Product UID %s", new_uid); + + bool need_save = strcmp(new_uid, g_blues_settings.product_uid) == 0 ? false : true; + + if (need_save) + { + snprintf(g_blues_settings.product_uid, 256, new_uid); + } + + // Save new master node address if changed + if (need_save) + { + save_blues_settings(); + } + return AT_SUCCESS; +} + +/** + * @brief Get Blues Product UID + * + * @return int AT_SUCCESS + */ +int at_query_blues_prod_uid(void) +{ + snprintf(g_at_query_buf, ATQUERY_SIZE, "%s", g_blues_settings.product_uid); + return AT_SUCCESS; +} + +/** + * @brief Set usage of eSIM or external SIM and APN + * + * @param str params as string, format 0 or 1:APN_NAME + * @return int + * AT_SUCCESS is params are set correct + * AT_ERRNO_PARA_NUM if params error + */ +int at_set_blues_ext_sim(char *str) +{ + char *param; + bool new_use_ext_sim; + char new_ext_sim_apn[256]; + + // Get string up to first : + param = strtok(str, ":"); + if (param != NULL) + { + if (param[0] == '0') + { + MYLOG("USR_AT", "Enable eSIM"); + new_use_ext_sim = false; + } + else if (param[0] == '1') + { + MYLOG("USR_AT", "Enable external SIM"); + new_use_ext_sim = true; + param = strtok(NULL, ":"); + if (param != NULL) + { + for (int i = 0; param[i] != '\0'; i++) + { + if (param[i] >= 'A' && param[i] <= 'Z') // checking for uppercase characters + param[i] = param[i] + 32; // converting uppercase to lowercase + } + snprintf(new_ext_sim_apn, 256, "%s", param); + } + else + { + MYLOG("USR_AT", "Missing external SIM APN"); + return AT_ERRNO_PARA_NUM; + } + } + else + { + MYLOG("USR_AT", "Invalid SIM flag %d", param[0]); + return AT_ERRNO_PARA_NUM; + } + } + + bool need_save = false; + if (new_use_ext_sim != g_blues_settings.use_ext_sim) + { + g_blues_settings.use_ext_sim = new_use_ext_sim; + need_save = true; + } + if (strcmp(new_ext_sim_apn, g_blues_settings.product_uid) != 0) + { + snprintf(g_blues_settings.ext_sim_apn, 256, new_ext_sim_apn); + need_save = true; + } + + if (need_save) + { + save_blues_settings(); + } + return AT_SUCCESS; +} + +/** + * @brief Get Blues SIM settings + * + * @return int AT_SUCCESS + */ +int at_query_blues_ext_sim(void) +{ + if (g_blues_settings.use_ext_sim) + { + snprintf(g_at_query_buf, ATQUERY_SIZE, "1:%s", g_blues_settings.ext_sim_apn); + MYLOG("USR_AT", "Using external SIM with APN = %s", g_blues_settings.ext_sim_apn); + } + else + { + snprintf(g_at_query_buf, ATQUERY_SIZE, "0"); + MYLOG("USR_AT", "Using eSIM"); + } + return AT_SUCCESS; +} + +/** + * @brief Set Blues NoteCard mode + * /// \todo work in progress + * + * @param str params as string, format 0 or 1 + * @return int + * AT_SUCCESS is params are set correct + * AT_ERRNO_PARA_NUM if params error + */ +int at_set_blues_mode(char *str) +{ + bool new_connection_mode; + + if (str[0] == '0') + { + MYLOG("USR_AT", "Set minimum connection mode"); + new_connection_mode = false; + blues_disable_attn(); + } + else if (str[0] == '1') + { + MYLOG("USR_AT", "Set continuous connection mode"); + new_connection_mode = true; + blues_enable_attn(); + } + else + { + MYLOG("USR_AT", "Invalid motion trigger flag %d", str[0]); + return AT_ERRNO_PARA_NUM; + } + + bool need_save = false; + if (new_connection_mode != g_blues_settings.conn_continous) + { + g_blues_settings.conn_continous = new_connection_mode; + need_save = true; + } + + if (need_save) + { + save_blues_settings(); + } + return AT_SUCCESS; +} + +/** + * @brief Get Blues mode settings + * + * @return int AT_SUCCESS + */ +int at_query_blues_mode(void) +{ + snprintf(g_at_query_buf, ATQUERY_SIZE, "%s", g_blues_settings.conn_continous ? "1" : "0"); + MYLOG("USR_AT", "Using %s connection", g_blues_settings.conn_continous ? "continous" : "periodic"); + return AT_SUCCESS; +} + +/** + * @brief Enable/disable the motion trigger + * + * @param str params as string, format 0 or 1 + * @return int + * AT_SUCCESS is params are set correct + * AT_ERRNO_PARA_NUM if params error + */ +int at_set_blues_trigger(char *str) +{ + bool new_motion_trigger; + + if (str[0] == '0') + { + MYLOG("USR_AT", "Disable motion trigger"); + new_motion_trigger = false; + blues_disable_attn(); + } + else if (str[0] == '1') + { + MYLOG("USR_AT", "Enable motion trigger"); + new_motion_trigger = true; + blues_enable_attn(); + } + else + { + MYLOG("USR_AT", "Invalid motion trigger flag %d", str[0]); + return AT_ERRNO_PARA_NUM; + } + + bool need_save = false; + if (new_motion_trigger != g_blues_settings.motion_trigger) + { + g_blues_settings.motion_trigger = new_motion_trigger; + need_save = true; + } + + if (need_save) + { + save_blues_settings(); + } + return AT_SUCCESS; +} + +/** + * @brief Get Blues motion trigger settings + * + * @return int AT_SUCCESS + */ +int at_query_blues_trigger(void) +{ + snprintf(g_at_query_buf, ATQUERY_SIZE, "%s", g_blues_settings.motion_trigger ? "1" : "0"); + MYLOG("USR_AT", "Motion trigger is %s", g_blues_settings.motion_trigger ? "enabled" : "disabled"); + return AT_SUCCESS; +} + +static int at_reset_blues_settings(void) +{ + if (InternalFS.exists(blues_file_name)) + { + InternalFS.remove(blues_file_name); + } + return AT_SUCCESS; +} + +/** + * @brief Read saved Blues Product ID + * + */ +bool read_blues_settings(void) +{ + bool structure_valid = false; + if (InternalFS.exists(blues_file_name)) + { + this_file.open(blues_file_name, FILE_O_READ); + this_file.read((void *)&g_blues_settings.valid_mark, sizeof(s_blues_settings)); + this_file.close(); + + // Check for valid data + if (g_blues_settings.valid_mark == 0xAA55) + { + structure_valid = true; + MYLOG("USR_AT", "Valid Blues settings found, Blues Product UID = %s", g_blues_settings.product_uid); + if (g_blues_settings.use_ext_sim) + { + MYLOG("USR_AT", "Using external SIM with APN = %s", g_blues_settings.ext_sim_apn); + } + else + { + MYLOG("USR_AT", "Using eSIM"); + } + } + else + { + MYLOG("USR_AT", "No valid Blues settings found"); + } + } + + if (!structure_valid) + { + return false; + + // No settings file found optional to set defaults (ommitted!) + // g_blues_settings.valid_mark = 0xAA55; // Validity marker + // sprintf(g_blues_settings.product_uid, "com.my-company.my-name:my-project"); // Blues Product UID + // g_blues_settings.conn_continous = false; // Use periodic connection + // g_blues_settings.use_ext_sim = false; // Use external SIM + // sprintf(g_blues_settings.ext_sim_apn, "-"); // APN to be used with external SIM + // g_blues_settings.motion_trigger = true; // Send data on motion trigger + // save_blues_settings(); + } + + return true; +} + +/** + * @brief Save the Blues Product ID + * + */ +void save_blues_settings(void) +{ + if (InternalFS.exists(blues_file_name)) + { + InternalFS.remove(blues_file_name); + } + + g_blues_settings.valid_mark = 0xAA55; + this_file.open(blues_file_name, FILE_O_WRITE); + this_file.write((const char *)&g_blues_settings.valid_mark, sizeof(s_blues_settings)); + this_file.close(); + MYLOG("USR_AT", "Saved Blues Settings"); +} + +/** + * @brief List of all available commands with short help and pointer to functions + * + */ +atcmd_t g_user_at_cmd_new_list[] = { + /*| CMD | AT+CMD? | AT+CMD=? | AT+CMD=value | AT+CMD | Permissions |*/ + // Module commands + {"+BUID", "Set/get the Blues product UID", at_query_blues_prod_uid, at_set_blues_prod_uid, NULL, "RW"}, + {"+BSIM", "Set/get Blues SIM settings", at_query_blues_ext_sim, at_set_blues_ext_sim, NULL, "RW"}, + {"+BMOD", "Set/get Blues NoteCard connection modes", at_query_blues_mode, at_set_blues_mode, NULL, "RW"}, + {"+BTRIG", "Set/get Blues send trigger", at_query_blues_trigger, at_set_blues_trigger, NULL, "RW"}, + {"+BR", "Remove all Blues Settings", NULL, NULL, at_reset_blues_settings, "RW"}, +}; + +/** Number of user defined AT commands */ +uint8_t g_user_at_cmd_num = 0; + +/** Pointer to the combined user AT command structure */ +atcmd_t *g_user_at_cmd_list; + +/** + * @brief Initialize the user defined AT command list + * + */ +void init_user_at(void) +{ + // Assign custom AT command list to pointer used by WisBlock API + g_user_at_cmd_list = g_user_at_cmd_new_list; + + // Add AT commands to structure + g_user_at_cmd_num += sizeof(g_user_at_cmd_new_list) / sizeof(atcmd_t); + MYLOG("USR_AT", "Added %d User AT commands", g_user_at_cmd_num); +} diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/README.md b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/README.md new file mode 100644 index 0000000..fa852dd --- /dev/null +++ b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/README.md @@ -0,0 +1,301 @@ +# WisBlock Goes Blues +| RAKWireless | RAKstar | Blues | +| :-: | :-: | :-: | + +---- + +While WisBlock is usually associated with _**LoRa**_ and _**LoRaWAN**_, this time we are diving into the cellular data transmission using the Blues.IO Notecard. This project is about building a location tracker that can connect to both LoRaWAN and a cellular connection with a [Blues NoteCard](https://blues.io/products/notecard/)↗️. + +# Overview +When I got a [Blues Notecard](https://blues.io/products/notecard/)↗️ for some testing, the first thing was of course to connect it to the WisBlock modules. After some initial testing like connecting the Notecard to my cellular provider and sending some sensor data, I was hungry for more. + +One of the requirements that often come up for location trackers is to have a combined LoRaWAN and cellular connectivity, both working as a fallback connection for the other. + +So, after building the [Hummingbird Sensor Network](https://github.com/beegee-tokyo/Hummingbird-Blues-Gateway)↗️, the logical next step was to crate a location tracker. + +---- + +# Setup the WisBlock Blues Location Tracker + +---- + +## Hardware +The only thing that requires some work is to setup the WisBlock system with the Blues Notecard using the [RAK13102 NoteCarrier](https://store.rakwireless.com/collections/wisblock-wireless)↗️. The RAK13102 plugs into the WisBlock Base Board IO slot, so only the RAK19007, RAK19001, RAK19010 or RAK19011 Base Boards can be used. +The RAK13102 module blocks the Sensor Slots A and B, but it has a mirror of these two slots, so they still can be used. +Optional you can add a RAK1906 environment sensor to the WisBlock Base Board. + +The code in this repository supports beside of the communication to the Blues Notecard, the LoRaWAN connection and a RAK1906 environment sensor. + +| Module | Function | Storepage | +| --- | --- | --- | +| Blues NoteCard | Cellular modem | [Choose one for your region](https://shop.blues.io/collections/notecard?_gl=1*1ikl0yz*_ga*MTA3NTk4Nzc2My4xNjg5NzI0NjI3*_ga_PJ7RGMWWBX*MTY5MzY0NjI5NS4xMzguMS4xNjkzNjQ2OTg2LjU2LjAuMA..&_ga=2.90751256.308929740.1693641831-1075987763.1689724627) ↗️ | +| RAK4631 | MCU & LoRa transceiver | [RAK4630](https://store.rakwireless.com/products/rak4631-lpwan-node) ↗️ | +| RAK13102 | WisBlock NoteCarrier for Blues NoteCard | [RAK13102](https://store.rakwireless.com/collections/wisblock-wireless) ↗️ | +| RAK1906 (optional) | Temperature and humidity sensor | [RAK1906](https://store.rakwireless.com/products/rak1906-bme680-environment-sensor) ↗️ | + +
RAKstarRAKstarRAKstar
+ +The enclosure is 3D printed and the STEP files are available in the [Enclosure folder](./Enclosure)↗️ in this repo. +The latest version has an additional opening on the side for a small slider switch. This slider switch does disconnect the battery from the WisBlock to shut the device complete down. + +## Setup + +You have to setup your Notecard at Blues.IO before it can be used. There are two options to setup the NoteCard. + +Option one is to follow the very good [Quickstart](https://dev.blues.io/quickstart/)↗️ guides provided by Blues. + +Option two is to setup the device with AT commands directly through the WisBlock's USB. + +### Option one, NoteCard Setup through the USB of the RAK13102 NoteCard + +Connect the RAK13102 NoteCarriers USB to your computer (WisBlock has to be powered separate!) and use the [Blues Quickstart](https://dev.blues.io/quickstart/)↗️ + +### Option two, setup through AT commands + +#### ⚠️ IMPORTANT ⚠️ +If setting up the NoteCard through AT commands, these settings will always override settings that are stored in the NoteCard. +To remove settings saved from AT commands use the AT command _**`ATC+BR`**_ to delete all settings saved from AT commands before. + +Connect the WisBlock USB port to your computer and connect a serial terminal application to the COM port. + +#### Setup the Product UID +To connect the Blues Notecard to the NoteHub, a _**Product UID**_ is required. This product UID is created when you create your project in NoteHub as shown in [Set up Notehub](https://dev.blues.io/quickstart/notecard-quickstart/notecard-and-notecarrier-f/#set-up-notehub)↗️. + +Get the Product UID from your NoteHub project: +
Product UID
+ +Then use the ATC+BEUI command to save the Product UID in the WisBlock: + +_**`ATC+BEUI=com.my-company.my-name:my-project`**_ + +Replace `com.my-company.my-name:my-project` with your project EUI. + +The current product UID can be queried with + +_**`ATC+BEUI=?`**_ + +#### Select SIM card +There are two options for the Blues NoteCard to connect. The primary option is to use the eSIM that is already on the NoteCard. However, there are countries where the eSIM is not working yet. In this case you need to use an external SIM card in the RAK13102 WisBlock module. This can be a SIM card from you local cellular provider or a IoT data SIM card like for example a SIM card from [Monogoto](https://monogoto.io/)↗️ or from another provider. You can purchase a MonoGoto card together with the Blues Notecard from the RAKwireless store [IoT SIM card for WisNode Modules](https://store.rakwireless.com/products/iot-sim-card-for-wisnode-modules?variant=42658018787526) + +Use the AT command ATC+BSIM to select the SIM card to be used. + +The syntax is _**`ATC+BSIM=:`**_ +`` == 0 to use the eSIM of the NoteCard +`` == 1 to use the external SIM card of the RAK13102 NoteCarrier + +If the external SIM card is selected, the next parameter is the APN that is required to connect the NoteCard +`` e.g. _**`internet`**_ to use with the Filipino network provider SMART. +Several carriers will have a website dedicated to manually configuring devices, while other can be discovered using APN discovery websites like [apn.how](https://apn.how/)↗️ + +The current settings can be queried with +_**`AT+BSIM=?`**_ + +#### Select NoteCard connection mode +The Blues NoteCard supports different connection modes. For testing purposes it might be required to have the NoteCard connected continuously to the cellular network, but in an battery powered application, the prefered connection type would be minimal, which connects to the cellular network only when data needs to be transfered. + +The connection mode can be setup with the AT command AT+BMOD. + +The syntax is _**`AT+BMOD=`**_ +`` == 0 to use the minimal connection mode +`` == 1 to use the continuous connection mode + +Default is to use minimal connection mode. + +The current status can be queried with +_**`AT+BMOD=?`**_. + +#### Select NoteCard location send trigger +##### ⚠️ _Motion trigger mode is not implemented yet_ ⚠️ + +There are two location transmission modes. Either in a defined timer interval or triggered by motion of the device. +The transmission mode can be set with the AT+BTRIG command. + +The syntax is _**`AT+BTRIG=`**_ +`` == 0 to use the time interval set with the AT command _**AT+SENDINT**_ +`` == 1 to use the continuous connection mode + +Default is to use time interval mode. + +The current status can be queried with +_**`AT+BTRIG=?`**_. + +#### Delete Blues NoteCard settings +If required all stored Blues NoteCard settings can be deleted from the WisBlock Core module with the AT+BR command. +##### ⚠️ _Requires restart or power cycle of the device_ ⚠️ + +The syntax is _**`AT+BR`**_ + +### ⚠️ _LoRaWAN Setup_ ⚠️ +Beside of the cellular connection, you need to setup as well the LoRaWAN connection. The WisBlock solutions can be connected to any LoRaWAN server like Helium, Chirpstack, TheThingsNetwork or others. Details how to setup the device on a LNS are available in the [RAK Documentation Center](). + +On the device itself, the required setup with AT commands is +```log + // Setup AppEUI +AT+APPEUI=70b3d57ed00201e1 + // Setup DevEUI +AT+DEVEUI=ac1f09fffe03efdc + // Setup AppKey +AT+APPKEY=2b84e0b09b68e5cb42176fe753dcee79 + // Set automatic send interval in seconds +AT+SENDINT=60 + // Set data rate +AT+DR=3 + // Set LoRaWAN region (here US915) +AT+BAND=5 + // Reset node to save the new parameters +ATZ + // After reboot, start join request +AT+JOIN=1,0,8,10 +``` +A detailed manual for the AT commands are in the [AT-Command-Manual](https://docs.rakwireless.com/RUI3/Serial-Operating-Modes/AT-Command-Manual/) ↗️ + +---- + +## Using the WisBlock Blues Tracker + +Once the WisBlock Blues Tracker is setup for both cellular and LoRaWAN connection, it will connect to the cellular network and join the LoRaWAN server. +Independent of a successful connection it will start acquiring the location with the GNSS engine that is built into the NoteCards cellular modem. + +The current application is not yet (work in progress) sending data based on movement, only in the specified time interval. The send interval can be setup with an AT command as well: + +_**`ATC+SENDINT=300`**_ +will set the sendinterval to 300 seconds. + +The current send interval can be queried with +_**`ATC+SENDINT=?`**_ + +### ⚠️ _Inaccurate location_ ⚠️ +As with most location trackers, an accurate location requires that the GNSS antenna can actually receive signals from the satellites. This means that it is working badly or not at all inside buildings. +If there is no GNSS location available, the device is using the tower location information from the Blues NoteCard instead! + +---- + + +# WisBlock Blues Tracker in Action + +---- + +## LoRaWAN server + +For testing, I used Chirpstack V4 as LoRaWAN server. The tracker has to be setup with it's DevEUI and AppEUI in an application on the Chirpstack LNS. +Optional you can add a payload decoder in the Device Profile. Then you can see the decoded payload in the Events list of the device. +Here is an example log output with the result of the CayenneLPP data parson the LNS before it is sent to the Blues NoteHub: +
Gateway Log
+ +Within the Chirpstack LNS application an integration is needed to forward the data to Datacake, the tool I chose for the visualization. The integration is a simple web hook to Datacake: +
Notehub Events Log
+ +You can of course use as well other LoRaWAN servers like TTN or Helium for the devices LoRaWAN connection. +For the location visualization, only the Datacake solution is explained here. If you want to use another location visualization, you need to figure out how to connect one device through both LoRaWAN and cellular connections. + +---- + +## Blues Notehub +The notes sent to the Blues Notehub can be seen in the _**Events**_ listing of the Nothub +
Notehub Events Log
+ +The location and sensor data is sent as binary payload, so there is nothing to see here in the body field. + +Next step is to create the _**Route**_ in NoteHub that forwards the data to Datacake. +Instead of the default URL for the Datacake route, we use the URL for LoRaWAN devices (read on below why we do this). +And the note we want to forward is the _**`data.qo`**_ note. + +
Notehub Route Setup
+ +### ⚠️ INFO ⚠️ +At this point it is getting a little bit complicate. Because the location data sent to Datacake can come _**EITHER**_ from the LoRaWAN server _**OR**_ from NoteHub.IO. The JSON object sent by the two looks of course very different. + +Because of the different formats, we use a very appreciated feature available in the NoteHub Routes, the JSONata Expression. With this data transformation option, we make the JSON packet coming from the NoteHub to look like a packet coming from a LoRaWAN server. I suggest to read the Blues documentation about [JSONata](https://dev.blues.io/guides-and-tutorials/notecard-guides/using-jsonata-to-transform-json/?_gl=1*15bxcs8*_ga*MTA3NTk4Nzc2My4xNjg5NzI0NjI3*_ga_PJ7RGMWWBX*MTY5MzcyMDAwNi4xNDAuMS4xNjkzNzIwMDExLjU1LjAuMA..&_ga=2.15364470.1351755121.1693639635-1075987763.1689724627#using-jsonata-to-transform-json) to understand how it actually works. + +The JSONata expression needed is very simple, we can simulate a LoRaWAN packet format with just a few JSON fields: +```JSON +{ + "deviceInfo": { + "tenantName":"ChirpStack", + "devEui": body.dev_eui + }, + "fPort": 6, + "data": payload +} +``` + +In the Route setup scroll down to the Data section. +Select JSONata Expression to transform the data, then copy the JSONata expression into the entry field. +
JSONata Exerciser
+ +The JSONata is pulling the required info from the Blues JSON data packet to build the "fake" LoRaWAN packet. You can check the functionality with the JSONata Exerciser: +
JSONata Exerciser
+ +The resulting JSON object is then sent to Datacake, which handles it as if it comes from a LoRaWAN server. + +The routing events are shown in the Routes log view: +
Notehub Routed Log
+ +---- + +## Datacake + +To visualize the data in Datacake a matching device has to be defined. As the data can come from two different paths, but we transformed the packet forward in NoteHub to be look like a LoRaWAN packet, the device _**must**_ be a LoRaWAN device. + +### ⚠️ INFO ⚠️ +On the device the payload is formatted in Cayenne LPP format. Both the LoRaWAN server and NoteHub are forwarding this format, so a single payload decoder can be used. +To distinguish whether the data is coming from the LNS or from NoteHub, a different fPort is used in the packets. +fPort 5 ==> data coming from the LNS +fPort 6 ==> data coming from NoteHub (see above in the JSONata expression that it sets the fPort to 6) + +The payload decoder I used can be found in the file [Decoder.js](./Decoder.js)↗️ in this repository. +The content of this file has to be copied into the _**Payload Decoder**_ of the device configuration in Datacake: +
Payload Decoder
+ +---- + +Then the matching fields for the sensor data have to been created. The easiest way to do this is to wait for incoming data from the sensors. If no matching field is existing, the data will be shown in the _**Suggested Fields**_ list in the configuration. +
Suggested Fields
+ +The sensor data can be easily assigned to fields using the _**Create Field**_ button. + +It will take some time before the suggested fields are listed complete. Instead of using the suggested fields, you can as well just create the following fields manually: + +| Name | Identifier | Type | Role | +| --- | --- | --- | --- | +| Voltage | VOLTAGE_1 | Float | Device Battery | +| Source | SOURCE | String | Primary | +| Islorawan | ISLORAWAN | Boolean | N/A | +| Location | LOCATION_10 | Location | Device Location | +| Temperature (only if RAK1906 is present) | TEMPERATURE | Float | Secondary | +| Humidity (only if RAK1906 is present) | HUMIDITY | Float | N/A | +| Barometer (only if RAK1906 is present) | BAROMETER | Float | N/A | + +
Create Fields
+---- + +Once all the sensor data is assigned to fields, we can start with the visualization of the data. +
Created Fields
+ + +---- + +In Datacake each device has it's own _**Device Dashboard**_ which we will use to display the location data. +I will not go into details how to create visualization widgets in Datacake, this step is handled in other tutorials already. + +---- + +The final result for the WisBlock Blues Tracker: + +You can see life data on my [public dashboard](https://app.datacake.de/pd/7bb04747-c5a1-42c3-8dc1-ee5de45ee610)↗️ +In the top part of the dashboard are the locations of the device (history enabled) on the map and device sensor values on the side (temperature, humidity are only available if a RAK1906 is present). +
Locations
+ +In the lower part a chart is showing at what times the sensor used LoRaWAN to transmit data and when it used the cellular connection: +
Sensor Gateway
+ +---- +---- + +# LoRa® is a registered trademark or service mark of Semtech Corporation or its affiliates. + + +# LoRaWAN® is a licensed mark. + +---- +---- \ No newline at end of file diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Blues-Io-Logo-Bloack-150px.png b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Blues-Io-Logo-Bloack-150px.png new file mode 100644 index 0000000..f552645 Binary files /dev/null and b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Blues-Io-Logo-Bloack-150px.png differ diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/BluesLogomark.png b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/BluesLogomark.png new file mode 100644 index 0000000..8395204 Binary files /dev/null and b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/BluesLogomark.png differ diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Chirpstack-Integration.png b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Chirpstack-Integration.png new file mode 100644 index 0000000..a7605e7 Binary files /dev/null and b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Chirpstack-Integration.png differ diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Datacake-Create-Fields.png b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Datacake-Create-Fields.png new file mode 100644 index 0000000..eea003f Binary files /dev/null and b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Datacake-Create-Fields.png differ diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Datacake-Created-Fields.png b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Datacake-Created-Fields.png new file mode 100644 index 0000000..c5e5293 Binary files /dev/null and b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Datacake-Created-Fields.png differ diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Datacake-Dashboard-1.png b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Datacake-Dashboard-1.png new file mode 100644 index 0000000..6aeb38c Binary files /dev/null and b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Datacake-Dashboard-1.png differ diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Datacake-Dashboard-2.png b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Datacake-Dashboard-2.png new file mode 100644 index 0000000..3f483a9 Binary files /dev/null and b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Datacake-Dashboard-2.png differ diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Datacake-Payload-Decoder.png b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Datacake-Payload-Decoder.png new file mode 100644 index 0000000..6b856b4 Binary files /dev/null and b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Datacake-Payload-Decoder.png differ diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Datacake-Suggested-Fields.png b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Datacake-Suggested-Fields.png new file mode 100644 index 0000000..1c2b3f1 Binary files /dev/null and b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Datacake-Suggested-Fields.png differ diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/JSONata-exerciser.png b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/JSONata-exerciser.png new file mode 100644 index 0000000..d7e4fff Binary files /dev/null and b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/JSONata-exerciser.png differ diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Notehub-Event-Log.png b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Notehub-Event-Log.png new file mode 100644 index 0000000..b91e64b Binary files /dev/null and b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Notehub-Event-Log.png differ diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Notehub-Product-UID.png b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Notehub-Product-UID.png new file mode 100644 index 0000000..27d2f78 Binary files /dev/null and b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Notehub-Product-UID.png differ diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Notehub-Routes-Log.png b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Notehub-Routes-Log.png new file mode 100644 index 0000000..bd9ae01 Binary files /dev/null and b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Notehub-Routes-Log.png differ diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Notehub-Routes-Setup.png b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Notehub-Routes-Setup.png new file mode 100644 index 0000000..b987b80 Binary files /dev/null and b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Notehub-Routes-Setup.png differ diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Notehub-Routes-Transform.png b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Notehub-Routes-Transform.png new file mode 100644 index 0000000..86a4eb7 Binary files /dev/null and b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/Notehub-Routes-Transform.png differ diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/RAK-Whirls.png b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/RAK-Whirls.png new file mode 100644 index 0000000..e0def36 Binary files /dev/null and b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/RAK-Whirls.png differ diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/blues_logo.jpg b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/blues_logo.jpg new file mode 100644 index 0000000..7a20847 Binary files /dev/null and b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/blues_logo.jpg differ diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/hardware.jpg b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/hardware.jpg new file mode 100644 index 0000000..2f00360 Binary files /dev/null and b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/hardware.jpg differ diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/hardware_2.jpg b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/hardware_2.jpg new file mode 100644 index 0000000..18db51a Binary files /dev/null and b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/hardware_2.jpg differ diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/hardware_3.jpg b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/hardware_3.jpg new file mode 100644 index 0000000..06f86ba Binary files /dev/null and b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/hardware_3.jpg differ diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/log_gateway.png b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/log_gateway.png new file mode 100644 index 0000000..deff79b Binary files /dev/null and b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/log_gateway.png differ diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/rakstar.jpg b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/rakstar.jpg new file mode 100644 index 0000000..f4ebb59 Binary files /dev/null and b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/assets/rakstar.jpg differ diff --git a/examples/RAK4630/solutions/Blues-WisBlock-Tracker/blues.cpp b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/blues.cpp new file mode 100644 index 0000000..295eb9f --- /dev/null +++ b/examples/RAK4630/solutions/Blues-WisBlock-Tracker/blues.cpp @@ -0,0 +1,506 @@ +/** + * @file blues.cpp + * @author Bernd Giesecke (bernd@giesecke.tk) + * @brief Blues.IO NoteCard handler + * @version 0.1 + * @date 2023-04-27 + * + * @copyright Copyright (c) 2023 + * + */ +#include "main.h" + +#ifndef PRODUCT_UID +#define PRODUCT_UID "com.my-company.my-name:my-project" +#endif +#define myProductID PRODUCT_UID + +Notecard notecard; + +void blues_attn_cb(void); + +J *req; + +/** + * @brief Initialize Blues NoteCard + * + * @return true if NoteCard was found and setup was successful + * @return false if NoteCard was not found or the setup failed + */ +bool init_blues(void) +{ + Wire.begin(); + notecard.begin(); + + // Get the ProductUID from the saved settings + // If no settings are found, use NoteCard internal settings! + if (read_blues_settings()) + { + Serial.printf("Found saved settings, override NoteCard internal settings!\n"); + if (memcmp(g_blues_settings.product_uid, "com.my-company.my-name", 22) == 0) + { + Serial.printf("No Product ID saved\n"); + AT_PRINTF(":EVT NO PUID"); + memcpy(g_blues_settings.product_uid, PRODUCT_UID, 33); + } + + Serial.printf("Set Product ID and connection mode\n"); + if (blues_start_req("hub.set")) + { + JAddStringToObject(req, "product", g_blues_settings.product_uid); + if (g_blues_settings.conn_continous) + { + JAddStringToObject(req, "mode", "continuous"); + } + else + { + JAddStringToObject(req, "mode", "minimum"); + } + // Set sync time to 20 times the sensor read time + JAddNumberToObject(req, "seconds", (g_lorawan_settings.send_repeat_time * 20 / 1000)); + JAddBoolToObject(req, "heartbeat", true); + + if (!blues_send_req()) + { + Serial.printf("hub.set request failed\n"); + return false; + } + } + else + { + Serial.printf("hub.set request failed\n"); + return false; + } + +#if USE_GNSS == 1 + Serial.printf("Set location mode\n"); + if (blues_start_req("card.location.mode")) + { + // Continous GNSS mode + // JAddStringToObject(req, "mode", "continous"); + + // Periodic GNSS mode + JAddStringToObject(req, "mode", "periodic"); + + // Set location acquisition time to the sensor read time + JAddNumberToObject(req, "seconds", (g_lorawan_settings.send_repeat_time / 2000)); + JAddBoolToObject(req, "heartbeat", true); + if (!blues_send_req()) + { + Serial.printf("card.location.mode request failed\n"); + return false; + } + } + else + { + Serial.printf("card.location.mode request failed\n"); + return false; + } +#else + Serial.printf("Stop location mode\n"); + if (blues_start_req("card.location.mode")) + { + // GNSS mode off + JAddStringToObject(req, "mode", "off"); + if (!blues_send_req()) + { + Serial.printf("card.location.mode request failed\n"); + return false; + } + } + else + { + Serial.printf("card.location.mode request failed\n"); + return false; + } +#endif + + /// \todo reset attn signal needs rework + // pinMode(WB_IO5, INPUT); + // if (g_blues_settings.motion_trigger) + // { + // if (blues_start_req("card.attn")) + // { + // JAddStringToObject(req, "mode", "disarm"); + // if (!blues_send_req()) + // { + // Serial.printf("card.attn request failed\n"); + // } + + // if (!blues_enable_attn()) + // { + // return false; + // } + // } + // } + // else + // { + // Serial.printf("card.attn request failed\n"); + // return false; + // } + + Serial.printf("Set APN\n"); + // {“req”:”card.wireless”} + if (blues_start_req("card.wireless")) + { + JAddStringToObject(req, "mode", "auto"); + + if (g_blues_settings.use_ext_sim) + { + // USING EXTERNAL SIM CARD + JAddStringToObject(req, "apn", g_blues_settings.ext_sim_apn); + JAddStringToObject(req, "method", "dual-secondary-primary"); + } + else + { + // USING BLUES eSIM CARD + JAddStringToObject(req, "method", "primary"); + } + if (!blues_send_req()) + { + Serial.printf("card.wireless request failed\n"); + return false; + } + } + else + { + Serial.printf("card.wireless request failed\n"); + return false; + } + +#if IS_V2 == 1 + // Only for V2 cards, setup the WiFi network + Serial.printf("Set WiFi\n"); + if (blues_start_req("card.wifi")) + { + JAddStringToObject(req, "ssid", "-"); + JAddStringToObject(req, "password", "-"); + JAddStringToObject(req, "name", "RAK-"); + JAddStringToObject(req, "org", "RAK-PH"); + JAddBoolToObject(req, "start", false); + + if (!blues_send_req()) + { + Serial.printf("card.wifi request failed\n"); + } + } + else + { + Serial.printf("card.wifi request failed\n"); + return false; + } +#endif + } + + // {"req": "card.version"} + if (blues_start_req("card.version")) + { + if (!blues_send_req()) + { + Serial.printf("card.version request failed\n"); + } + } + return true; +} + +/** + * @brief Send a data packet to NoteHub.IO + * + * @param data Payload as byte array (CayenneLPP formatted) + * @param data_len Length of payload + * @return true if note could be sent to NoteCard + * @return false if note send failed + */ +bool blues_send_payload(uint8_t *data, uint16_t data_len) +{ + if (blues_start_req("note.add")) + { + JAddStringToObject(req, "file", "data.qo"); + JAddBoolToObject(req, "sync", true); + J *body = JCreateObject(); + if (body != NULL) + { + char node_id[24]; + sprintf(node_id, "%02x%02x%02x%02x%02x%02x%02x%02x", + g_lorawan_settings.node_device_eui[0], g_lorawan_settings.node_device_eui[1], + g_lorawan_settings.node_device_eui[2], g_lorawan_settings.node_device_eui[3], + g_lorawan_settings.node_device_eui[4], g_lorawan_settings.node_device_eui[5], + g_lorawan_settings.node_device_eui[6], g_lorawan_settings.node_device_eui[7]); + JAddStringToObject(body, "dev_eui", node_id); + + JAddItemToObject(req, "body", body); + + JAddBinaryToObject(req, "payload", data, data_len); + + Serial.printf("Finished parsing\n"); + if (!blues_send_req()) + { + Serial.printf("Send request failed\n"); + return false; + } + return true; + } + else + { + Serial.printf("Error creating body\n"); + } + } + return false; +} + +/** + * @brief Create a request structure to be sent to the NoteCard + * + * @param request_name name of request, e.g. card.wireless + * @return true if request could be created + * @return false if request could not be created + */ +bool blues_start_req(String request_name) +{ + req = notecard.newRequest(request_name.c_str()); + if (req != NULL) + { + return true; + } + return false; +} + +/** + * @brief Send a completed request to the NoteCard. + * + * @return true if request could be sent and the response does not have "err" + * @return false if request could not be sent or the response did have "err" + */ +bool blues_send_req(void) +{ + char *json = JPrintUnformatted(req); + Serial.printf("Card request = %s\n", json); + + J *rsp; + rsp = notecard.requestAndResponse(req); + if (rsp == NULL) + { + return false; + } + json = JPrintUnformatted(rsp); + if (JIsPresent(rsp, "err")) + { + Serial.printf("Card error response = %s\n", json); + notecard.deleteResponse(rsp); + return false; + } + Serial.printf("Card response = %s\n", json); + notecard.deleteResponse(rsp); + + return true; +} + +/** + * @brief Request NoteHub status, mainly for debug purposes + * + */ +void blues_hub_status(void) +{ + blues_start_req("hub.status"); + if (!blues_send_req()) + { + Serial.printf("hub.status request failed\n"); + } +} + +/** + * @brief Get the location information from the NoteCard + * + * @return true if a location could be acquired + * @return false if request failed or no location is available + */ +bool blues_get_location(void) +{ + bool result = false; + if (blues_start_req("card.location")) + { + J *rsp; + rsp = notecard.requestAndResponse(req); + if (rsp == NULL) + { + Serial.printf("card.location failed, report no location\n"); + return false; + } + char *json = JPrintUnformatted(rsp); + Serial.printf("Card response = %s\n", json); + + if (JHasObjectItem(rsp, "lat") && JHasObjectItem(rsp, "lat")) + { + float blues_latitude = JGetNumber(rsp, "lat"); + float blues_longitude = JGetNumber(rsp, "lon"); + float blues_altitude = 0; + + if ((blues_latitude == 0.0) && (blues_longitude == 0.0)) + { + Serial.printf("No valid GPS data, report no location\n"); + } + else + { + Serial.printf("Got location Lat %.6f Long %0.6f\n", blues_latitude, blues_longitude); + g_solution_data.addGNSS_6(LPP_CHANNEL_GPS, (uint32_t)(blues_latitude * 10000000), (uint32_t)(blues_longitude * 10000000), blues_altitude); + result = true; + } + } + + notecard.deleteResponse(rsp); + } + + if (!result) + { + // No GPS coordinates, get last tower location + if (blues_start_req("card.time")) + { + J *rsp; + rsp = notecard.requestAndResponse(req); + if (rsp == NULL) + { + Serial.printf("card.time failed, report no location\n"); + return false; + } + char *json = JPrintUnformatted(rsp); + Serial.printf("Card response = %s\n", json); + + if (JHasObjectItem(rsp, "lat") && JHasObjectItem(rsp, "lat")) + { + float blues_latitude = JGetNumber(rsp, "lat"); + float blues_longitude = JGetNumber(rsp, "lon"); + float blues_altitude = 0; + + if ((blues_latitude == 0.0) && (blues_longitude == 0.0)) + { + Serial.printf("No valid GPS data, report no location\n"); + } + else + { + Serial.printf("Got tower location Lat %.6f Long %0.6f\n", blues_latitude, blues_longitude); + g_solution_data.addGNSS_6(LPP_CHANNEL_GPS, (uint32_t)(blues_latitude * 10000000), (uint32_t)(blues_longitude * 10000000), blues_altitude); + result = true; + } + } + + notecard.deleteResponse(rsp); + } + } + + // Clear last GPS location + if (blues_start_req("card.location.mode")) + { + JAddBoolToObject(req, "delete", true); + J *rsp; + rsp = notecard.requestAndResponse(req); + if (rsp == NULL) + { + Serial.printf("card.location.mode\n"); + } + char *json = JPrintUnformatted(rsp); + Serial.printf("Card response = %s\n", json); + notecard.deleteResponse(rsp); + } + return result; +} + +/** + * @brief Enable ATTN interrupt + * At the moment enables only the alarm on motion + * + * @return true if ATTN could be enabled + * @return false if ATTN could not be enabled + */ +bool blues_enable_attn(void) +{ + Serial.printf("Enable ATTN on motion\n"); + if (blues_start_req("card.attn")) + { + JAddStringToObject(req, "mode", "motion"); + if (!blues_send_req()) + { + Serial.printf("card.attn request failed\n"); + return false; + } + } + else + { + Serial.printf("Request creation failed\n"); + } + attachInterrupt(WB_IO5, blues_attn_cb, RISING); + + Serial.printf("Arm ATTN on motion\n"); + if (blues_start_req("card.attn")) + { + JAddStringToObject(req, "mode", "arm"); + if (!blues_send_req()) + { + Serial.printf("card.attn request failed\n"); + return false; + } + } + else + { + Serial.printf("Request creation failed\n"); + } + return true; +} + +/** + * @brief Disable ATTN interrupt + * + * @return true if ATTN could be disabled + * @return false if ATTN could not be disabled + */ +bool blues_disable_attn(void) +{ + Serial.printf("Disable ATTN on motion\n"); + detachInterrupt(WB_IO5); + + if (blues_start_req("card.attn")) + { + JAddStringToObject(req, "mode", "disarm"); + if (!blues_send_req()) + { + Serial.printf("card.attn request failed\n"); + } + } + else + { + Serial.printf("Request creation failed\n"); + } + if (blues_start_req("card.attn")) + { + JAddStringToObject(req, "mode", "-motion"); + if (!blues_send_req()) + { + Serial.printf("card.attn request failed\n"); + } + } + else + { + Serial.printf("Request creation failed\n"); + } + + return true; +} + +/** + * @brief Get the reason for the ATTN interrup + * /// \todo work in progress + * @return String reason /// \todo return value not final yet + */ +String blues_attn_reason(void) +{ + return ""; +} + +/** + * @brief Callback for ATTN interrupt + * Wakes up the app_handler with an BLUES_ATTN event + * + */ +void blues_attn_cb(void) +{ + api_wake_loop(BLUES_ATTN); +} diff --git a/examples/common/communications/Cellular/RAK13102_Blues_Module/WisBlock-Blues-Sensor/WisBlock-Blues-Sensor.ino b/examples/common/communications/Cellular/RAK13102_Blues_Module/WisBlock-Blues-Sensor/WisBlock-Blues-Sensor.ino new file mode 100644 index 0000000..efc8a17 --- /dev/null +++ b/examples/common/communications/Cellular/RAK13102_Blues_Module/WisBlock-Blues-Sensor/WisBlock-Blues-Sensor.ino @@ -0,0 +1,262 @@ +// +// Copyright 2019 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. +// +// This example contains the complete source for the Sensor Tutorial at dev.blues.io +// https://dev.blues.io/build/tutorials/sensor-tutorial/notecarrier-af/esp32/arduino-wiring/ +// +// This tutorial requires an external Adafruit BME680 Sensor. +// + +// ********************************************************************** +// ********************************************************************** +// ********************************************************************** +// This example is adapted to work with RAKwireless WisBlock Core module +// It works with the following modules: +// WisBlock Core +// RAK4631 +// RAK11200 +// RAK11310 +// WisBlock Base Boards +// RAK19007 +// RAK19001 +// WisBlock Sensor +// RAK1906 +// ********************************************************************** +// ********************************************************************** +// ********************************************************************** + +// Include the Arduino library for the Notecard +#include // Click to install library: http://librarymanager/All#Blues-Wireless-Notecard +#ifdef NRF52 +#include +#endif +#include +#include // Click to install library: http://librarymanager/All#Adafruit-BME680 + +Adafruit_BME680 bmeSensor(&Wire); + +// This is the unique Product Identifier for your device +#ifndef PRODUCT_UID +#define PRODUCT_UID "" // "com.my-company.my-name:my-project" +#pragma message "PRODUCT_UID is not defined in this example. Please ensure your Notecard has a product identifier set before running this example or define it in code here. More details at https://dev.blues.io/tools-and-sdks/samples/product-uid" +#endif + +#define myProductID PRODUCT_UID +Notecard notecard; + +uint8_t sync_request_counter = 59; + +bool has_rak1906 = false; + +J *req; + +char prn_buff[2048]; + +bool blues_send_req(void) +{ + char *json = JPrintUnformatted(req); + Serial.printf("Card request = %s\n", json); + + notecard.sendRequest(req); + // J *rsp; + // rsp = notecard.requestAndResponse(req); + // if (rsp == NULL) + // { + // return false; + // } + // json = JPrintUnformatted(rsp); + // Serial.printf("Card response = %s\n", json); + // notecard.deleteResponse(rsp); + + return true; +} + +// One-time Arduino initialization +void setup() +{ + pinMode(LED_BUILTIN, OUTPUT); + // Initialize Serial for debug output + Serial.begin(115200); + + time_t serial_timeout = millis(); + // On nRF52840 the USB serial is not available immediately + while (!Serial) + { + if ((millis() - serial_timeout) < 5000) + { + delay(100); + digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); + } + else + { + break; + } + } + digitalWrite(LED_BUILTIN, LOW); + + Serial.println("**********************************************"); + Serial.println("Blues Notecard Test"); + Serial.println("**********************************************"); + + notecard.setDebugOutputStream(Serial); + + // Initialize the physical I/O channel to the Notecard + Wire.begin(); + delay(500); + notecard.begin(); + + Serial.println("**********************************************"); + Serial.println("Hub setup:"); +// req = notecard.newRequest("hub.set"); +// if (myProductID[0]) +// { +// JAddStringToObject(req, "product", myProductID); +// } +// JAddStringToObject(req, "mode", "continuous"); +// blues_send_req(); + + Serial.println("**********************************************"); + + if (!bmeSensor.begin(0x76)) + { + Serial.println("Could not find a valid BME680 sensor..."); + } + else + { + Serial.println("BME680 Connected..."); + bmeSensor.setTemperatureOversampling(BME680_OS_8X); + bmeSensor.setHumidityOversampling(BME680_OS_2X); + + has_rak1906 = true; + } + + delay(2000); + + Serial.println("Get hub sync status:"); + // {“req”:”hub.sync.status”} + req = notecard.newRequest("hub.sync.status"); + blues_send_req(); + + Serial.println("**********************************************"); + delay(2000); + + Serial.println("Get note card status:"); + // {“req”:”card.wireless”} + req = notecard.newRequest("card.wireless"); + blues_send_req(); + + Serial.println("**********************************************"); + delay(2000); + + Serial.println("Get note card version:"); + // {“req”:”card.wireless”} + req = notecard.newRequest("card.version"); + blues_send_req(); + + Serial.println("**********************************************"); + delay(2000); +} + +void loop() +{ + Serial.println("**********************************************"); + Serial.println("Get hub sync status:"); + // {“req”:”hub.sync.status”} + req = notecard.newRequest("hub.sync.status"); + blues_send_req(); + + Serial.println("**********************************************"); + Serial.println("Get hub status:"); + // {“req”:”hub.status”} + req = notecard.newRequest("hub.status"); + blues_send_req(); + + Serial.println("**********************************************"); + delay(2000); + + Serial.println("Get note card status:"); + // {“req”:”card.wireless”} + req = notecard.newRequest("card.wireless"); + blues_send_req(); + + Serial.println("**********************************************"); + delay(2000); + + float temperature, humidity, pressure; + if (has_rak1906) + { + if (!bmeSensor.performReading()) + { + Serial.println("Failed to obtain a reading..."); + delay(15000); + return; + } + + temperature = bmeSensor.temperature; + humidity = bmeSensor.humidity; + pressure = (float)bmeSensor.pressure / 100.0; + + Serial.printf("Temperature = %.2f *C\n", bmeSensor.temperature); + Serial.printf("Humidity = %.2f %%RH\n", bmeSensor.humidity); + Serial.printf("Pressure = %.2f mBar\n", (float)bmeSensor.pressure / 100.0); + } + else + { + temperature = 28.3; + humidity = 65.7; + pressure = 1008.05; + } + + req = notecard.newRequest("note.add"); + if (req != NULL) + { + JAddStringToObject(req, "file", "data.qo"); + // JAddBoolToObject(req, "sync", true); + + J *body = JCreateObject(); + if (body != NULL) + { + JAddStringToObject(body, "sensor", "rak4631"); + JAddNumberToObject(body, "temperature", temperature); + JAddNumberToObject(body, "humidity", humidity); + JAddNumberToObject(body, "pressure", pressure); + JAddItemToObject(req, "body", body); + } + + blues_send_req(); + } + + req = NoteNewRequest("card.location"); + + if (req != NULL) + { + blues_send_req(); + } + + sync_request_counter++; + if (sync_request_counter == 60) + { + sync_request_counter = 0; + Serial.println("Get hub sync status:"); + // {“req”:”hub.sync”} + req = notecard.newRequest("hub.sync"); + blues_send_req(); + + Serial.println("**********************************************"); + + for (uint8_t check = 0; check < 6; check++) + { + Serial.println("Get hub status:"); + // {“req”:”hub.sync.status”} + req = notecard.newRequest("hub.status"); + blues_send_req(); + + Serial.println("**********************************************"); + + delay(10000); + } + } + delay(60000); +}