diff --git a/README.md b/README.md index d47f323b..bd1ec6d1 100644 --- a/README.md +++ b/README.md @@ -162,7 +162,7 @@ Output of sensor and peripheral data is internally switched by a bitmask registe # Time sync -Paxcounter can keep it's time-of-day synced with an external time source. Set *#define TIME_SYNC_INTERVAL* in paxcounter.conf to enable time sync. Supported external time sources are GPS, LORAWAN network time and LORAWAN application timeserver time. An on board DS3231 RTC is kept sycned as fallback time source. Time accuracy depends on board's time base which generates the pulse per second. Supported are GPS PPS, SQW output of RTC, and internal ESP32 hardware timer. Time base is selected by #defines in the board's hal file, see example in [**generic.h**](src/hal/generic.h). If your LORAWAN network does not support network time, you can run a Node-Red timeserver application using the [**Timeserver code**](/src/Timeserver/Nodered-Timeserver.json) in TTN subdirectory. Configure MQTT nodes in Node-Red to the same LORAWAN application as paxocunter device is using. +Paxcounter can keep it's time-of-day synced with an external time source. Set *#define TIME_SYNC_INTERVAL* in paxcounter.conf to enable time sync. Supported external time sources are GPS, LORAWAN network time and LORAWAN application timeserver time. An on board DS3231 RTC is kept sycned as fallback time source. Time accuracy depends on board's time base which generates the pulse per second. Supported are GPS PPS, SQW output of RTC, and internal ESP32 hardware timer. Time base is selected by #defines in the board's hal file, see example in [**generic.h**](src/hal/generic.h). Bonus: If your LORAWAN network does not support network time, you can run a Node-Red timeserver application using the enclosed [**Timeserver code**](/src/Timeserver/Nodered-Timeserver.json). Configure MQTT nodes in Node-Red to the same LORAWAN application as paxocunter device is using. # Wall clock controller @@ -389,7 +389,7 @@ Note: all settings are stored in NVRAM and will be reloaded when device starts. 0x86 get time/date - Device answers with it's local time/date (UTC Unix epoch) on Port 9. + Device answers with it's local time/date (UTC Unix epoch) on Port 2. 0x87 set time/date diff --git a/include/mobaserial.h b/include/mobaserial.h new file mode 100644 index 00000000..c3b47ab8 --- /dev/null +++ b/include/mobaserial.h @@ -0,0 +1,16 @@ +#ifndef _MOBALINE_H +#define _MOBALINE_H + +#include "globals.h" +#include "dcf77.h" + +#define MOBALINE_FRAME_SIZE (33) +#define MOBALINE_PULSE_LENGTH (100) +#define MOBALINE_HEAD_PULSE_LENGTH (1500) + +void MOBALINE_Pulse(time_t t, uint8_t const *DCFpulse); +uint8_t *IRAM_ATTR MOBALINE_Frame(time_t const t); +void IRAM_ATTR dec2bcd(uint8_t const dec, uint8_t const startpos, + uint8_t const endpos, uint8_t *DCFpulse); + +#endif \ No newline at end of file diff --git a/include/timesync.h b/include/timesync.h index 3d5cb059..9df72171 100644 --- a/include/timesync.h +++ b/include/timesync.h @@ -14,5 +14,6 @@ void send_timesync_req(void); int recv_timesync_ans(uint8_t buf[], uint8_t buf_len); void process_timesync_req(void *taskparameter); void store_time_sync_req(uint32_t t_millisec); +int adjustTime(uint32_t t_sec, uint16_t t_msec); #endif \ No newline at end of file diff --git a/src/TTN/packed_converter.js b/src/TTN/packed_converter.js index 7548e3ce..0d691a9b 100644 --- a/src/TTN/packed_converter.js +++ b/src/TTN/packed_converter.js @@ -18,6 +18,7 @@ function Converter(decoded, port) { } if (port === 2) { + if('voltage' in converted) converted.voltage /= 1000; } diff --git a/src/TTN/packed_decoder.js b/src/TTN/packed_decoder.js index 49f1b5b9..346df0ca 100644 --- a/src/TTN/packed_decoder.js +++ b/src/TTN/packed_decoder.js @@ -30,7 +30,13 @@ function Decoder(bytes, port) { if (port === 2) { // device status data - return decode(bytes, [uint16, uptime, uint8, uint32, uint8, uint8], ['voltage', 'uptime', 'cputemp', 'memory', 'reset0', 'reset1']); + if (bytes.length === 17) { + return decode(bytes, [uint16, uptime, uint8, uint32, uint8, uint8], ['voltage', 'uptime', 'cputemp', 'memory', 'reset0', 'reset1']); + } + // epoch time answer + if (bytes.length === 4) { + return decode(bytes, [uint32], ['time']); + } } if (port === 3) { diff --git a/src/lorawan.cpp b/src/lorawan.cpp index 55c6785f..21fe100b 100644 --- a/src/lorawan.cpp +++ b/src/lorawan.cpp @@ -462,8 +462,8 @@ void lora_housekeeping(void) { // uxTaskGetStackHighWaterMark(LoraTask)); } -void user_request_network_time_callback(void *pVoidUserUTCTime, - int flagSuccess) { +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; @@ -487,6 +487,11 @@ void user_request_network_time_callback(void *pVoidUserUTCTime, return; } + // begin of time critical section: lock I2C bus to ensure accurate timing + // don't move the mutex, will impact accuracy of time up to 1 sec! + if (!I2C_MUTEX_LOCK()) + return; // failure + // Update userUTCTime, considering the difference between the GPS and UTC // time, and the leap seconds until year 2019 *pUserUTCTime = lmicTimeReference.tNetwork + 315964800; @@ -497,19 +502,13 @@ void user_request_network_time_callback(void *pVoidUserUTCTime, // Add the delay between the instant the time was transmitted and // the current time time_t requestDelaySec = osticks2ms(ticksNow - ticksRequestSent) / 1000; - *pUserUTCTime += requestDelaySec; // Update system time with time read from the network - if (timeIsValid(*pUserUTCTime)) { - setTime(*pUserUTCTime); -#ifdef HAS_RTC - set_rtctime(*pUserUTCTime, do_mutex); // calibrate RTC if we have one -#endif - timeSource = _lora; - timesyncer.attach(TIME_SYNC_INTERVAL * 60, timeSync); // regular repeat - ESP_LOGI(TAG, "Received recent time from LoRa"); - } else - ESP_LOGI(TAG, "Invalid time received from LoRa"); + adjustTime(*pUserUTCTime + requestDelaySec, 0); + + // end of time critical section: release I2C bus + I2C_MUTEX_UNLOCK(); + } // user_request_network_time_callback #endif // HAS_LORA diff --git a/src/main.cpp b/src/main.cpp index dd6eab68..1d0b82f0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -31,7 +31,7 @@ ledloop 0 3 blinks LEDs spiloop 0 2 reads/writes data on spi interface IDLE 0 0 ESP32 arduino scheduler -> runs wifi sniffer -clockloop 1 4 generates realtime telegrams for external clock +clockloop 1 3 generates realtime telegrams for external clock looptask 1 1 arduino core -> runs the LMIC LoRa stack irqhandler 1 1 executes tasks triggered by timer irq gpsloop 1 2 reads data from GPS via serial or i2c diff --git a/src/mobaserial.cpp b/src/mobaserial.cpp new file mode 100644 index 00000000..7c330e15 --- /dev/null +++ b/src/mobaserial.cpp @@ -0,0 +1,109 @@ +/* +// Emulate a MOBATIME serial clock controller +// +// Protocol published and described here: +// +// +http://www.elektrorevue.cz/cz/download/time-distribution-within-industry-4-0-platform--controlling-slave-clocks-via-master-clock-hn50/ +*/ + +#ifdef HAS_MOBALINE + +#include "mobaline.h" + +// Local logging tag +static const char TAG[] = __FILE__; + +// triggered by pulse per second to ticker out mobaline frame +void MOBALINE_Pulse(time_t t, uint8_t const *DCFpulse) { + + TickType_t startTime = xTaskGetTickCount(); + uint8_t sec = second(t); + + t = myTZ.toLocal(now()); + ESP_LOGD(TAG, "[%02d:%02d:%02d.%03d] MOBALINE bit %d", hour(t), minute(t), + second(t), millisecond(), sec); + + // induce 3 pulses + for (uint8_t pulse = 0; pulse <= 3; pulse++) { + + switch (pulse) { + + case 0: // start of bit -> start of timeframe for logic signal + if (DCFpulse[sec] != dcf_Z) { + digitalWrite(HAS_DCF77, dcf_high); + vTaskDelay(pdMS_TO_TICKS(MOBALINE_HEAD_PULSE_LENGTH)); + digitalWrite(HAS_DCF77, dcf_high); + vTaskDelay(pdMS_TO_TICKS(MOBALINE_HEAD_PULSE_LENGTH)); + return; // next bit + } else // start the signalling for the next bit + digitalWrite(HAS_DCF77, dcf_high); + break; + + case 1: // 100ms after start of bit -> end of timeframe for logic 0 + if (DCFpulse[sec] == dcf_1) + digitalWrite(HAS_DCF77, dcf_low); + break; + + case 2: // 200ms after start of bit -> end of timeframe for logic 1 + if (DCFpulse[sec] == dcf_0) + digitalWrite(HAS_DCF77, dcf_low); + break; + + case 3: // 300ms after start -> last pulse + break; + + } // switch + + // pulse pause + vTaskDelayUntil(&startTime, pdMS_TO_TICKS(MOBALINE_PULSE_LENGTH)); + + } // for +} // DCF77_Pulse() + +uint8_t *IRAM_ATTR MOBALINE_Frame(time_t const tt) { + + // array of dcf pulses for one minute, secs 0..16 and 20 are never touched, so + // we keep them statically to avoid same recalculation every minute + + static uint8_t DCFpulse[DCF77_FRAME_SIZE + 1]; + + time_t t = myTZ.toLocal(tt); // convert to local time + + // ENCODE HEAD (bit 0)) + DCFpulse[0] = dcf_Z; // not yet implemented + + // ENCODE DAYLIGHTSAVING (bit 1) + DCFpulse[1] = myTZ.locIsDST(t) ? dcf_1 : dcf_0; + + // ENCODE DATE (bits 2..20) + dec2bcd(false, year(t) - 2000, 2, 9, DCFpulse); + dec2bcd(false, month(t), 10, 14, DCFpulse); + dec2bcd(false, day(t), 15, 20, DCFpulse); + + // ENCODE HOUR (bits 21..26) + dec2bcd2(false, hour(t), 21, 26, DCFpulse); + + // ENCODE MINUTE (bits 27..33) + dec2bcd2(false, minute(t), 27, 33, DCFpulse); + + // timestamp this frame with it's minute + DCFpulse[34] = minute(t); + + return DCFpulse; + +} // MOBALINE_Frame() + +// helper function to convert decimal to bcd digit msb +void IRAM_ATTR dec2bcd(uint8_t const dec, uint8_t const startpos, + uint8_t const endpos, uint8_t *DCFpulse) { + + uint8_t data = (dec < 10) ? dec : ((dec / 10) << 4) + (dec % 10); + + for (uint8_t i = endpos; i >= startpos; i--) { + DCFpulse[i] = (data & 1) ? dcf_1 : dcf_0; + data >>= 1; + } +} + +#endif // HAS_MOBALINE \ No newline at end of file diff --git a/src/rcommand.cpp b/src/rcommand.cpp index e0b8637f..1b124ca8 100644 --- a/src/rcommand.cpp +++ b/src/rcommand.cpp @@ -277,7 +277,7 @@ void get_time(uint8_t val[]) { ESP_LOGI(TAG, "Remote command: get time"); payload.reset(); payload.addTime(now()); - SendPayload(TIMEPORT, prio_high); + SendPayload(STATUSPORT, prio_high); }; void set_time(uint8_t val[]) { diff --git a/src/timekeeper.cpp b/src/timekeeper.cpp index 8ba94b0b..53907a98 100644 --- a/src/timekeeper.cpp +++ b/src/timekeeper.cpp @@ -196,7 +196,7 @@ void clock_init(void) { "clockloop", // name of task 2048, // stack size of task (void *)&userUTCTime, // start time as task parameter - 4, // priority of the task + 3, // priority of the task &ClockTask, // task handle 1); // CPU core diff --git a/src/timesync.cpp b/src/timesync.cpp index 8ecea0cd..bf141814 100644 --- a/src/timesync.cpp +++ b/src/timesync.cpp @@ -53,7 +53,7 @@ void send_timesync_req() { "timesync_req", // name of task 2048, // stack size of task (void *)1, // task parameter - 4, // priority of the task + 2, // priority of the task &timeSyncReqTask, // task handle 1); // CPU core } @@ -62,10 +62,9 @@ void send_timesync_req() { // task for sending time sync requests void process_timesync_req(void *taskparameter) { - uint8_t k = 0, i = 0; + uint8_t k = 0; uint16_t time_to_set_fraction_msec; - uint32_t seq_no = 0; - time_t time_to_set; + uint32_t seq_no = 0, time_to_set; auto time_offset_ms = myClock_msecTick::zero(); // wait until we are joined @@ -87,12 +86,8 @@ void process_timesync_req(void *taskparameter) { // process answer, wait for notification from recv_timesync_ans() if ((xTaskNotifyWait(0x00, ULONG_MAX, &seq_no, pdMS_TO_TICKS(TIME_SYNC_TIMEOUT * 1000)) == pdFALSE) || - (seq_no != time_sync_seqNo)) { - - ESP_LOGW(TAG, "[%0.3f] Timeserver error: handshake timed out", - millis() / 1000.0); - goto finish; - } // no valid sequence received before timeout + (seq_no != time_sync_seqNo)) + goto error; // no valid sequence received before timeout else { // calculate time diff from collected timestamps k = seq_no % TIME_SYNC_SAMPLES; @@ -116,7 +111,9 @@ void process_timesync_req(void *taskparameter) { } // for // begin of time critical section: lock I2C bus to ensure accurate timing - I2C_MUTEX_LOCK(); + // don't move the mutex, will impact accuracy of time up to 1 sec! + if (!I2C_MUTEX_LOCK()) + goto error; // failure // average time offset from collected diffs time_offset_ms /= TIME_SYNC_SAMPLES; @@ -127,63 +124,38 @@ void process_timesync_req(void *taskparameter) { time_offset_ms += milliseconds(osticks2ms(os_getTime())) + milliseconds(TIME_SYNC_FIXUP); - // calculate absolute time in UTC epoch - // convert to whole seconds, floor - time_to_set = (time_t)(time_offset_ms.count() / 1000) + 1; + // 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); - ESP_LOGD(TAG, "[%0.3f] Calculated UTC epoch time: %d.%03d sec", - millis() / 1000.0, time_to_set, time_to_set_fraction_msec); + adjustTime(time_to_set, time_to_set_fraction_msec); - // adjust system time - if (timeIsValid(time_to_set)) { - - // wait until top of second with 4ms precision - vTaskDelay(pdMS_TO_TICKS(1000 - time_to_set_fraction_msec)); - -#ifdef HAS_RTC - time_to_set++; // advance time 1 sec wait time - // set RTC time and calibrate RTC_INT pulse on top of second - set_rtctime(time_to_set, no_mutex); -#endif - -#if (!defined GPS_INT && !defined RTC_INT) - // sync pps timer to top of second - timerRestart(ppsIRQ); // reset pps timer - CLOCKIRQ(); // fire clock pps, advances time 1 sec -#endif - - setTime(time_to_set); // set the time on top of second - - // end of time critical section: release I2C bus - I2C_MUTEX_UNLOCK(); - - timeSource = _lora; - timesyncer.attach(TIME_SYNC_INTERVAL * 60, timeSync); // regular repeat - ESP_LOGI(TAG, "[%0.3f] Timesync finished, time was adjusted", - millis() / 1000.0); - } else - ESP_LOGW(TAG, "[%0.3f] Timesync failed, outdated time calculated", - millis() / 1000.0); + // end of time critical section: release I2C bus + I2C_MUTEX_UNLOCK(); finish: - lora_time_sync_pending = false; timeSyncReqTask = NULL; vTaskDelete(NULL); // end task + +error: + ESP_LOGW(TAG, "[%0.3f] Timeserver error: handshake timed out", + millis() / 1000.0); + goto finish; // end task } // called from lorawan.cpp after time_sync_req was sent -void store_time_sync_req(uint32_t t_txEnd_ms) { +void store_time_sync_req(uint32_t timestamp) { uint8_t k = time_sync_seqNo % TIME_SYNC_SAMPLES; - time_sync_tx[k] += milliseconds(t_txEnd_ms); + time_sync_tx[k] += milliseconds(timestamp); ESP_LOGD(TAG, "[%0.3f] Timesync request #%d sent at %d.%03d", - millis() / 1000.0, time_sync_seqNo, t_txEnd_ms / 1000, - t_txEnd_ms % 1000); + millis() / 1000.0, time_sync_seqNo, timestamp / 1000, + timestamp % 1000); } // process timeserver timestamp answer, called from lorawan.cpp @@ -240,4 +212,40 @@ int recv_timesync_ans(uint8_t buf[], uint8_t buf_len) { } } +// adjust system time, calibrate RTC and RTC_INT pps +int IRAM_ATTR adjustTime(uint32_t t_sec, uint16_t t_msec) { + + time_t time_to_set = (time_t)t_sec; + + ESP_LOGD(TAG, "[%0.3f] Calculated UTC epoch time: %d.%03d sec", + millis() / 1000.0, time_to_set, t_msec); + + if (timeIsValid(time_to_set)) { + + // wait until top of second with millisecond precision + vTaskDelay(pdMS_TO_TICKS(1000 - t_msec)); + +#ifdef HAS_RTC + time_to_set++; // advance time 1 sec wait time + // set RTC time and calibrate RTC_INT pulse on top of second + set_rtctime(time_to_set, no_mutex); +#endif + +#if (!defined GPS_INT && !defined RTC_INT) + // sync pps timer to top of second + timerWrite(ppsIRQ, 0); // reset pps timer + CLOCKIRQ(); // fire clock pps, this advances time 1 sec +#endif + + setTime(time_to_set); // set the time on top of second + + timeSource = _lora; + timesyncer.attach(TIME_SYNC_INTERVAL * 60, timeSync); // regular repeat + ESP_LOGI(TAG, "[%0.3f] Timesync finished, time was adjusted", + millis() / 1000.0); + } else + ESP_LOGW(TAG, "[%0.3f] Timesync failed, outdated time calculated", + millis() / 1000.0); +} + #endif \ No newline at end of file