Merge pull request #570 from cyberman54/development

update branch sds011 to dev
This commit is contained in:
Verkehrsrot 2020-03-14 07:52:07 +01:00 committed by GitHub
commit ffa4c6d1c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 623 additions and 501 deletions

View File

@ -54,7 +54,7 @@ Depending on board hardware following features are supported:
- LED (shows power & status)
- OLED Display (shows detailed status)
- RGB LED (shows colorized status)
- Button (used to flip display pages if device has display, else sends alarm message)
- Button (short press: flip display page / long press: send alarm message)
- Silicon unique ID
- Battery voltage monitoring
- GPS (Generic serial NMEA, or Quectel L76 I2C)

View File

@ -3,15 +3,37 @@
#include "cyclic.h"
#include "qrcode.h"
#define DISPLAY_PAGES (7) // number of paxcounter display pages
// settings for oled display library
#define USE_BACKBUFFER 1
#define MY_OLED OLED_128x64
#define OLED_ADDR -1
#define OLED_INVERT 0
#define USE_HW_I2C 1
#ifndef DISPLAY_FLIP
#define DISPLAY_FLIP 0
#endif
// 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
extern uint8_t DisplayIsOn, displaybuf[];
void setup_display(int contrast = 0);
void refreshTheDisplay(bool nextPage = false);
void init_display(bool verbose = false);
void shutdown_display(void);
void draw_page(time_t t, bool nextpage);
void dp_printf(uint16_t x, uint16_t y, uint8_t font, uint8_t inv,
const char *format, ...);
void dp_dump(uint8_t *pBuffer);
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);

View File

@ -132,7 +132,6 @@ extern SemaphoreHandle_t I2Caccess;
extern TaskHandle_t irqHandlerTask, ClockTask;
extern TimerHandle_t WifiChanTimer;
extern Timezone myTZ;
extern time_t userUTCTime;
extern RTC_DATA_ATTR runmode_t RTC_runmode;
// application includes

View File

@ -20,7 +20,7 @@ int gps_config();
bool gps_hasfix();
void gps_storelocation(gpsStatus_t *gps_store);
void gps_loop(void *pvParameters);
time_t fetch_gpsTime(uint16_t *msec);
time_t fetch_gpsTime(void);
time_t get_gpstime(uint16_t *msec);
time_t get_gpstime(void);
#endif

View File

@ -5,9 +5,6 @@
#include "rcommand.h"
#include "timekeeper.h"
#include <driver/rtc_io.h>
#if (TIME_SYNC_LORASERVER)
#include "timesync.h"
#endif
// LMIC-Arduino LoRaWAN Stack
#include <lmic.h>
@ -23,13 +20,6 @@
extern TaskHandle_t lmicTask, lorasendTask;
// table of LORAWAN MAC commands
typedef struct {
const uint8_t opcode;
const char cmdname[20];
const uint8_t params;
} mac_t;
esp_err_t lora_stack_init(bool do_join);
void lora_setupForNetwork(bool preJoin);
void lmictask(void *pvParameters);
@ -39,24 +29,56 @@ void get_hard_deveui(uint8_t *pdeveui);
void os_getDevKey(u1_t *buf);
void os_getArtEui(u1_t *buf);
void os_getDevEui(u1_t *buf);
void showLoraKeys(void);
void lora_send(void *pvParameters);
void lora_enqueuedata(MessageBuffer_t *message);
void lora_queuereset(void);
void IRAM_ATTR myEventCallback(void *pUserData, ev_t ev);
void IRAM_ATTR myRxCallback(void *pUserData, uint8_t port,
const uint8_t *pMsg, size_t nMsg);
void IRAM_ATTR myRxCallback(void *pUserData, uint8_t port, const uint8_t *pMsg,
size_t nMsg);
void IRAM_ATTR myTxCallback(void *pUserData, int fSuccess);
void mac_decode(const uint8_t cmd[], const uint8_t cmdlen, const mac_t table[],
const uint8_t tablesize);
//u1_t os_getBattLevel(void);
const char *getSfName(rps_t rps);
const char *getBwName(rps_t rps);
const char *getCrName(rps_t rps);
// u1_t os_getBattLevel(void);
#if (TIME_SYNC_LORAWAN)
void user_request_network_time_callback(void *pVoidUserUTCTime,
int flagSuccess);
#endif
#if (VERBOSE)
// a table for storage of LORAWAN MAC commands
typedef struct {
const uint8_t cid;
const char cmdname[20];
const uint8_t params;
} mac_t;
// table of LORAWAN MAC messages sent by the network to the device
// format: CDI, Command (max 19 chars), #parameters (bytes)
// 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}};
static const uint8_t MACdn_tSize = sizeof(MACdn_table) / sizeof(MACdn_table[0]);
// 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}};
static const uint8_t MACup_tSize = sizeof(MACup_table) / sizeof(MACup_table[0]);
void mac_decode(const uint8_t cmd[], const uint8_t cmdlen, bool is_down);
void showLoraKeys(void);
#endif // VERBOSE
#endif

View File

@ -5,6 +5,7 @@
#include "rtctime.h"
#include "TimeLib.h"
#include "irqhandler.h"
#include "timesync.h"
#if (HAS_GPS)
#include "gpsread.h"
@ -31,7 +32,5 @@ void IRAM_ATTR setMyTime(uint32_t t_sec, uint16_t t_msec, timesource_t mytimesou
time_t compiledUTC(void);
TickType_t tx_Ticks(uint32_t framesize, unsigned long baud, uint32_t config,
int8_t rxPin, int8_t txPins);
time_t TimeSyncAns(uint8_t seqNo, uint64_t unixTime);
void TimeSyncReq(uint8_t seqNo);
#endif // _timekeeper_H

View File

@ -1,22 +1,28 @@
#ifndef _TIMESYNC_H
#define _TIMESYNC_H
#include <chrono>
#include "globals.h"
#include "irqhandler.h"
#include "timekeeper.h"
//#define TIME_SYNC_TRIGGER 100 // threshold for time sync [milliseconds]
#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
#define TIME_SYNC_FRAME_LENGTH 6 // timeserver answer frame length [bytes]
#define TIME_SYNC_FIXUP 16 // compensation for processing time [milliseconds]
#define TIME_SYNC_MAX_SEQNO 0xfe // threshold for wrap around time_sync_seqNo
#define TIME_SYNC_END_FLAG (TIME_SYNC_MAX_SEQNO + 1) // end of handshake marker
#define GPS_UTC_DIFF 315964800 // seconds diff between gps and utc epoch
enum timesync_t {
timesync_tx,
timesync_rx,
gwtime_sec,
gwtime_msec,
no_of_timestamps
};
void timesync_init(void);
void send_timesync_req(void);
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);
void timesync_request(void);
void timesync_store(uint32_t timestamp, timesync_t timestamp_type);
void IRAM_ATTR timesync_processReq(void *taskparameter);
void IRAM_ATTR timesync_serverAnswer(void *pUserData, int flag);
#endif

