diff --git a/README.md b/README.md index eaea51cd..a5149bf3 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,16 @@ Paxcounter generates identifiers for sniffed MAC adresses and collects them temp # Display -If you're using a device with OLED display, or if you add such one to the I2C bus, the device shows live data on the display. You can flip between pages showing pax, time, GPS and BME sensor data by pressing the button of the device. +If you're using a device with OLED display, or if you add such one to the I2C bus, the device shows live data on the display. You can flip display pages showing + +- recent count of pax +- histogram +- GPS data +- BME sensor data +- time of day +- blank page + +by pressing the button of the device. # Sensors and Peripherals @@ -312,8 +321,8 @@ Note: all settings are stored in NVRAM and will be reloaded when device starts. Example for EU868: DataRate Configuration Bit/s - 0 LoRa: SF12 / 125 kHz 250 - 1 LoRa: SF11 / 125 kHz 440 + 0 LoRa: SF12 / 125 kHz 250 + 1 LoRa: SF11 / 125 kHz 440 2 LoRa: SF10 / 125 kHz 980 3 LoRa: SF9 / 125 kHz 1760 4 LoRa: SF8 / 125 kHz 3125 diff --git a/include/cyclic.h b/include/cyclic.h index b9512c53..4b402360 100644 --- a/include/cyclic.h +++ b/include/cyclic.h @@ -5,6 +5,7 @@ #include "senddata.h" #include "rcommand.h" #include "spislave.h" + #if(HAS_LORA) #include #endif @@ -13,6 +14,10 @@ #include "bmesensor.h" #endif +#ifdef HAS_DISPLAY +#include "display.h" +#endif + extern Ticker housekeeper; void housekeeping(void); diff --git a/include/display.h b/include/display.h index bbaa568c..0e94b806 100644 --- a/include/display.h +++ b/include/display.h @@ -1,16 +1,26 @@ #ifndef _DISPLAY_H #define _DISPLAY_H -#include #include "cyclic.h" +#include "qrcode.h" extern uint8_t DisplayIsOn; -extern HAS_DISPLAY u8x8; - -void init_display(const char *Productname, const char *Version); void refreshTheDisplay(bool nextPage = false); +void init_display(uint8_t verbose = 0); void draw_page(time_t t, uint8_t page); -void DisplayKey(const uint8_t *key, uint8_t len, bool lsb); +void dp_printf(uint16_t x, uint16_t y, uint8_t font, uint8_t inv, + const char *format, ...); +void dp_printqr(uint16_t offset_x, uint16_t offset_y, const char *Message); +void oledfillRect(uint16_t x, uint16_t y, uint16_t width, uint16_t height, + uint8_t bRender); +void oledScrollBufferHorizontal(uint8_t *buf, const uint16_t width, + const uint16_t height, bool left = true); +void oledScrollBufferVertical(uint8_t *buf, const uint16_t width, + const uint16_t height, int offset = 0); +int oledDrawPixel(uint8_t *buf, const uint16_t x, const uint16_t y, + const uint8_t dot); +void oledPlotCurve(uint16_t count, bool reset); +void oledRescaleBuffer(uint8_t *buf, const int factor); #endif \ No newline at end of file diff --git a/include/globals.h b/include/globals.h index a753d5fb..76b733d4 100644 --- a/include/globals.h +++ b/include/globals.h @@ -41,9 +41,12 @@ #define BLE_MODE (0x40) #define SCREEN_MODE (0x80) +// length of display buffer for lmic event messages +#define LMIC_EVENTMSG_LEN 17 + // I2C bus access control #define I2C_MUTEX_LOCK() \ - (xSemaphoreTake(I2Caccess, pdMS_TO_TICKS(10)) == pdTRUE) + (xSemaphoreTake(I2Caccess, pdMS_TO_TICKS(DISPLAYREFRESH_MS)) == pdTRUE) #define I2C_MUTEX_UNLOCK() (xSemaphoreGive(I2Caccess)) enum sendprio_t { prio_low, prio_normal, prio_high }; @@ -104,9 +107,9 @@ extern std::set, Mallocator> macs; extern std::array::iterator it; extern std::array beacons; -extern configData_t cfg; // current device configuration -extern char lmic_event_msg[]; // display buffer -extern uint8_t volatile channel; // wifi channel rotation counter +extern configData_t cfg; // current device configuration +extern char lmic_event_msg[LMIC_EVENTMSG_LEN]; // display buffer +extern uint8_t volatile channel; // wifi channel rotation counter extern uint16_t volatile macs_total, macs_wifi, macs_ble, batt_voltage; // display values extern bool volatile TimePulseTick; // 1sec pps flag set by GPS or RTC diff --git a/include/ledmatrixdisplay.h b/include/ledmatrixdisplay.h index b7ec5a3b..b2863146 100644 --- a/include/ledmatrixdisplay.h +++ b/include/ledmatrixdisplay.h @@ -13,6 +13,6 @@ void refreshTheMatrixDisplay(bool nextPage = false); void DrawNumber(String strNum, uint8_t iDotPos = 0); uint8_t GetCharFromFont(char cChar); uint8_t GetCharWidth(char cChar); -void ScrollLeft(uint8_t *buf, const uint16_t cols, const uint16_t rows); +void ScrollMatrixLeft(uint8_t *buf, const uint16_t cols, const uint16_t rows); #endif \ No newline at end of file diff --git a/include/lorawan.h b/include/lorawan.h index a69aa4ba..4341b397 100644 --- a/include/lorawan.h +++ b/include/lorawan.h @@ -20,7 +20,6 @@ #include #endif -extern QueueHandle_t LoraSendQueue; extern TaskHandle_t lmicTask, lorasendTask; // table of LORAWAN MAC commands @@ -31,8 +30,8 @@ typedef struct { } mac_t; esp_err_t lora_stack_init(); +void lora_setupForNetwork(bool preJoin); void lmictask(void *pvParameters); -void onEvent(ev_t ev); void gen_lora_deveui(uint8_t *pdeveui); void RevBytes(unsigned char *b, size_t c); void get_hard_deveui(uint8_t *pdeveui); @@ -43,6 +42,7 @@ void showLoraKeys(void); void lora_send(void *pvParameters); void lora_enqueuedata(MessageBuffer_t *message); void lora_queuereset(void); +void myEventCallback(void *pUserData, ev_t ev); void myRxCallback(void *pUserData, uint8_t port, const uint8_t *pMsg, size_t nMsg); void myTxCallback(void *pUserData, int fSuccess); diff --git a/include/ota.h b/include/ota.h index 59bf876c..cec28750 100644 --- a/include/ota.h +++ b/include/ota.h @@ -4,6 +4,7 @@ #ifdef USE_OTA #include "globals.h" +#include #include #include #include @@ -14,11 +15,9 @@ int do_ota_update(); void start_ota_update(); int version_compare(const String v1, const String v2); -void display(const uint8_t row, const std::string status, - const std::string msg); -#ifdef HAS_DISPLAY +void ota_display(const uint8_t row, const std::string status, + const std::string msg); void show_progress(unsigned long current, unsigned long size); -#endif #endif // USE_OTA diff --git a/include/power.h b/include/power.h index 1ef571e1..1af87f09 100644 --- a/include/power.h +++ b/include/power.h @@ -19,6 +19,8 @@ void power_event_IRQ(void); void AXP192_power(bool on); void AXP192_init(void); void AXP192_showstatus(void); +uint8_t i2c_writeBytes(uint8_t addr, uint8_t reg, uint8_t *data, uint8_t len); +uint8_t i2c_readBytes(uint8_t addr, uint8_t reg, uint8_t *data, uint8_t len); #endif // HAS_PMU #endif \ No newline at end of file diff --git a/include/senddata.h b/include/senddata.h index 4a748445..0151f7e2 100644 --- a/include/senddata.h +++ b/include/senddata.h @@ -2,10 +2,15 @@ #define _SENDDATA_H #include "spislave.h" +#include "cyclic.h" + #if(HAS_LORA) #include "lorawan.h" #endif -#include "cyclic.h" + +#ifdef HAS_DISPLAY +#include "display.h" +#endif extern Ticker sendcycler; diff --git a/include/timesync.h b/include/timesync.h index c102c1c4..bf74700e 100644 --- a/include/timesync.h +++ b/include/timesync.h @@ -7,13 +7,16 @@ #include "timekeeper.h" //#define TIME_SYNC_TRIGGER 100 // threshold for time sync [milliseconds] -#define TIME_SYNC_FRAME_LENGTH 0x05 // timeserver answer frame length [bytes] +#define TIME_SYNC_FRAME_LENGTH 0x07 // timeserver answer frame length [bytes] #define TIME_SYNC_FIXUP 4 // calibration to fixup processing time [milliseconds] +#define TIMEREQUEST_MAX_SEQNO 0xf0 // threshold for wrap around seqno void timesync_init(void); void send_timesync_req(void); -int recv_timesync_ans(const uint8_t seq_no, const uint8_t buf[], const uint8_t buf_len); + +int recv_timesync_ans(const uint8_t buf[], uint8_t buf_len); + void process_timesync_req(void *taskparameter); void store_time_sync_req(uint32_t t_millisec); -#endif \ No newline at end of file +#endif diff --git a/platformio.ini b/platformio.ini index 3d1eda59..1975fe69 100644 --- a/platformio.ini +++ b/platformio.ini @@ -43,7 +43,7 @@ description = Paxcounter is a device for metering passenger flows in realtime. I [common] ; for release_version use max. 10 chars total, use any decimal format like "a.b.c" -release_version = 1.8.34 +release_version = 1.9.6 ; DEBUG LEVEL: For production run set to 0, otherwise device will leak RAM while running! ; 0=None, 1=Error, 2=Warn, 3=Info, 4=Debug, 5=Verbose debug_level = 3 @@ -51,29 +51,32 @@ extra_scripts = pre:build.py otakeyfile = ota.conf lorakeyfile = loraconf.h lmicconfigfile = lmic_config.h -platform_espressif32 = espressif32@1.9.0 +platform_espressif32 = espressif32@1.11.0 monitor_speed = 115200 upload_speed = 115200 lib_deps_lora = - MCCI LoRaWAN LMIC library@>=3.0.99 + ;MCCI LoRaWAN LMIC library@>=3.1.0 + https://github.com/mcci-catena/arduino-lmic.git#5322dd1 lib_deps_display = - U8g2@>=2.26.13 + ss_oled@>=3.0.0 + BitBang_I2C@>=1.2.0 + QRCode@>=0.0.1 lib_deps_matrix_display = https://github.com/Seeed-Studio/Ultrathin_LED_Matrix.git lib_deps_rgbled = SmartLeds@>=1.1.6 lib_deps_gps = - 1655@>=1.0.2 ;TinyGPSPlus by Mikal Hart + 1655@>=1.0.2 ; #1655 TinyGPSPlus by Mikal Hart lib_deps_sensors = Adafruit Unified Sensor@>=1.0.3 - Adafruit BME280 Library@>=1.0.9 + Adafruit BME280 Library@>=1.0.10 lib_deps_basic = ArduinoJson@^5.13.1 - 76@>=1.2.2 ;Timezone by Jack Christensen - 274@>=2.3.3 ;RTC by Michael Miller + 76@>=1.2.2 ; #76 Timezone by Jack Christensen + 274@>=2.3.3 ; #274 RTC by Michael Miller SimpleButton - ;AXP202X_Library@^1.0.1 - https://github.com/lewisxhe/AXP202X_Library.git#8045ddf + https://github.com/lewisxhe/AXP202X_Library.git + ;AXP202X_Library@>=1.0.1 lib_deps_all = ${common.lib_deps_basic} ${common.lib_deps_lora} diff --git a/src/Timeserver/Nodered-Timeserver.json b/src/Timeserver/Nodered-Timeserver.json index 85e52607..085b132d 100644 --- a/src/Timeserver/Nodered-Timeserver.json +++ b/src/Timeserver/Nodered-Timeserver.json @@ -1,6 +1,6 @@ [ { - "id": "49e3c067.e782e", + "id": "9b4f492d.fbfd18", "type": "change", "z": "449c1517.e25f4c", "name": "Payload", @@ -48,32 +48,32 @@ "from": "", "to": "", "reg": false, - "x": 240, - "y": 513, + "x": 220, + "y": 520, "wires": [ [ - "84f1cda2.069e7" + "53a85e2c.2728d" ] ] }, { - "id": "cc140589.dea168", + "id": "9c105726.613a58", "type": "mqtt in", "z": "449c1517.e25f4c", "name": "listen", "topic": "+/devices/+/up", "qos": "2", "broker": "2a15ab6f.ab2244", - "x": 110, - "y": 120, + "x": 90, + "y": 127, "wires": [ [ - "4f97d75.6c87528" + "113ef524.57edeb" ] ] }, { - "id": "72d5e7ee.d1eba8", + "id": "1c9a7438.6e38ec", "type": "mqtt out", "z": "449c1517.e25f4c", "name": "send", @@ -81,28 +81,28 @@ "qos": "", "retain": "", "broker": "2a15ab6f.ab2244", - "x": 730, - "y": 513, + "x": 710, + "y": 520, "wires": [] }, { - "id": "4f97d75.6c87528", + "id": "113ef524.57edeb", "type": "json", "z": "449c1517.e25f4c", "name": "Convert", "property": "payload", "action": "", "pretty": false, - "x": 260, - "y": 120, + "x": 240, + "y": 127, "wires": [ [ - "9f4b8dd3.2f0d2" + "120561a.088359e" ] ] }, { - "id": "9f4b8dd3.2f0d2", + "id": "120561a.088359e", "type": "switch", "z": "449c1517.e25f4c", "name": "Timeport", @@ -118,97 +118,98 @@ "checkall": "true", "repair": false, "outputs": 1, - "x": 420, - "y": 120, + "x": 400, + "y": 127, "wires": [ [ - "8ed813a9.a9319" + "d6f27e8e.93242" ] ] }, { - "id": "dac8aafa.389298", + "id": "90e76b02.6298f8", "type": "json", "z": "449c1517.e25f4c", "name": "Convert", "property": "payload", "action": "", "pretty": false, - "x": 580, - "y": 513, + "x": 560, + "y": 520, "wires": [ [ - "72d5e7ee.d1eba8" + "1c9a7438.6e38ec" ] ] }, { - "id": "8ed813a9.a9319", + "id": "d6f27e8e.93242", "type": "base64", "z": "449c1517.e25f4c", "name": "Decode", "action": "", "property": "payload.payload_raw", - "x": 580, - "y": 120, + "x": 560, + "y": 127, "wires": [ [ - "831ab883.d6a238" + "b8bd33fd.61caa", + "cc245719.3c4cd8" ] ] }, { - "id": "84f1cda2.069e7", + "id": "53a85e2c.2728d", "type": "base64", "z": "449c1517.e25f4c", "name": "Encode", "action": "", "property": "payload.payload_raw", - "x": 420, - "y": 513, + "x": 400, + "y": 520, "wires": [ [ - "dac8aafa.389298" + "90e76b02.6298f8" ] ] }, { - "id": "6190967b.01f758", + "id": "15980d22.6f4663", "type": "comment", "z": "449c1517.e25f4c", - "name": "LoRaWAN Timeserver v1.1", + "name": "LoRaWAN Timeserver v1.21", "info": "PLEASE NOTE: There is a patent filed for the time sync algorithm used in the\ncode of this file. The shown implementation example is covered by the\nrepository's licencse, but you may not be eligible to deploy the applied\nalgorithm in applications without granted license by the patent holder.", - "x": 170, - "y": 40, + "x": 160, + "y": 47, "wires": [] }, { - "id": "831ab883.d6a238", + "id": "b8bd33fd.61caa", "type": "function", "z": "449c1517.e25f4c", "name": "Timeserver Logic", - "func": "/* LoRaWAN Timeserver\n\nconstruct 5 byte timesync_answer from gateway timestamp and node's time_sync_req\n\nbyte meaning\n1..4 current second (from epoch time 1970)\n5 1/250ths fractions of current second\n\nFPort = sequence number (taken from node's time_sync_req)\n\n*/\n\nfunction timecompare(a, b) {\n \n const timeA = a.time;\n const timeB = b.time;\n\n let comparison = 0;\n if (timeA > timeB) {\n comparison = 1;\n } else if (timeA < timeB) {\n comparison = -1;\n }\n return comparison;\n}\n\nlet confidence = 2000; // max millisecond diff gateway time to server time\n\n// guess if we have received a valid time_sync_req command\nif (msg.payload.payload_raw.length != 1)\n return;\n\nvar deviceMsg = { payload: msg.payload.dev_id };\nvar seqNo = msg.payload.payload_raw[0];\nvar seqNoMsg = { payload: seqNo };\nvar gateway_list = msg.payload.metadata.gateways;\n\n// filter all gateway timestamps that have milliseconds part (which we assume have a \".\")\nvar gateways = gateway_list.filter(function (element) {\n return (element.time.includes(\".\"));\n});\n\nvar gateway_time = gateways.map(gw => {\n return {\n time: new Date(gw.time),\n eui: gw.gtw_id,\n }\n });\nvar server_time = new Date(msg.payload.metadata.time);\n\n// validate all gateway timestamps against lorawan server_time (which is assumed to be recent)\nvar gw_timestamps = gateway_time.filter(function (element) {\n return ((element.time > (server_time - confidence) && element.time <= server_time));\n});\n\n// if no timestamp left, we have no valid one and exit\nif (gw_timestamps.length === 0) {\n var notavailMsg = { payload: \"n/a\" };\n var notimeMsg = { payload: 0xff }; \n var buf2 = Buffer.alloc(1);\n msg.payload = new Buffer(buf2.fill(0xff));\n return [notavailMsg, notavailMsg, deviceMsg, seqNoMsg, msg];}\n\n// sort time array in ascending order to find most recent timestamp for time answer\ngw_timestamps.sort(timecompare);\n\nvar timestamp = gw_timestamps[0].time;\nvar eui = gw_timestamps[0].eui;\nvar offset = server_time - timestamp;\n\nvar seconds = Math.floor(timestamp/1000);\nvar fractions = (timestamp % 1000) / 4;\n\nlet buf = new ArrayBuffer(5);\nnew DataView(buf).setUint32(0, seconds);\nnew DataView(buf).setUint8(4, fractions);\n\nmsg.payload = new Buffer(new Uint8Array(buf));\nmsg.port = seqNo;\nvar euiMsg = { payload: eui };\nvar offsetMsg = { payload: offset };\n\nreturn [euiMsg, offsetMsg, deviceMsg, seqNoMsg, msg];", + "func": "/* LoRaWAN Timeserver\n\nconstruct 7 byte timesync_answer from gateway timestamp and node's time_sync_req\n\nbyte meaning\n1 sequence number (taken from node's time_sync_req)\n2 timezone in 15 minutes steps\n3..6 current second (from epoch time 1970)\n7 1/250ths fractions of current second\n\n*/\n\nfunction timecompare(a, b) {\n \n const timeA = a.time;\n const timeB = b.time;\n\n let comparison = 0;\n if (timeA > timeB) {\n comparison = 1;\n } else if (timeA < timeB) {\n comparison = -1;\n }\n return comparison;\n}\n\nlet confidence = 2000; // max millisecond diff gateway time to server time\n\n// guess if we have received a valid time_sync_req command\nif (msg.payload.payload_raw.length != 1)\n return;\n\nvar deviceMsg = { payload: msg.payload.dev_id };\nvar seqNo = msg.payload.payload_raw[0];\nvar seqNoMsg = { payload: seqNo };\nvar gateway_list = msg.payload.metadata.gateways;\n\n// filter all gateway timestamps that have milliseconds part (which we assume have a \".\")\nvar gateways = gateway_list.filter(function (element) {\n return (element.time.includes(\".\"));\n});\n\nvar gateway_time = gateways.map(gw => {\n return {\n time: new Date(gw.time),\n eui: gw.gtw_id,\n }\n });\nvar server_time = new Date(msg.payload.metadata.time);\n\n// validate all gateway timestamps against lorawan server_time (which is assumed to be recent)\nvar gw_timestamps = gateway_time.filter(function (element) {\n return ((element.time > (server_time - confidence) && element.time <= server_time));\n});\n\n// if no timestamp left, we have no valid one and exit\nif (gw_timestamps.length === 0) {\n var notavailMsg = { payload: \"n/a\" };\n var notimeMsg = { payload: 0xff }; \n var buf2 = Buffer.alloc(1);\n msg.payload = new Buffer(buf2.fill(0xff));\n msg.port = 9; // Paxcounter TIMEPORT\n return [notavailMsg, notavailMsg, deviceMsg, seqNoMsg, msg];}\n\n// sort time array in ascending order to find most recent timestamp for time answer\ngw_timestamps.sort(timecompare);\n\nvar timestamp = gw_timestamps[0].time;\nvar eui = gw_timestamps[0].eui;\nvar offset = server_time - timestamp;\n\nvar seconds = Math.floor(timestamp/1000);\nvar fractions = (timestamp % 1000) / 4;\n\nlet buf = new ArrayBuffer(7);\nnew DataView(buf).setUint8(0, seqNo);\n// Timezone (in 15min steps)\nvar timezone = 8; // CET = UTC+2h\nnew DataView(buf).setUint8(1, timezone);\nnew DataView(buf).setUint32(2, seconds);\nnew DataView(buf).setUint8(6, fractions);\n\nmsg.payload = new Buffer(new Uint8Array(buf));\nmsg.port = 9; // Paxcounter TIMEPORT\nvar euiMsg = { payload: eui };\nvar offsetMsg = { payload: offset };\n\nreturn [euiMsg, offsetMsg, deviceMsg, seqNoMsg, msg];", "outputs": 5, "noerr": 0, - "x": 350, - "y": 320, + "x": 330, + "y": 327, "wires": [ [ - "37722d4b.08e3c2", - "a8a04c7a.c5fbd", - "a15454a9.fa0948" + "c9a83ac9.50fd18", + "6aeb3720.a89618", + "6ac55bbe.12ac54" ], [ - "46ce842a.614d5c" + "de908e66.b6fd3" ], [ - "a5dbb4ef.019168" + "d5a35bab.44cb18" ], [ - "1cb58e7f.221362" + "3a661f0a.c61b1" ], [ - "49e3c067.e782e" + "9b4f492d.fbfd18" ] ], "outputLabels": [ @@ -220,7 +221,7 @@ ] }, { - "id": "37722d4b.08e3c2", + "id": "c9a83ac9.50fd18", "type": "debug", "z": "449c1517.e25f4c", "name": "Timeserver Gw", @@ -229,13 +230,13 @@ "console": false, "tostatus": true, "complete": "payload", - "x": 700, - "y": 240, + "x": 680, + "y": 247, "wires": [], "icon": "node-red/bridge.png" }, { - "id": "8712a5ac.ed18e8", + "id": "247204ab.a9f83c", "type": "ui_text", "z": "449c1517.e25f4c", "group": "edb7cc8d.a3817", @@ -246,12 +247,12 @@ "label": "Last answer at:", "format": "{{msg.payload}}", "layout": "col-center", - "x": 810, - "y": 300, + "x": 790, + "y": 307, "wires": [] }, { - "id": "46ce842a.614d5c", + "id": "de908e66.b6fd3", "type": "ui_gauge", "z": "449c1517.e25f4c", "name": "Timeserver offset", @@ -272,12 +273,12 @@ ], "seg1": "", "seg2": "", - "x": 710, - "y": 380, + "x": 690, + "y": 387, "wires": [] }, { - "id": "a8a04c7a.c5fbd", + "id": "6aeb3720.a89618", "type": "ui_text", "z": "449c1517.e25f4c", "group": "edb7cc8d.a3817", @@ -288,28 +289,28 @@ "label": "Gateway", "format": "{{msg.payload}}", "layout": "col-center", - "x": 700, - "y": 340, + "x": 680, + "y": 347, "wires": [] }, { - "id": "a15454a9.fa0948", + "id": "6ac55bbe.12ac54", "type": "function", "z": "449c1517.e25f4c", "name": "Time", "func": "msg.payload = new Date().toLocaleString('en-GB', {timeZone: 'Europe/Berlin'});\nreturn msg;", "outputs": 1, "noerr": 0, - "x": 670, - "y": 300, + "x": 650, + "y": 307, "wires": [ [ - "8712a5ac.ed18e8" + "247204ab.a9f83c" ] ] }, { - "id": "a5dbb4ef.019168", + "id": "d5a35bab.44cb18", "type": "ui_text", "z": "449c1517.e25f4c", "group": "edb7cc8d.a3817", @@ -320,12 +321,12 @@ "label": "Device", "format": "{{msg.payload}}", "layout": "col-center", - "x": 700, - "y": 420, + "x": 680, + "y": 427, "wires": [] }, { - "id": "1cb58e7f.221362", + "id": "3a661f0a.c61b1", "type": "ui_text", "z": "449c1517.e25f4c", "group": "edb7cc8d.a3817", @@ -336,8 +337,22 @@ "label": "Sequence", "format": "{{msg.payload}}", "layout": "col-center", - "x": 700, - "y": 460, + "x": 680, + "y": 467, + "wires": [] + }, + { + "id": "cc245719.3c4cd8", + "type": "debug", + "z": "449c1517.e25f4c", + "name": "", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "true", + "x": 860, + "y": 140, "wires": [] }, { diff --git a/src/configmanager.cpp b/src/configmanager.cpp index 5b235f70..0fce294f 100644 --- a/src/configmanager.cpp +++ b/src/configmanager.cpp @@ -15,12 +15,12 @@ esp_err_t err; // populate cfg vars with factory settings void defaultConfig() { - cfg.loradr = LORADRDEFAULT; // 0-15, lora datarate, see pacounter.conf + cfg.loradr = LORADRDEFAULT; // 0-15, lora datarate, see paxcounter.conf cfg.txpower = LORATXPOWDEFAULT; // 0-15, lora tx power cfg.adrmode = 1; // 0=disabled, 1=enabled cfg.screensaver = 0; // 0=disabled, 1=enabled cfg.screenon = 1; // 0=disabled, 1=enabled - cfg.countermode = 0; // 0=cyclic, 1=cumulative, 2=cyclic confirmed + cfg.countermode = COUNTERMODE; // 0=cyclic, 1=cumulative, 2=cyclic confirmed cfg.rssilimit = 0; // threshold for rssilimiter, negative value! cfg.sendcycle = SENDCYCLE; // payload send cycle [seconds/2] cfg.wifichancycle = @@ -179,7 +179,7 @@ void saveConfig() { // set and save cfg.version void migrateVersion() { - sprintf(cfg.version, "%s", PROGVERSION); + snprintf(cfg.version, 10, "%s", PROGVERSION); ESP_LOGI(TAG, "version set to %s", cfg.version); saveConfig(); } diff --git a/src/cyclic.cpp b/src/cyclic.cpp index ef2ce200..9b75ecef 100644 --- a/src/cyclic.cpp +++ b/src/cyclic.cpp @@ -128,5 +128,9 @@ void reset_counters() { macs_total = 0; // reset all counters macs_wifi = 0; macs_ble = 0; +#ifdef HAS_DISPLAY + oledPlotCurve(0, true); +#endif + #endif } \ No newline at end of file diff --git a/src/display.cpp b/src/display.cpp index b89f551d..1e888f21 100644 --- a/src/display.cpp +++ b/src/display.cpp @@ -4,116 +4,142 @@ Display-Mask (128 x 64 pixel): - | 111111 - |0123456789012345 ------------------- -0|PAX:aabbccddee -1|PAX:aabbccddee -2|B:a.bcV Sats:ab -3|BLTH:abcde SFab -4|WIFI:abcde ch:ab -5|RLIM:abcd abcdKB -6|20:27:00* 27.Feb -7|yyyyyyyyyyyyyyab + | | | + | 11111111112 + |012345678901234567890 Font +----------------------- --------- +0|PAX:aabbccdd STRETCHED +1|PAX:aabbccdd STRETCHED +2| +3|B:a.bcV Sats:ab ch:ab SMALL +4|WIFI:abcde BLTH:abcde SMALL +5|RLIM:abcd Mem:abcdKB SMALL +6|27.Feb 2019 20:27:00* SMALL +7|yyyyyyyyyyyyy xx SFab SMALL -line 6: * = char {L|G|R|?} indicates time source, - inverse = clock controller is active, - pulsed = pps input signal is active +* = char {L|G|R|?} indicates time source, + inverse = clock controller is active, + pulsed = pps input signal is active -line 7: y = LMIC event message; ab = payload queue length +y = LMIC event message +xx = payload sendqueue length +ab = LMIC spread factor + +FONT_SMALL: 6x8px = 21 chars / line +FONT_NORMAL: 8x8px = 16 chars / line +FONT_STRETCHED: 16x32px = 8 chars / line */ // Basic Config #include "globals.h" +#include #include // needed for reading ESP32 chip attributes -#define DISPLAY_PAGES (4) // number of display pages +// local Tag for logging +static const char TAG[] = __FILE__; -HAS_DISPLAY u8x8(MY_OLED_RST, MY_OLED_SCL, MY_OLED_SDA); +#define DISPLAY_PAGES (6) // number of paxcounter display pages -// helper arry for converting month values to text +// settings for oled display library +#define USE_BACKBUFFER + +// settings for qr code generator +#define QR_VERSION 3 // 29 x 29px +#define QR_SCALEFACTOR 2 // 29 -> 58x < 64px + +// settings for curve plotter +#define DISPLAY_WIDTH 128 // Width in pixels of OLED-display, must be 32X +#define DISPLAY_HEIGHT 64 // Height in pixels of OLED-display, must be 64X + +// helper array for converting month values to text const char *printmonth[] = {"xxx", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; - uint8_t DisplayIsOn = 0; +uint8_t displaybuf[DISPLAY_WIDTH * DISPLAY_HEIGHT / 8] = {0}; -// helper function, prints a hex key on display -void DisplayKey(const uint8_t *key, uint8_t len, bool lsb) { - const uint8_t *p; - for (uint8_t i = 0; i < len; i++) { - p = lsb ? key + len - i - 1 : key + i; - u8x8.printf("%02X", *p); - } - u8x8.printf("\n"); -} +QRCode qrcode; -void init_display(const char *Productname, const char *Version) { +void init_display(uint8_t verbose) { // block i2c bus access if (!I2C_MUTEX_LOCK()) ESP_LOGV(TAG, "[%0.3f] i2c mutex lock failed", millis() / 1000.0); else { - // show startup screen - uint8_t buf[32]; - u8x8.begin(); - u8x8.setFont(u8x8_font_chroma48medium8_r); - u8x8.clear(); - u8x8.setFlipMode(0); - u8x8.setInverseFont(1); - u8x8.draw2x2String(0, 0, Productname); - u8x8.setInverseFont(0); - u8x8.draw2x2String(2, 2, Productname); - delay(500); - u8x8.clear(); - u8x8.setFlipMode(1); - u8x8.setInverseFont(1); - u8x8.draw2x2String(0, 0, Productname); - u8x8.setInverseFont(0); - u8x8.draw2x2String(2, 2, Productname); - delay(500); - u8x8.setFlipMode(0); - u8x8.clear(); - -#ifdef DISPLAY_FLIP - u8x8.setFlipMode(1); + // is we have display RST line we toggle it to re-initialize display +#ifdef MY_OLED_RST + pinMode(MY_OLED_RST, OUTPUT); + digitalWrite(MY_OLED_RST, 0); // iniialization of SSD1306 chip is executed + delay(1); // keep RES low for at least 3us according to SSD1306 datasheet + digitalWrite(MY_OLED_RST, 1); // normal operation #endif -// Display chip information + // init display +#ifndef DISPLAY_FLIP + oledInit(OLED_128x64, ANGLE_0, false, -1, -1, 400000L); +#else + oledInit(OLED_128x64, ANGLE_FLIPY, false, -1, -1, 400000L); +#endif + + // clear display + oledSetContrast(DISPLAYCONTRAST); + oledFill(0, 1); + + if (verbose) { + + // show startup screen + // to come -> display .bmp file with logo + +// show chip information #if (VERBOSE) - esp_chip_info_t chip_info; - esp_chip_info(&chip_info); - u8x8.printf("ESP32 %d cores\nWiFi%s%s\n", chip_info.cores, + esp_chip_info_t chip_info; + esp_chip_info(&chip_info); + dp_printf(0, 0, 0, 0, "** PAXCOUNTER **"); + dp_printf(0, 1, 0, 0, "Software v%s", PROGVERSION); + dp_printf(0, 3, 0, 0, "ESP32 %d cores", chip_info.cores); + dp_printf(0, 4, 0, 0, "Chip Rev.%d", chip_info.revision); + dp_printf(0, 5, 0, 0, "WiFi%s%s", (chip_info.features & CHIP_FEATURE_BT) ? "/BT" : "", (chip_info.features & CHIP_FEATURE_BLE) ? "/BLE" : ""); - u8x8.printf("ESP Rev.%d\n", chip_info.revision); - u8x8.printf("%dMB %s Flash\n", spi_flash_get_chip_size() / (1024 * 1024), + dp_printf(0, 6, 0, 0, "%dMB %s Flash", + spi_flash_get_chip_size() / (1024 * 1024), (chip_info.features & CHIP_FEATURE_EMB_FLASH) ? "int." : "ext."); + + // give user some time to read or take picture + oledDumpBuffer(NULL); + delay(2000); + oledFill(0x00, 1); #endif // VERBOSE - u8x8.print(Productname); - u8x8.print(" v"); - u8x8.println(PROGVERSION); - #if (HAS_LORA) - u8x8.println("DEVEUI:"); - os_getDevEui((u1_t *)buf); - DisplayKey(buf, 8, true); - delay(3000); + // generate DEVEUI as QR code and text + uint8_t buf[8]; + char deveui[17]; + os_getDevEui((u1_t *)buf); + snprintf(deveui, 17, "%016llX", *((uint64_t *)&buf)); + + // display DEVEUI as QR code on the left + oledSetContrast(30); + dp_printqr(3, 3, deveui); + + // display DEVEUI as plain text on the right + dp_printf(72, 0, FONT_NORMAL, 0, "LORAWAN"); + dp_printf(72, 1, FONT_NORMAL, 0, "DEVEUI:"); + for (uint8_t i = 0; i <= 3; i++) + dp_printf(80, i + 3, FONT_NORMAL, 0, "%4.4s", deveui + i * 4); + + // give user some time to read or take picture + oledDumpBuffer(NULL); + delay(8000); + oledSetContrast(DISPLAYCONTRAST); + oledFill(0x00, 1); #endif // HAS_LORA - u8x8.clear(); - u8x8.setPowerSave(!cfg.screenon); // set display off if disabled - u8x8.draw2x2String(0, 0, "PAX:0"); -#if (BLECOUNTER) - u8x8.setCursor(0, 3); - u8x8.printf("BLTH:0"); -#endif - u8x8.setCursor(0, 4); - u8x8.printf("WIFI:0"); - u8x8.setCursor(0, 5); - u8x8.printf(!cfg.rssilimit ? "RLIM:off " : "RLIM:%d", cfg.rssilimit); + + } // verbose + + oledPower(cfg.screenon); // set display off if disabled I2C_MUTEX_UNLOCK(); // release i2c bus access } // mutex @@ -123,6 +149,9 @@ void refreshTheDisplay(bool nextPage) { static uint8_t DisplayPage = 0; + // update histogram if we have a display + oledPlotCurve(macs.size(), false); + // if display is switched off we don't refresh it to relax cpu if (!DisplayIsOn && (DisplayIsOn == cfg.screenon)) return; @@ -137,15 +166,16 @@ void refreshTheDisplay(bool nextPage) { // set display on/off according to current device configuration if (DisplayIsOn != cfg.screenon) { DisplayIsOn = cfg.screenon; - u8x8.setPowerSave(!cfg.screenon); + oledPower(cfg.screenon); } if (nextPage) { DisplayPage = (DisplayPage >= DISPLAY_PAGES - 1) ? 0 : (DisplayPage + 1); - u8x8.clear(); + oledFill(0, 1); } draw_page(t, DisplayPage); + oledDumpBuffer(NULL); I2C_MUTEX_UNLOCK(); // release i2c bus access @@ -154,191 +184,155 @@ void refreshTheDisplay(bool nextPage) { void draw_page(time_t t, uint8_t page) { - char timeState, buff[16]; - uint8_t msgWaiting; + char timeState; #if (HAS_GPS) static bool wasnofix = true; #endif - // update counter (lines 0-1) - snprintf( - buff, sizeof(buff), "PAX:%-4d", - (int)macs.size()); // convert 16-bit MAC counter to decimal counter value - u8x8.draw2x2String(0, 0, - buff); // display number on unique macs total Wifi + BLE + // line 1/2: pax counter + dp_printf(0, 0, FONT_STRETCHED, 0, "PAX:%-4d", + macs.size()); // display number of unique macs total Wifi + BLE switch (page % DISPLAY_PAGES) { // page 0: parameters overview - // page 1: time + // page 1: pax graph // page 2: GPS // page 3: BME280/680 + // page 4: time + // page 5: blank screen + // page 0: parameters overview case 0: -// update Battery status (line 2) -#if (defined BAT_MEASURE_ADC || defined HAS_PMU) - u8x8.setCursor(0, 2); - if (batt_voltage == 0xffff) - u8x8.printf("B:USB "); - else - u8x8.printf("B:%.2fV", batt_voltage / 1000.0); -#endif - -// update GPS status (line 2) -#if (HAS_GPS) - u8x8.setCursor(9, 2); - if (gps.location.age() < 1500) // if no fix then display Sats value inverse - u8x8.printf("Sats:%.2d", gps.satellites.value()); - else { - u8x8.setInverseFont(1); - u8x8.printf("Sats:%.2d", gps.satellites.value()); - u8x8.setInverseFont(0); - } -#endif - - // update bluetooth counter + LoRa SF (line 3) + // line 3: wifi + bluetooth counters + dp_printf(0, 3, FONT_SMALL, 0, "WIFI:%-5d", macs_wifi); #if (BLECOUNTER) - u8x8.setCursor(0, 3); if (cfg.blescan) - u8x8.printf("BLTH:%-5d", macs_ble); + dp_printf(66, 3, FONT_SMALL, 0, "BLTH:%-5d", macs_ble); else - u8x8.printf("%s", "BLTH:off"); + dp_printf(66, 3, FONT_SMALL, 0, "%s", "BLTH:off"); #endif -#if (HAS_LORA) - u8x8.setCursor(12, 3); - if (!cfg.adrmode) // if ADR=off then display SF value inverse - u8x8.setInverseFont(1); - u8x8.printf("%-4s", getSfName(updr2rps(LMIC.datarate))); - if (!cfg.adrmode) // switch off inverse if it was turned on - u8x8.setInverseFont(0); -#endif // HAS_LORA +// line 4: Battery + GPS status + Wifi channel +#if (defined BAT_MEASURE_ADC || defined HAS_PMU) + if (batt_voltage == 0xffff) + dp_printf(0, 4, FONT_SMALL, 0, "%s", "USB "); + else if (batt_voltage == 0) + dp_printf(0, 4, FONT_SMALL, 0, "%s", "No batt"); + else + dp_printf(0, 4, FONT_SMALL, 0, "B:%.2fV", batt_voltage / 1000.0); +#endif +#if (HAS_GPS) + if (gps.location.age() < 1500) // if no fix then display Sats value inverse + dp_printf(48, 4, FONT_SMALL, 0, "Sats:%.2d", gps.satellites.value()); + else + dp_printf(48, 4, FONT_SMALL, 1, "Sats:%.2d", gps.satellites.value()); +#endif + dp_printf(96, 4, FONT_SMALL, 0, "ch:%02d", channel); - // line 4: update wifi counter + channel display - u8x8.setCursor(0, 4); - u8x8.printf("WIFI:%-5d", macs_wifi); - u8x8.setCursor(11, 4); - u8x8.printf("ch:%02d", channel); + // line 5: RSSI limiter + free memory + dp_printf(0, 5, FONT_SMALL, 0, !cfg.rssilimit ? "RLIM:off " : "RLIM:%-4d", + cfg.rssilimit); + dp_printf(66, 5, FONT_SMALL, 0, "Mem:%4dKB", getFreeRAM() / 1024); - // line 5: update RSSI limiter status & free memory display - u8x8.setCursor(0, 5); - u8x8.printf(!cfg.rssilimit ? "RLIM:off " : "RLIM:%-4d", cfg.rssilimit); - u8x8.setCursor(10, 5); - u8x8.printf("%4dKB", getFreeRAM() / 1024); - - // line 6: update time-of-day or LoRa status display - u8x8.setCursor(0, 6); + // line 6: time + date #if (TIME_SYNC_INTERVAL) - // we want a systime display instead LoRa status timeState = TimePulseTick ? ' ' : timeSetSymbols[timeSource]; TimePulseTick = false; + + dp_printf(0, 6, FONT_SMALL, 0, "%02d.%3s %4d", day(t), printmonth[month(t)], + year(t)); + dp_printf(72, 6, FONT_SMALL, 0, "%02d:%02d:%02d", hour(t), minute(t), + second(t)); + // display inverse timeState if clock controller is enabled #if (defined HAS_DCF77) || (defined HAS_IF482) - u8x8.printf("%02d:%02d:%02d", hour(t), minute(t), second(t)); - u8x8.setInverseFont(1); - u8x8.printf("%c", timeState); - u8x8.setInverseFont(0); + dp_printf(120, 6, FONT_SMALL, 1, "%c", timeState); #else - u8x8.printf("%02d:%02d:%02d%c", hour(t), minute(t), second(t), timeState); -#endif // HAS_DCF77 || HAS_IF482 - if (timeSource != _unsynced) - u8x8.printf(" %2d.%3s", day(t), printmonth[month(t)]); + dp_printf(120, 6, FONT_SMALL, 0, "%c", timeState); +#endif #endif // TIME_SYNC_INTERVAL + // line 7: LORA network status #if (HAS_LORA) - // line 7: update LMiC event display - u8x8.setCursor(0, 7); - u8x8.printf("%-14s", lmic_event_msg); - - // update LoRa send queue display - msgWaiting = uxQueueMessagesWaiting(LoraSendQueue); - if (msgWaiting) { - sprintf(buff, "%2d", msgWaiting); - u8x8.setCursor(14, 7); - u8x8.printf("%-2s", msgWaiting == SEND_QUEUE_SIZE ? "<>" : buff); - } else - u8x8.printf(" "); + // LMiC event display + dp_printf(0, 7, FONT_SMALL, 0, "%-16s", lmic_event_msg); + // LORA datarate, display inverse if ADR disabled + dp_printf(102, 7, FONT_SMALL, !cfg.adrmode, "%-4s", + getSfName(updr2rps(LMIC.datarate))); #endif // HAS_LORA break; // page0 + // page 1: pax graph case 1: - - // line 4-5: update time-of-day - snprintf(buff, sizeof(buff), "%02d:%02d:%02d", hour(t), minute(t), - second(t)); - u8x8.draw2x2String(0, 4, buff); - + oledDumpBuffer(displaybuf); break; // page1 + // page 2: GPS case 2: - // update counter (lines 0-1) - snprintf( - buff, sizeof(buff), "PAX:%-4d", - (int) - macs.size()); // convert 16-bit MAC counter to decimal counter value - u8x8.draw2x2String(0, 0, - buff); // display number on unique macs total Wifi + BLE - #if (HAS_GPS) if (gps.location.age() < 1500) { // line 5: clear "No fix" if (wasnofix) { - snprintf(buff, sizeof(buff), " "); - u8x8.draw2x2String(2, 5, buff); + dp_printf(16, 5, FONT_STRETCHED, 0, " "); wasnofix = false; } // line 3-4: GPS latitude - snprintf(buff, sizeof(buff), "%c%07.4f", - gps.location.rawLat().negative ? 'S' : 'N', gps.location.lat()); - u8x8.draw2x2String(0, 3, buff); + dp_printf(0, 3, FONT_STRETCHED, 0, "%c%07.4f", + gps.location.rawLat().negative ? 'S' : 'N', gps.location.lat()); // line 6-7: GPS longitude - snprintf(buff, sizeof(buff), "%c%07.4f", - gps.location.rawLat().negative ? 'W' : 'E', gps.location.lng()); - u8x8.draw2x2String(0, 6, buff); + dp_printf(0, 6, FONT_STRETCHED, 0, "%c%07.4f", + gps.location.rawLat().negative ? 'W' : 'E', gps.location.lng()); } else { - snprintf(buff, sizeof(buff), "No fix"); - u8x8.setInverseFont(1); - u8x8.draw2x2String(2, 5, buff); - u8x8.setInverseFont(0); + dp_printf(16, 5, FONT_STRETCHED, 1, "No fix"); wasnofix = true; } #else - snprintf(buff, sizeof(buff), "No GPS"); - u8x8.draw2x2String(2, 5, buff); + dp_printf(16, 5, FONT_STRETCHED, 1, "No GPS"); #endif break; // page2 + // page 3: BME280/680 case 3: #if (HAS_BME) // line 2-3: Temp - snprintf(buff, sizeof(buff), "TMP:%-2.1f", bme_status.temperature); - u8x8.draw2x2String(0, 2, buff); + dp_printf(0, 2, FONT_STRETCHED, 0, "TMP:%-2.1f", bme_status.temperature); // line 4-5: Hum - snprintf(buff, sizeof(buff), "HUM:%-2.1f", bme_status.humidity); - u8x8.draw2x2String(0, 4, buff); + dp_printf(0, 4, FONT_STRETCHED, 0, "HUM:%-2.1f", bme_status.humidity); #ifdef HAS_BME680 // line 6-7: IAQ - snprintf(buff, sizeof(buff), "IAQ:%-3.0f", bme_status.iaq); - u8x8.draw2x2String(0, 6, buff); + dp_printf(0, 6, FONT_STRETCHED, 0, "IAQ:%-3.0f", bme_status.iaq); #endif #else - snprintf(buff, sizeof(buff), "No BME"); - u8x8.draw2x2String(2, 5, buff); + dp_printf(16, 5, FONT_STRETCHED, 1, "No BME"); #endif break; // page3 + // page 4: time + case 4: + + dp_printf(0, 4, FONT_LARGE, 0, "%02d:%02d:%02d", hour(t), minute(t), + second(t)); + break; + + // page 5: blank screen + case 5: + + oledFill(0, 1); + break; + default: break; // default @@ -346,4 +340,156 @@ void draw_page(time_t t, uint8_t page) { } // draw_page +// display helper functions +void dp_printf(uint16_t x, uint16_t y, uint8_t font, uint8_t inv, + const char *format, ...) { + char loc_buf[64]; + char *temp = loc_buf; + va_list arg; + va_list copy; + va_start(arg, format); + va_copy(copy, arg); + int len = vsnprintf(temp, sizeof(loc_buf), format, copy); + va_end(copy); + if (len < 0) { + va_end(arg); + return; + }; + if (len >= sizeof(loc_buf)) { + temp = (char *)malloc(len + 1); + if (temp == NULL) { + va_end(arg); + return; + } + len = vsnprintf(temp, len + 1, format, arg); + } + va_end(arg); + oledWriteString(0, x, y, temp, font, inv, false); + if (temp != loc_buf) { + free(temp); + } +} + +void dp_printqr(uint16_t offset_x, uint16_t offset_y, const char *Message) { + uint8_t qrcodeData[qrcode_getBufferSize(QR_VERSION)]; + qrcode_initText(&qrcode, qrcodeData, QR_VERSION, ECC_HIGH, Message); + + // draw QR code + for (uint16_t y = 0; y < qrcode.size; y++) + for (uint16_t x = 0; x < qrcode.size; x++) + if (!qrcode_getModule(&qrcode, x, y)) // "black" + oledfillRect(x * QR_SCALEFACTOR + offset_x, + y * QR_SCALEFACTOR + offset_y, QR_SCALEFACTOR, + QR_SCALEFACTOR, false); + // draw horizontal frame lines + oledfillRect(0, 0, qrcode.size * QR_SCALEFACTOR + 2 * offset_x, offset_y, + false); + oledfillRect(0, qrcode.size * QR_SCALEFACTOR + offset_y, + qrcode.size * QR_SCALEFACTOR + 2 * offset_x, offset_y, false); + // draw vertical frame lines + oledfillRect(0, 0, offset_x, qrcode.size * QR_SCALEFACTOR + 2 * offset_y, + false); + oledfillRect(qrcode.size * QR_SCALEFACTOR + offset_x, 0, offset_x, + qrcode.size * QR_SCALEFACTOR + 2 * offset_y, false); +} + +void oledfillRect(uint16_t x, uint16_t y, uint16_t width, uint16_t height, + uint8_t bRender) { + for (uint16_t xi = x; xi < x + width; xi++) + oledDrawLine(xi, y, xi, y + height - 1, bRender); +} + +int oledDrawPixel(uint8_t *buf, const uint16_t x, const uint16_t y, + const uint8_t dot) { + + if (x > DISPLAY_WIDTH || y > DISPLAY_HEIGHT) + return -1; + + uint8_t bit = y & 7; + uint16_t idx = y / 8 * DISPLAY_WIDTH + x; + + buf[idx] &= ~(1 << bit); // clear pixel + if (dot) + buf[idx] |= (1 << bit); // set pixel + + return 0; +} + +void oledScrollBufferHorizontal(uint8_t *buf, const uint16_t width, + const uint16_t height, bool left) { + + uint16_t col, page, idx = 0; + + for (page = 0; page < height / 8; page++) { + if (left) { // scroll left + for (col = 0; col < width - 1; col++) { + idx = page * width + col; + buf[idx] = buf[idx + 1]; + } + buf[idx + 1] = 0; + } else // scroll right + { + for (col = width - 1; col > 0; col--) { + idx = page * width + col; + buf[idx] = buf[idx - 1]; + } + buf[idx - 1] = 0; + } + } +} + +void oledScrollBufferVertical(uint8_t *buf, const uint16_t width, + const uint16_t height, int offset) { + + uint64_t buf_col; + + if (!offset) + return; // nothing to do + + for (uint16_t col = 0; col < DISPLAY_WIDTH; col++) { + // convert column bytes from display buffer to uint64_t + buf_col = *(uint64_t *)&buf[col * DISPLAY_HEIGHT / 8]; + + if (offset > 0) // scroll down + buf_col <<= offset; + else // scroll up + buf_col >>= abs(offset); + + // write back uint64_t to uint8_t display buffer + *(uint64_t *)&buf[col * DISPLAY_HEIGHT / 8] = buf_col; + } +} + +void oledPlotCurve(uint16_t count, bool reset) { + + static uint16_t last_count = 0, col = 0, row = 0; + uint16_t v_scroll = 0; + + if ((last_count == count) && !reset) + return; + + if (reset) { // next count cycle? + if (col < DISPLAY_WIDTH - 1) // matrix not full -> increment column + col++; + else // matrix full -> scroll left 1 dot + oledScrollBufferHorizontal(displaybuf, DISPLAY_WIDTH, DISPLAY_HEIGHT, + true); + + } else // clear current dot + oledDrawPixel(displaybuf, col, row, 0); + + // scroll down, if necessary + while ((count - v_scroll) > DISPLAY_HEIGHT - 1) + v_scroll++; + if (v_scroll) + oledScrollBufferVertical(displaybuf, DISPLAY_WIDTH, DISPLAY_HEIGHT, + v_scroll); + + // set new dot + // row = DISPLAY_HEIGHT - 1 - (count - v_scroll) % DISPLAY_HEIGHT; + row = DISPLAY_HEIGHT - 1 - count - v_scroll; + last_count = count; + oledDrawPixel(displaybuf, col, row, 1); +} + #endif // HAS_DISPLAY \ No newline at end of file diff --git a/src/hal/ebox.h b/src/hal/ebox.h index ebd70136..6da29292 100644 --- a/src/hal/ebox.h +++ b/src/hal/ebox.h @@ -1,5 +1,5 @@ // clang-format off -// upload_speed 115200 +// upload_speed 921600 // board esp32dev #ifndef _EBOX_H diff --git a/src/hal/eboxtube.h b/src/hal/eboxtube.h index 34abe865..86a69d7d 100644 --- a/src/hal/eboxtube.h +++ b/src/hal/eboxtube.h @@ -1,5 +1,5 @@ // clang-format off -// upload_speed 115200 +// upload_speed 921600 // board esp32dev diff --git a/src/hal/ecopower.h b/src/hal/ecopower.h index 4f4b303a..deca82d7 100644 --- a/src/hal/ecopower.h +++ b/src/hal/ecopower.h @@ -12,7 +12,7 @@ //#define DISABLE_BROWNOUT 1 // comment out if you want to keep brownout feature -#define HAS_DISPLAY U8X8_SSD1306_128X64_NONAME_HW_I2C +#define HAS_DISPLAY 1 //#define DISPLAY_FLIP 1 // use if display is rotated #define BAT_MEASURE_ADC ADC1_GPIO35_CHANNEL // battery probe GPIO pin -> ADC1_CHANNEL_7 #define BAT_VOLTAGE_DIVIDER ((82.0+220.0)/82.0) // 82k + 220k 1% @@ -29,7 +29,7 @@ // Pins for I2C interface of OLED Display #define MY_OLED_SDA SDA #define MY_OLED_SCL SCL -#define MY_OLED_RST U8X8_PIN_NONE +#define MY_OLED_RST NOT_A_PIN // Settings for on board DS3231 RTC chip // note: to use RTC_INT, capacitor 100nF next to red LED must be removed to sharpen interrupt signal slope diff --git a/src/hal/generic.h b/src/hal/generic.h index c606e5ba..909bef38 100644 --- a/src/hal/generic.h +++ b/src/hal/generic.h @@ -44,7 +44,7 @@ #define BOARD_HAS_PSRAM // use if board has external PSRAM #define DISABLE_BROWNOUT 1 // comment out if you want to keep brownout feature -#define HAS_DISPLAY U8X8_SSD1306_128X64_NONAME_HW_I2C +#define HAS_DISPLAY 1 //#define DISPLAY_FLIP 1 // use if display is rotated #define BAT_MEASURE_ADC ADC1_GPIO35_CHANNEL // battery probe GPIO pin -> ADC1_CHANNEL_7 #define BAT_VOLTAGE_DIVIDER 2 // voltage divider 100k/100k on board diff --git a/src/hal/heltec.h b/src/hal/heltec.h index 465793a1..4432c9a3 100644 --- a/src/hal/heltec.h +++ b/src/hal/heltec.h @@ -16,7 +16,7 @@ #define HAS_LORA 1 // comment out if device shall not send data via LoRa #define CFG_sx1276_radio 1 -#define HAS_DISPLAY U8X8_SSD1306_128X64_NONAME_HW_I2C // OLED-Display on board +#define HAS_DISPLAY 1 // OLED-Display on board #define HAS_LED LED_BUILTIN // white LED on board #define HAS_BUTTON KEY_BUILTIN // button "PROG" on board diff --git a/src/hal/heltecv2.h b/src/hal/heltecv2.h index adb74981..96024e38 100644 --- a/src/hal/heltecv2.h +++ b/src/hal/heltecv2.h @@ -16,7 +16,7 @@ #define HAS_LORA 1 // comment out if device shall not send data via LoRa #define CFG_sx1276_radio 1 -#define HAS_DISPLAY U8X8_SSD1306_128X64_NONAME_HW_I2C // OLED-Display on board +#define HAS_DISPLAY 1 // OLED-Display on board #define HAS_LED LED_BUILTIN // white LED on board #define HAS_BUTTON KEY_BUILTIN // button "PROG" on board diff --git a/src/hal/lolin32litelora.h b/src/hal/lolin32litelora.h index 11f3f156..07af6837 100644 --- a/src/hal/lolin32litelora.h +++ b/src/hal/lolin32litelora.h @@ -13,7 +13,7 @@ // disable brownout detection (avoid unexpected reset on some boards) #define DISABLE_BROWNOUT 1 // comment out if you want to keep brownout feature -#define HAS_DISPLAY U8X8_SSD1306_128X64_NONAME_HW_I2C // OLED-Display on board +#define HAS_DISPLAY 1 // OLED-Display on board //#define DISPLAY_FLIP 1 // uncomment this for rotated display #define HAS_LED 22 // ESP32 GPIO12 (pin22) On Board LED #define LED_ACTIVE_LOW 1 // Onboard LED is active when pin is LOW @@ -37,7 +37,7 @@ // Pins for I2C interface of OLED Display #define MY_OLED_SDA (14) #define MY_OLED_SCL (12) -#define MY_OLED_RST U8X8_PIN_NONE +#define MY_OLED_RST NOT_A_PIN // I2C config for Microchip 24AA02E64 DEVEUI unique address #define MCP_24AA02E64_I2C_ADDRESS 0x50 // I2C address for the 24AA02E64 diff --git a/src/hal/lolin32lora.h b/src/hal/lolin32lora.h index a79b0d84..a14c9605 100644 --- a/src/hal/lolin32lora.h +++ b/src/hal/lolin32lora.h @@ -13,7 +13,7 @@ // disable brownout detection (avoid unexpected reset on some boards) #define DISABLE_BROWNOUT 1 // comment out if you want to keep brownout feature -#define HAS_DISPLAY U8X8_SSD1306_128X64_NONAME_HW_I2C // OLED-Display on board +#define HAS_DISPLAY 1 // OLED-Display on board //#define DISPLAY_FLIP 1 // uncomment this for rotated display #define HAS_LED NOT_A_PIN // Led os on same pin as Lora SS pin, to avoid problems, we don't use it #define LED_ACTIVE_LOW 1 // Onboard LED is active when pin is LOW @@ -39,7 +39,7 @@ // Pins for I2C interface of OLED Display #define MY_OLED_SDA (21) #define MY_OLED_SCL (22) -#define MY_OLED_RST U8X8_PIN_NONE +#define MY_OLED_RST NOT_A_PIN // I2C config for Microchip 24AA02E64 DEVEUI unique address #define MCP_24AA02E64_I2C_ADDRESS 0x50 // I2C address for the 24AA02E64 diff --git a/src/hal/octopus32.h b/src/hal/octopus32.h index d245a8c2..f34f609b 100644 --- a/src/hal/octopus32.h +++ b/src/hal/octopus32.h @@ -41,10 +41,10 @@ #define LORA_IO2 LMIC_UNUSED_PIN // Pins for I2C interface of OLED Display -#define HAS_DISPLAY U8X8_SSD1306_128X64_NONAME_HW_I2C // U8X8_SSD1306_128X32_UNIVISION_SW_I2C // +#define HAS_DISPLAY 1 //#define DISPLAY_FLIP 1 // uncomment this for rotated display #define MY_OLED_SDA (23) #define MY_OLED_SCL (22) -#define MY_OLED_RST U8X8_PIN_NONE +#define MY_OLED_RST NOT_A_PIN #endif diff --git a/src/hal/ttgobeam.h b/src/hal/ttgobeam.h index f8be7ee0..be9c1ff7 100644 --- a/src/hal/ttgobeam.h +++ b/src/hal/ttgobeam.h @@ -28,15 +28,15 @@ // enable only if device has these sensors, otherwise comment these lines // BME680 sensor on I2C bus -#define HAS_BME 1 // Enable BME sensors in general -#define HAS_BME680 SDA, SCL -#define BME680_ADDR BME680_I2C_ADDR_PRIMARY // !! connect SDIO of BME680 to GND !! +//#define HAS_BME 1 // Enable BME sensors in general +//#define HAS_BME680 SDA, SCL +//#define BME680_ADDR BME680_I2C_ADDR_PRIMARY // !! connect SDIO of BME680 to GND !! // display (if connected) -#define HAS_DISPLAY U8X8_SSD1306_128X64_NONAME_HW_I2C +#define HAS_DISPLAY 1 #define MY_OLED_SDA SDA #define MY_OLED_SCL SCL -#define MY_OLED_RST U8X8_PIN_NONE +#define MY_OLED_RST NOT_A_PIN //#define DISPLAY_FLIP 1 // use if display is rotated // user defined sensors (if connected) diff --git a/src/hal/ttgobeam10.h b/src/hal/ttgobeam10.h index fa0b3ffa..35bd78da 100644 --- a/src/hal/ttgobeam10.h +++ b/src/hal/ttgobeam10.h @@ -20,10 +20,10 @@ User, long press -> send LORA message Reset -> reset device */ -//#define HAS_DISPLAY U8X8_SSD1306_128X64_NONAME_HW_I2C +#define HAS_DISPLAY 1 #define MY_OLED_SDA SDA #define MY_OLED_SCL SCL -#define MY_OLED_RST U8X8_PIN_NONE +#define MY_OLED_RST NOT_A_PIN //#define DISPLAY_FLIP 1 // use if display is rotated #define HAS_LORA 1 // comment out if device shall not send data via LoRa diff --git a/src/hal/ttgofox.h b/src/hal/ttgofox.h index 94b2b60f..8a7e3a9c 100644 --- a/src/hal/ttgofox.h +++ b/src/hal/ttgofox.h @@ -10,7 +10,7 @@ #define HAS_LORA 1 // comment out if device shall not send data via LoRa #define CFG_sx1276_radio 1 // HPD13A LoRa SoC -#define HAS_DISPLAY U8X8_SSD1306_128X64_NONAME_HW_I2C +#define HAS_DISPLAY 1 #define HAS_LED NOT_A_PIN // green on board LED is useless, is GPIO25, which switches power for Lora+Display #define EXT_POWER_SW GPIO_NUM_25 // switches power for LoRa chip @@ -23,7 +23,7 @@ // Pins for I2C interface of OLED Display #define MY_OLED_SDA (21) #define MY_OLED_SCL (22) -#define MY_OLED_RST U8X8_PIN_NONE +#define MY_OLED_RST NOT_A_PIN // Settings for on board DS3231 RTC chip #define HAS_RTC MY_OLED_SDA, MY_OLED_SCL // SDA, SCL diff --git a/src/hal/ttgov1.h b/src/hal/ttgov1.h index fddc3ef5..1aaf20c3 100644 --- a/src/hal/ttgov1.h +++ b/src/hal/ttgov1.h @@ -12,7 +12,7 @@ #define HAS_LORA 1 // comment out if device shall not send data via LoRa #define CFG_sx1276_radio 1 -#define HAS_DISPLAY U8X8_SSD1306_128X64_NONAME_HW_I2C // OLED-Display on board +#define HAS_DISPLAY 1 // OLED-Display on board //#define DISPLAY_FLIP 1 // uncomment this for rotated display #define HAS_LED LED_BUILTIN #define LED_ACTIVE_LOW 1 // Onboard LED is active when pin is LOW diff --git a/src/hal/ttgov2.h b/src/hal/ttgov2.h index 54f1657a..e5e784f3 100644 --- a/src/hal/ttgov2.h +++ b/src/hal/ttgov2.h @@ -12,7 +12,7 @@ #define HAS_LORA 1 // comment out if device shall not send data via LoRa #define CFG_sx1276_radio 1 // HPD13A LoRa SoC -#define HAS_DISPLAY U8X8_SSD1306_128X64_NONAME_HW_I2C +#define HAS_DISPLAY 1 //#define DISPLAY_FLIP 1 // uncomment this for rotated display #define HAS_LED NOT_A_PIN // on-board LED is wired to SCL (used by display) therefore totally useless @@ -22,7 +22,7 @@ // Pins for I2C interface of OLED Display #define MY_OLED_SDA (21) #define MY_OLED_SCL (22) -#define MY_OLED_RST U8X8_PIN_NONE +#define MY_OLED_RST NOT_A_PIN // Pins for LORA chip SPI interface come from board file, we need some // additional definitions for LMIC diff --git a/src/hal/ttgov21new.h b/src/hal/ttgov21new.h index c50869b2..4c1cd4fc 100644 --- a/src/hal/ttgov21new.h +++ b/src/hal/ttgov21new.h @@ -20,7 +20,7 @@ //#define HAS_BME 1 // Enable BME sensors in general //#define HAS_BME280 GPIO_NUM_21, GPIO_NUM_22 // SDA, SCL -#define HAS_DISPLAY U8X8_SSD1306_128X64_NONAME_HW_I2C +#define HAS_DISPLAY 1 #define HAS_LED (25) // green on board LED #define BAT_MEASURE_ADC ADC1_GPIO35_CHANNEL // battery probe GPIO pin -> ADC1_CHANNEL_7 #define BAT_VOLTAGE_DIVIDER 2 // voltage divider 100k/100k on board @@ -28,7 +28,7 @@ // Pins for I2C interface of OLED Display #define MY_OLED_SDA (21) #define MY_OLED_SCL (22) -#define MY_OLED_RST U8X8_PIN_NONE +#define MY_OLED_RST NOT_A_PIN // Pins for LORA chip SPI interface, reset line and interrupt lines #define LORA_SCK (5) diff --git a/src/hal/ttgov21old.h b/src/hal/ttgov21old.h index 0a7c386c..20cc4a88 100644 --- a/src/hal/ttgov21old.h +++ b/src/hal/ttgov21old.h @@ -19,15 +19,15 @@ #define HAS_LED NOT_A_PIN // no usable LED on board #define DISABLE_BROWNOUT 1 // comment out if you want to keep brownout feature -#define HAS_DISPLAY U8X8_SSD1306_128X64_NONAME_HW_I2C -//#define DISPLAY_FLIP 1 // rotated display +#define HAS_DISPLAY 1 +#define DISPLAY_FLIP 1 // rotated display //#define BAT_MEASURE_ADC ADC1_GPIO35_CHANNEL // battery probe GPIO pin -> ADC1_CHANNEL_7 //#define BAT_VOLTAGE_DIVIDER 2 // voltage divider 100k/100k on board // Pins for I2C interface of OLED Display #define MY_OLED_SDA (21) #define MY_OLED_SCL (22) -#define MY_OLED_RST U8X8_PIN_NONE +#define MY_OLED_RST NOT_A_PIN // Pins for LORA chip SPI interface, reset line and interrupt lines #define LORA_SCK (5) diff --git a/src/hal/wemos32oled.h b/src/hal/wemos32oled.h index 3b59e450..2955fbaa 100644 --- a/src/hal/wemos32oled.h +++ b/src/hal/wemos32oled.h @@ -9,10 +9,10 @@ #define HAS_LED NOT_A_PIN // no LED -#define HAS_DISPLAY U8X8_SSD1306_128X64_NONAME_HW_I2C +#define HAS_DISPLAY 1 #define MY_OLED_SDA (5) #define MY_OLED_SCL (4) -#define MY_OLED_RST U8X8_PIN_NONE +#define MY_OLED_RST NOT_A_PIN #define DISPLAY_FLIP 1 // use if display is rotated #define DISABLE_BROWNOUT 1 // comment out if you want to keep brownout feature diff --git a/src/i2cscan.cpp b/src/i2cscan.cpp index c80c2af7..d6e7ff64 100644 --- a/src/i2cscan.cpp +++ b/src/i2cscan.cpp @@ -15,9 +15,11 @@ int i2c_scan(void) { // block i2c bus access if (I2C_MUTEX_LOCK()) { + // Scan at 100KHz low speed + Wire.setClock(100000); + for (addr = 8; addr <= 119; addr++) { - // scan i2c bus with no more to 100KHz Wire.beginTransmission(addr); Wire.write(addr); i2c_ret = Wire.endTransmission(); @@ -58,6 +60,9 @@ int i2c_scan(void) { ESP_LOGI(TAG, "I2C scan done, %u devices found.", devices); + // Set back to 400KHz + Wire.setClock(400000); + I2C_MUTEX_UNLOCK(); // release i2c bus access } else ESP_LOGE(TAG, "I2c bus busy - scan error"); diff --git a/src/ledmatrixdisplay.cpp b/src/ledmatrixdisplay.cpp index 881f57fe..e33ea6e0 100644 --- a/src/ledmatrixdisplay.cpp +++ b/src/ledmatrixdisplay.cpp @@ -94,7 +94,7 @@ void refreshTheMatrixDisplay(bool nextPage) { if (col < (LED_MATRIX_WIDTH - 1)) col++; else - ScrollLeft(displaybuf, LED_MATRIX_WIDTH, LED_MATRIX_HEIGHT); + ScrollMatrixLeft(displaybuf, LED_MATRIX_WIDTH, LED_MATRIX_HEIGHT); } else matrix.drawPoint(col, row, 0); // clear current dot @@ -204,7 +204,7 @@ uint8_t GetCharWidth(char cChar) { return CharDescriptor.width; } -void ScrollLeft(uint8_t *buf, const uint16_t cols, const uint16_t rows) { +void ScrollMatrixLeft(uint8_t *buf, const uint16_t cols, const uint16_t rows) { uint32_t i, k, idx; const uint32_t x = cols / 8; diff --git a/src/lmic_config.h b/src/lmic_config.h index 788a07e1..e668bc70 100644 --- a/src/lmic_config.h +++ b/src/lmic_config.h @@ -28,14 +28,14 @@ // so consuming more power. You may sharpen (reduce) this value if you are // limited on battery. // ATTN: VALUES > 7 WILL CAUSE RECEPTION AND JOIN PROBLEMS WITH HIGH SF RATES -//#define CLOCK_ERROR_PROCENTAGE 7 +//#define CLOCK_ERROR_PROCENTAGE 5 // Set this to 1 to enable some basic debug output (using printf) about // RF settings used during transmission and reception. Set to 2 to // enable more verbose output. Make sure that printf is actually // configured (e.g. on AVR it is not by default), otherwise using it can // cause crashing. -#define LMIC_DEBUG_LEVEL 0 +//#define LMIC_DEBUG_LEVEL 1 // Enable this to allow using printf() to print to the given serial port // (or any other Print object). This can be easy for debugging. The diff --git a/src/lorawan.cpp b/src/lorawan.cpp index bec7c90e..398e86f4 100644 --- a/src/lorawan.cpp +++ b/src/lorawan.cpp @@ -1,671 +1,626 @@ -// Basic Config -#if (HAS_LORA) -#include "lorawan.h" -#endif - -// Local logging Tag -static const char TAG[] = "lora"; - -#if (HAS_LORA) - -#if CLOCK_ERROR_PROCENTAGE > 7 -#warning CLOCK_ERROR_PROCENTAGE value in lmic_config.h is too high; values > 7 will cause side effects -#endif - -#if (TIME_SYNC_LORAWAN) -#ifndef LMIC_ENABLE_DeviceTimeReq -#define LMIC_ENABLE_DeviceTimeReq 1 -#endif -#endif - -QueueHandle_t LoraSendQueue; -TaskHandle_t lmicTask = NULL, lorasendTask = NULL; - -// table of LORAWAN MAC messages sent by the network to the device -// format: opcode, cmdname (max 19 chars), #bytes params -// source: LoRaWAN 1.1 Specification (October 11, 2017) -static const mac_t MACdn_table[] = { - {0x01, "ResetConf", 1}, {0x02, "LinkCheckAns", 2}, - {0x03, "LinkADRReq", 4}, {0x04, "DutyCycleReq", 1}, - {0x05, "RXParamSetupReq", 4}, {0x06, "DevStatusReq", 0}, - {0x07, "NewChannelReq", 5}, {0x08, "RxTimingSetupReq", 1}, - {0x09, "TxParamSetupReq", 1}, {0x0A, "DlChannelReq", 4}, - {0x0B, "RekeyConf", 1}, {0x0C, "ADRParamSetupReq", 1}, - {0x0D, "DeviceTimeAns", 5}, {0x0E, "ForceRejoinReq", 2}, - {0x0F, "RejoinParamSetupReq", 1}}; - -// table of LORAWAN MAC messages sent by the device to the network -static const mac_t MACup_table[] = { - {0x01, "ResetInd", 1}, {0x02, "LinkCheckReq", 0}, - {0x03, "LinkADRAns", 1}, {0x04, "DutyCycleAns", 0}, - {0x05, "RXParamSetupAns", 1}, {0x06, "DevStatusAns", 2}, - {0x07, "NewChannelAns", 1}, {0x08, "RxTimingSetupAns", 0}, - {0x09, "TxParamSetupAns", 0}, {0x0A, "DlChannelAns", 1}, - {0x0B, "RekeyInd", 1}, {0x0C, "ADRParamSetupAns", 0}, - {0x0D, "DeviceTimeReq", 0}, {0x0F, "RejoinParamSetupAns", 1}}; - -class MyHalConfig_t : public Arduino_LMIC::HalConfiguration_t { - -public: - MyHalConfig_t(){}; - virtual void begin(void) override { - SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); - } -}; - -MyHalConfig_t myHalConfig{}; - -// LMIC pin mapping - -const lmic_pinmap lmic_pins = { - .nss = LORA_CS, - .rxtx = LMIC_UNUSED_PIN, - .rst = LORA_RST == NOT_A_PIN ? LMIC_UNUSED_PIN : LORA_RST, - .dio = {LORA_IRQ, LORA_IO1, - LORA_IO2 == NOT_A_PIN ? LMIC_UNUSED_PIN : LORA_IO2}, - // optional: set polarity of rxtx pin. - .rxtx_rx_active = 0, - // optional: set RSSI cal for listen-before-talk - // this value is in dB, and is added to RSSI - // measured prior to decision. - // Must include noise guardband! Ignored in US, - // EU, IN, other markets where LBT is not required. - .rssi_cal = 0, - // optional: override LMIC_SPI_FREQ if non-zero - .spi_freq = 0, - .pConfig = &myHalConfig}; - -// DevEUI generator using devices's MAC address -void gen_lora_deveui(uint8_t *pdeveui) { - uint8_t *p = pdeveui, dmac[6]; - int i = 0; - esp_efuse_mac_get_default(dmac); - // deveui is LSB, we reverse it so TTN DEVEUI display - // will remain the same as MAC address - // MAC is 6 bytes, devEUI 8, set first 2 ones - // with an arbitrary value - *p++ = 0xFF; - *p++ = 0xFE; - // Then next 6 bytes are mac address reversed - for (i = 0; i < 6; i++) { - *p++ = dmac[5 - i]; - } -} - -/* new version, does it with well formed mac according IEEE spec, but is -breaking change -// DevEUI generator using devices's MAC address -void gen_lora_deveui(uint8_t *pdeveui) { - uint8_t *p = pdeveui, dmac[6]; - ESP_ERROR_CHECK(esp_efuse_mac_get_default(dmac)); - // deveui is LSB, we reverse it so TTN DEVEUI display - // will remain the same as MAC address - // MAC is 6 bytes, devEUI 8, set middle 2 ones - // to an arbitrary value - *p++ = dmac[5]; - *p++ = dmac[4]; - *p++ = dmac[3]; - *p++ = 0xfe; - *p++ = 0xff; - *p++ = dmac[2]; - *p++ = dmac[1]; - *p++ = dmac[0]; -} -*/ - -// Function to do a byte swap in a byte array -void RevBytes(unsigned char *b, size_t c) { - u1_t i; - for (i = 0; i < c / 2; i++) { - unsigned char t = b[i]; - b[i] = b[c - 1 - i]; - b[c - 1 - i] = t; - } -} - -// LMIC callback functions -void os_getDevKey(u1_t *buf) { memcpy(buf, APPKEY, 16); } - -void os_getArtEui(u1_t *buf) { - memcpy(buf, APPEUI, 8); - RevBytes(buf, 8); // TTN requires it in LSB First order, so we swap bytes -} - -void os_getDevEui(u1_t *buf) { - int i = 0, k = 0; - memcpy(buf, DEVEUI, 8); // get fixed DEVEUI from loraconf.h - for (i = 0; i < 8; i++) { - k += buf[i]; - } - if (k) { - RevBytes(buf, 8); // use fixed DEVEUI and swap bytes to LSB format - } else { - gen_lora_deveui(buf); // generate DEVEUI from device's MAC - } - -// Get MCP 24AA02E64 hardware DEVEUI (override default settings if found) -#ifdef MCP_24AA02E64_I2C_ADDRESS - get_hard_deveui(buf); - RevBytes(buf, 8); // swap bytes to LSB format -#endif -} - -void get_hard_deveui(uint8_t *pdeveui) { - // read DEVEUI from Microchip 24AA02E64 2Kb serial eeprom if present -#ifdef MCP_24AA02E64_I2C_ADDRESS - - uint8_t i2c_ret; - - // Init this just in case, no more to 100KHz - Wire.begin(SDA, SCL, 100000); - Wire.beginTransmission(MCP_24AA02E64_I2C_ADDRESS); - Wire.write(MCP_24AA02E64_MAC_ADDRESS); - i2c_ret = Wire.endTransmission(); - - // check if device was seen on i2c bus - if (i2c_ret == 0) { - char deveui[32] = ""; - uint8_t data; - - Wire.beginTransmission(MCP_24AA02E64_I2C_ADDRESS); - Wire.write(MCP_24AA02E64_MAC_ADDRESS); - Wire.endTransmission(); - - Wire.requestFrom(MCP_24AA02E64_I2C_ADDRESS, 8); - while (Wire.available()) { - data = Wire.read(); - sprintf(deveui + strlen(deveui), "%02X ", data); - *pdeveui++ = data; - } - ESP_LOGI(TAG, "Serial EEPROM found, read DEVEUI %s", deveui); - } else - ESP_LOGI(TAG, "Could not read DEVEUI from serial EEPROM"); - - // Set back to 400KHz to speed up OLED - Wire.setClock(400000); -#endif // MCP 24AA02E64 -} - -#if (VERBOSE) - -// Display OTAA keys -void showLoraKeys(void) { - // LMIC may not have used callback to fill - // all EUI buffer so we do it here to a temp - // buffer to be able to display them - uint8_t buf[32]; - os_getDevEui((u1_t *)buf); - printKey("DevEUI", buf, 8, true); - os_getArtEui((u1_t *)buf); - printKey("AppEUI", buf, 8, true); - os_getDevKey((u1_t *)buf); - printKey("AppKey", buf, 16, false); -} - -#endif // VERBOSE - -void onEvent(ev_t ev) { - char buff[24] = ""; - - switch (ev) { - - case EV_SCAN_TIMEOUT: - strcpy_P(buff, PSTR("SCAN TIMEOUT")); - break; - - case EV_BEACON_FOUND: - strcpy_P(buff, PSTR("BEACON FOUND")); - break; - - case EV_BEACON_MISSED: - strcpy_P(buff, PSTR("BEACON MISSED")); - break; - - case EV_BEACON_TRACKED: - strcpy_P(buff, PSTR("BEACON TRACKED")); - break; - - case EV_JOINING: - strcpy_P(buff, PSTR("JOINING")); - break; - - case EV_JOINED: - strcpy_P(buff, PSTR("JOINED")); - // set data rate adaptation according to saved setting - LMIC_setAdrMode(cfg.adrmode); - // set data rate and transmit power to defaults only if we have no ADR - if (!cfg.adrmode) - LMIC_setDrTxpow(assertDR(cfg.loradr), cfg.txpower); - // show current devaddr - ESP_LOGI(TAG, "DEVaddr=%08X", LMIC.devaddr); - ESP_LOGI(TAG, "Radio parameters %s / %s / %s", - getSfName(updr2rps(LMIC.datarate)), - getBwName(updr2rps(LMIC.datarate)), - getCrName(updr2rps(LMIC.datarate))); - break; - - case EV_RFU1: - strcpy_P(buff, PSTR("RFU1")); - break; - - case EV_JOIN_FAILED: - strcpy_P(buff, PSTR("JOIN FAILED")); - break; - - case EV_REJOIN_FAILED: - strcpy_P(buff, PSTR("REJOIN FAILED")); - break; - - case EV_TXCOMPLETE: - strcpy_P(buff, PSTR("TX COMPLETE")); - break; - - case EV_LOST_TSYNC: - strcpy_P(buff, PSTR("LOST TSYNC")); - break; - - case EV_RESET: - strcpy_P(buff, PSTR("RESET")); - break; - - case EV_RXCOMPLETE: - // data received in ping slot - strcpy_P(buff, PSTR("RX COMPLETE")); - break; - - case EV_LINK_DEAD: - strcpy_P(buff, PSTR("LINK DEAD")); - break; - - case EV_LINK_ALIVE: - strcpy_P(buff, PSTR("LINK_ALIVE")); - break; - - case EV_SCAN_FOUND: - strcpy_P(buff, PSTR("SCAN FOUND")); - break; - - case EV_TXSTART: - if (!(LMIC.opmode & OP_JOINING)) { - strcpy_P(buff, PSTR("TX START")); - } - break; - - case EV_TXCANCELED: - strcpy_P(buff, PSTR("TX CANCELLED")); - break; - - case EV_RXSTART: - strcpy_P(buff, PSTR("RX START")); - break; - - case EV_JOIN_TXCOMPLETE: - strcpy_P(buff, PSTR("JOIN WAIT")); - break; - - default: - sprintf_P(buff, PSTR("LMIC EV %d"), ev); - break; - } - - // Log & Display if asked - if (*buff) { - ESP_LOGI(TAG, "%s", buff); - sprintf(lmic_event_msg, buff); - } -} - -// LMIC send task -void lora_send(void *pvParameters) { - configASSERT(((uint32_t)pvParameters) == 1); // FreeRTOS check - - MessageBuffer_t SendBuffer; - - while (1) { - - // postpone until we are joined if we are not - while (!LMIC.devaddr) { - vTaskDelay(pdMS_TO_TICKS(500)); - } - - // fetch next or wait for payload to send from queue - if (xQueueReceive(LoraSendQueue, &SendBuffer, portMAX_DELAY) != pdTRUE) { - ESP_LOGE(TAG, "Premature return from xQueueReceive() with no data!"); - continue; - } - - // attempt to transmit payload - else { - - switch (LMIC_sendWithCallback_strict( - SendBuffer.MessagePort, SendBuffer.Message, SendBuffer.MessageSize, - (cfg.countermode & 0x02), myTxCallback, NULL)) { - - case LMIC_ERROR_SUCCESS: - ESP_LOGI(TAG, "%d byte(s) sent to LORA", SendBuffer.MessageSize); - break; - case LMIC_ERROR_TX_BUSY: // LMIC already has a tx message pending - case LMIC_ERROR_TX_FAILED: // message was not sent - // ESP_LOGD(TAG, "LMIC busy, message re-enqueued"); // very noisy - vTaskDelay(pdMS_TO_TICKS(1000 + random(500))); // wait a while - lora_enqueuedata(&SendBuffer); // re-enqueue the undelivered message - break; - case LMIC_ERROR_TX_TOO_LARGE: // message size exceeds LMIC buffer size - case LMIC_ERROR_TX_NOT_FEASIBLE: // message too large for current datarate - ESP_LOGI(TAG, - "Message too large to send, message not sent and deleted"); - // we need some kind of error handling here -> to be done - break; - default: // other LMIC return code - ESP_LOGE(TAG, "LMIC error, message not sent and deleted"); - - } // switch - } - delay(2); // yield to CPU - } -} - -esp_err_t lora_stack_init() { - assert(SEND_QUEUE_SIZE); - LoraSendQueue = xQueueCreate(SEND_QUEUE_SIZE, sizeof(MessageBuffer_t)); - if (LoraSendQueue == 0) { - ESP_LOGE(TAG, "Could not create LORA send queue. Aborting."); - return ESP_FAIL; - } - ESP_LOGI(TAG, "LORA send queue created, size %d Bytes", - SEND_QUEUE_SIZE * sizeof(MessageBuffer_t)); - - // start lorawan stack - ESP_LOGI(TAG, "Starting LMIC..."); - xTaskCreatePinnedToCore(lmictask, // task function - "lmictask", // name of task - 4096, // stack size of task - (void *)1, // parameter of the task - 5, // priority of the task - &lmicTask, // task handle - 1); // CPU core - - if (!LMIC_startJoining()) { // start joining - ESP_LOGI(TAG, "Already joined"); - } - - // start lmic send task - xTaskCreatePinnedToCore(lora_send, // task function - "lorasendtask", // name of task - 3072, // stack size of task - (void *)1, // parameter of the task - 1, // priority of the task - &lorasendTask, // task handle - 1); // CPU core - - return ESP_OK; -} - -void lora_enqueuedata(MessageBuffer_t *message) { - // enqueue message in LORA send queue - BaseType_t ret = pdFALSE; - MessageBuffer_t DummyBuffer; - sendprio_t prio = message->MessagePrio; - - switch (prio) { - case prio_high: - // clear some space in queue if full, then fallthrough to prio_normal - if (uxQueueSpacesAvailable(LoraSendQueue) == 0) { - xQueueReceive(LoraSendQueue, &DummyBuffer, (TickType_t)0); - ESP_LOGW(TAG, "LORA sendqueue purged, data is lost"); - } - case prio_normal: - ret = xQueueSendToFront(LoraSendQueue, (void *)message, (TickType_t)0); - break; - case prio_low: - default: - ret = xQueueSendToBack(LoraSendQueue, (void *)message, (TickType_t)0); - break; - } - if (ret != pdTRUE) - ESP_LOGW(TAG, "LORA sendqueue is full"); -} - -void lora_queuereset(void) { xQueueReset(LoraSendQueue); } - -#if (TIME_SYNC_LORAWAN) -void IRAM_ATTR user_request_network_time_callback(void *pVoidUserUTCTime, - int flagSuccess) { - // Explicit conversion from void* to uint32_t* to avoid compiler errors - time_t *pUserUTCTime = (time_t *)pVoidUserUTCTime; - - // A struct that will be populated by LMIC_getNetworkTimeReference. - // It contains the following fields: - // - tLocal: the value returned by os_GetTime() when the time - // request was sent to the gateway, and - // - tNetwork: the seconds between the GPS epoch and the time - // the gateway received the time request - lmic_time_reference_t lmicTimeReference; - - if (flagSuccess != 1) { - ESP_LOGW(TAG, "LoRaWAN network did not answer time request"); - return; - } - - // Populate lmic_time_reference - flagSuccess = LMIC_getNetworkTimeReference(&lmicTimeReference); - if (flagSuccess != 1) { - ESP_LOGW(TAG, "LoRaWAN time request failed"); - return; - } - - // mask application irq to ensure accurate timing - mask_user_IRQ(); - - // Update userUTCTime, considering the difference between the GPS and UTC - // time, and the leap seconds until year 2019 - *pUserUTCTime = lmicTimeReference.tNetwork + 315964800; - // Current time, in ticks - ostime_t ticksNow = os_getTime(); - // Time when the request was sent, in ticks - ostime_t ticksRequestSent = lmicTimeReference.tLocal; - // Add the delay between the instant the time was transmitted and - // the current time - time_t requestDelaySec = osticks2ms(ticksNow - ticksRequestSent) / 1000; - - // Update system time with time read from the network - setMyTime(*pUserUTCTime + requestDelaySec, 0, _lora); - -finish: - // end of time critical section: release app irq lock - unmask_user_IRQ(); - -} // user_request_network_time_callback -#endif // TIME_SYNC_LORAWAN - -// LMIC lorawan stack task -void lmictask(void *pvParameters) { - configASSERT(((uint32_t)pvParameters) == 1); // FreeRTOS check - - os_init(); // initialize lmic run-time environment - LMIC_reset(); // initialize lmic MAC - LMIC_setLinkCheckMode(0); - -#if defined(CFG_eu868) - // Note that The Things Network uses the non-standard SF9BW125 data rate for - // RX2 in Europe and switches between RX1 and RX2 based on network congestion. - // Thus, to avoid occasionally join failures, we set datarate to SF9 and - // bypass the LORAWAN spec-compliant RX2 == SF12 setting - LMIC_setDrTxpow(EU868_DR_SF9, KEEP_TXPOW); -#else - // Set the data rate to Spreading Factor 7. This is the fastest supported - // rate for 125 kHz channels, and it minimizes air time and battery power. - // Set the transmission power to 14 dBi (25 mW). - LMIC_setDrTxpow(DR_SF7, 14); -#endif - -// This tells LMIC to make the receive windows bigger, in case your clock is -// faster or slower. This causes the transceiver to be earlier switched on, -// so consuming more power. You may sharpen (reduce) CLOCK_ERROR_PERCENTAGE -// in src/lmic_config.h if you are limited on battery. -#ifdef CLOCK_ERROR_PROCENTAGE - LMIC_setClockError(MAX_CLOCK_ERROR * CLOCK_ERROR_PROCENTAGE / 100); -#endif - -//#if defined(CFG_US915) || defined(CFG_au921) -#if CFG_LMIC_US_like - // in the US, with TTN, it saves join time if we start on subband 1 - // (channels 8-15). This will get overridden after the join by parameters - // from the network. If working with other networks or in other regions, - // this will need to be changed. - LMIC_selectSubBand(1); -#endif - - // register a callback for downlink messages. We aren't trying to write - // reentrant code, so pUserData is NULL. - LMIC_registerRxMessageCb(myRxCallback, NULL); - - while (1) { - os_runloop_once(); // execute lmic scheduled jobs and events - delay(2); // yield to CPU - } -} // lmictask - -// receive message handler -void myRxCallback(void *pUserData, uint8_t port, const uint8_t *pMsg, - size_t nMsg) { - - // display type of received data - if (nMsg) - ESP_LOGI(TAG, "Received %u byte(s) of payload on port %u", nMsg, port); - else if (port) - ESP_LOGI(TAG, "Received empty message on port %u", port); - - // list MAC messages, if any - uint8_t nMac = pMsg - &LMIC.frame[0]; - if (port != MACPORT) - --nMac; - if (nMac) { - ESP_LOGI(TAG, "%u byte(s) downlink MAC commands", nMac); - // NOT WORKING YET - // whe need to unwrap the MAC command from LMIC.frame here - // mac_decode(LMIC.frame, nMac, MACdn_table, sizeof(MACdn_table) / - // sizeof(MACdn_table[0])); - } - - if (LMIC.pendMacLen) { - ESP_LOGI(TAG, "%u byte(s) uplink MAC commands", LMIC.pendMacLen); - mac_decode(LMIC.pendMacData, LMIC.pendMacLen, MACup_table, - sizeof(MACup_table) / sizeof(MACup_table[0])); - } - - switch (port) { - - // ignore mac messages - case MACPORT: - break; - - // rcommand received -> call interpreter - case RCMDPORT: - rcommand(pMsg, nMsg); - break; - - default: - -#if (TIME_SYNC_LORASERVER) - // valid timesync answer -> call timesync processor - if ((port >= TIMEANSWERPORT_MIN) && (port <= TIMEANSWERPORT_MAX)) { - recv_timesync_ans(port, pMsg, nMsg); - break; - } -#endif - - // unknown port -> display info - ESP_LOGI(TAG, "Received data on unsupported port %u", port); - break; - } // switch -} - -// transmit complete message handler -void myTxCallback(void *pUserData, int fSuccess) { - -#if (TIME_SYNC_LORASERVER) - // if last packet sent was a timesync request, store TX timestamp - if (LMIC.pendTxPort == TIMEPORT) - store_time_sync_req(osticks2ms(LMIC.txend)); // milliseconds -#endif -} - -// decode LORAWAN MAC message -void mac_decode(const uint8_t cmd[], const uint8_t cmdlen, const mac_t table[], - const uint8_t tablesize) { - - if (!cmdlen) - return; - - uint8_t foundcmd[cmdlen], cursor = 0; - - while (cursor < cmdlen) { - - int i = tablesize; // number of commands in table - - while (i--) { - if (cmd[cursor] == table[i].opcode) { // lookup command in opcode table - cursor++; // strip 1 byte opcode - if ((cursor + table[i].params) <= cmdlen) { - memmove(foundcmd, cmd + cursor, - table[i].params); // strip opcode from cmd array - cursor += table[i].params; - ESP_LOGD(TAG, "MAC command %s", table[i].cmdname); - } else - ESP_LOGD(TAG, "MAC command 0x%02X with missing parameter(s)", - table[i].opcode); - break; // command found -> exit table lookup loop - } // end of command validation - } // end of command table lookup loop - if (i < 0) { // command not found -> skip it - ESP_LOGD(TAG, "Unknown MAC command 0x%02X", cmd[cursor]); - cursor++; - } - } // command parsing loop - -} // mac_decode() - -uint8_t getBattLevel() { - /* - return values: - MCMD_DEVS_EXT_POWER = 0x00, // external power supply - MCMD_DEVS_BATT_MIN = 0x01, // min battery value - MCMD_DEVS_BATT_MAX = 0xFE, // max battery value - MCMD_DEVS_BATT_NOINFO = 0xFF, // unknown battery level - */ -#if (defined HAS_PMU || defined BAT_MEASURE_ADC) - uint16_t voltage = read_voltage(); - - switch (voltage) { - case 0: - return MCMD_DEVS_BATT_NOINFO; - case 0xffff: - return MCMD_DEVS_EXT_POWER; - default: - return (voltage > OTA_MIN_BATT ? MCMD_DEVS_BATT_MAX : MCMD_DEVS_BATT_MIN); - } -#else // we don't have any info on battery level - return MCMD_DEVS_BATT_NOINFO; -#endif -} // getBattLevel() - -// u1_t os_getBattLevel(void) { return getBattLevel(); }; - -const char *getSfName(rps_t rps) { - const char *const t[] = {"FSK", "SF7", "SF8", "SF9", - "SF10", "SF11", "SF12", "SF?"}; - return t[getSf(rps)]; -} - -const char *getBwName(rps_t rps) { - const char *const t[] = {"BW125", "BW250", "BW500", "BW?"}; - return t[getBw(rps)]; -} - -const char *getCrName(rps_t rps) { - const char *const t[] = {"CR 4/5", "CR 4/6", "CR 4/7", "CR 4/8"}; - return t[getCr(rps)]; -} - +// Basic Config +#if (HAS_LORA) +#include "lorawan.h" +#endif + +// Local logging Tag +static const char TAG[] = "lora"; + +#if (HAS_LORA) + +#if CLOCK_ERROR_PROCENTAGE > 7 +#warning CLOCK_ERROR_PROCENTAGE value in lmic_config.h is too high; values > 7 will cause side effects +#endif + +#if (TIME_SYNC_LORAWAN) +#ifndef LMIC_ENABLE_DeviceTimeReq +#define LMIC_ENABLE_DeviceTimeReq 1 +#endif +#endif + +QueueHandle_t LoraSendQueue; +TaskHandle_t lmicTask = NULL, lorasendTask = NULL; + +// table of LORAWAN MAC messages sent by the network to the device +// format: opcode, cmdname (max 19 chars), #bytes params +// source: LoRaWAN 1.1 Specification (October 11, 2017) +static const mac_t MACdn_table[] = { + {0x01, "ResetConf", 1}, {0x02, "LinkCheckAns", 2}, + {0x03, "LinkADRReq", 4}, {0x04, "DutyCycleReq", 1}, + {0x05, "RXParamSetupReq", 4}, {0x06, "DevStatusReq", 0}, + {0x07, "NewChannelReq", 5}, {0x08, "RxTimingSetupReq", 1}, + {0x09, "TxParamSetupReq", 1}, {0x0A, "DlChannelReq", 4}, + {0x0B, "RekeyConf", 1}, {0x0C, "ADRParamSetupReq", 1}, + {0x0D, "DeviceTimeAns", 5}, {0x0E, "ForceRejoinReq", 2}, + {0x0F, "RejoinParamSetupReq", 1}}; + +// table of LORAWAN MAC messages sent by the device to the network +static const mac_t MACup_table[] = { + {0x01, "ResetInd", 1}, {0x02, "LinkCheckReq", 0}, + {0x03, "LinkADRAns", 1}, {0x04, "DutyCycleAns", 0}, + {0x05, "RXParamSetupAns", 1}, {0x06, "DevStatusAns", 2}, + {0x07, "NewChannelAns", 1}, {0x08, "RxTimingSetupAns", 0}, + {0x09, "TxParamSetupAns", 0}, {0x0A, "DlChannelAns", 1}, + {0x0B, "RekeyInd", 1}, {0x0C, "ADRParamSetupAns", 0}, + {0x0D, "DeviceTimeReq", 0}, {0x0F, "RejoinParamSetupAns", 1}}; + +class MyHalConfig_t : public Arduino_LMIC::HalConfiguration_t { + +public: + MyHalConfig_t(){}; + + // set SPI pins to board configuration, pins may come from pins_arduino.h + virtual void begin(void) override { + SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); + } + + // virtual void end(void) override + + // virtual ostime_t setModuleActive(bool state) override +}; + +static MyHalConfig_t myHalConfig{}; + +// LMIC pin mapping for Hope RFM95 / HPDtek HPD13A transceivers +static const lmic_pinmap myPinmap = { + .nss = LORA_CS, + .rxtx = LMIC_UNUSED_PIN, + .rst = LORA_RST == NOT_A_PIN ? LMIC_UNUSED_PIN : LORA_RST, + .dio = {LORA_IRQ, LORA_IO1, + LORA_IO2 == NOT_A_PIN ? LMIC_UNUSED_PIN : LORA_IO2}, + .rxtx_rx_active = LMIC_UNUSED_PIN, + .rssi_cal = 10, + .spi_freq = 8000000, // 8MHz + .pConfig = &myHalConfig}; + +void lora_setupForNetwork(bool preJoin) { + + if (preJoin) { + +#if CFG_LMIC_US_like + // in the US, with TTN, it saves join time if we start on subband 1 + // (channels 8-15). This will get overridden after the join by + // parameters from the network. If working with other networks or in + // other regions, this will need to be changed. + LMIC_selectSubBand(1); +#elif CFG_LMIC_EU_like + // setting for TheThingsNetwork + // TTN uses SF9, not SF12, for RX2 window + LMIC.dn2Dr = EU868_DR_SF9; + // Disable link check validation + LMIC_setLinkCheckMode(0); +#endif + + } else { + // set data rate adaptation according to saved setting + LMIC_setAdrMode(cfg.adrmode); + // set data rate and transmit power to stored device values if no ADR + if (!cfg.adrmode) + LMIC_setDrTxpow(assertDR(cfg.loradr), cfg.txpower); + // show current devaddr + ESP_LOGI(TAG, "DEVaddr: %08X", LMIC.devaddr); + ESP_LOGI(TAG, "Radio parameters: %s / %s / %s", + getSfName(updr2rps(LMIC.datarate)), + getBwName(updr2rps(LMIC.datarate)), + getCrName(updr2rps(LMIC.datarate))); + } +} + +// DevEUI generator using devices's MAC address +void gen_lora_deveui(uint8_t *pdeveui) { + uint8_t *p = pdeveui, dmac[6]; + int i = 0; + esp_efuse_mac_get_default(dmac); + // deveui is LSB, we reverse it so TTN DEVEUI display + // will remain the same as MAC address + // MAC is 6 bytes, devEUI 8, set first 2 ones + // with an arbitrary value + *p++ = 0xFF; + *p++ = 0xFE; + // Then next 6 bytes are mac address reversed + for (i = 0; i < 6; i++) { + *p++ = dmac[5 - i]; + } +} + +/* new version, does it with well formed mac according IEEE spec, but is +breaking change +// DevEUI generator using devices's MAC address +void gen_lora_deveui(uint8_t *pdeveui) { + uint8_t *p = pdeveui, dmac[6]; + ESP_ERROR_CHECK(esp_efuse_mac_get_default(dmac)); + // deveui is LSB, we reverse it so TTN DEVEUI display + // will remain the same as MAC address + // MAC is 6 bytes, devEUI 8, set middle 2 ones + // to an arbitrary value + *p++ = dmac[5]; + *p++ = dmac[4]; + *p++ = dmac[3]; + *p++ = 0xfe; + *p++ = 0xff; + *p++ = dmac[2]; + *p++ = dmac[1]; + *p++ = dmac[0]; +} +*/ + +// Function to do a byte swap in a byte array +void RevBytes(unsigned char *b, size_t c) { + u1_t i; + for (i = 0; i < c / 2; i++) { + unsigned char t = b[i]; + b[i] = b[c - 1 - i]; + b[c - 1 - i] = t; + } +} + +// LMIC callback functions +void os_getDevKey(u1_t *buf) { memcpy(buf, APPKEY, 16); } + +void os_getArtEui(u1_t *buf) { + memcpy(buf, APPEUI, 8); + RevBytes(buf, 8); // TTN requires it in LSB First order, so we swap bytes +} + +void os_getDevEui(u1_t *buf) { + int i = 0, k = 0; + memcpy(buf, DEVEUI, 8); // get fixed DEVEUI from loraconf.h + for (i = 0; i < 8; i++) { + k += buf[i]; + } + if (k) { + RevBytes(buf, 8); // use fixed DEVEUI and swap bytes to LSB format + } else { + gen_lora_deveui(buf); // generate DEVEUI from device's MAC + } + +// Get MCP 24AA02E64 hardware DEVEUI (override default settings if found) +#ifdef MCP_24AA02E64_I2C_ADDRESS + get_hard_deveui(buf); + RevBytes(buf, 8); // swap bytes to LSB format +#endif +} + +void get_hard_deveui(uint8_t *pdeveui) { + // read DEVEUI from Microchip 24AA02E64 2Kb serial eeprom if present +#ifdef MCP_24AA02E64_I2C_ADDRESS + + uint8_t i2c_ret; + + // Init this just in case, no more to 100KHz + Wire.begin(SDA, SCL, 100000); + Wire.beginTransmission(MCP_24AA02E64_I2C_ADDRESS); + Wire.write(MCP_24AA02E64_MAC_ADDRESS); + i2c_ret = Wire.endTransmission(); + + // check if device was seen on i2c bus + if (i2c_ret == 0) { + char deveui[32] = ""; + uint8_t data; + + Wire.beginTransmission(MCP_24AA02E64_I2C_ADDRESS); + Wire.write(MCP_24AA02E64_MAC_ADDRESS); + Wire.endTransmission(); + + Wire.requestFrom(MCP_24AA02E64_I2C_ADDRESS, 8); + while (Wire.available()) { + data = Wire.read(); + sprintf(deveui + strlen(deveui), "%02X ", data); + *pdeveui++ = data; + } + ESP_LOGI(TAG, "Serial EEPROM found, read DEVEUI %s", deveui); + } else + ESP_LOGI(TAG, "Could not read DEVEUI from serial EEPROM"); + + // Set back to 400KHz to speed up OLED + Wire.setClock(400000); +#endif // MCP 24AA02E64 +} + +#if (VERBOSE) + +// Display OTAA keys +void showLoraKeys(void) { + // LMIC may not have used callback to fill + // all EUI buffer so we do it here to a temp + // buffer to be able to display them + uint8_t buf[32]; + os_getDevEui((u1_t *)buf); + printKey("DevEUI", buf, 8, true); + os_getArtEui((u1_t *)buf); + printKey("AppEUI", buf, 8, true); + os_getDevKey((u1_t *)buf); + printKey("AppKey", buf, 16, false); +} + +#endif // VERBOSE + +// LMIC send task +void lora_send(void *pvParameters) { + configASSERT(((uint32_t)pvParameters) == 1); // FreeRTOS check + + MessageBuffer_t SendBuffer; + + while (1) { + + // postpone until we are joined if we are not + // while (!LMIC.devaddr) { + // vTaskDelay(pdMS_TO_TICKS(500)); + //} + + // fetch next or wait for payload to send from queue + if (xQueueReceive(LoraSendQueue, &SendBuffer, portMAX_DELAY) != pdTRUE) { + ESP_LOGE(TAG, "Premature return from xQueueReceive() with no data!"); + continue; + } + + // attempt to transmit payload + else { + + // switch (LMIC_sendWithCallback_strict( + switch (LMIC_sendWithCallback( + SendBuffer.MessagePort, SendBuffer.Message, SendBuffer.MessageSize, + (cfg.countermode & 0x02), myTxCallback, NULL)) { + + case LMIC_ERROR_SUCCESS: + ESP_LOGI(TAG, "%d byte(s) sent to LORA", SendBuffer.MessageSize); + break; + case LMIC_ERROR_TX_BUSY: // LMIC already has a tx message pending + case LMIC_ERROR_TX_FAILED: // message was not sent + // ESP_LOGD(TAG, "LMIC busy, message re-enqueued"); // very noisy + vTaskDelay(pdMS_TO_TICKS(1000 + random(500))); // wait a while + lora_enqueuedata(&SendBuffer); // re-enqueue the undelivered message + break; + case LMIC_ERROR_TX_TOO_LARGE: // message size exceeds LMIC buffer size + case LMIC_ERROR_TX_NOT_FEASIBLE: // message too large for current + // datarate + ESP_LOGI(TAG, + "Message too large to send, message not sent and deleted"); + // we need some kind of error handling here -> to be done + break; + default: // other LMIC return code + ESP_LOGE(TAG, "LMIC error, message not sent and deleted"); + + } // switch + } + delay(2); // yield to CPU + } +} + +esp_err_t lora_stack_init() { + assert(SEND_QUEUE_SIZE); + LoraSendQueue = xQueueCreate(SEND_QUEUE_SIZE, sizeof(MessageBuffer_t)); + if (LoraSendQueue == 0) { + ESP_LOGE(TAG, "Could not create LORA send queue. Aborting."); + return ESP_FAIL; + } + ESP_LOGI(TAG, "LORA send queue created, size %d Bytes", + SEND_QUEUE_SIZE * sizeof(MessageBuffer_t)); + + // start lorawan stack + ESP_LOGI(TAG, "Starting LMIC..."); + xTaskCreatePinnedToCore(lmictask, // task function + "lmictask", // name of task + 4096, // stack size of task + (void *)1, // parameter of the task + 5, // priority of the task + &lmicTask, // task handle + 1); // CPU core + + // start join + if (!LMIC_startJoining()) + ESP_LOGI(TAG, "Already joined"); + + // start lmic send task + xTaskCreatePinnedToCore(lora_send, // task function + "lorasendtask", // name of task + 3072, // stack size of task + (void *)1, // parameter of the task + 1, // priority of the task + &lorasendTask, // task handle + 1); // CPU core + + return ESP_OK; +} + +void lora_enqueuedata(MessageBuffer_t *message) { + // enqueue message in LORA send queue + BaseType_t ret = pdFALSE; + MessageBuffer_t DummyBuffer; + sendprio_t prio = message->MessagePrio; + + switch (prio) { + case prio_high: + // clear some space in queue if full, then fallthrough to prio_normal + if (uxQueueSpacesAvailable(LoraSendQueue) == 0) { + xQueueReceive(LoraSendQueue, &DummyBuffer, (TickType_t)0); + ESP_LOGW(TAG, "LORA sendqueue purged, data is lost"); + } + case prio_normal: + ret = xQueueSendToFront(LoraSendQueue, (void *)message, (TickType_t)0); + break; + case prio_low: + default: + ret = xQueueSendToBack(LoraSendQueue, (void *)message, (TickType_t)0); + break; + } + if (ret != pdTRUE) { + snprintf(lmic_event_msg + 14, LMIC_EVENTMSG_LEN - 14, "<>"); + ESP_LOGW(TAG, "LORA sendqueue is full"); + } else { + // add Lora send queue length to display + snprintf(lmic_event_msg + 14, LMIC_EVENTMSG_LEN - 14, "%2u", + uxQueueMessagesWaiting(LoraSendQueue)); + } +} + +void lora_queuereset(void) { xQueueReset(LoraSendQueue); } + +#if (TIME_SYNC_LORAWAN) +void IRAM_ATTR user_request_network_time_callback(void *pVoidUserUTCTime, + int flagSuccess) { + // Explicit conversion from void* to uint32_t* to avoid compiler errors + time_t *pUserUTCTime = (time_t *)pVoidUserUTCTime; + + // A struct that will be populated by LMIC_getNetworkTimeReference. + // It contains the following fields: + // - tLocal: the value returned by os_GetTime() when the time + // request was sent to the gateway, and + // - tNetwork: the seconds between the GPS epoch and the time + // the gateway received the time request + lmic_time_reference_t lmicTimeReference; + + if (flagSuccess != 1) { + ESP_LOGW(TAG, "LoRaWAN network did not answer time request"); + return; + } + + // Populate lmic_time_reference + flagSuccess = LMIC_getNetworkTimeReference(&lmicTimeReference); + if (flagSuccess != 1) { + ESP_LOGW(TAG, "LoRaWAN time request failed"); + return; + } + + // mask application irq to ensure accurate timing + mask_user_IRQ(); + + // Update userUTCTime, considering the difference between the GPS and UTC + // time, and the leap seconds until year 2019 + *pUserUTCTime = lmicTimeReference.tNetwork + 315964800; + // Current time, in ticks + ostime_t ticksNow = os_getTime(); + // Time when the request was sent, in ticks + ostime_t ticksRequestSent = lmicTimeReference.tLocal; + // Add the delay between the instant the time was transmitted and + // the current time + time_t requestDelaySec = osticks2ms(ticksNow - ticksRequestSent) / 1000; + + // Update system time with time read from the network + setMyTime(*pUserUTCTime + requestDelaySec, 0, _lora); + +finish: + // end of time critical section: release app irq lock + unmask_user_IRQ(); + +} // user_request_network_time_callback +#endif // TIME_SYNC_LORAWAN + +// LMIC lorawan stack task +void lmictask(void *pvParameters) { + configASSERT(((uint32_t)pvParameters) == 1); + + // setup LMIC stack + os_init_ex(&myPinmap); // initialize lmic run-time environment + + // register a callback for downlink messages and lmic events. + // We aren't trying to write reentrant code, so pUserData is NULL. + // LMIC_reset() doesn't affect callbacks, so we can do this first. + LMIC_registerRxMessageCb(myRxCallback, NULL); + LMIC_registerEventCb(myEventCallback, NULL); + + // Reset the MAC state. Session and pending data transfers will be + // discarded. + LMIC_reset(); + +// This tells LMIC to make the receive windows bigger, in case your clock is +// faster or slower. This causes the transceiver to be earlier switched on, +// so consuming more power. You may sharpen (reduce) CLOCK_ERROR_PERCENTAGE +// in src/lmic_config.h if you are limited on battery. +#ifdef CLOCK_ERROR_PROCENTAGE + LMIC_setClockError(CLOCK_ERROR_PROCENTAGE * MAX_CLOCK_ERROR / 100); +#endif + + while (1) { + os_runloop_once(); // execute lmic scheduled jobs and events + delay(2); // yield to CPU + } +} // lmictask + +// lmic event handler +void myEventCallback(void *pUserData, ev_t ev) { + + // using message descriptors from LMIC library + static const char *const evNames[] = {LMIC_EVENT_NAME_TABLE__INIT}; + // get current length of lora send queue + uint8_t const msgWaiting = uxQueueMessagesWaiting(LoraSendQueue); + + // get current event message + if (ev < sizeof(evNames) / sizeof(evNames[0])) + snprintf(lmic_event_msg, LMIC_EVENTMSG_LEN, "%-16s", + evNames[ev] + 3); // +3 to strip "EV_" + else + snprintf(lmic_event_msg, LMIC_EVENTMSG_LEN, "LMIC event %-4u ", ev); + + // process current event message + switch (ev) { + case EV_JOINING: + // do the network-specific setup prior to join. + lora_setupForNetwork(true); + break; + + case EV_JOINED: + // do the after join network-specific setup. + lora_setupForNetwork(false); + break; + + case EV_JOIN_TXCOMPLETE: + // replace descriptor from library with more descriptive term + snprintf(lmic_event_msg, LMIC_EVENTMSG_LEN, "%-16s", "JOIN_WAIT"); + break; + + default: + break; + } + + // add Lora send queue length to display + if (msgWaiting) + snprintf(lmic_event_msg + 14, LMIC_EVENTMSG_LEN - 14, "%2u", msgWaiting); + + // print event + ESP_LOGD(TAG, "%s", lmic_event_msg); +} + +// receive message handler +void myRxCallback(void *pUserData, uint8_t port, const uint8_t *pMsg, + size_t nMsg) { + + // display type of received data + if (nMsg) + ESP_LOGI(TAG, "Received %u byte(s) of payload on port %u", nMsg, port); + else if (port) + ESP_LOGI(TAG, "Received empty message on port %u", port); + + // list MAC messages, if any + uint8_t nMac = pMsg - &LMIC.frame[0]; + if (port != MACPORT) + --nMac; + if (nMac) { + ESP_LOGI(TAG, "%u byte(s) downlink MAC commands", nMac); + // NOT WORKING YET + // whe need to unwrap the MAC command from LMIC.frame here + // mac_decode(LMIC.frame, nMac, MACdn_table, sizeof(MACdn_table) / + // sizeof(MACdn_table[0])); + } + + if (LMIC.pendMacLen) { + ESP_LOGI(TAG, "%u byte(s) uplink MAC commands", LMIC.pendMacLen); + mac_decode(LMIC.pendMacData, LMIC.pendMacLen, MACup_table, + sizeof(MACup_table) / sizeof(MACup_table[0])); + } + + switch (port) { + + // ignore mac messages + case MACPORT: + break; + + // rcommand received -> call interpreter + case RCMDPORT: + rcommand(pMsg, nMsg); + break; + + default: + +#if (TIME_SYNC_LORASERVER) + // valid timesync answer -> call timesync processor + if (port == TIMEPORT) { + recv_timesync_ans(pMsg, nMsg); + break; + } +#endif + + // unknown port -> display info + ESP_LOGI(TAG, "Received data on unsupported port %u", port); + break; + } // switch +} + +// transmit complete message handler +void myTxCallback(void *pUserData, int fSuccess) { + +#if (TIME_SYNC_LORASERVER) + // if last packet sent was a timesync request, store TX timestamp + if (LMIC.pendTxPort == TIMEPORT) + store_time_sync_req(osticks2ms(LMIC.txend)); // milliseconds +#endif +} + +// decode LORAWAN MAC message +void mac_decode(const uint8_t cmd[], const uint8_t cmdlen, const mac_t table[], + const uint8_t tablesize) { + + if (!cmdlen) + return; + + uint8_t foundcmd[cmdlen], cursor = 0; + + while (cursor < cmdlen) { + + int i = tablesize; // number of commands in table + + while (i--) { + if (cmd[cursor] == table[i].opcode) { // lookup command in opcode table + cursor++; // strip 1 byte opcode + if ((cursor + table[i].params) <= cmdlen) { + memmove(foundcmd, cmd + cursor, + table[i].params); // strip opcode from cmd array + cursor += table[i].params; + ESP_LOGD(TAG, "MAC command %s", table[i].cmdname); + } else + ESP_LOGD(TAG, "MAC command 0x%02X with missing parameter(s)", + table[i].opcode); + break; // command found -> exit table lookup loop + } // end of command validation + } // end of command table lookup loop + if (i < 0) { // command not found -> skip it + ESP_LOGD(TAG, "Unknown MAC command 0x%02X", cmd[cursor]); + cursor++; + } + } // command parsing loop + +} // mac_decode() + +uint8_t getBattLevel() { + /* + return values: + MCMD_DEVS_EXT_POWER = 0x00, // external power supply + MCMD_DEVS_BATT_MIN = 0x01, // min battery value + MCMD_DEVS_BATT_MAX = 0xFE, // max battery value + MCMD_DEVS_BATT_NOINFO = 0xFF, // unknown battery level + */ +#if (defined HAS_PMU || defined BAT_MEASURE_ADC) + uint16_t voltage = read_voltage(); + + switch (voltage) { + case 0: + return MCMD_DEVS_BATT_NOINFO; + case 0xffff: + return MCMD_DEVS_EXT_POWER; + default: + return (voltage > OTA_MIN_BATT ? MCMD_DEVS_BATT_MAX : MCMD_DEVS_BATT_MIN); + } +#else // we don't have any info on battery level + return MCMD_DEVS_BATT_NOINFO; +#endif +} // getBattLevel() + +// u1_t os_getBattLevel(void) { return getBattLevel(); }; + +const char *getSfName(rps_t rps) { + const char *const t[] = {"FSK", "SF7", "SF8", "SF9", + "SF10", "SF11", "SF12", "SF?"}; + return t[getSf(rps)]; +} + +const char *getBwName(rps_t rps) { + const char *const t[] = {"BW125", "BW250", "BW500", "BW?"}; + return t[getBw(rps)]; +} + +const char *getCrName(rps_t rps) { + const char *const t[] = {"CR 4/5", "CR 4/6", "CR 4/7", "CR 4/8"}; + return t[getCr(rps)]; +} + #endif // HAS_LORA \ No newline at end of file diff --git a/src/macsniff.cpp b/src/macsniff.cpp index 0b00e7c1..38a1a991 100644 --- a/src/macsniff.cpp +++ b/src/macsniff.cpp @@ -30,7 +30,7 @@ void printKey(const char *name, const uint8_t *key, uint8_t len, bool lsb) { char keystring[len + 1] = "", keybyte[3]; for (uint8_t i = 0; i < len; i++) { p = lsb ? key + len - i - 1 : key + i; - sprintf(keybyte, "%02X", *p); + snprintf(keybyte, 3, "%02X", *p); strncat(keystring, keybyte, 2); } ESP_LOGI(TAG, "%s: %s", name, keystring); diff --git a/src/main.cpp b/src/main.cpp index 91741688..f9bdce47 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -76,9 +76,9 @@ triggers pps 1 sec impulse // Basic Config #include "main.h" -configData_t cfg; // struct holds current device configuration -char lmic_event_msg[16]; // display buffer for LMIC event message -uint8_t volatile channel = 0; // channel rotation counter +configData_t cfg; // struct holds current device configuration +char lmic_event_msg[LMIC_EVENTMSG_LEN]; // display buffer for LMIC event message +uint8_t volatile channel = 0; // channel rotation counter uint16_t volatile macs_total = 0, macs_wifi = 0, macs_ble = 0, batt_voltage = 0; // globals for display @@ -129,7 +129,7 @@ void setup() { esp_log_level_set("*", ESP_LOG_NONE); #endif - ESP_LOGI(TAG, "Starting %s v%s", PRODUCTNAME, PROGVERSION); + ESP_LOGI(TAG, "Starting Software v%s", PROGVERSION); // print chip information on startup if in verbose mode #if (VERBOSE) @@ -172,9 +172,9 @@ void setup() { // open i2c bus #ifdef HAS_DISPLAY - Wire.begin(MY_OLED_SDA, MY_OLED_SCL, 100000); + Wire.begin(MY_OLED_SDA, MY_OLED_SCL, 400000); #else - Wire.begin(SDA, SCL, 100000); + Wire.begin(SDA, SCL, 400000); #endif // setup power on boards with power management logic @@ -188,9 +188,6 @@ void setup() { strcat_P(features, " PMU"); #endif - // scan i2c bus for devices - i2c_scan(); - #endif // verbose // read (and initialize on first run) runtime settings from NVRAM @@ -200,9 +197,12 @@ void setup() { #ifdef HAS_DISPLAY strcat_P(features, " OLED"); DisplayIsOn = cfg.screenon; - init_display(PRODUCTNAME, PROGVERSION); // note: blocking call + init_display(!cfg.runmode); // note: blocking call #endif + // scan i2c bus for devices + i2c_scan(); + #ifdef BOARD_HAS_PSRAM assert(psramFound()); ESP_LOGI(TAG, "PSRAM found and initialized"); diff --git a/src/ota.cpp b/src/ota.cpp index fc1662bc..f27b4a8c 100644 --- a/src/ota.cpp +++ b/src/ota.cpp @@ -48,25 +48,25 @@ void start_ota_update() { switch_LED(LED_ON); +// init display #ifdef HAS_DISPLAY - u8x8.begin(); - u8x8.setFont(u8x8_font_chroma48medium8_r); - u8x8.clear(); -#ifdef DISPLAY_FLIP - u8x8.setFlipMode(1); +#ifndef DISPLAY_FLIP + oledInit(OLED_128x64, ANGLE_0, false, -1, -1, 400000L); +#else + oledInit(OLED_128x64, ANGLE_FLIPY, false, -1, -1, 400000L); #endif - u8x8.setInverseFont(1); - u8x8.print("SOFTWARE UPDATE \n"); - u8x8.setInverseFont(0); - u8x8.print("WiFi connect ..\n"); - u8x8.print("Has Update? ..\n"); - u8x8.print("Fetching ..\n"); - u8x8.print("Downloading ..\n"); - u8x8.print("Rebooting .."); + oledFill(0, 1); + dp_printf(0, 0, 0, 1, "SOFTWARE UPDATE"); + dp_printf(0, 1, 0, 0, "WiFi connect .."); + dp_printf(0, 2, 0, 0, "Has Update? .."); + dp_printf(0, 3, 0, 0, "Fetching .."); + dp_printf(0, 4, 0, 0, "Downloading .."); + dp_printf(0, 5, 0, 0, "Rebooting .."); + oledDumpBuffer(NULL); #endif ESP_LOGI(TAG, "Starting Wifi OTA update"); - display(1, "**", WIFI_SSID); + ota_display(1, "**", WIFI_SSID); WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASS); @@ -81,7 +81,7 @@ void start_ota_update() { if (WiFi.status() == WL_CONNECTED) { // we now have wifi connection and try to do an OTA over wifi update ESP_LOGI(TAG, "Connected to %s", WIFI_SSID); - display(1, "OK", "WiFi connected"); + ota_display(1, "OK", "WiFi connected"); // do a number of tries to update firmware limited by OTA_MAX_TRY uint8_t j = OTA_MAX_TRY; while ((j--) && (ret > 0)) { @@ -97,13 +97,13 @@ void start_ota_update() { // wifi did not connect ESP_LOGI(TAG, "Could not connect to %s", WIFI_SSID); - display(1, " E", "no WiFi connect"); + ota_display(1, " E", "no WiFi connect"); delay(5000); end: switch_LED(LED_OFF); ESP_LOGI(TAG, "Rebooting to %s firmware", (ret == 0) ? "new" : "current"); - display(5, "**", ""); // mark line rebooting + ota_display(5, "**", ""); // mark line rebooting delay(5000); ESP.restart(); @@ -119,7 +119,7 @@ int do_ota_update() { // Fetch the latest firmware version ESP_LOGI(TAG, "Checking latest firmware version on server"); - display(2, "**", "checking version"); + ota_display(2, "**", "checking version"); if (WiFi.status() != WL_CONNECTED) return 1; @@ -128,23 +128,23 @@ int do_ota_update() { if (latest.length() == 0) { ESP_LOGI(TAG, "Could not fetch info on latest firmware"); - display(2, " E", "file not found"); + ota_display(2, " E", "file not found"); return -1; } else if (version_compare(latest, cfg.version) <= 0) { ESP_LOGI(TAG, "Current firmware is up to date"); - display(2, "NO", "no update found"); + ota_display(2, "NO", "no update found"); return -1; } ESP_LOGI(TAG, "New firmware version v%s available", latest.c_str()); - display(2, "OK", latest.c_str()); + ota_display(2, "OK", latest.c_str()); - display(3, "**", ""); + ota_display(3, "**", ""); if (WiFi.status() != WL_CONNECTED) return 1; String firmwarePath = bintray.getBinaryPath(latest); if (!firmwarePath.endsWith(".bin")) { ESP_LOGI(TAG, "Unsupported binary format"); - display(3, " E", "file type error"); + ota_display(3, " E", "file type error"); return -1; } @@ -158,7 +158,7 @@ int do_ota_update() { if (!client.connect(currentHost.c_str(), port)) { ESP_LOGI(TAG, "Cannot connect to %s", currentHost.c_str()); - display(3, " E", "connection lost"); + ota_display(3, " E", "connection lost"); goto abort; } @@ -169,7 +169,7 @@ int do_ota_update() { if (!client.connect(currentHost.c_str(), port)) { ESP_LOGI(TAG, "Redirect detected, but cannot connect to %s", currentHost.c_str()); - display(3, " E", "server error"); + ota_display(3, " E", "server error"); goto abort; } } @@ -185,7 +185,7 @@ int do_ota_update() { while (client.available() == 0) { if ((millis() - timeout) > (RESPONSE_TIMEOUT_MS)) { ESP_LOGI(TAG, "Client timeout"); - display(3, " E", "client timeout"); + ota_display(3, " E", "client timeout"); goto abort; } } @@ -243,12 +243,12 @@ int do_ota_update() { } // while (client.available()) } // while (redirect) - display(3, "OK", ""); // line download + ota_display(3, "OK", ""); // line download // check whether we have everything for OTA update if (!(contentLength && isValidContentType)) { ESP_LOGI(TAG, "Invalid OTA server response"); - display(4, " E", "response error"); + ota_display(4, " E", "response error"); goto retry; } @@ -262,7 +262,7 @@ int do_ota_update() { if (!Update.begin(contentLength)) { #endif ESP_LOGI(TAG, "Not enough space to start OTA update"); - display(4, " E", "disk full"); + ota_display(4, " E", "disk full"); goto abort; } @@ -271,13 +271,13 @@ int do_ota_update() { Update.onProgress(&show_progress); #endif - display(4, "**", "writing..."); + ota_display(4, "**", "writing..."); written = Update.writeStream(client); // this is a blocking call if (written == contentLength) { ESP_LOGI(TAG, "Written %u bytes successfully", written); snprintf(buf, 17, "%ukB Done!", (uint16_t)(written / 1024)); - display(4, "OK", buf); + ota_display(4, "OK", buf); } else { ESP_LOGI(TAG, "Written only %u of %u bytes, OTA update attempt cancelled", written, contentLength); @@ -288,7 +288,7 @@ int do_ota_update() { } else { ESP_LOGI(TAG, "An error occurred. Error#: %d", Update.getError()); snprintf(buf, 17, "Error#: %d", Update.getError()); - display(4, " E", buf); + ota_display(4, " E", buf); goto retry; } @@ -307,27 +307,26 @@ retry: } // do_ota_update -void display(const uint8_t row, const std::string status, - const std::string msg) { +void ota_display(const uint8_t row, const std::string status, + const std::string msg) { #ifdef HAS_DISPLAY - u8x8.setCursor(14, row); - u8x8.print((status.substr(0, 2)).c_str()); + dp_printf(112, row, 0, 0, status.substr(0, 2).c_str()); if (!msg.empty()) { - u8x8.clearLine(7); - u8x8.setCursor(0, 7); - u8x8.print(msg.substr(0, 16).c_str()); + dp_printf(0, 7, 0, 0, " "); + dp_printf(0, 7, 0, 0, msg.substr(0, 16).c_str()); } + oledDumpBuffer(NULL); #endif } -#ifdef HAS_DISPLAY // callback function to show download progress while streaming data void show_progress(unsigned long current, unsigned long size) { +#ifdef HAS_DISPLAY char buf[17]; snprintf(buf, 17, "%-9lu (%3lu%%)", current, current * 100 / size); - display(4, "**", buf); -} + ota_display(4, "**", buf); #endif +} // helper function to convert strings into lower case bool comp(char s1, char s2) { return tolower(s1) < tolower(s2); } diff --git a/src/paxcounter.conf b/src/paxcounter.conf index 40852f59..c29d5d77 100644 --- a/src/paxcounter.conf +++ b/src/paxcounter.conf @@ -6,14 +6,13 @@ // // Note: After editing, before "build", use "clean" button in PlatformIO! -#define PRODUCTNAME "PAXCNT" - // Verbose enables serial output #define VERBOSE 1 // set to 0 to silence the device, for mute use build option // Payload send cycle and encoding #define SENDCYCLE 30 // payload send cycle [seconds/2], 0 .. 255 #define PAYLOAD_ENCODER 2 // payload encoder: 1=Plain, 2=Packed, 3=Cayenne LPP dynamic, 4=Cayenne LPP packed +#define COUNTERMODE 0 // 0=cyclic, 1=cumulative, 2=cyclic confirmed // Set this to include BLE counting and vendor filter functions, or to switch off WIFI counting #define VENDORFILTER 1 // set to 0 if you want to count things, not people @@ -30,7 +29,7 @@ * |< Scan Window > |< Scan Window > | ... |< Scan Window > | * |< Scan Interval >|< Scan Interval >| ... |< Scan Interval >| * |< Scan duration >| -* +* * Scan duration sets how long scanning should be going on, before starting a new scan cycle. 0 means infinite (default). * Scan window sets how much of the interval should be occupied by scanning. Should be >= BLESCANINTERVAL. * Scan interval is how long scanning should be done on each channel. BLE uses 3 channels for advertising. @@ -49,13 +48,14 @@ #define RETRANSMIT_RCMD 5 // [seconds] wait time before retransmitting rcommand results #define PAYLOAD_BUFFER_SIZE 51 // maximum size of payload block per transmit #define LORADRDEFAULT 5 // 0 .. 15, LoRaWAN datarate, according to regional LoRaWAN specs [default = 5] -#define LORATXPOWDEFAULT 7 // 0 .. 255, LoRaWAN TX power in dBm [default = 14] +#define LORATXPOWDEFAULT 14 // 0 .. 255, LoRaWAN TX power in dBm [default = 14] #define MAXLORARETRY 500 // maximum count of TX retries if LoRa busy #define SEND_QUEUE_SIZE 10 // maximum number of messages in payload send queue [1 = no queue] // Hardware settings #define RGBLUMINOSITY 30 // RGB LED luminosity [default = 30%] #define DISPLAYREFRESH_MS 40 // OLED refresh cycle in ms [default = 40] -> 1000/40 = 25 frames per second +#define DISPLAYCONTRAST 80 // 0 .. 255, OLED display contrast [default = 80] #define HOMECYCLE 30 // house keeping cycle in seconds [default = 30 secs] // Settings for BME680 environmental sensor @@ -97,9 +97,8 @@ #define BEACONPORT 6 // beacon alarms #define BMEPORT 7 // BME680 sensor #define BATTPORT 8 // battery voltage -#define TIMEPORT 9 // time query -#define TIMEANSWERPORT_MIN 0xA0 // time answer, start of port range -#define TIMEANSWERPORT_MAX 0xDF // time answer, end of port range +#define TIMEPORT 9 // time query and response +#define TIMEDIFFPORT 13 // time adjust diff #define SENSOR1PORT 10 // user sensor #1 #define SENSOR2PORT 11 // user sensor #2 #define SENSOR3PORT 12 // user sensor #3 diff --git a/src/power.cpp b/src/power.cpp index 83748286..597f68e8 100644 --- a/src/power.cpp +++ b/src/power.cpp @@ -11,48 +11,41 @@ AXP20X_Class pmu; void power_event_IRQ(void) { - if (!I2C_MUTEX_LOCK()) - ESP_LOGW(TAG, "[%0.3f] i2c mutex lock failed", millis() / 1000.0); - else { + pmu.readIRQ(); - pmu.readIRQ(); - // put your power event handler code here + if (pmu.isVbusOverVoltageIRQ()) + ESP_LOGI(TAG, "USB voltage %.2fV too high.", pmu.getVbusVoltage() / 1000); + if (pmu.isVbusPlugInIRQ()) + ESP_LOGI(TAG, "USB plugged, %.2fV @ %.0mA", pmu.getVbusVoltage() / 1000, + pmu.getVbusCurrent()); + if (pmu.isVbusRemoveIRQ()) + ESP_LOGI(TAG, "USB unplugged."); - if (pmu.isVbusOverVoltageIRQ()) - ESP_LOGI(TAG, "USB voltage %.2fV too high.", pmu.getVbusVoltage() / 1000); - if (pmu.isVbusPlugInIRQ()) - ESP_LOGI(TAG, "USB plugged, %.2fV @ %.0mA", pmu.getVbusVoltage() / 1000, - pmu.getVbusCurrent()); - if (pmu.isVbusRemoveIRQ()) - ESP_LOGI(TAG, "USB unplugged."); + if (pmu.isBattPlugInIRQ()) + ESP_LOGI(TAG, "Battery is connected."); + if (pmu.isBattRemoveIRQ()) + ESP_LOGI(TAG, "Battery was removed."); + if (pmu.isChargingIRQ()) + ESP_LOGI(TAG, "Battery charging."); + if (pmu.isChargingDoneIRQ()) + ESP_LOGI(TAG, "Battery charging done."); + if (pmu.isBattTempLowIRQ()) + ESP_LOGI(TAG, "Battery high temperature."); + if (pmu.isBattTempHighIRQ()) + ESP_LOGI(TAG, "Battery low temperature."); - if (pmu.isBattPlugInIRQ()) - ESP_LOGI(TAG, "Battery is connected."); - if (pmu.isBattRemoveIRQ()) - ESP_LOGI(TAG, "Battery was removed."); - if (pmu.isChargingIRQ()) - ESP_LOGI(TAG, "Battery charging."); - if (pmu.isChargingDoneIRQ()) - ESP_LOGI(TAG, "Battery charging done."); - if (pmu.isBattTempLowIRQ()) - ESP_LOGI(TAG, "Battery high temperature."); - if (pmu.isBattTempHighIRQ()) - ESP_LOGI(TAG, "Battery low temperature."); + // display on/off + // if (pmu.isPEKShortPressIRQ()) { + // cfg.screenon = !cfg.screenon; + //} - // display on/off - if (pmu.isPEKShortPressIRQ()) { - cfg.screenon = !cfg.screenon; - } + // shutdown power + if (pmu.isPEKLongtPressIRQ()) { + AXP192_power(false); // switch off Lora, GPS, display + pmu.shutdown(); // switch off device + } - // shutdown power - if (pmu.isPEKLongtPressIRQ()) { - AXP192_power(false); // switch off Lora, GPS, display - pmu.shutdown(); - } - - pmu.clearIRQ(); - I2C_MUTEX_UNLOCK(); - } // mutex + pmu.clearIRQ(); // refresh stored voltage value read_voltage(); @@ -75,70 +68,106 @@ void AXP192_power(bool on) { void AXP192_showstatus(void) { - if (!I2C_MUTEX_LOCK()) - ESP_LOGW(TAG, "[%0.3f] i2c mutex lock failed", millis() / 1000.0); - else { - - if (pmu.isBatteryConnect()) - if (pmu.isChargeing()) - ESP_LOGI(TAG, "Battery charging, %.2fV @ %.0fmAh", - pmu.getBattVoltage() / 1000, pmu.getBattChargeCurrent()); - else - ESP_LOGI(TAG, "Battery not charging"); + if (pmu.isBatteryConnect()) + if (pmu.isChargeing()) + ESP_LOGI(TAG, "Battery charging, %.2fV @ %.0fmAh", + pmu.getBattVoltage() / 1000, pmu.getBattChargeCurrent()); else - ESP_LOGI(TAG, "No Battery"); + ESP_LOGI(TAG, "Battery not charging"); + else + ESP_LOGI(TAG, "No Battery"); - if (pmu.isVBUSPlug()) - ESP_LOGI(TAG, "USB powered, %.0fmW", - pmu.getVbusVoltage() / 1000 * pmu.getVbusCurrent()); - else - ESP_LOGI(TAG, "USB not present"); - - I2C_MUTEX_UNLOCK(); - } // mutex + if (pmu.isVBUSPlug()) + ESP_LOGI(TAG, "USB powered, %.0fmW", + pmu.getVbusVoltage() / 1000 * pmu.getVbusCurrent()); + else + ESP_LOGI(TAG, "USB not present"); } void AXP192_init(void) { - // block i2c bus access - if (I2C_MUTEX_LOCK()) { + if (pmu.begin(i2c_readBytes, i2c_writeBytes, AXP192_PRIMARY_ADDRESS) == + AXP_FAIL) + ESP_LOGI(TAG, "AXP192 PMU initialization failed"); + else { - if (pmu.begin(Wire, AXP192_PRIMARY_ADDRESS)) - ESP_LOGI(TAG, "AXP192 PMU initialization failed"); - else { + // configure AXP192 + pmu.setDCDC1Voltage(3300); // for external OLED display + pmu.setTimeOutShutdown(false); // no automatic shutdown + pmu.setTSmode(AXP_TS_PIN_MODE_DISABLE); // TS pin mode off to save power - // configure AXP192 - pmu.setDCDC1Voltage(3300); // for external OLED display - pmu.setTimeOutShutdown(false); // no automatic shutdown - pmu.setTSmode(AXP_TS_PIN_MODE_DISABLE); // TS pin mode off to save power + // switch ADCs on + pmu.adc1Enable(AXP202_BATT_VOL_ADC1, true); + pmu.adc1Enable(AXP202_BATT_CUR_ADC1, true); + pmu.adc1Enable(AXP202_VBUS_VOL_ADC1, true); + pmu.adc1Enable(AXP202_VBUS_CUR_ADC1, true); - // switch ADCs on - pmu.adc1Enable(AXP202_BATT_VOL_ADC1, true); - pmu.adc1Enable(AXP202_BATT_CUR_ADC1, true); - pmu.adc1Enable(AXP202_VBUS_VOL_ADC1, true); - pmu.adc1Enable(AXP202_VBUS_CUR_ADC1, true); + // switch power rails on + AXP192_power(true); - // switch power rails on - AXP192_power(true); - - // I2C access of AXP202X library currently is not mutexable - // so we better should disable AXP interrupts... ? #ifdef PMU_INT - pinMode(PMU_INT, INPUT_PULLUP); - attachInterrupt(digitalPinToInterrupt(PMU_INT), PMUIRQ, FALLING); - pmu.enableIRQ(AXP202_VBUS_REMOVED_IRQ | AXP202_VBUS_CONNECT_IRQ | - AXP202_BATT_REMOVED_IRQ | AXP202_BATT_CONNECT_IRQ | - AXP202_CHARGING_FINISHED_IRQ, - 1); - pmu.clearIRQ(); + pinMode(PMU_INT, INPUT_PULLUP); + attachInterrupt(digitalPinToInterrupt(PMU_INT), PMUIRQ, FALLING); + pmu.enableIRQ(AXP202_VBUS_REMOVED_IRQ | AXP202_VBUS_CONNECT_IRQ | + AXP202_BATT_REMOVED_IRQ | AXP202_BATT_CONNECT_IRQ | + AXP202_CHARGING_FINISHED_IRQ, + 1); + pmu.clearIRQ(); #endif // PMU_INT - ESP_LOGI(TAG, "AXP192 PMU initialized"); - } - I2C_MUTEX_UNLOCK(); // release i2c bus access - } else - ESP_LOGE(TAG, "I2c bus busy - PMU initialization error"); + ESP_LOGI(TAG, "AXP192 PMU initialized"); + } } + +// helper functions for mutexing i2c access +uint8_t i2c_readBytes(uint8_t addr, uint8_t reg, uint8_t *data, uint8_t len) { + if (I2C_MUTEX_LOCK()) { + + uint8_t ret = 0; + Wire.beginTransmission(addr); + Wire.write(reg); + Wire.endTransmission(false); + uint8_t cnt = Wire.requestFrom(addr, (uint8_t)len, (uint8_t)1); + if (!cnt) + ret = 0xFF; + uint16_t index = 0; + while (Wire.available()) { + if (index > len) { + ret = 0xFF; + goto finish; + } + data[index++] = Wire.read(); + } + + finish: + I2C_MUTEX_UNLOCK(); // release i2c bus access + return ret; + } else { + ESP_LOGW(TAG, "[%0.3f] i2c mutex lock failed", millis() / 1000.0); + return 0xFF; + } +} + +uint8_t i2c_writeBytes(uint8_t addr, uint8_t reg, uint8_t *data, uint8_t len) { + if (I2C_MUTEX_LOCK()) { + + uint8_t ret = 0; + Wire.beginTransmission(addr); + Wire.write(reg); + for (uint16_t i = 0; i < len; i++) { + Wire.write(data[i]); + } + ret = Wire.endTransmission(); + + I2C_MUTEX_UNLOCK(); // release i2c bus access + // return ret ? 0xFF : ret; + return ret ? ret : 0xFF; + } else { + ESP_LOGW(TAG, "[%0.3f] i2c mutex lock failed", millis() / 1000.0); + return 0xFF; + } +} + #endif // HAS_PMU #ifdef BAT_MEASURE_ADC @@ -196,30 +225,27 @@ uint16_t read_voltage() { uint16_t voltage = 0; #ifdef HAS_PMU - if (!I2C_MUTEX_LOCK()) - ESP_LOGW(TAG, "[%0.3f] i2c mutex lock failed", millis() / 1000.0); - else { - voltage = pmu.isVBUSPlug() ? 0xffff : pmu.getBattVoltage(); - I2C_MUTEX_UNLOCK(); - } + voltage = pmu.isVBUSPlug() ? 0xffff : pmu.getBattVoltage(); #else #ifdef BAT_MEASURE_ADC // multisample ADC uint32_t adc_reading = 0; +#ifndef BAT_MEASURE_ADC_UNIT // ADC1 + for (int i = 0; i < NO_OF_SAMPLES; i++) { + adc_reading += adc1_get_raw(adc_channel); + } +#else // ADC2 int adc_buf = 0; for (int i = 0; i < NO_OF_SAMPLES; i++) { -#ifndef BAT_MEASURE_ADC_UNIT // ADC1 - adc_reading += adc1_get_raw(adc_channel); -#else // ADC2 ESP_ERROR_CHECK(adc2_get_raw(adc_channel, ADC_WIDTH_BIT_12, &adc_buf)); adc_reading += adc_buf; -#endif } +#endif // BAT_MEASURE_ADC_UNIT adc_reading /= NO_OF_SAMPLES; // Convert ADC reading to voltage in mV voltage = esp_adc_cal_raw_to_voltage(adc_reading, adc_characs); -#endif // BAT_MEASURE_ADC +#endif // BAT_MEASURE_ADC #ifdef BAT_VOLTAGE_DIVIDER voltage *= BAT_VOLTAGE_DIVIDER; diff --git a/src/rcommand.cpp b/src/rcommand.cpp index e8b9f3da..4290555c 100644 --- a/src/rcommand.cpp +++ b/src/rcommand.cpp @@ -191,7 +191,7 @@ void set_loradr(uint8_t val[]) { if (validDR(val[0])) { cfg.loradr = val[0]; ESP_LOGI(TAG, "Remote command: set LoRa Datarate to %d", cfg.loradr); - LMIC_setDrTxpow(assertDR(cfg.loradr), cfg.txpower); + LMIC_setDrTxpow(assertDR(cfg.loradr), KEEP_TXPOW); ESP_LOGI(TAG, "Radio parameters now %s / %s / %s", getSfName(updr2rps(LMIC.datarate)), getBwName(updr2rps(LMIC.datarate)), diff --git a/src/senddata.cpp b/src/senddata.cpp index 76d412e2..11b0d81a 100644 --- a/src/senddata.cpp +++ b/src/senddata.cpp @@ -75,6 +75,10 @@ void sendData() { get_salt(); // get new salt for salting hashes ESP_LOGI(TAG, "Counter cleared"); } +#ifdef HAS_DISPLAY + else + oledPlotCurve(macs.size(), true); +#endif break; #endif diff --git a/src/timesync.cpp b/src/timesync.cpp index 8ce3238f..f2f8d14a 100644 --- a/src/timesync.cpp +++ b/src/timesync.cpp @@ -25,7 +25,7 @@ typedef std::chrono::duration> TaskHandle_t timeSyncReqTask = NULL; -static uint8_t time_sync_seqNo = random(TIMEANSWERPORT_MIN, TIMEANSWERPORT_MAX); +static uint8_t time_sync_seqNo = (uint8_t)random(TIMEREQUEST_MAX_SEQNO); static bool timeSyncPending = false; static myClock_timepoint time_sync_tx[TIME_SYNC_SAMPLES]; static myClock_timepoint time_sync_rx[TIME_SYNC_SAMPLES]; @@ -93,9 +93,10 @@ void process_timesync_req(void *taskparameter) { time_point_cast(time_sync_tx[k]); // wrap around seqNo, keeping it in time port range - time_sync_seqNo = (time_sync_seqNo < TIMEANSWERPORT_MAX) - ? time_sync_seqNo + 1 - : TIMEANSWERPORT_MIN; + time_sync_seqNo++; + if (time_sync_seqNo > TIMEREQUEST_MAX_SEQNO) { + time_sync_seqNo = 0; + } if (i < TIME_SYNC_SAMPLES - 1) { // wait until next cycle @@ -153,16 +154,30 @@ void store_time_sync_req(uint32_t timestamp) { timestamp % 1000); } -// process timeserver timestamp answer, called from lorawan.cpp -int recv_timesync_ans(const uint8_t seq_no, const uint8_t buf[], const uint8_t buf_len) { +// process timeserver timestamp answer, called by myRxCallback() in lorawan.cpp +int recv_timesync_ans(const uint8_t buf[], const uint8_t buf_len) { + + /* + parse 7 byte timesync_answer: + + byte meaning + 1 sequence number (taken from node's time_sync_req) + 2 timezone in 15 minutes steps + 3..6 current second (from epoch time 1970) + 7 1/250ths fractions of current second + */ // if no timesync handshake is pending then exit if (!timeSyncPending) return 0; // failure + // extract 1 byte timerequest sequence number from buffer + uint8_t seq_no = buf[0]; + buf++; + // if no time is available or spurious buffer then exit if (buf_len != TIME_SYNC_FRAME_LENGTH) { - if (buf[0] == 0xff) + if (seq_no == 0xff) ESP_LOGI(TAG, "[%0.3f] Timeserver error: no confident time available", millis() / 1000.0); else @@ -175,19 +190,26 @@ int recv_timesync_ans(const uint8_t seq_no, const uint8_t buf[], const uint8_t b uint8_t k = seq_no % TIME_SYNC_SAMPLES; - // the 5th byte contains the fractional seconds in 2^-8 second steps - // (= 1/250th sec), we convert this to ms - uint16_t timestamp_msec = 4 * buf[4]; // pointers to 4 bytes containing UTC seconds since unix epoch, msb uint32_t timestamp_sec, *timestamp_ptr; - // convert buffer to uint32_t, octet order is big endian + // extract 1 byte timezone from buffer (one step being 15min * 60s = 900s) + // uint32_t timezone_sec = buf[0] * 900; // for future use + buf++; + + // extract 4 bytes timestamp from buffer + // and convert it to uint32_t, octet order is big endian timestamp_ptr = (uint32_t *)buf; // swap byte order from msb to lsb, note: this is platform dependent timestamp_sec = __builtin_bswap32(*timestamp_ptr); + buf += 4; + // extract 1 byte fractional seconds in 2^-8 second steps + // (= 1/250th sec), we convert this to ms + uint16_t timestamp_msec = 4 * buf[0]; // construct the timepoint when message was seen on gateway - time_sync_rx[k] += seconds(timestamp_sec) + milliseconds(timestamp_msec); + time_sync_rx[k] += + seconds(timestamp_sec) + milliseconds(timestamp_msec); // we guess timepoint is recent if it newer than code compile date if (timeIsValid(myClock::to_time_t(time_sync_rx[k]))) { @@ -218,4 +240,4 @@ void timesync_init() { 1); // CPU core } -#endif \ No newline at end of file +#endif