View File

@ -45,7 +45,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.9.91
release_version = 1.9.96
; 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
@ -59,8 +59,8 @@ upload_speed = 115200
lib_deps_lora =
MCCI LoRaWAN LMIC library@>=3.1.0 ; MCCI LMIC by Terrill Moore
lib_deps_display =
ss_oled@>=3.3.1 ; simple and small OLED lib by Larry Bank
BitBang_I2C@>=1.3.0
ss_oled@4.0.1 ; simple and small OLED lib by Larry Bank
BitBang_I2C@2.0.0
QRCode@>=0.0.1
lib_deps_matrix_display =
Ultrathin_LED_Matrix@>=1.0.0

View File

@ -4,8 +4,6 @@
function Decoder(bytes, port) {
var decoded = {};
decoded.wifi = 0;
decoded.ble = 0;
if (bytes.length === 0) {
return {};

View File

@ -4,14 +4,24 @@
function Converter(decoded, port) {
var converted = decoded;
var pax = 0;
if (port === 1) {
converted.pax = converted.ble + converted.wifi;
if ('wifi' in converted) {
pax += converted.wifi
}
if ('ble' in converted) {
pax += converted.ble
}
converted.pax = pax;
if (converted.hdop) {
converted.hdop /= 100;
converted.latitude /= 1000000;
converted.longitude /= 1000000;
}
}
return converted;

View File

@ -3,8 +3,6 @@
function Decoder(bytes, port) {
var decoded = {};
decoded.wifi = 0;
decoded.ble = 0;
if (port === 1) {
var i = 0;

View File

@ -1,4 +1,41 @@
[
{
"id": "b8bd33fd.61caa",
"type": "function",
"z": "449c1517.e25f4c",
"name": "Timeserver Logic",
"func": "/* LoRaWAN Timeserver\n\nVERSION: 1.3\n\nconstruct 6 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..5 current second (from GPS epoch starting 1980)\n6 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 = 1000; // 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(6);\nnew DataView(buf).setUint8(0, seqNo);\nnew DataView(buf).setUint32(1, seconds);\nnew DataView(buf).setUint8(5, 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": 330,
"y": 327,
"wires": [
[
"c9a83ac9.50fd18",
"6aeb3720.a89618",
"6ac55bbe.12ac54"
],
[
"de908e66.b6fd3"
],
[
"d5a35bab.44cb18"
],
[
"3a661f0a.c61b1"
],
[
"9b4f492d.fbfd18"
]
],
"outputLabels": [
"gw_eui",
"offset_ms",
"device",
"seq_no",
"time_sync_ans"
]
},
{
"id": "9b4f492d.fbfd18",
"type": "change",
@ -177,49 +214,12 @@
"id": "15980d22.6f4663",
"type": "comment",
"z": "449c1517.e25f4c",
"name": "LoRaWAN Timeserver v1.21",
"name": "LoRaWAN Timeserver v1.3",
"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": 160,
"x": 150,
"y": 47,
"wires": []
},
{
"id": "b8bd33fd.61caa",
"type": "function",
"z": "449c1517.e25f4c",
"name": "Timeserver Logic",
"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": 330,
"y": 327,
"wires": [
[
"c9a83ac9.50fd18",
"6aeb3720.a89618",
"6ac55bbe.12ac54"
],
[
"de908e66.b6fd3"
],
[
"d5a35bab.44cb18"
],
[
"3a661f0a.c61b1"
],
[
"9b4f492d.fbfd18"
]
],
"outputLabels": [
"gw_eui",
"offset_ms",
"device",
"seq_no",
"time_sync_ans"
]
},
{
"id": "c9a83ac9.50fd18",
"type": "debug",
@ -346,7 +346,7 @@
"type": "debug",
"z": "449c1517.e25f4c",
"name": "",
"active": true,
"active": false,
"tosidebar": true,
"console": false,
"tostatus": false,

View File

@ -0,0 +1,86 @@
/* LoRaWAN Timeserver
VERSION: 1.3
construct 6 byte timesync_answer from gateway timestamp and node's time_sync_req
byte meaning
1 sequence number (taken from node's time_sync_req)
2..5 current second (from GPS epoch starting 1980)
6 1/250ths fractions of current second
*/
function timecompare(a, b) {
const timeA = a.time;
const timeB = b.time;
let comparison = 0;
if (timeA > timeB) {
comparison = 1;
} else if (timeA < timeB) {
comparison = -1;
}
return comparison;
}
let confidence = 1000; // max millisecond diff gateway time to server time
// guess if we have received a valid time_sync_req command
if (msg.payload.payload_raw.length != 1)
return;
var deviceMsg = { payload: msg.payload.dev_id };
var seqNo = msg.payload.payload_raw[0];
var seqNoMsg = { payload: seqNo };
var gateway_list = msg.payload.metadata.gateways;
// filter all gateway timestamps that have milliseconds part (which we assume have a ".")
var gateways = gateway_list.filter(function (element) {
return (element.time.includes("."));
});
var gateway_time = gateways.map(gw => {
return {
time: new Date(gw.time),
eui: gw.gtw_id,
}
});
var server_time = new Date(msg.payload.metadata.time);
// validate all gateway timestamps against lorawan server_time (which is assumed to be recent)
var gw_timestamps = gateway_time.filter(function (element) {
return ((element.time > (server_time - confidence) && element.time <= server_time));
});
// if no timestamp left, we have no valid one and exit
if (gw_timestamps.length === 0) {
var notavailMsg = { payload: "n/a" };
var notimeMsg = { payload: 0xff };
var buf2 = Buffer.alloc(1);
msg.payload = new Buffer(buf2.fill(0xff));
msg.port = 9; // Paxcounter TIMEPORT
return [notavailMsg, notavailMsg, deviceMsg, seqNoMsg, msg];}
// sort time array in ascending order to find most recent timestamp for time answer
gw_timestamps.sort(timecompare);
var timestamp = gw_timestamps[0].time;
var eui = gw_timestamps[0].eui;
var offset = server_time - timestamp;
var seconds = Math.floor(timestamp/1000);
var fractions = (timestamp % 1000) / 4;
let buf = new ArrayBuffer(6);
new DataView(buf).setUint8(0, seqNo);
new DataView(buf).setUint32(1, seconds);
new DataView(buf).setUint8(5, fractions);
msg.payload = new Buffer(new Uint8Array(buf));
msg.port = 9; // Paxcounter TIMEPORT
var euiMsg = { payload: eui };
var offsetMsg = { payload: offset };
return [euiMsg, offsetMsg, deviceMsg, seqNoMsg, msg];

View File

@ -39,19 +39,6 @@ FONT_STRETCHED: 16x32px = 8 chars / line
// local Tag for logging
static const char TAG[] = __FILE__;
#define DISPLAY_PAGES (6) // number of paxcounter display pages
// 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"};
@ -60,6 +47,23 @@ uint8_t displaybuf[DISPLAY_WIDTH * DISPLAY_HEIGHT / 8] = {0};
static uint8_t plotbuf[DISPLAY_WIDTH * DISPLAY_HEIGHT / 8] = {0};
QRCode qrcode;
SSOLED ssoled;
void setup_display(int contrast) {
int rc = oledInit(&ssoled, MY_OLED, OLED_ADDR, DISPLAY_FLIP, OLED_INVERT,
USE_HW_I2C, MY_OLED_SDA, MY_OLED_SCL, MY_OLED_RST,
400000L); // use standard I2C bus at 400Khz
assert(rc != OLED_NOT_FOUND);
// set display buffer
oledSetBackBuffer(&ssoled, displaybuf);
if (contrast)
oledSetContrast(&ssoled, contrast);
// clear display
oledFill(&ssoled, 0, 1);
}
void init_display(bool verbose) {
@ -68,19 +72,7 @@ void init_display(bool verbose) {
ESP_LOGV(TAG, "[%0.3f] i2c mutex lock failed", millis() / 1000.0);
else {
// init display
#ifndef DISPLAY_FLIP
oledInit(OLED_128x64, false, false, -1, -1, MY_OLED_RST, 400000L);
#else
oledInit(OLED_128x64, true, false, -1, -1, MY_OLED_RST, 400000L);
#endif
// set display buffer
oledSetBackBuffer(displaybuf);
// clear display
oledSetContrast(DISPLAYCONTRAST);
oledFill(0, 1);
setup_display(DISPLAYCONTRAST);
if (verbose) {
@ -104,9 +96,9 @@ void init_display(bool verbose) {
: "ext.");
// give user some time to read or take picture
oledDumpBuffer(displaybuf);
dp_dump(displaybuf);
delay(2000);
oledFill(0x00, 1);
oledFill(&ssoled, 0x00, 1);
#endif // VERBOSE
#if (HAS_LORA)
@ -117,7 +109,7 @@ void init_display(bool verbose) {
snprintf(deveui, 17, "%016llX", *((uint64_t *)&buf));
// display DEVEUI as QR code on the left
oledSetContrast(30);
oledSetContrast(&ssoled, 30);
dp_printqr(3, 3, deveui);
// display DEVEUI as plain text on the right
@ -127,15 +119,15 @@ void init_display(bool verbose) {
dp_printf(80, i + 3, FONT_NORMAL, 0, "%4.4s", deveui + i * 4);
// give user some time to read or take picture
oledDumpBuffer(displaybuf);
dp_dump(displaybuf);
delay(8000);
oledSetContrast(DISPLAYCONTRAST);
oledFill(0x00, 1);
oledSetContrast(&ssoled, DISPLAYCONTRAST);
oledFill(&ssoled, 0x00, 1);
#endif // HAS_LORA
} // verbose
oledPower(cfg.screenon); // set display off if disabled
oledPower(&ssoled, cfg.screenon); // set display off if disabled
I2C_MUTEX_UNLOCK(); // release i2c bus access
} // mutex
@ -164,7 +156,7 @@ void refreshTheDisplay(bool nextPage) {
// set display on/off according to current device configuration
if (DisplayIsOn != cfg.screenon) {
DisplayIsOn = cfg.screenon;
oledPower(cfg.screenon);
oledPower(&ssoled, cfg.screenon);
}
#ifndef HAS_BUTTON
@ -176,7 +168,7 @@ void refreshTheDisplay(bool nextPage) {
#endif
draw_page(t, nextPage);
oledDumpBuffer(displaybuf);
dp_dump(displaybuf);
I2C_MUTEX_UNLOCK(); // release i2c bus access
@ -214,7 +206,7 @@ start:
if (nextpage) {
DisplayPage = (DisplayPage >= DISPLAY_PAGES - 1) ? 0 : (DisplayPage + 1);
oledFill(0, 1);
oledFill(&ssoled, 0, 1);
}
switch (DisplayPage) {
@ -224,7 +216,8 @@ start:
// page 2: GPS
// page 3: BME280/680
// page 4: time
// page 5: blank screen
// page 5: lorawan parameters
// page 6: blank screen
// page 0: parameters overview
case 0:
@ -306,7 +299,7 @@ start:
// page 1: pax graph
case 1:
oledDumpBuffer(plotbuf);
dp_dump(plotbuf);
break; // page1
// page 2: GPS
@ -362,10 +355,35 @@ start:
second(t));
break;
// page 5: blank screen
// page 5: lorawan parameters
case 5:
#if (HAS_LORA)
// 3|NtwkID:000000 TXpw:aa
// 4|DevAdd:00000000 DR:0
// 5|CHMsk:0000 Nonce:0000
// 6|CUp:000000 CDn:000000
// 7|SNR:-0000 RSSI:-0000
dp_printf(0, 3, FONT_SMALL, 0, "NetwID:%06X TXpw:%-2d",
LMIC.netid & 0x001FFFFF, LMIC.radio_txpow);
dp_printf(0, 4, FONT_SMALL, 0, "DevAdd:%08X DR:%1d", LMIC.devaddr,
LMIC.datarate);
dp_printf(0, 5, FONT_SMALL, 0, "ChMsk:%04X Nonce:%04X", LMIC.channelMap,
LMIC.devNonce);
dp_printf(0, 6, FONT_SMALL, 0, "fUp:%-6d fDn:%-6d",
LMIC.seqnoUp ? LMIC.seqnoUp - 1 : 0,
LMIC.seqnoDn ? LMIC.seqnoDn - 1 : 0);
dp_printf(0, 7, FONT_SMALL, 0, "SNR:%-5d RSSI:%-5d", (LMIC.snr + 2) / 4,
LMIC.rssi);
break; // page5
#else // don't show blank page if we are unattended
DisplayPage++; // next page
#endif // HAS_LORA
// page 6: blank screen
case 6:
#ifdef HAS_BUTTON
oledFill(0, 1);
oledFill(&ssoled, 0, 1);
break;
#else // don't show blank page if we are unattended
DisplayPage++; // next page
@ -402,12 +420,14 @@ void dp_printf(uint16_t x, uint16_t y, uint8_t font, uint8_t inv,
len = vsnprintf(temp, len + 1, format, arg);
}
va_end(arg);
oledWriteString(0, x, y, temp, font, inv, false);
oledWriteString(&ssoled, 0, x, y, temp, font, inv, false);
if (temp != loc_buf) {
free(temp);
}
}
void dp_dump(uint8_t *pBuffer) { oledDumpBuffer(&ssoled, pBuffer); }
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);
@ -434,7 +454,7 @@ 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) {
for (uint16_t xi = x; xi < x + width; xi++)
oledDrawLine(xi, y, xi, y + height - 1, bRender);
oledDrawLine(&ssoled, xi, y, xi, y + height - 1, bRender);
}
int oledDrawPixel(uint8_t *buf, const uint16_t x, const uint16_t y,

View File

@ -91,7 +91,7 @@ bool gps_hasfix() {
}
// function to fetch current time from struct; note: this is costly
time_t fetch_gpsTime(uint16_t *msec) {
time_t get_gpstime(uint16_t *msec) {
time_t time_sec = 0;
@ -132,11 +132,11 @@ time_t fetch_gpsTime(uint16_t *msec) {
return timeIsValid(time_sec);
} // fetch_gpsTime()
} // get_gpstime()
time_t fetch_gpsTime(void) {
time_t get_gpstime(void) {
uint16_t msec;
return fetch_gpsTime(&msec);
return get_gpstime(&msec);
}
// GPS serial feed FreeRTos Task

View File

@ -21,10 +21,7 @@
#define LMIC_USE_INTERRUPTS 1
// time sync via LoRaWAN network, note: not supported by TTNv2
//#define LMIC_ENABLE_DeviceTimeReq 1
// use callback event handlers, not onEvent() reference
#define LMIC_ENABLE_onEvent 0
#define LMIC_ENABLE_DeviceTimeReq 1
// 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,

View File

@ -23,29 +23,6 @@ RTC_NOINIT_ATTR int RTCseqnoUp, RTCseqnoDn;
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:
@ -98,9 +75,9 @@ void lora_setupForNetwork(bool preJoin) {
if (!cfg.adrmode)
LMIC_setDrTxpow(assertDR(cfg.loradr), cfg.txpower);
// show current devaddr
ESP_LOGI(TAG, "DEVaddr: 0x%08X | Network ID: 0x%03X | Network Type: %d",
ESP_LOGI(TAG, "DEVaddr: 0x%08X | Network ID: 0x%06X | Network Type: %d",
LMIC.devaddr, LMIC.netid & 0x001FFFFF, LMIC.netid & 0x00E00000);
ESP_LOGI(TAG, "RSSI: %d | SNR: %d", LMIC.rssi, LMIC.snr / 4);
ESP_LOGI(TAG, "RSSI: %d | SNR: %d", LMIC.rssi, (LMIC.snr + 2) / 4);
ESP_LOGI(TAG, "Radio parameters: %s | %s | %s",
getSfName(updr2rps(LMIC.datarate)),
getBwName(updr2rps(LMIC.datarate)),
@ -261,12 +238,22 @@ void lora_send(void *pvParameters) {
// attempt to transmit payload
else {
// switch (LMIC_sendWithCallback_strict(
switch (LMIC_sendWithCallback(
SendBuffer.MessagePort, SendBuffer.Message, SendBuffer.MessageSize,
(cfg.countermode & 0x02), myTxCallback, NULL)) {
switch (LMIC_setTxData2_strict(SendBuffer.MessagePort, SendBuffer.Message,
SendBuffer.MessageSize,
(cfg.countermode & 0x02))) {
case LMIC_ERROR_SUCCESS:
// save current Fcnt to RTC RAM
RTCseqnoUp = LMIC.seqnoUp;
RTCseqnoDn = LMIC.seqnoDn;
#if (TIME_SYNC_LORASERVER)
// if last packet sent was a timesync request, store TX timestamp
if (SendBuffer.MessagePort == TIMEPORT)
// store LMIC time when we started transmit of timesync request
timesync_store(osticks2ms(os_getTime()), timesync_tx);
#endif
ESP_LOGI(TAG, "%d byte(s) sent to LORA", SendBuffer.MessageSize);
break;
case LMIC_ERROR_TX_BUSY: // LMIC already has a tx message pending
@ -369,56 +356,6 @@ void lora_enqueuedata(MessageBuffer_t *message) {
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);
@ -467,6 +404,15 @@ void myEventCallback(void *pUserData, ev_t ev) {
// process current event message
switch (ev) {
case EV_TXCOMPLETE:
// -> processed in lora_send()
break;
case EV_RXCOMPLETE:
// -> processed in myRxCallback()
break;
case EV_JOINING:
// do the network-specific setup prior to join.
lora_setupForNetwork(true);
@ -477,12 +423,6 @@ void myEventCallback(void *pUserData, ev_t ev) {
lora_setupForNetwork(false);
break;
case EV_TXCOMPLETE:
// save current Fcnt to RTC RAM
RTCseqnoUp = LMIC.seqnoUp;
RTCseqnoDn = LMIC.seqnoDn;
break;
case EV_JOIN_TXCOMPLETE:
// replace descriptor from library with more descriptive term
snprintf(lmic_event_msg, LMIC_EVENTMSG_LEN, "%-16s", "JOIN_WAIT");
@ -500,105 +440,68 @@ void myEventCallback(void *pUserData, ev_t ev) {
ESP_LOGD(TAG, "%s", lmic_event_msg);
}
// receive message handler
// event EV_RXCOMPLETE message handler
void myRxCallback(void *pUserData, uint8_t port, const uint8_t *pMsg,
size_t nMsg) {
// display type of received data
// display amount 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
// decode mac messages if we want to print those
#if (VERBOSE)
case MACPORT:
break;
// decode downlink MAC commands
if (LMIC.dataBeg)
mac_decode(LMIC.frame, LMIC.dataBeg, true);
// decode uplink MAC commands
if (LMIC.pendMacLen)
mac_decode(LMIC.pendMacData, LMIC.pendMacLen, false);
break; // do not fallthrough to default, we are done
#endif
// rcommand received -> call interpreter
case RCMDPORT:
rcommand(pMsg, nMsg);
break;
default:
// timeserver answer -> call timesync processor
#if (TIME_SYNC_LORASERVER)
// valid timesync answer -> call timesync processor
if (port == TIMEPORT) {
recv_timesync_ans(pMsg, nMsg);
break;
}
case TIMEPORT:
// get and store gwtime from payload
timesync_serverAnswer(const_cast<uint8_t *>(pMsg), nMsg);
#endif
// unknown port -> display info
ESP_LOGI(TAG, "Received data on unsupported port %u", port);
// decode any piggybacked downlink MAC commands if we want to print those
default:
#if (VERBOSE)
if (LMIC.dataBeg > 1)
mac_decode(LMIC.frame, LMIC.dataBeg - 1, true);
#endif // VERBOSE
break;
} // switch
}
// transmit complete message handler
/*
// event EV_TXCOMPLETE 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
uint8_t *const pMsg = (uint8_t *)pUserData;
// LMIC did successful transmit data
if (fSuccess) {
RTCseqnoUp = LMIC.seqnoUp;
RTCseqnoDn = LMIC.seqnoDn;
} else {
// LMIC could not transmit data
// -> error handling yet to come
}
}
// 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()
*/
const char *getSfName(rps_t rps) {
const char *const t[] = {"FSK", "SF7", "SF8", "SF9",
@ -618,7 +521,7 @@ const char *getCrName(rps_t rps) {
/*
u1_t os_getBattLevel() {
//return values:
//MCMD_DEVS_EXT_POWER = 0x00, // external power supply
//MCMD_DEVS_BATT_MIN = 0x01, // min battery value
@ -642,4 +545,48 @@ u1_t os_getBattLevel() {
} // getBattLevel()
*/
#if (VERBOSE)
// decode LORAWAN MAC message
void mac_decode(const uint8_t cmd[], const uint8_t cmdlen, bool is_down) {
if (!cmdlen)
return;
uint8_t foundcmd[cmdlen], cursor = 0;
// select CID resolve table
const mac_t *p;
p = is_down ? MACdn_table : MACup_table;
const int tablesize = is_down ? MACdn_tSize : MACup_tSize;
const String MACdir = is_down ? "-->" : "<--";
while (cursor < cmdlen) {
// get number of commands in CID table
int i = tablesize;
// lookup cmd in CID table
while (i--) {
if (cmd[cursor] == (p + i)->cid) { // lookup command in CID table
cursor++; // strip 1 byte CID
if ((cursor + (p + i)->params) <= cmdlen) {
memmove(foundcmd, cmd + cursor,
(p + i)->params); // strip opcode from cmd array
cursor += (p + i)->params;
ESP_LOGD(TAG, "%s %s", MACdir, (p + i)->cmdname);
} else
ESP_LOGD(TAG, "%s MAC command 0x%02X with missing parameter(s)",
MACdir, (p + i)->cid);
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, "%s Unknown MAC command 0x%02X", MACdir, cmd[cursor]);
cursor++;
}
} // command parsing loop
} // mac_decode()
#endif // VERBOSE
#endif // HAS_LORA

View File

@ -33,7 +33,7 @@ IDLE 0 0 ESP32 arduino scheduler -> runs wifi sniffer
lmictask 1 2 MCCI LMiC LORAWAN stack
clockloop 1 4 generates realtime telegrams for external clock
timesync_req 1 3 processes realtime time sync requests
timesync_proc 1 3 processes realtime time sync requests
irqhandler 1 1 cyclic tasks (i.e. displayrefresh) triggered by timers
gpsloop 1 1 reads data from GPS via serial or i2c
lorasendtask 1 1 feeds data from lora sendqueue to lmcic
@ -87,7 +87,6 @@ hw_timer_t *ppsIRQ = NULL, *displayIRQ = NULL, *matrixDisplayIRQ = NULL;
TaskHandle_t irqHandlerTask = NULL, ClockTask = NULL;
SemaphoreHandle_t I2Caccess;
bool volatile TimePulseTick = false;
time_t userUTCTime = 0;
timesource_t timeSource = _unsynced;
// container holding unique MAC address hashes with Memory Alloctor using PSRAM,
@ -456,7 +455,7 @@ void setup() {
// initialize gps time
#if (HAS_GPS)
fetch_gpsTime();
get_gpstime();
#endif
#if (defined HAS_IF482 || defined HAS_DCF77)
@ -464,7 +463,7 @@ void setup() {
clock_init();
#endif
#if (TIME_SYNC_LORASERVER)
#if (TIME_SYNC_LORASERVER) || (TIME_SYNC_LORAWAN)
timesync_init(); // create loraserver time sync task
#endif

View File

@ -45,23 +45,15 @@ void start_ota_update() {
// init display
#ifdef HAS_DISPLAY
#ifndef DISPLAY_FLIP
oledInit(OLED_128x64, false, false, -1, -1, MY_OLED_RST, 400000L);
#else
oledInit(OLED_128x64, true, false, -1, -1, MY_OLED_RST, 400000L);
#endif
setup_display();
// set display buffer
oledSetBackBuffer(displaybuf);
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(displaybuf);
dp_dump(displaybuf);
#endif
ESP_LOGI(TAG, "Starting Wifi OTA update");
@ -314,7 +306,7 @@ void ota_display(const uint8_t row, const std::string status,
dp_printf(0, 7, 0, 0, " ");
dp_printf(0, 7, 0, 0, msg.substr(0, 16).c_str());
}
oledDumpBuffer(displaybuf);
dp_dump(displaybuf);
#endif
}

View File

@ -6,7 +6,7 @@
//
// Note: After editing, before "build", use "clean" button in PlatformIO!
// Verbose enables serial output
// Verbose enables additional serial debug output
#define VERBOSE 0 // set to 0 to silence the device, for mute use build option
// Payload send cycle and encoding
@ -72,17 +72,15 @@
#define OTA_MIN_BATT 3600 // minimum battery level for OTA [millivolt]
#define RESPONSE_TIMEOUT_MS 60000 // firmware binary server connection timeout [milliseconds]
// settings for syncing time of node with external time source
#define TIME_SYNC_INTERVAL 60 // sync time attempt each .. minutes from time source (GPS/LORA/RTC) [default = 60], 0 means off
#define TIME_SYNC_INTERVAL_RETRY 10 // retry time sync after lost sync each .. minutes [default = 10], 0 means off
#define TIME_SYNC_COMPILEDATE 0 // set to 1 to use compile date to initialize RTC after power outage [default = 0]
#define TIME_SYNC_LORAWAN 0 // set to 1 to use LORA network as time source, 0 means off [default = 0]
// settings for syncing time of node with a time source (network / gps / rtc / timeserver)
#define TIME_SYNC_LORAWAN 1 // set to 1 to use LORA network as time source, 0 means off [default = 1]
#define TIME_SYNC_LORASERVER 0 // set to 1 to use LORA timeserver as time source, 0 means off [default = 0]
// settings for syncing time with timeserver applications
#define TIME_SYNC_SAMPLES 1 // number of time requests for averaging
#define TIME_SYNC_INTERVAL 60 // sync time attempt each .. minutes from time source [default = 60], 0 means off
#define TIME_SYNC_INTERVAL_RETRY 10 // retry time sync after lost sync each .. minutes [default = 10], 0 means off
#define TIME_SYNC_SAMPLES 1 // number of time requests for averaging, max. 255
#define TIME_SYNC_CYCLE 60 // delay between two time samples [seconds]
#define TIME_SYNC_TIMEOUT 300 // timeout waiting for timeserver answer [seconds]
#define TIME_SYNC_COMPILEDATE 0 // set to 1 to use compile date to initialize RTC after power outage [default = 0]
// time zone, see https://github.com/JChristensen/Timezone/blob/master/examples/WorldClock/WorldClock.ino
#define DAYLIGHT_TIME {"CEST", Last, Sun, Mar, 2, 120} // Central European Summer Time

View File

@ -343,7 +343,7 @@ void set_flush(uint8_t val[]) {
// format: opcode, function, #bytes params,
// flag (true = do make settings persistent / false = don't)
//
static cmd_t table[] = {
static const cmd_t table[] = {
{0x01, set_rssi, 1, true}, {0x02, set_countmode, 1, true},
{0x03, set_gps, 1, true}, {0x04, set_display, 1, true},
{0x05, set_loradr, 1, true}, {0x06, set_lorapower, 1, true},

View File

@ -31,37 +31,35 @@ void calibrateTime(void) {
time_t t = 0;
uint16_t t_msec = 0;
#if (HAS_GPS)
// fetch recent time from last NMEA record
t = fetch_gpsTime(&t_msec);
if (t) {
timeSource = _gps;
goto finish;
}
// kick off asychronous lora timesync if we have
#if (HAS_LORA) && (TIME_SYNC_LORASERVER) || (TIME_SYNC_LORAWAN)
timesync_request();
#endif
// kick off asychronous Lora timeserver timesync if we have
#if (HAS_LORA) && (TIME_SYNC_LORASERVER)
send_timesync_req();
// kick off asychronous lora network sync if we have
#elif (HAS_LORA) && (TIME_SYNC_LORAWAN)
LMIC_requestNetworkTime(user_request_network_time_callback, &userUTCTime);
#endif
// (only!) if we lost time, we try to fallback to local time source RTS or GPS
if (timeSource == _unsynced) {
// no time from GPS -> fallback to RTC time while trying lora sync
// has RTC -> fallback to RTC time
#ifdef HAS_RTC
t = get_rtctime();
if (t) {
t = get_rtctime();
timeSource = _rtc;
goto finish;
}
#endif
goto finish;
// no RTC -> fallback to GPS time
#if (HAS_GPS)
t = get_gpstime(&t_msec);
timeSource = _gps;
#endif
finish:
if (t)
setMyTime((uint32_t)t, t_msec, timeSource); // set time
setMyTime((uint32_t)t, t_msec, timeSource); // set time
} // fallback
else
// no fallback time source available -> we can't set time
return;
} // calibrateTime()
@ -86,7 +84,7 @@ void IRAM_ATTR setMyTime(uint32_t t_sec, uint16_t t_msec,
vTaskDelay(pdMS_TO_TICKS(1000 - t_msec % 1000));
}
ESP_LOGD(TAG, "[%0.3f] UTC epoch time: %d.%03d sec", millis() / 1000.0,
ESP_LOGD(TAG, "[%0.3f] UTC time: %d.%03d sec", millis() / 1000.0,
time_to_set, t_msec % 1000);
// if we have got an external timesource, set RTC time and shift RTC_INT pulse
@ -107,11 +105,11 @@ void IRAM_ATTR setMyTime(uint32_t t_sec, uint16_t t_msec,
timeSource = mytimesource; // set global variable
timesyncer.attach(TIME_SYNC_INTERVAL * 60, timeSync);
ESP_LOGI(TAG, "[%0.3f] Timesync finished, time was set | source: %c",
millis() / 1000.0, timeSetSymbols[timeSource]);
millis() / 1000.0, timeSetSymbols[mytimesource]);
} else {
timesyncer.attach(TIME_SYNC_INTERVAL_RETRY * 60, timeSync);
ESP_LOGI(TAG, "[%0.3f] Timesync failed, invalid time fetched | source: %c",
millis() / 1000.0, timeSetSymbols[timeSource]);
millis() / 1000.0, timeSetSymbols[mytimesource]);
}
}
@ -236,7 +234,7 @@ void clock_init(void) {
pinMode(HAS_DCF77, OUTPUT);
#endif
userUTCTime = now();
time_t userUTCTime = now();
xTaskCreatePinnedToCore(clock_loop, // task function
"clockloop", // name of task

View File

@ -1,243 +1,274 @@
/*
///--> IMPORTANT LICENSE NOTE for this file <--///
///--> IMPORTANT LICENSE NOTE for timesync option 1 in this file <--///
PLEASE NOTE: There is a patent filed for the time sync algorithm used in the
code of this file. The shown implementation example is covered by the
repository's licencse, but you may not be eligible to deploy the applied
algorithm in applications without granted license by the patent holder.
code of this file for timesync option TIME_SYNC_LORASERVER. The shown
implementation example is covered by the repository's licencse, but you may not
be eligible to deploy the applied algorithm in applications without granted
license by the patent holder.
You may use timesync option TIME_SYNC_LORAWAN if you do not want or cannot
accept this.
*/
#if (TIME_SYNC_LORASERVER) && (HAS_LORA)
#if (HAS_LORA)
#if (TIME_SYNC_LORASERVER) && (TIME_SYNC_LORAWAN)
#error Duplicate timesync method selected. You must select either LORASERVER or LORAWAN timesync.
#endif
#include "timesync.h"
// Local logging tag
static const char TAG[] = __FILE__;
using namespace std::chrono;
typedef std::chrono::system_clock myClock;
typedef myClock::time_point myClock_timepoint;
typedef std::chrono::duration<long long int, std::ratio<1, 1000>>
myClock_msecTick;
TaskHandle_t timeSyncReqTask = NULL;
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];
static uint8_t time_sync_seqNo = (uint8_t)random(TIME_SYNC_MAX_SEQNO),
sample_idx;
static uint32_t timesync_timestamp[TIME_SYNC_SAMPLES][no_of_timestamps];
static TaskHandle_t timeSyncProcTask;
// send time request message
void send_timesync_req() {
// create task for timeserver handshake processing, called from main.cpp
void timesync_init() {
xTaskCreatePinnedToCore(timesync_processReq, // task function
"timesync_proc", // name of task
2048, // stack size of task
(void *)1, // task parameter
3, // priority of the task
&timeSyncProcTask, // task handle
1); // CPU core
}
// if a timesync handshake is pending then exit
// kickoff asnychronous timesync handshake
void timesync_request(void) {
// exit if a timesync handshake is already running
if (timeSyncPending)
return;
// else unblock timesync task
// start timesync handshake
else {
ESP_LOGI(TAG, "[%0.3f] Timeserver sync request started", millis() / 1000.0);
xTaskNotifyGive(timeSyncReqTask);
ESP_LOGI(TAG, "[%0.3f] Timeserver sync request seqNo#%d started",
millis() / 1000.0, time_sync_seqNo);
xTaskNotifyGive(timeSyncProcTask); // unblock timesync task
}
}
// task for sending time sync requests
void process_timesync_req(void *taskparameter) {
// task for processing time sync request
void IRAM_ATTR timesync_processReq(void *taskparameter) {
uint8_t k;
uint16_t time_to_set_fraction_msec;
uint32_t seq_no = 0, time_to_set;
auto time_offset_ms = myClock_msecTick::zero();
uint32_t rcv_seqNo = TIME_SYNC_END_FLAG, time_offset_ms;
// this task is an endless loop, waiting in blocked mode, until it is
// unblocked by timesync_request(). It then waits to be notified from
// timesync_serverAnswer(), which is called from LMIC each time a timestamp
// from the timesource via LORAWAN arrived.
// --- asnychronous part: generate and collect timestamps from gateway ---
while (1) {
// reset all timestamps before next sync run
time_offset_ms = myClock_msecTick::zero();
for (uint8_t i = 0; i < TIME_SYNC_SAMPLES; i++)
time_sync_tx[i] = time_sync_rx[i] = myClock_timepoint();
// wait for kickoff
ulTaskNotifyTake(pdFALSE, portMAX_DELAY);
timeSyncPending = true;
time_offset_ms = sample_idx = 0;
// wait until we are joined if we are not
while (!LMIC.devaddr) {
vTaskDelay(pdMS_TO_TICKS(3000));
}
// collect timestamp samples
// collect timestamp samples in timestamp array
for (uint8_t i = 0; i < TIME_SYNC_SAMPLES; i++) {
// send sync request to server
// send timesync request
#if (TIME_SYNC_LORASERVER) // aks user's timeserver (for LoRAWAN < 1.0.3)
payload.reset();
payload.addByte(time_sync_seqNo);
SendPayload(TIMEPORT, prio_high);
// wait for a valid timestamp from recv_timesync_ans()
while (seq_no != time_sync_seqNo) {
if (xTaskNotifyWait(0x00, ULONG_MAX, &seq_no,
pdMS_TO_TICKS(TIME_SYNC_TIMEOUT * 1000)) ==
pdFALSE) {
ESP_LOGW(TAG, "[%0.3f] Timesync handshake error: timeout",
millis() / 1000.0);
goto finish; // no valid sequence received before timeout
}
#elif (TIME_SYNC_LORAWAN) // ask network (requires LoRAWAN >= 1.0.3)
LMIC_requestNetworkTime(timesync_serverAnswer, &time_sync_seqNo);
// trigger send to immediately get DevTimeAns on class A device
LMIC_sendAlive();
#endif
// wait until a timestamp was received
if (xTaskNotifyWait(0x00, ULONG_MAX, &rcv_seqNo,
pdMS_TO_TICKS(TIME_SYNC_TIMEOUT * 1000)) == pdFALSE) {
ESP_LOGW(TAG, "[%0.3f] Timesync aborted: timed out", millis() / 1000.0);
goto Fail; // no timestamp received before timeout
}
// process answer
k = seq_no % TIME_SYNC_SAMPLES;
// check if we are in handshake with server
if (rcv_seqNo != time_sync_seqNo) {
ESP_LOGW(TAG, "[%0.3f] Timesync aborted: handshake out of sync",
millis() / 1000.0);
goto Fail;
}
// calculate time diff from collected timestamps
time_offset_ms += time_point_cast<milliseconds>(time_sync_rx[k]) -
time_point_cast<milliseconds>(time_sync_tx[k]);
#if (TIME_SYNC_LORASERVER)
// calculate time diff with received timestamp
time_offset_ms += timesync_timestamp[sample_idx][timesync_rx] -
timesync_timestamp[sample_idx][timesync_tx];
#endif
// wrap around seqNo, keeping it in time port range
time_sync_seqNo++;
if (time_sync_seqNo > TIMEREQUEST_MAX_SEQNO) {
// increment sample_idx and time_sync_seqNo, keeping it in range
if (++time_sync_seqNo > TIME_SYNC_MAX_SEQNO)
time_sync_seqNo = 0;
}
sample_idx++;
if (i < TIME_SYNC_SAMPLES - 1) {
// wait until next cycle
// if we are not in last cycle, pause until next cycle
if (i < TIME_SYNC_SAMPLES - 1)
vTaskDelay(pdMS_TO_TICKS(TIME_SYNC_CYCLE * 1000));
} else { // before sending last time sample...
// ...send flush to open a receive window for last time_sync_answer
payload.reset();
payload.addByte(0x99);
SendPayload(RCMDPORT, prio_high);
// ...send a alive open a receive window for last time_sync_answer
LMIC_sendAlive();
}
} // end of for loop to collect timestamp samples
} // for i
// --- time critial part: evaluate timestamps and calculate time ---
// mask application irq to ensure accurate timing
mask_user_IRQ();
// average time offset over all collected diffs
// calculate average time offset over the summed up difference
// add msec from latest gateway time, and apply a compensation constant for
// processing times on node and gateway
time_offset_ms /= TIME_SYNC_SAMPLES;
// calculate time offset with millisecond precision using LMIC's time base,
// since we use LMIC's ostime_t txEnd as tx timestamp.
// Also apply calibration const to compensate processing time.
time_offset_ms +=
milliseconds(osticks2ms(os_getTime())) + milliseconds(TIME_SYNC_FIXUP);
TIME_SYNC_FIXUP + timesync_timestamp[sample_idx - 1][gwtime_msec];
// calculate absolute time in UTC epoch: convert to whole seconds, round to
// ceil, and calculate fraction milliseconds
time_to_set = (uint32_t)(time_offset_ms.count() / 1000) + 1;
// calculate fraction milliseconds
time_to_set_fraction_msec = (uint16_t)(time_offset_ms.count() % 1000);
// calculate absolute UTC time: take latest timestamp received from
// gateway, convert to whole seconds, round to ceil, add fraction seconds
setMyTime(timesync_timestamp[sample_idx - 1][gwtime_sec] +
time_offset_ms / 1000,
time_offset_ms % 1000, _lora);
setMyTime(time_to_set, time_to_set_fraction_msec, _lora);
// send timesync end char to show timesync was successful
payload.reset();
payload.addByte(TIME_SYNC_END_FLAG);
SendPayload(RCMDPORT, prio_high);
goto Finish;
finish:
Fail:
// set retry timer
timesyncer.attach(TIME_SYNC_INTERVAL_RETRY * 60, timeSync);
Finish:
// end of time critical section: release app irq lock
timeSyncPending = false;
unmask_user_IRQ();
} // infinite while(1)
}
// called from lorawan.cpp after time_sync_req was sent
void store_time_sync_req(uint32_t timestamp) {
// store incoming timestamps
void timesync_store(uint32_t timestamp, timesync_t timestamp_type) {
ESP_LOGD(TAG, "[%0.3f] seq#%d[%d]: timestamp(t%d)=%d", millis() / 1000.0,
time_sync_seqNo, sample_idx, timestamp_type, timestamp);
timesync_timestamp[sample_idx][timestamp_type] = timestamp;
}
// callback function to receive time answer from network or answer
void IRAM_ATTR timesync_serverAnswer(void *pUserData, int flag) {
// if no timesync handshake is pending then exit
if (!timeSyncPending)
return;
uint8_t k = time_sync_seqNo % TIME_SYNC_SAMPLES;
time_sync_tx[k] += milliseconds(timestamp);
// mask application irq to ensure accurate timing
mask_user_IRQ();
ESP_LOGD(TAG, "[%0.3f] Timesync request #%d of %d sent at %d.%03d",
millis() / 1000.0, k + 1, TIME_SYNC_SAMPLES, timestamp / 1000,
timestamp % 1000);
}
// store LMIC time when we received the timesync answer
ostime_t rxTime = osticks2ms(os_getTime());
// process timeserver timestamp answer, called by myRxCallback() in lorawan.cpp
int recv_timesync_ans(const uint8_t buf[], const uint8_t buf_len) {
int rc = 0;
uint8_t rcv_seqNo = *(uint8_t *)pUserData;
uint16_t timestamp_msec = 0;
uint32_t timestamp_sec = 0;
/*
parse 7 byte timesync_answer:
#if (TIME_SYNC_LORASERVER)
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
*/
// pUserData: contains pointer to payload buffer
// flag: length of buffer
// if no timesync handshake is pending then exit
if (!timeSyncPending)
return 0; // failure
// Store the instant the time request of the node was received on the gateway
timesync_store(rxTime, timesync_rx);
// extract 1 byte timerequest sequence number from buffer
uint8_t seq_no = buf[0];
buf++;
// parse timesync_answer:
// byte meaning
// 0 sequence number (taken from node's time_sync_req)
// 1..4 current second (from UTC epoch)
// 5 1/250ths fractions of current second
// swap byte order from msb to lsb, note: this is a platform dependent hack
timestamp_sec = __builtin_bswap32(*(uint32_t *)(pUserData + 1));
// one step being 1/250th sec * 1000 = 4msec
timestamp_msec = *(uint8_t *)(pUserData + 5);
timestamp_msec *= 4;
// if no time is available or spurious buffer then exit
if (buf_len != TIME_SYNC_FRAME_LENGTH) {
if (seq_no == 0xff)
if (flag != TIME_SYNC_FRAME_LENGTH) {
if (rcv_seqNo == TIME_SYNC_END_FLAG)
ESP_LOGI(TAG, "[%0.3f] Timeserver error: no confident time available",
millis() / 1000.0);
else
ESP_LOGW(TAG, "[%0.3f] Timeserver error: spurious data received",
millis() / 1000.0);
return 0; // failure
goto Exit; // failure
}
else { // we received a probably valid time frame
goto Finish;
uint8_t k = seq_no % TIME_SYNC_SAMPLES;
#elif (TIME_SYNC_LORAWAN)
// pointers to 4 bytes containing UTC seconds since unix epoch, msb
uint32_t timestamp_sec, *timestamp_ptr;
// pUserData: contains pointer to SeqNo
// flag: indicates if we got a recent time from the network
// 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);
// we guess timepoint is recent if it newer than code compile date
if (timeIsValid(myClock::to_time_t(time_sync_rx[k]))) {
ESP_LOGD(TAG, "[%0.3f] Timesync request #%d of %d rcvd at %d.%03d",
millis() / 1000.0, k + 1, TIME_SYNC_SAMPLES, timestamp_sec,
timestamp_msec);
// inform processing task
xTaskNotify(timeSyncReqTask, seq_no, eSetBits);
return 1; // success
} else {
ESP_LOGW(TAG, "[%0.3f] Timeserver error: outdated time received",
millis() / 1000.0);
return 0; // failure
}
if (flag != 1) {
ESP_LOGW(TAG, "[%0.3f] Network did not answer time request",
millis() / 1000.0);
goto Exit;
}
// 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 lmicTime;
// Populate lmic_time_reference
if ((LMIC_getNetworkTimeReference(&lmicTime)) != 1) {
ESP_LOGW(TAG, "[%0.3f] Network time request failed", millis() / 1000.0);
goto Exit;
}
// Calculate UTCTime, considering the difference between GPS and UTC time
timestamp_sec = lmicTime.tNetwork + GPS_UTC_DIFF;
// Add delay between the instant the time was received on the gateway and the
// current time on the node
timestamp_msec = rxTime - lmicTime.tLocal;
goto Finish;
#endif // (TIME_SYNC_LORAWAN)
Finish:
// check if calculated time is recent
if (timeIsValid(timestamp_sec)) {
// store time received from gateway
timesync_store(timestamp_sec, gwtime_sec);
timesync_store(timestamp_msec, gwtime_msec);
// success
rc = 1;
} else {
ESP_LOGW(TAG, "[%0.3f] Timeserver error: outdated time received",
millis() / 1000.0);
}
Exit:
// end of time critical section: release app irq lock
unmask_user_IRQ();
// inform processing task
xTaskNotify(timeSyncProcTask, (rc ? rcv_seqNo : TIME_SYNC_END_FLAG),
eSetBits);
}
// create task for timeserver handshake processing, called from main.cpp
void timesync_init() {
xTaskCreatePinnedToCore(process_timesync_req, // task function
"timesync_req", // name of task
2048, // stack size of task
(void *)1, // task parameter
3, // priority of the task
&timeSyncReqTask, // task handle
1); // CPU core
}
#endif
#endif // HAS_LORA