52 files changed
+580
-638
No files matched your search
@@ -84,11 +84,6 @@ function Decoder(bytes, port) {
|
||||
}
|
||||
}
|
||||
|
||||
if (port === 10) {
|
||||
// ENS count
|
||||
return decode(bytes, [uint16], ['ens']);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -90,11 +90,6 @@ function decodeUplink(input) {
|
||||
data = decode(input.bytes, [uint32, uint8], ['time', 'timestatus']);
|
||||
}
|
||||
}
|
||||
|
||||
if (input.fPort === 10) {
|
||||
// ENS count
|
||||
data = decode(input.bytes, [uint16], ['ens']);
|
||||
}
|
||||
|
||||
data.bytes = input.bytes; // comment out if you do not want to include the original payload
|
||||
data.port = input.fPort; // comment out if you do not want to inlude the port
|
||||
|
||||
@@ -84,13 +84,6 @@ function decodeUplink(input) {
|
||||
}
|
||||
}
|
||||
|
||||
if (input.fPort === 10) {
|
||||
var i = 0;
|
||||
if (input.bytes.length >= 2) {
|
||||
data.ens = (input.bytes[i++] << 8) | input.bytes[i++];
|
||||
}
|
||||
}
|
||||
|
||||
if (data.hdop) {
|
||||
data.hdop /= 100;
|
||||
data.latitude /= 1000000;
|
||||
|
||||
@@ -6,12 +6,6 @@
|
||||
// Local logging tag
|
||||
static const char TAG[] = __FILE__;
|
||||
|
||||
// default settings for device data to be sent
|
||||
#define PAYLOADMASK \
|
||||
((GPS_DATA | MEMS_DATA | COUNT_DATA | SENSOR1_DATA | SENSOR2_DATA | \
|
||||
SENSOR3_DATA) & \
|
||||
(~BATT_DATA) & (~RESERVED_DATA))
|
||||
|
||||
// namespace for device runtime preferences
|
||||
#define DEVCONFIG "paxcntcfg"
|
||||
|
||||
@@ -57,7 +51,6 @@ static void defaultConfig(configData_t *myconfig) {
|
||||
myconfig->wifiant = 0; // 0=internal, 1=external (for LoPy/LoPy4)
|
||||
myconfig->rgblum = RGBLUMINOSITY; // RGB Led luminosity (0..100%)
|
||||
myconfig->payloadmask = PAYLOADMASK; // payloads as defined in default
|
||||
myconfig->enscount = COUNT_ENS; // 0=disabled, 1=enabled
|
||||
|
||||
#ifdef HAS_BME680
|
||||
// initial BSEC state for BME680 sensor
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
// routines for counting the number of devices which advertise Exposure
|
||||
// Notification Service e.g. "Corona Warn App" in Germany
|
||||
|
||||
// copied from https://github.com/kmetz/BLEExposureNotificationBeeper
|
||||
// (c) by Kaspar Metz
|
||||
// modified for use in the Paxcounter by AQ
|
||||
|
||||
#if (COUNT_ENS) && !(BLECOUNTER)
|
||||
#warning ENS-Counter needs Bluetooth, but Bluetooth compile option is disabled
|
||||
#endif
|
||||
|
||||
#if (COUNT_ENS)
|
||||
|
||||
// Local logging tag
|
||||
static const char TAG[] = __FILE__;
|
||||
|
||||
#define BT_BD_ADDR_HEX(addr) \
|
||||
addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]
|
||||
|
||||
#include "corona.h"
|
||||
|
||||
// When to forget old senders ** currently not used **
|
||||
#define FORGET_AFTER_MINUTES 2
|
||||
|
||||
// array of timestamps for seen notifiers: hash -> timestamp[ms]
|
||||
static std::map<uint16_t, unsigned long> cwaSeenNotifiers;
|
||||
|
||||
// Remove notifiers last seen over FORGET_AFTER_MINUTES ago.
|
||||
void cwa_clear() {
|
||||
#ifdef VERBOSE
|
||||
ESP_LOGV(TAG, "CWA: forget old notifier: %d", cwaSeenNotifiers.size());
|
||||
for (auto const ¬ifier : cwaSeenNotifiers) {
|
||||
ESP_LOGD(TAG, "CWA forget <%04X>", notifier.first);
|
||||
// }
|
||||
}
|
||||
#endif
|
||||
// clear everything, otherwise we would count the same device again, as in the
|
||||
// next cycle it likely will advertise with a different hash-value
|
||||
cwaSeenNotifiers.clear();
|
||||
}
|
||||
|
||||
// return the total number of devices seen advertising ENS
|
||||
uint16_t cwa_report(void) { return cwaSeenNotifiers.size(); }
|
||||
|
||||
bool cwa_init(void) {
|
||||
ESP_LOGD(TAG, "init BLE-scanner for ENS");
|
||||
return true;
|
||||
}
|
||||
|
||||
void cwa_mac_add(uint16_t hashedmac) {
|
||||
cwaSeenNotifiers[hashedmac] = millis(); // hash last seen at ....
|
||||
}
|
||||
|
||||
#endif
|
||||
+40
-28
@@ -9,10 +9,6 @@ static const char TAG[] = __FILE__;
|
||||
|
||||
Ticker cyclicTimer;
|
||||
|
||||
#if (HAS_SDS011)
|
||||
extern boolean isSDS011Active;
|
||||
#endif
|
||||
|
||||
void setCyclicIRQ() { xTaskNotify(irqHandlerTask, CYCLIC_IRQ, eSetBits); }
|
||||
|
||||
// do all housekeeping
|
||||
@@ -22,44 +18,62 @@ void doHousekeeping() {
|
||||
if ((RTC_runmode == RUNMODE_UPDATE) || (RTC_runmode == RUNMODE_MAINTENANCE))
|
||||
do_reset(true); // warmstart
|
||||
|
||||
// heap and task storage debugging
|
||||
// try to get time if we don't yet have a recent timesource
|
||||
if (timeSource == _unsynced || timeSource == _set)
|
||||
calibrateTime();
|
||||
|
||||
// print heap and task storage information
|
||||
ESP_LOGD(TAG, "Heap: Free:%d, Min:%d, Size:%d, Alloc:%d, StackHWM:%d",
|
||||
ESP.getFreeHeap(), ESP.getMinFreeHeap(), ESP.getHeapSize(),
|
||||
ESP.getMaxAllocHeap(), uxTaskGetStackHighWaterMark(NULL));
|
||||
ESP_LOGD(TAG, "IRQhandler %d bytes left | Taskstate = %d",
|
||||
uxTaskGetStackHighWaterMark(irqHandlerTask),
|
||||
eTaskGetState(irqHandlerTask));
|
||||
ESP_LOGD(TAG, "Rcommand interpreter %d bytes left | Taskstate = %d",
|
||||
uxTaskGetStackHighWaterMark(rcmdTask), eTaskGetState(rcmdTask));
|
||||
|
||||
if (irqHandlerTask != NULL)
|
||||
ESP_LOGD(TAG, "IRQhandler %d bytes left | Taskstate = %d",
|
||||
uxTaskGetStackHighWaterMark(irqHandlerTask),
|
||||
eTaskGetState(irqHandlerTask));
|
||||
if (rcmdTask != NULL)
|
||||
ESP_LOGD(TAG, "Rcommand interpreter %d bytes left | Taskstate = %d",
|
||||
uxTaskGetStackHighWaterMark(rcmdTask), eTaskGetState(rcmdTask));
|
||||
|
||||
#if (HAS_LORA)
|
||||
ESP_LOGD(TAG, "LMiCtask %d bytes left | Taskstate = %d",
|
||||
uxTaskGetStackHighWaterMark(lmicTask), eTaskGetState(lmicTask));
|
||||
ESP_LOGD(TAG, "Lorasendtask %d bytes left | Taskstate = %d",
|
||||
uxTaskGetStackHighWaterMark(lorasendTask),
|
||||
eTaskGetState(lorasendTask));
|
||||
if (lmicTask != NULL)
|
||||
ESP_LOGD(TAG, "LMiCtask %d bytes left | Taskstate = %d",
|
||||
uxTaskGetStackHighWaterMark(lmicTask), eTaskGetState(lmicTask));
|
||||
if (lorasendTask != NULL)
|
||||
ESP_LOGD(TAG, "Lorasendtask %d bytes left | Taskstate = %d",
|
||||
uxTaskGetStackHighWaterMark(lorasendTask),
|
||||
eTaskGetState(lorasendTask));
|
||||
#endif
|
||||
|
||||
#if (HAS_GPS)
|
||||
ESP_LOGD(TAG, "Gpsloop %d bytes left | Taskstate = %d",
|
||||
uxTaskGetStackHighWaterMark(GpsTask), eTaskGetState(GpsTask));
|
||||
if (GpsTask != NULL)
|
||||
ESP_LOGD(TAG, "Gpsloop %d bytes left | Taskstate = %d",
|
||||
uxTaskGetStackHighWaterMark(GpsTask), eTaskGetState(GpsTask));
|
||||
#endif
|
||||
|
||||
#ifdef HAS_SPI
|
||||
ESP_LOGD(TAG, "spiloop %d bytes left | Taskstate = %d",
|
||||
uxTaskGetStackHighWaterMark(spiTask), eTaskGetState(spiTask));
|
||||
if (spiTask != NULL)
|
||||
ESP_LOGD(TAG, "spiloop %d bytes left | Taskstate = %d",
|
||||
uxTaskGetStackHighWaterMark(spiTask), eTaskGetState(spiTask));
|
||||
#endif
|
||||
|
||||
#ifdef HAS_MQTT
|
||||
ESP_LOGD(TAG, "MQTTloop %d bytes left | Taskstate = %d",
|
||||
uxTaskGetStackHighWaterMark(mqttTask), eTaskGetState(mqttTask));
|
||||
if (mqttTask != NULL)
|
||||
ESP_LOGD(TAG, "MQTTloop %d bytes left | Taskstate = %d",
|
||||
uxTaskGetStackHighWaterMark(mqttTask), eTaskGetState(mqttTask));
|
||||
#endif
|
||||
|
||||
#if (defined HAS_DCF77 || defined HAS_IF482)
|
||||
ESP_LOGD(TAG, "Clockloop %d bytes left | Taskstate = %d",
|
||||
uxTaskGetStackHighWaterMark(ClockTask), eTaskGetState(ClockTask));
|
||||
if (ClockTask != NULL)
|
||||
ESP_LOGD(TAG, "Clockloop %d bytes left | Taskstate = %d",
|
||||
uxTaskGetStackHighWaterMark(ClockTask), eTaskGetState(ClockTask));
|
||||
#endif
|
||||
|
||||
#if (HAS_LED != NOT_A_PIN) || defined(HAS_RGB_LED)
|
||||
ESP_LOGD(TAG, "LEDloop %d bytes left | Taskstate = %d",
|
||||
uxTaskGetStackHighWaterMark(ledLoopTask),
|
||||
eTaskGetState(ledLoopTask));
|
||||
if (ledLoopTask != NULL)
|
||||
ESP_LOGD(TAG, "LEDloop %d bytes left | Taskstate = %d",
|
||||
uxTaskGetStackHighWaterMark(ledLoopTask),
|
||||
eTaskGetState(ledLoopTask));
|
||||
#endif
|
||||
|
||||
// read battery voltage into global variable
|
||||
@@ -106,10 +120,8 @@ void doHousekeeping() {
|
||||
|
||||
#if (HAS_SDS011)
|
||||
if (isSDS011Active) {
|
||||
ESP_LOGD(TAG, "SDS011: go to sleep");
|
||||
sds011_loop();
|
||||
} else {
|
||||
ESP_LOGD(TAG, "SDS011: wakeup");
|
||||
sds011_wakeup();
|
||||
}
|
||||
#endif
|
||||
|
||||
+10
-11
@@ -51,18 +51,17 @@ void DCF77_Pulse(uint8_t bit) {
|
||||
|
||||
// helper function to convert decimal to bcd digit
|
||||
uint64_t dec2bcd(uint8_t const dec, uint8_t const startpos,
|
||||
uint8_t const endpos, uint8_t *odd_parity) {
|
||||
uint8_t const endpos, uint8_t *parity) {
|
||||
|
||||
uint8_t data = (dec < 10) ? dec : ((dec / 10) << 4) + (dec % 10);
|
||||
uint8_t data = dec < 10 ? dec : ((dec / 10) << 4) + dec % 10;
|
||||
uint64_t bcd = 0;
|
||||
|
||||
*odd_parity = 0;
|
||||
*parity = 0;
|
||||
for (uint8_t i = startpos; i <= endpos; i++) {
|
||||
bcd += (data & 1) ? set_dcfbit(i) : 0;
|
||||
*odd_parity += (data & 1);
|
||||
bcd += data & 1 ? set_dcfbit(i) : 0;
|
||||
*parity ^= data & 1;
|
||||
data >>= 1;
|
||||
}
|
||||
*odd_parity %= 2;
|
||||
|
||||
return bcd;
|
||||
}
|
||||
@@ -96,14 +95,14 @@ uint64_t DCF77_Frame(const struct tm t) {
|
||||
|
||||
// DATE (36..58)
|
||||
frame += dec2bcd(t.tm_mday, 36, 41, &parity);
|
||||
parity_sum += parity;
|
||||
parity_sum ^= parity;
|
||||
frame += dec2bcd((t.tm_wday == 0) ? 7 : t.tm_wday, 42, 44, &parity);
|
||||
parity_sum += parity;
|
||||
parity_sum ^= parity;
|
||||
frame += dec2bcd(t.tm_mon + 1, 45, 49, &parity);
|
||||
parity_sum += parity;
|
||||
parity_sum ^= parity;
|
||||
frame += dec2bcd(t.tm_year + 1900 - 2000, 50, 57, &parity);
|
||||
parity_sum += parity;
|
||||
frame += parity_sum % 2 ? set_dcfbit(58) : 0;
|
||||
parity_sum ^= parity;
|
||||
frame += parity_sum ? set_dcfbit(58) : 0;
|
||||
|
||||
return frame;
|
||||
|
||||
|
||||
+24
-29
@@ -174,16 +174,10 @@ void dp_init(bool verbose) {
|
||||
|
||||
void dp_refresh(bool nextPage) {
|
||||
|
||||
// update counter values from libpax
|
||||
libpax_counter_count(&count_from_libpax);
|
||||
|
||||
#ifndef HAS_BUTTON
|
||||
static uint32_t framecounter = 0;
|
||||
#endif
|
||||
|
||||
// update histogram
|
||||
dp_plotCurve(count_from_libpax.pax, false);
|
||||
|
||||
// if display is switched off we don't refresh it to relax cpu
|
||||
if (!DisplayIsOn && (DisplayIsOn == cfg.screenon))
|
||||
return;
|
||||
@@ -207,7 +201,6 @@ void dp_refresh(bool nextPage) {
|
||||
#endif
|
||||
|
||||
dp_drawPage(nextPage);
|
||||
dp_dump(displaybuf);
|
||||
|
||||
I2C_MUTEX_UNLOCK(); // release i2c bus access
|
||||
|
||||
@@ -219,6 +212,7 @@ void dp_drawPage(bool nextpage) {
|
||||
// write display content to display buffer
|
||||
// nextpage = true -> flip 1 page
|
||||
|
||||
struct count_payload_t count; // libpax count storage
|
||||
static uint8_t DisplayPage = 0;
|
||||
char timeState, strftime_buf[64];
|
||||
time_t now;
|
||||
@@ -233,6 +227,9 @@ void dp_drawPage(bool nextpage) {
|
||||
dp_clear();
|
||||
}
|
||||
|
||||
// update counter values from libpax
|
||||
libpax_counter_count(&count);
|
||||
|
||||
// cursor home
|
||||
dp_setTextCursor(0, 0);
|
||||
|
||||
@@ -240,7 +237,7 @@ void dp_drawPage(bool nextpage) {
|
||||
// display number of unique macs total Wifi + BLE
|
||||
if (DisplayPage < 5) {
|
||||
dp_setFont(MY_FONT_STRETCHED);
|
||||
dp_printf("%-5d", count_from_libpax.pax);
|
||||
dp_printf("%-5d", count.pax);
|
||||
}
|
||||
|
||||
switch (DisplayPage) {
|
||||
@@ -264,32 +261,22 @@ void dp_drawPage(bool nextpage) {
|
||||
|
||||
#if ((WIFICOUNTER) && (BLECOUNTER))
|
||||
if (cfg.wifiscan)
|
||||
dp_printf("WIFI:%-5d", count_from_libpax.wifi_count);
|
||||
dp_printf("WIFI:%-5d", count.wifi_count);
|
||||
else
|
||||
dp_printf("WIFI:off");
|
||||
if (cfg.blescan)
|
||||
#if (COUNT_ENS)
|
||||
if (cfg.enscount)
|
||||
dp_printf(" CWA:%-5d", cwa_report());
|
||||
else
|
||||
#endif
|
||||
dp_printf("BLTH:%-5d", count_from_libpax.ble_count);
|
||||
dp_printf("BLTH:%-5d", count.ble_count);
|
||||
else
|
||||
dp_printf(" BLTH:off");
|
||||
#elif ((WIFICOUNTER) && (!BLECOUNTER))
|
||||
if (cfg.wifiscan)
|
||||
dp_printf("WIFI:%-5d", count_from_libpax.wifi_count);
|
||||
dp_printf("WIFI:%-5d", count.wifi_count);
|
||||
else
|
||||
dp_printf("WIFI:off");
|
||||
#elif ((!WIFICOUNTER) && (BLECOUNTER))
|
||||
if (cfg.blescan)
|
||||
dp_printf("BLTH:%-5d", count_from_libpax.ble_count);
|
||||
#if (COUNT_ENS)
|
||||
if (cfg.enscount)
|
||||
dp_printf("(CWA:%d)", cwa_report());
|
||||
else
|
||||
#endif
|
||||
dp_printf("BLTH:off");
|
||||
dp_printf("BLTH:%-5d", count.ble_count);
|
||||
dp_printf("BLTH:off");
|
||||
#else
|
||||
dp_printf("Sniffer disabled");
|
||||
#endif
|
||||
@@ -327,7 +314,6 @@ void dp_drawPage(bool nextpage) {
|
||||
|
||||
#if (TIME_SYNC_INTERVAL)
|
||||
timeState = TimePulseTick ? ' ' : timeSetSymbols[timeSource];
|
||||
TimePulseTick = false;
|
||||
time(&now);
|
||||
localtime_r(&now, &timeinfo);
|
||||
strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo);
|
||||
@@ -355,6 +341,8 @@ void dp_drawPage(bool nextpage) {
|
||||
dp_printf("%-4s", getSfName(updr2rps(LMIC.datarate)));
|
||||
dp_setFont(MY_FONT_SMALL, 0);
|
||||
#endif // HAS_LORA
|
||||
|
||||
dp_dump(displaybuf);
|
||||
break;
|
||||
|
||||
// ---------- page 1: lorawan parameters ----------
|
||||
@@ -380,6 +368,8 @@ void dp_drawPage(bool nextpage) {
|
||||
LMIC.seqnoDn ? LMIC.seqnoDn - 1 : 0);
|
||||
dp_println();
|
||||
dp_printf("SNR:%-5d RSSI:%-5d", (LMIC.snr + 2) / 4, LMIC.rssi);
|
||||
|
||||
dp_dump(displaybuf);
|
||||
break;
|
||||
#else // flip page if we are unattended
|
||||
DisplayPage++;
|
||||
@@ -412,6 +402,8 @@ void dp_drawPage(bool nextpage) {
|
||||
dp_printf("No fix");
|
||||
wasnofix = true;
|
||||
}
|
||||
|
||||
dp_dump(displaybuf);
|
||||
break;
|
||||
#else // flip page if we are unattended
|
||||
DisplayPage++;
|
||||
@@ -435,10 +427,12 @@ void dp_drawPage(bool nextpage) {
|
||||
#ifdef HAS_BME680
|
||||
// line 6-7: IAQ
|
||||
dp_printf("IAQ:%-3.0f", bme_status.iaq);
|
||||
#else // is BME280 or BMP180
|
||||
#else // is BME280 or BMP180
|
||||
// line 6-7: Pre
|
||||
dp_printf("PRE:%-2.1f", bme_status.pressure);
|
||||
#endif // HAS_BME680
|
||||
#endif // HAS_BME680
|
||||
|
||||
dp_dump(displaybuf);
|
||||
break; // page 3
|
||||
#else // flip page if we are unattended
|
||||
DisplayPage++;
|
||||
@@ -453,14 +447,15 @@ void dp_drawPage(bool nextpage) {
|
||||
localtime_r(&now, &timeinfo);
|
||||
strftime(strftime_buf, sizeof(strftime_buf), "%T", &timeinfo);
|
||||
dp_printf("%.8s", strftime_buf);
|
||||
|
||||
dp_dump(displaybuf);
|
||||
break;
|
||||
|
||||
// ---------- page 5: pax graph ----------
|
||||
case 5:
|
||||
|
||||
dp_setFont(MY_FONT_NORMAL);
|
||||
dp_setTextCursor(0, 0);
|
||||
dp_printf("Pax graph");
|
||||
// update histogram
|
||||
dp_plotCurve(count.pax, false);
|
||||
dp_dump(plotbuf);
|
||||
break;
|
||||
|
||||
|
||||
+161
-108
@@ -6,68 +6,147 @@
|
||||
// Local logging tag
|
||||
static const char TAG[] = __FILE__;
|
||||
|
||||
// we use NMEA ZDA sentence field 1 for time synchronization
|
||||
// ZDA gives time for preceding pps pulse
|
||||
// downsight is that it does not have a constant offset
|
||||
// thus precision is only +/- 1 second
|
||||
|
||||
TinyGPSPlus gps;
|
||||
TinyGPSCustom gpstime(gps, "GPZDA", 1); // field 1 = UTC time (hhmmss.ss)
|
||||
TinyGPSCustom gpsday(gps, "GPZDA", 2); // field 2 = day (01..31)
|
||||
TinyGPSCustom gpsmonth(gps, "GPZDA", 3); // field 3 = month (01..12)
|
||||
TinyGPSCustom gpsyear(gps, "GPZDA", 4); // field 4 = year (4-digit)
|
||||
static const String ZDA_Request = "$EIGPQ,ZDA*39\r\n";
|
||||
TaskHandle_t GpsTask;
|
||||
|
||||
#ifdef GPS_SERIAL
|
||||
HardwareSerial GPS_Serial(1); // use UART #1
|
||||
static uint16_t nmea_txDelay_ms =
|
||||
(tx_Ticks(NMEA_FRAME_SIZE, GPS_SERIAL) / portTICK_PERIOD_MS);
|
||||
#else
|
||||
static uint16_t nmea_txDelay_ms = 0;
|
||||
#endif
|
||||
|
||||
// Ublox UBX packet data
|
||||
|
||||
// UBX CFG-PRT packet
|
||||
byte CFG_PRT[] = {
|
||||
0xB5, // sync char 1
|
||||
0x62, // sync char 2
|
||||
0x06, // class
|
||||
0x00, // id
|
||||
0x14, // length
|
||||
0x00, // .
|
||||
0x01, // portID (UART 1)
|
||||
0x00, // reserved
|
||||
0x00, // txReady
|
||||
0x00, // .
|
||||
0b11010000, // UART mode: 8N1
|
||||
0b00001000, // .
|
||||
0x00, // .
|
||||
0x00, // .
|
||||
(byte)GPS_BAUDRATE, // baudrate
|
||||
(byte)(GPS_BAUDRATE >> 8), // .
|
||||
(byte)(GPS_BAUDRATE >> 16), // .
|
||||
(byte)(GPS_BAUDRATE >> 24), // .
|
||||
0b00000011, // input protocols: NMEA + UBX
|
||||
0b00000000, // .
|
||||
0b00000010, // output protocols: NMEA
|
||||
0x00000000, // .
|
||||
0x00, // reserved
|
||||
0x00, // .
|
||||
0x00, // .
|
||||
0x00 // .
|
||||
};
|
||||
|
||||
// Array of two bytes for CFG-MSG packets payload.
|
||||
byte CFG_MSG_CID[][2] = {{0xF0, 0x01}, {0xF0, 0x02}, {0xF0, 0x03}, {0xF0, 0x05},
|
||||
{0xF0, 0x06}, {0xF0, 0x07}, {0xF0, 0x08}, {0xF0, 0x09},
|
||||
{0xF0, 0x0A}, {0xF0, 0x0E}, {0xF1, 0x00}, {0xF1, 0x03},
|
||||
{0xF1, 0x04}, {0xF1, 0x05}, {0xF1, 0x06}};
|
||||
|
||||
// UBX CFG-MSG packet
|
||||
byte CFG_MSG[] = {
|
||||
0xB5, // sync char 1
|
||||
0x62, // sync char 2
|
||||
0x06, // class
|
||||
0x01, // id
|
||||
0x03, // length
|
||||
0x00, // .
|
||||
0x00, // payload (first byte from messages array element)
|
||||
0x00, // payload (second byte from messages array element)
|
||||
0x00 // payload (zero to disable message)
|
||||
};
|
||||
|
||||
// UBX CFG-CFG packet
|
||||
byte CFG_CFG[] = {
|
||||
0xB5, // sync char 1
|
||||
0x62, // sync char 2
|
||||
0x06, // class
|
||||
0x09, // id
|
||||
0x0D, // length
|
||||
0x00, // .
|
||||
0b00011111, // clearmask
|
||||
0b00000110, // .
|
||||
0x00, // .
|
||||
0x00, // .
|
||||
0x00, // savemask
|
||||
0x00, // .
|
||||
0x00, // .
|
||||
0x00, // .
|
||||
0b00011111, // loadmask
|
||||
0b00000110, // .
|
||||
0x00, // .
|
||||
0x00, // .
|
||||
0b00010001 // devicemask
|
||||
};
|
||||
|
||||
// helper functions to send UBX commands to ublox gps chip
|
||||
|
||||
void sendPacket(byte *packet, byte len) {
|
||||
|
||||
uint8_t CK_A = 0;
|
||||
uint8_t CK_B = 0;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
GPS_Serial.write(packet[i]);
|
||||
|
||||
// calculate and send Fletcher checksum
|
||||
for (int i = 2; i < len; i++) {
|
||||
CK_A += packet[i];
|
||||
CK_B += CK_A;
|
||||
}
|
||||
GPS_Serial.write(CK_A);
|
||||
GPS_Serial.write(CK_B);
|
||||
}
|
||||
|
||||
void restoreDefaults() { sendPacket(CFG_CFG, sizeof(CFG_CFG)); }
|
||||
void changeBaudrate() { sendPacket(CFG_PRT, sizeof(CFG_PRT)); }
|
||||
|
||||
void disableNmea() {
|
||||
|
||||
// tinygps++ processes only $GPGGA/$GNGGA and $GPRMC/$GNRMC
|
||||
// thus, we disable all other NMEA messages
|
||||
|
||||
byte packetSize = sizeof(CFG_MSG);
|
||||
|
||||
// Offset to the place where payload starts.
|
||||
byte payloadOffset = 6;
|
||||
|
||||
// Iterate over the messages array.
|
||||
for (byte i = 0; i < sizeof(CFG_MSG_CID) / sizeof(*CFG_MSG_CID); i++) {
|
||||
// Copy two bytes of payload to the packet buffer.
|
||||
for (byte j = 0; j < sizeof(*CFG_MSG_CID); j++) {
|
||||
CFG_MSG[payloadOffset + j] = CFG_MSG_CID[i][j];
|
||||
}
|
||||
sendPacket(CFG_MSG, packetSize);
|
||||
}
|
||||
}
|
||||
|
||||
// initialize and configure GPS
|
||||
int gps_init(void) {
|
||||
|
||||
if (!gps_config()) {
|
||||
ESP_LOGE(TAG, "GPS chip initializiation error");
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef GPS_SERIAL
|
||||
ESP_LOGI(TAG, "Opening serial GPS");
|
||||
|
||||
GPS_Serial.begin(GPS_SERIAL);
|
||||
#elif defined GPS_I2C
|
||||
ESP_LOGI(TAG, "Opening I2C GPS");
|
||||
Wire.begin(GPS_I2C, 400000); // I2C connect to GPS device with 400 KHz
|
||||
Wire.beginTransmission(GPS_ADDR);
|
||||
Wire.write(0x00); // dummy write
|
||||
if (Wire.endTransmission()) {
|
||||
ESP_LOGE(TAG, "Quectel L76 GPS chip not found");
|
||||
return 0;
|
||||
} else
|
||||
ESP_LOGI(TAG, "Quectel L76 GPS chip found");
|
||||
#endif
|
||||
|
||||
restoreDefaults();
|
||||
delay(100);
|
||||
|
||||
changeBaudrate();
|
||||
delay(100);
|
||||
GPS_Serial.flush();
|
||||
GPS_Serial.updateBaudRate(GPS_BAUDRATE);
|
||||
|
||||
disableNmea();
|
||||
|
||||
return 1;
|
||||
|
||||
} // gps_init()
|
||||
|
||||
// detect gps chipset type and configure it with device specific settings
|
||||
int gps_config() {
|
||||
int rslt = 1; // success
|
||||
#if defined GPS_SERIAL
|
||||
|
||||
/* insert user configuration here, if needed */
|
||||
|
||||
#elif defined GPS_I2C
|
||||
|
||||
/* insert user configuration here, if needed */
|
||||
|
||||
#endif
|
||||
return rslt;
|
||||
}
|
||||
|
||||
// store current GPS location data in struct
|
||||
void gps_storelocation(gpsStatus_t *gps_store) {
|
||||
if (gps.location.isUpdated() && gps.location.isValid() &&
|
||||
@@ -90,46 +169,45 @@ bool gps_hasfix() {
|
||||
}
|
||||
|
||||
// function to poll UTC time from GPS NMEA data; note: this is costly
|
||||
time_t get_gpstime(uint16_t *msec) {
|
||||
time_t get_gpstime(uint16_t *msec = 0) {
|
||||
|
||||
// poll NMEA ZDA sentence
|
||||
#ifdef GPS_SERIAL
|
||||
GPS_Serial.print(ZDA_Request);
|
||||
// wait for gps NMEA answer
|
||||
// vTaskDelay(tx_Ticks(NMEA_FRAME_SIZE, GPS_SERIAL));
|
||||
#elif defined GPS_I2C
|
||||
Wire.print(ZDA_Request);
|
||||
#endif
|
||||
const uint16_t txDelay =
|
||||
70U * 1000 / (GPS_BAUDRATE / 9); // serial tx of 70 NMEA chars
|
||||
|
||||
// did we get a current date & time?
|
||||
if (gpstime.isValid()) {
|
||||
if (gps.time.age() < 1000) {
|
||||
|
||||
uint32_t delay_ms =
|
||||
gpstime.age() + nmea_txDelay_ms + NMEA_COMPENSATION_FACTOR;
|
||||
uint32_t zdatime = atof(gpstime.value());
|
||||
|
||||
// convert UTC time from gps NMEA ZDA sentence to tm format
|
||||
// convert tinygps time format to struct tm format
|
||||
struct tm gps_tm = {0};
|
||||
gps_tm.tm_sec = zdatime % 100; // second (UTC)
|
||||
gps_tm.tm_min = (zdatime / 100) % 100; // minute (UTC)
|
||||
gps_tm.tm_hour = zdatime / 10000; // hour (UTC)
|
||||
gps_tm.tm_mday = atoi(gpsday.value()); // day, 01 to 31
|
||||
gps_tm.tm_mon = atoi(gpsmonth.value()) - 1; // month, 01 to 12
|
||||
gps_tm.tm_year = atoi(gpsyear.value()) - 1900; // year, YYYY
|
||||
|
||||
gps_tm.tm_sec = gps.time.second();
|
||||
gps_tm.tm_min = gps.time.minute();
|
||||
gps_tm.tm_hour = gps.time.hour();
|
||||
gps_tm.tm_mday = gps.date.day();
|
||||
gps_tm.tm_mon = gps.date.month() - 1; // 1-12 -> 0-11
|
||||
gps_tm.tm_year = gps.date.year() - 1900; // 2000+ -> years since 1900
|
||||
|
||||
// convert UTC tm to time_t epoch
|
||||
gps_tm.tm_isdst = 0; // UTC has no DST
|
||||
gps_tm.tm_isdst = 0; // UTC has no DST
|
||||
time_t t = mkgmtime(&gps_tm);
|
||||
|
||||
// add protocol delay with millisecond precision
|
||||
t += (time_t)(delay_ms / 1000);
|
||||
*msec = delay_ms % 1000; // fractional seconds
|
||||
#ifdef GPS_INT
|
||||
// if we have a recent GPS PPS pulse, sync on top of next second
|
||||
uint16_t ppsDiff = millis() - lastPPS;
|
||||
if (ppsDiff < 1000)
|
||||
*msec = ppsDiff;
|
||||
else {
|
||||
ESP_LOGD(TAG, "no PPS from GPS");
|
||||
return 0;
|
||||
}
|
||||
#else
|
||||
// best guess for sync on top of next second
|
||||
*msec = gps.time.centisecond() * 10U + txDelay;
|
||||
#endif
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "no valid GPS time");
|
||||
|
||||
return 0;
|
||||
|
||||
} // get_gpstime()
|
||||
@@ -139,42 +217,17 @@ void gps_loop(void *pvParameters) {
|
||||
|
||||
_ASSERT((uint32_t)pvParameters == 1); // FreeRTOS check
|
||||
|
||||
// feed GPS decoder with serial NMEA data from GPS device
|
||||
while (1) {
|
||||
while (cfg.payloadmask & GPS_DATA) {
|
||||
|
||||
if (cfg.payloadmask & GPS_DATA) {
|
||||
#ifdef GPS_SERIAL
|
||||
// feed GPS decoder with serial NMEA data from GPS device
|
||||
while (GPS_Serial.available()) {
|
||||
while (GPS_Serial.available())
|
||||
gps.encode(GPS_Serial.read());
|
||||
yield();
|
||||
}
|
||||
#elif defined GPS_I2C
|
||||
Wire.requestFrom(GPS_ADDR, 32); // caution: this is a blocking call
|
||||
while (Wire.available()) {
|
||||
gps.encode(Wire.read());
|
||||
delay(2); // 2ms delay according L76 datasheet
|
||||
yield();
|
||||
}
|
||||
#endif
|
||||
|
||||
// (only) while device time is not set or unsynched, and we have a valid
|
||||
// GPS time, we trigger a device time update to poll time from GPS
|
||||
if ((timeSource == _unsynced || timeSource == _set) &&
|
||||
(gpstime.isUpdated() && gpstime.isValid() && gpstime.age() < 1000)) {
|
||||
calibrateTime();
|
||||
}
|
||||
|
||||
} // if
|
||||
|
||||
// show NMEA data in verbose mode, useful only for debugging GPS, very
|
||||
// noisy ESP_LOGV(TAG, "GPS NMEA data: passed %u / failed: %u / with fix:
|
||||
// %u",
|
||||
// gps.passedChecksum(), gps.failedChecksum(),
|
||||
// gps.sentencesWithFix());
|
||||
|
||||
yield(); // yield to CPU
|
||||
|
||||
} // end of infinite loop
|
||||
delay(5);
|
||||
}
|
||||
delay(1000);
|
||||
} // infinite while loop
|
||||
|
||||
} // gps_loop()
|
||||
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
#define CFG_sx1276_radio 1
|
||||
|
||||
#define HAS_DISPLAY 1 // OLED-Display on board
|
||||
#define HAS_LED LED_BUILTIN // white LED on board
|
||||
#define HAS_LED LED_BUILTIN // white LED on board (set to NOT_A_PIN to disable)
|
||||
#define HAS_BUTTON KEY_BUILTIN // button "PROG" on board
|
||||
|
||||
// Pins for I2C interface of OLED Display
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@
|
||||
#define CFG_sx1276_radio 1
|
||||
|
||||
#define HAS_DISPLAY 1 // OLED-Display on board
|
||||
#define HAS_LED LED_BUILTIN // white LED on board
|
||||
#define HAS_LED LED_BUILTIN // white LED on board (set to NOT_A_PIN to disable)
|
||||
#define HAS_BUTTON KEY_BUILTIN // button "PROG" on board
|
||||
|
||||
#define BAT_MEASURE_ADC ADC2_GPIO13_CHANNEL // battery probe GPIO pin
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@
|
||||
#define CFG_sx1276_radio 1
|
||||
|
||||
#define HAS_DISPLAY 1 // OLED-Display on board
|
||||
#define HAS_LED LED_BUILTIN // white LED on board
|
||||
#define HAS_LED LED_BUILTIN // white LED on board (set to NOT_A_PIN to disable)
|
||||
#define HAS_BUTTON KEY_BUILTIN // button "PROG" on board
|
||||
|
||||
#define BAT_MEASURE_ADC ADC1_GPIO37_CHANNEL // battery probe GPIO pin
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
// Hardware related definitions for lolin32lite (without LoRa shield)
|
||||
|
||||
#define HAS_LED LED_BUILTIN // on board LED on GPIO5
|
||||
#define LED_ACTIVE_LOW 1 // Onboard LED is active when pin is LOW
|
||||
#define LED_ACTIVE_LOW 1 // Onboard LED is active when pin is LOW (set to NOT_A_PIN to disable)
|
||||
|
||||
#define MY_SDA SDA
|
||||
#define MY_SCL SCL
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
#define HAS_DISPLAY 1 // OLED-Display on board
|
||||
//#define MY_DISPLAY_FLIP 1 // uncomment this for rotated display
|
||||
#define HAS_LED 22 // ESP32 GPIO12 (pin22) On Board LED
|
||||
#define HAS_LED 22 // ESP32 GPIO12 (pin22) On Board LED (set to NOT_A_PIN to disable)
|
||||
#define LED_ACTIVE_LOW 1 // Onboard LED is active when pin is LOW
|
||||
#define HAS_RGB_LED SmartLed rgb_led(LED_WS2812, 1, GPIO_NUM_13) // ESP32 GPIO13 (pin13) On Board Shield WS2812B RGB LED
|
||||
#define HAS_BUTTON 15 // ESP32 GPIO15 (pin15) Button is on the LoraNode32 shield
|
||||
|
||||
@@ -20,11 +20,6 @@
|
||||
#define HAS_ANTENNA_SWITCH (16) // pin for switching wifi antenna
|
||||
#define WIFI_ANTENNA 0 // 0 = internal, 1 = external
|
||||
|
||||
// uncomment this only if your LoPy runs on a PYTRACK BOARD
|
||||
#define HAS_GPS 1
|
||||
#define GPS_I2C GPIO_NUM_25, GPIO_NUM_26 // SDA (P22), SCL (P21)
|
||||
#define GPS_ADDR 0x10
|
||||
|
||||
// uncomment this only if your LoPy runs on a EXPANSION BOARD
|
||||
//#define HAS_LED (12) // use if LoPy is on Expansion Board, this has a user LED
|
||||
//#define LED_ACTIVE_LOW 1 // use if LoPy is on Expansion Board, this has a user LED
|
||||
|
||||
@@ -30,11 +30,6 @@
|
||||
#define HAS_ANTENNA_SWITCH (21) // pin for switching wifi antenna (P12)
|
||||
#define WIFI_ANTENNA 0 // 0 = internal, 1 = external
|
||||
|
||||
// uncomment defines in this section ONLY if your LoPy lives on a PYTRACK BOARD
|
||||
//#define HAS_GPS 1
|
||||
//#define GPS_I2C GPIO_NUM_25, GPIO_NUM_26 // SDA (P22), SCL (P21)
|
||||
//#define GPS_ADDR 0x10
|
||||
|
||||
// uncomment defines in this section ONLY if your LoPy lives on a EXPANSION BOARD
|
||||
//#define HAS_LED (12) // use if LoPy is on Expansion Board, this has a user LED
|
||||
//#define LED_ACTIVE_LOW 1 // use if LoPy is on Expansion Board, this has a user LED
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
|
||||
#define HAS_DISPLAY 1 // OLED-Display on board
|
||||
//#define MY_DISPLAY_FLIP 1 // uncomment this for rotated display
|
||||
#define HAS_LED LED_BUILTIN
|
||||
#define HAS_LED LED_BUILTIN // set to NOT_A_PIN to disable
|
||||
#define LED_ACTIVE_LOW 1 // Onboard LED is active when pin is LOW
|
||||
#define HAS_BUTTON KEY_BUILTIN
|
||||
|
||||
|
||||
@@ -16,10 +16,12 @@
|
||||
#define CFG_sx1276_radio 1 // HPD13A LoRa SoC
|
||||
|
||||
// enable only if you want to store a local paxcount table on the device
|
||||
#define HAS_SDCARD 2 // // this board has a SDMMC card-reader/writer
|
||||
#define HAS_SDCARD 2 // this board has a SDMMC card-reader/writer
|
||||
|
||||
#define HAS_DISPLAY 1
|
||||
#define HAS_LED (25) // green on board LED
|
||||
//#define HAS_LED NOT_A_PIN
|
||||
|
||||
#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
|
||||
|
||||
|
||||
+7
-6
@@ -86,18 +86,19 @@ static const char TAG[] = __FILE__;
|
||||
|
||||
String IF482_Frame(time_t t) {
|
||||
|
||||
char mon, out[IF482_FRAME_SIZE + 1];
|
||||
char mon, out[IF482_FRAME_SIZE + 1], buf[IF482_FRAME_SIZE - 3];
|
||||
|
||||
if (sntp_get_sync_status() == SNTP_SYNC_STATUS_IN_PROGRESS)
|
||||
mon = 'M'; // time had been set but sync not completed
|
||||
else
|
||||
mon = 'A'; // time has been set and was recently synced
|
||||
|
||||
// generate IF482 telegram
|
||||
// snprintf(out, sizeof(out), "O%cL%s\r", mon, myTZ.dateTime(t, UTC_TIME,
|
||||
// "ymdwHis").c_str());
|
||||
|
||||
// ESP_LOGD(TAG, "[%s] IF482 date/time: %s", ctime(time(NULL), out);
|
||||
// generate IF482 telegram for local time
|
||||
struct tm tt;
|
||||
localtime_r(&t, &tt);
|
||||
mktime(&tt);
|
||||
strftime(buf, sizeof(buf), "%y%m%d%u%H%M%S", &tt);
|
||||
snprintf(out, sizeof(out), "O%cL%s\r", mon, buf);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
+1
-1
@@ -212,7 +212,7 @@ void ledLoop(void *parameter) {
|
||||
previousLEDState = LEDState;
|
||||
}
|
||||
// give yield to CPU
|
||||
delay(2);
|
||||
delay(5);
|
||||
} // while(1)
|
||||
}; // ledloop()
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ uint8_t MatrixDisplayIsOn = 0;
|
||||
static uint8_t displaybuf[LED_MATRIX_WIDTH * LED_MATRIX_HEIGHT / 8] = {0};
|
||||
static unsigned long ulLastNumMacs = 0;
|
||||
static time_t ulLastTime = time(NULL);
|
||||
static struct count_payload_t count; // libpax count storage
|
||||
|
||||
hw_timer_t *matrixDisplayIRQ = NULL;
|
||||
|
||||
@@ -76,12 +77,12 @@ void refreshTheMatrixDisplay(bool nextPage) {
|
||||
case 0:
|
||||
|
||||
// update counter values from libpax
|
||||
libpax_counter_count(&count_from_libpax);
|
||||
libpax_counter_count(&count);
|
||||
|
||||
if (cfg.countermode == 1) {
|
||||
// cumulative counter mode -> display total number of pax
|
||||
if (ulLastNumMacs != count_from_libpax.pax) {
|
||||
ulLastNumMacs = count_from_libpax.pax;
|
||||
if (ulLastNumMacs != count.pax) {
|
||||
ulLastNumMacs = count.pax;
|
||||
matrix.clear();
|
||||
DrawNumber(String(ulLastNumMacs));
|
||||
}
|
||||
@@ -89,10 +90,10 @@ void refreshTheMatrixDisplay(bool nextPage) {
|
||||
|
||||
else { // cyclic counter mode -> plot a line diagram
|
||||
|
||||
if (ulLastNumMacs != count_from_libpax.pax) {
|
||||
if (ulLastNumMacs != count.pax) {
|
||||
|
||||
// next count cycle?
|
||||
if (count_from_libpax.pax == 0) {
|
||||
if (count.pax == 0) {
|
||||
|
||||
// matrix full? then scroll left 1 dot, else increment column
|
||||
if (col < (LED_MATRIX_WIDTH - 1))
|
||||
@@ -104,7 +105,7 @@ void refreshTheMatrixDisplay(bool nextPage) {
|
||||
matrix.drawPoint(col, row, 0); // clear current dot
|
||||
|
||||
// scale and set new dot
|
||||
ulLastNumMacs = count_from_libpax.pax;
|
||||
ulLastNumMacs = count.pax;
|
||||
level = ulLastNumMacs / LINE_DIAGRAM_DIVIDER;
|
||||
row = level <= LED_MATRIX_HEIGHT
|
||||
? LED_MATRIX_HEIGHT - 1 - level % LED_MATRIX_HEIGHT
|
||||
@@ -120,7 +121,7 @@ void refreshTheMatrixDisplay(bool nextPage) {
|
||||
if (ulLastTime != t) {
|
||||
ulLastTime = t;
|
||||
matrix.clear();
|
||||
//DrawNumber(myTZ.dateTime("H:i:s").c_str());
|
||||
// DrawNumber(myTZ.dateTime("H:i:s").c_str());
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
+1
-10
@@ -5,18 +5,9 @@ static const char TAG[] = __FILE__;
|
||||
|
||||
// libpax payload
|
||||
struct count_payload_t count_from_libpax;
|
||||
uint16_t volatile libpax_macs_ble, libpax_macs_wifi;
|
||||
|
||||
void process_count(void) {
|
||||
ESP_LOGD(TAG, "pax: %d / %d / %d", count_from_libpax.pax,
|
||||
count_from_libpax.wifi_count, count_from_libpax.ble_count);
|
||||
libpax_macs_ble = count_from_libpax.ble_count;
|
||||
libpax_macs_wifi = count_from_libpax.wifi_count;
|
||||
setSendIRQ();
|
||||
}
|
||||
|
||||
void init_libpax(void) {
|
||||
libpax_counter_init(process_count, &count_from_libpax, cfg.sendcycle * 2,
|
||||
libpax_counter_init(setSendIRQ, &count_from_libpax, cfg.sendcycle * 2,
|
||||
cfg.countermode);
|
||||
libpax_counter_start();
|
||||
}
|
||||
@@ -90,12 +90,7 @@ function decodeUplink(input) {
|
||||
data = decode(input.bytes, [uint32, uint8], ['time', 'timestatus']);
|
||||
}
|
||||
}
|
||||
|
||||
if (input.fPort === 10) {
|
||||
// ENS count
|
||||
data = decode(input.bytes, [uint16], ['ens']);
|
||||
}
|
||||
|
||||
|
||||
data.bytes = input.bytes; // comment out if you do not want to include the original payload
|
||||
data.port = input.fPort; // comment out if you do not want to inlude the port
|
||||
|
||||
|
||||
+3
-3
@@ -179,10 +179,10 @@ void showLoraKeys(void) {
|
||||
// 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_getDevEui((u1_t *)buf);
|
||||
printKey("DevEUI", buf, 8, true);
|
||||
os_getDevKey((u1_t *)buf);
|
||||
printKey("AppKey", buf, 16, false);
|
||||
}
|
||||
@@ -314,7 +314,7 @@ esp_err_t lmic_init(void) {
|
||||
"lorasendtask", // name of task
|
||||
3072, // stack size of task
|
||||
(void *)1, // parameter of the task
|
||||
1, // priority of the task
|
||||
2, // priority of the task
|
||||
&lorasendTask, // task handle
|
||||
1); // CPU core
|
||||
|
||||
|
||||
+39
-55
@@ -25,62 +25,54 @@ licenses. Refer to LICENSE.txt file in repository for more details.
|
||||
|
||||
// Tasks and timers:
|
||||
|
||||
Task Core Prio Purpose
|
||||
Task Core Prio Purpose
|
||||
-------------------------------------------------------------------------------
|
||||
ledloop 0 3 blinks LEDs
|
||||
spiloop 0 2 reads/writes data on spi interface
|
||||
IDLE 0 0 ESP32 arduino scheduler -> runs wifi sniffer
|
||||
ledloop* 1 1 blinks LEDs
|
||||
spiloop# 0 2 reads/writes data on spi interface
|
||||
lmictask* 1 2 MCCI LMiC LORAWAN stack
|
||||
clockloop# 1 6 generates realtime telegrams for external clock
|
||||
mqttloop# 1 5 reads/writes data on ETH interface
|
||||
timesync_proc# 1 7 processes realtime time sync requests
|
||||
irqhandler# 1 4 application IRQ (i.e. displayrefresh)
|
||||
gpsloop* 1 1 reads data from GPS via serial or i2c
|
||||
lorasendtask# 1 2 feeds data from lora sendqueue to lmcic
|
||||
rmcd_process# 1 1 Remote command interpreter loop
|
||||
|
||||
lmictask 1 2 MCCI LMiC LORAWAN stack
|
||||
clockloop 1 4 generates realtime telegrams for external clock
|
||||
mqttloop 1 2 reads/writes data on ETH interface
|
||||
timesync_proc 1 3 processes realtime time sync requests
|
||||
irqhandler 1 2 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
|
||||
rmcd_process 1 1 Remote command interpreter loop
|
||||
IDLE 1 0 ESP32 arduino scheduler -> runs wifi channel rotator
|
||||
* spinning task, always ready
|
||||
# blocked/waiting task
|
||||
|
||||
Low priority numbers denote low priority tasks.
|
||||
|
||||
NOTE: Changing any timings will have impact on time accuracy of whole code.
|
||||
So don't do it if you do not own a digital oscilloscope.
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
// ESP32 hardware timers
|
||||
-------------------------------------------------------------------------------
|
||||
0 displayIRQ -> display refresh -> 40ms (DISPLAYREFRESH_MS)
|
||||
1 ppsIRQ -> pps clock irq -> 1sec
|
||||
2 (unused)
|
||||
3 MatrixDisplayIRQ -> matrix mux cycle -> 0,5ms (MATRIX_DISPLAY_SCAN_US)
|
||||
|
||||
|
||||
// Interrupt routines
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
irqHandlerTask (Core 1), see irqhandler.cpp
|
||||
|
||||
fired by hardware
|
||||
DisplayIRQ -> esp32 timer 0
|
||||
CLOCKIRQ -> esp32 timer 1 or external GPIO (RTC_INT or GPS_INT)
|
||||
MatrixDisplayIRQ-> esp32 timer 3
|
||||
ButtonIRQ -> external GPIO
|
||||
PMUIRQ -> PMU chip GPIO
|
||||
|
||||
fired by software
|
||||
TIMESYNC_IRQ -> setTimeSyncIRQ() -> Ticker.h
|
||||
CYCLIC_IRQ -> setCyclicIRQ() -> Ticker.h
|
||||
SENDCYCLE_IRQ -> setSendIRQ() -> xTimer
|
||||
BME_IRQ -> setBMEIRQ() -> Ticker.h
|
||||
|
||||
ClockTask (Core 1), see timekeeper.cpp
|
||||
|
||||
fired by hardware
|
||||
CLOCKIRQ -> esp32 timer 1
|
||||
|
||||
|
||||
// External RTC timer (if present)
|
||||
-------------------------------------------------------------------------------
|
||||
triggers pps 1 sec impulse
|
||||
|
||||
|
||||
// Interrupt routines
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
ISRs fired by CPU or GPIO:
|
||||
DisplayIRQ <- esp32 timer 0
|
||||
CLOCKIRQ <- esp32 timer 1 or GPIO (RTC_INT)
|
||||
MatrixDisplayIRQ<- esp32 timer 3
|
||||
ButtonIRQ <- GPIO <- Button
|
||||
PMUIRQ <- GPIO <- PMU chip
|
||||
|
||||
Application IRQs fired by software:
|
||||
TIMESYNC_IRQ <- setTimeSyncIRQ() <- Ticker.h
|
||||
CYCLIC_IRQ <- setCyclicIRQ() <- Ticker.h
|
||||
SENDCYCLE_IRQ <- setSendIRQ() <- xTimer or libpax callback
|
||||
BME_IRQ <- setBMEIRQ() <- Ticker.h
|
||||
|
||||
*/
|
||||
|
||||
// Basic Config
|
||||
@@ -126,7 +118,7 @@ void setup() {
|
||||
snprintf(clientId, 20, "paxcounter_%08x", hashedmac);
|
||||
ESP_LOGI(TAG, "Starting %s v%s (runmode=%d / restarts=%d)", clientId,
|
||||
PROGVERSION, RTC_runmode, RTC_restarts);
|
||||
ESP_LOGI(TAG, "code build date: %d", _COMPILETIME);
|
||||
ESP_LOGI(TAG, "code build date: %d", compileTime());
|
||||
|
||||
// print chip information on startup if in verbose mode after coldstart
|
||||
#if (VERBOSE)
|
||||
@@ -244,9 +236,9 @@ void setup() {
|
||||
"ledloop", // name of task
|
||||
1024, // stack size of task
|
||||
(void *)1, // parameter of the task
|
||||
3, // priority of the task
|
||||
1, // priority of the task
|
||||
&ledLoopTask, // task handle
|
||||
0); // CPU core
|
||||
1); // CPU core
|
||||
#endif
|
||||
|
||||
// initialize wifi antenna
|
||||
@@ -328,7 +320,7 @@ void setup() {
|
||||
ESP_LOGI(TAG, "Starting GPS Feed...");
|
||||
xTaskCreatePinnedToCore(gps_loop, // task function
|
||||
"gpsloop", // name of task
|
||||
4096, // stack size of task
|
||||
8192, // stack size of task
|
||||
(void *)1, // parameter of the task
|
||||
1, // priority of the task
|
||||
&GpsTask, // task handle
|
||||
@@ -339,15 +331,9 @@ void setup() {
|
||||
// initialize sensors
|
||||
#if (HAS_SENSORS)
|
||||
#if (HAS_SENSOR_1)
|
||||
#if (COUNT_ENS)
|
||||
ESP_LOGI(TAG, "init CWA-counter");
|
||||
if (cwa_init())
|
||||
strcat_P(features, " CWA");
|
||||
#else
|
||||
strcat_P(features, " SENS(1)");
|
||||
sensor_init();
|
||||
#endif
|
||||
#endif
|
||||
#if (HAS_SENSOR_2)
|
||||
strcat_P(features, " SENS(2)");
|
||||
sensor_init();
|
||||
@@ -432,7 +418,7 @@ void setup() {
|
||||
"irqhandler", // name of task
|
||||
4096, // stack size of task
|
||||
(void *)1, // parameter of the task
|
||||
2, // priority of the task
|
||||
4, // priority of the task
|
||||
&irqHandlerTask, // task handle
|
||||
1); // CPU core
|
||||
|
||||
@@ -494,8 +480,7 @@ void setup() {
|
||||
cyclicTimer.attach(HOMECYCLE, setCyclicIRQ);
|
||||
|
||||
// only if we have a timesource we do timesync
|
||||
#if ((TIME_SYNC_LORAWAN) || (TIME_SYNC_LORASERVER) || (HAS_GPS) || \
|
||||
defined HAS_RTC)
|
||||
#if ((TIME_SYNC_LORAWAN) || (TIME_SYNC_LORASERVER) || (HAS_GPS) || (HAS_RTC))
|
||||
|
||||
#if (defined HAS_IF482 || defined HAS_DCF77)
|
||||
ESP_LOGI(TAG, "Starting Clock Controller...");
|
||||
@@ -507,8 +492,7 @@ void setup() {
|
||||
#endif
|
||||
|
||||
ESP_LOGI(TAG, "Starting Timekeeper...");
|
||||
_ASSERT(timepulse_init()); // setup pps timepulse
|
||||
timepulse_start(); // starts pps and cyclic time sync
|
||||
_ASSERT(timepulse_init()); // starts pps and cyclic time sync
|
||||
strcat_P(features, " TIME");
|
||||
|
||||
#endif // timesync
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ esp_err_t mqtt_init(void) {
|
||||
SEND_QUEUE_SIZE * PAYLOAD_BUFFER_SIZE);
|
||||
|
||||
ESP_LOGI(TAG, "Starting MQTTloop...");
|
||||
xTaskCreatePinnedToCore(mqtt_client_task, "mqttloop", 4096, (void *)NULL, 1,
|
||||
xTaskCreatePinnedToCore(mqtt_client_task, "mqttloop", 4096, (void *)NULL, 5,
|
||||
&mqttTask, 1);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,11 @@
|
||||
#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
|
||||
|
||||
// default settings for transmission of sensor data (first list = data on / second line = data off)
|
||||
#define PAYLOADMASK \
|
||||
((GPS_DATA | MEMS_DATA | COUNT_DATA | SENSOR1_DATA | SENSOR2_DATA | SENSOR3_DATA) & \
|
||||
(~BATT_DATA) & (~RESERVED_DATA))
|
||||
|
||||
// MAC sniffing parameters
|
||||
#define BLECOUNTER 1 // set to 0 if you do not want to install the BLE sniffer
|
||||
#define WIFICOUNTER 1 // set to 0 if you do not want to install the WIFI sniffer
|
||||
@@ -27,10 +32,6 @@
|
||||
#define BLESCANWINDOW 80 // [milliseconds] scan window, see below, 3 .. 10240, default 80ms
|
||||
#define BLESCANINTERVAL 80 // [illiseconds] scan interval, see below, 3 .. 10240, default 80ms = 100% duty cycle
|
||||
|
||||
// Corona Exposure Notification Service(ENS) counter
|
||||
#define COUNT_ENS 0 // count found number of devices which advertise Exposure Notification Service
|
||||
// set to 1 if you want to enable this function [default=0]
|
||||
|
||||
/* Note: guide for setting bluetooth parameters
|
||||
*
|
||||
* |< Scan Window > |< Scan Window > | ... |< Scan Window > |
|
||||
|
||||
+2
-12
@@ -1,7 +1,6 @@
|
||||
// Basic Config
|
||||
#include "globals.h"
|
||||
#include "rcommand.h"
|
||||
#include "libpax_helpers.h"
|
||||
|
||||
// Local logging tag
|
||||
static const char TAG[] = __FILE__;
|
||||
@@ -397,15 +396,6 @@ void set_flush(uint8_t val[]) {
|
||||
// used to open receive window on LoRaWAN class a nodes
|
||||
};
|
||||
|
||||
void set_enscount(uint8_t val[]) {
|
||||
ESP_LOGI(TAG, "Remote command: set ENS_COUNT to %s", val[0] ? "on" : "off");
|
||||
cfg.enscount = val[0] ? 1 : 0;
|
||||
if (val[0])
|
||||
cfg.payloadmask |= (uint8_t)SENSOR1_DATA;
|
||||
else
|
||||
cfg.payloadmask &= (uint8_t)~SENSOR1_DATA;
|
||||
}
|
||||
|
||||
void set_loadconfig(uint8_t val[]) {
|
||||
ESP_LOGI(TAG, "Remote command: load config from NVRAM");
|
||||
loadConfig();
|
||||
@@ -430,7 +420,7 @@ static const cmd_t table[] = {
|
||||
{0x10, set_rgblum, 1}, {0x13, set_sensor, 2},
|
||||
{0x14, set_payloadmask, 1}, {0x15, set_bme, 1},
|
||||
{0x16, set_batt, 1}, {0x17, set_wifiscan, 1},
|
||||
{0x18, set_enscount, 1}, {0x19, set_sleepcycle, 2},
|
||||
{0x18, set_flush, 0}, {0x19, set_sleepcycle, 2},
|
||||
{0x20, set_loadconfig, 0}, {0x21, set_saveconfig, 0},
|
||||
{0x80, get_config, 0}, {0x81, get_status, 0},
|
||||
{0x83, get_batt, 0}, {0x84, get_gps, 0},
|
||||
@@ -493,7 +483,7 @@ void rcmd_process(void *pvParameters) {
|
||||
rcmd_execute(RcmdBuffer.cmd, RcmdBuffer.cmdLen);
|
||||
}
|
||||
|
||||
delay(2); // yield to CPU
|
||||
delay(5); // yield to CPU
|
||||
} // rcmd_process()
|
||||
|
||||
// enqueue remote command
|
||||
|
||||
@@ -126,6 +126,11 @@ void enter_deepsleep(const uint64_t wakeup_sec, gpio_num_t wakeup_gpio) {
|
||||
sds011_sleep();
|
||||
#endif
|
||||
|
||||
// flush & close sd card, if we have
|
||||
#if (HAS_SDCARD)
|
||||
sdcard_close();
|
||||
#endif
|
||||
|
||||
// wait a while (max 100 sec) to clear send queues
|
||||
ESP_LOGI(TAG, "Waiting until send queues are empty...");
|
||||
for (i = 100; i > 0; i--) {
|
||||
|
||||
+14
-6
@@ -25,14 +25,14 @@ uint8_t rtc_init(void) {
|
||||
}
|
||||
|
||||
#if (TIME_SYNC_COMPILEDATE)
|
||||
// initialize a blank RTC without battery backup with compiled time
|
||||
// initialize a blank RTC without battery backup with build time
|
||||
RtcDateTime tt = Rtc.GetDateTime();
|
||||
time_t t = tt.Epoch32Time(); // sec2000 -> epoch
|
||||
|
||||
if (!Rtc.IsDateTimeValid() || !timeIsValid(t)) {
|
||||
ESP_LOGW(TAG, "RTC has no recent time, setting to compiled time");
|
||||
Rtc.SetDateTime(
|
||||
RtcDateTime(_COMPILETIME - SECS_YR_2000)); // epoch -> sec2000
|
||||
ESP_LOGW(TAG, "RTC has no recent time, setting to compiletime");
|
||||
Rtc.SetDateTime(RtcDateTime(mkgmtime(compileTime()) -
|
||||
SECS_YR_2000)); // epoch -> sec2000
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -62,15 +62,23 @@ uint8_t set_rtctime(time_t t) { // t is sec epoch time
|
||||
}
|
||||
} // set_rtctime()
|
||||
|
||||
time_t get_rtctime(void) {
|
||||
time_t get_rtctime(uint16_t *msec) {
|
||||
time_t t = 0;
|
||||
*msec = 0;
|
||||
if (I2C_MUTEX_LOCK()) {
|
||||
if (Rtc.IsDateTimeValid() && Rtc.GetIsRunning()) {
|
||||
RtcDateTime tt = Rtc.GetDateTime();
|
||||
t = tt.Epoch32Time(); // sec2000 -> epoch
|
||||
}
|
||||
I2C_MUTEX_UNLOCK();
|
||||
return timeIsValid(t);
|
||||
#ifdef RTC_INT
|
||||
// adjust time to top of next second by waiting TimePulseTick to flip
|
||||
bool lastTick = TimePulseTick;
|
||||
while (TimePulseTick == lastTick) {
|
||||
};
|
||||
t++;
|
||||
#endif
|
||||
return t;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "RTC get time failure");
|
||||
return 0; // failure
|
||||
|
||||
+72
-42
@@ -1,4 +1,7 @@
|
||||
// routines for writing data to an SD-card, if present
|
||||
// use FAT32 formatted card
|
||||
// check whether your card reader supports SPI oder SDMMC and select appropriate
|
||||
// SD low level driver in board hal file
|
||||
|
||||
// Local logging tag
|
||||
static const char TAG[] = __FILE__;
|
||||
@@ -8,20 +11,26 @@ static const char TAG[] = __FILE__;
|
||||
#ifdef HAS_SDCARD
|
||||
|
||||
static bool useSDCard;
|
||||
|
||||
static void createFile(void);
|
||||
static void openFile(void);
|
||||
|
||||
File fileSDCard;
|
||||
|
||||
bool sdcard_init() {
|
||||
ESP_LOGI(TAG, "looking for SD-card...");
|
||||
#if HAS_SDCARD == 1
|
||||
SPIClass sd_spi;
|
||||
#endif
|
||||
|
||||
bool sdcard_init(bool create) {
|
||||
|
||||
// for usage of SD drivers on ESP32 platform see
|
||||
// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/sdspi_host.html
|
||||
// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/sdmmc_host.html
|
||||
|
||||
ESP_LOGI(TAG, "looking for SD-card...");
|
||||
#if HAS_SDCARD == 1 // use SD SPI host driver
|
||||
useSDCard = SD.begin(SDCARD_CS, SDCARD_MOSI, SDCARD_MISO, SDCARD_SCLK);
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
sd_spi.begin(SDCARD_SCLK, SDCARD_MISO, SDCARD_MOSI, SDCARD_CS);
|
||||
digitalWrite(SDCARD_CS, LOW);
|
||||
useSDCard = SD.begin(SDCARD_CS, sd_spi);
|
||||
#elif HAS_SDCARD == 2 // use SD MMC host driver
|
||||
// enable internal pullups of sd-data lines
|
||||
gpio_set_pull_mode(gpio_num_t(SDCARD_DATA0), GPIO_PULLUP_ONLY);
|
||||
@@ -33,20 +42,27 @@ bool sdcard_init() {
|
||||
|
||||
if (useSDCard) {
|
||||
ESP_LOGI(TAG, "SD-card found");
|
||||
createFile();
|
||||
} else
|
||||
openFile();
|
||||
return true;
|
||||
} else {
|
||||
ESP_LOGI(TAG, "SD-card not found");
|
||||
return useSDCard;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void sdcard_close(void) {
|
||||
ESP_LOGI(TAG, "closing SD-card");
|
||||
fileSDCard.flush();
|
||||
fileSDCard.close();
|
||||
}
|
||||
|
||||
void sdcardWriteData(uint16_t noWifi, uint16_t noBle,
|
||||
__attribute__((unused)) uint16_t noBleCWA) {
|
||||
__attribute__((unused)) uint16_t voltage) {
|
||||
static int counterWrites = 0;
|
||||
char tempBuffer[12 + 1];
|
||||
char tempBuffer[20 + 1];
|
||||
time_t t = time(NULL);
|
||||
struct tm tt;
|
||||
localtime_r(&t, &tt);
|
||||
mktime(&tt);
|
||||
gmtime_r(&t, &tt); // make UTC timestamp
|
||||
|
||||
#if (HAS_SDS011)
|
||||
sdsStatus_t sds;
|
||||
@@ -55,71 +71,85 @@ void sdcardWriteData(uint16_t noWifi, uint16_t noBle,
|
||||
if (!useSDCard)
|
||||
return;
|
||||
|
||||
ESP_LOGD(TAG, "writing to SD-card");
|
||||
strftime(tempBuffer, sizeof(tempBuffer), "%d.%m.%Y", &tt);
|
||||
ESP_LOGI(TAG, "SD: writing data");
|
||||
strftime(tempBuffer, sizeof(tempBuffer), "%FT%TZ", &tt);
|
||||
fileSDCard.print(tempBuffer);
|
||||
strftime(tempBuffer, sizeof(tempBuffer), "%H.%M.%S", &tt);
|
||||
snprintf(tempBuffer, sizeof(tempBuffer), ",%d,%d", noWifi, noBle);
|
||||
fileSDCard.print(tempBuffer);
|
||||
sprintf(tempBuffer, "%d,%d", noWifi, noBle);
|
||||
fileSDCard.print(tempBuffer);
|
||||
#if (COUNT_ENS)
|
||||
sprintf(tempBuffer, ",%d", noBleCWA);
|
||||
#if (defined BAT_MEASURE_ADC || defined HAS_PMU)
|
||||
snprintf(tempBuffer, sizeof(tempBuffer), ",%d", voltage);
|
||||
fileSDCard.print(tempBuffer);
|
||||
#endif
|
||||
#if (HAS_SDS011)
|
||||
sds011_store(&sds);
|
||||
sprintf(tempBuffer, ",%5.1f,%4.1f", sds.pm10, sds.pm25);
|
||||
snprintf(tempBuffer, sizeof(tempBuffer), ",%5.1f,%4.1f", sds.pm10 / 10,
|
||||
sds.pm25 / 10);
|
||||
fileSDCard.print(tempBuffer);
|
||||
#endif
|
||||
fileSDCard.println();
|
||||
|
||||
if (++counterWrites > 2) {
|
||||
// force writing to SD-card
|
||||
ESP_LOGD(TAG, "flushing data to card");
|
||||
ESP_LOGI(TAG, "SD: flushing data");
|
||||
fileSDCard.flush();
|
||||
counterWrites = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void createFile(void) {
|
||||
char bufferFilename[8 + 1 + 3 + 1];
|
||||
void openFile(void) {
|
||||
char bufferFilename[30];
|
||||
|
||||
useSDCard = false;
|
||||
|
||||
for (int i = 0; i < 100; i++) {
|
||||
sprintf(bufferFilename, SDCARD_FILE_NAME, i);
|
||||
// ESP_LOGD(TAG, "SD: looking for file <%s>", bufferFilename);
|
||||
snprintf(bufferFilename, sizeof(bufferFilename), "/%s.csv", SDCARD_FILE_NAME);
|
||||
ESP_LOGI(TAG, "SD: looking for file <%s>", bufferFilename);
|
||||
|
||||
#if HAS_SDCARD == 1
|
||||
bool fileExists = SD.exists(bufferFilename);
|
||||
bool fileExists = SD.exists(bufferFilename);
|
||||
#elif HAS_SDCARD == 2
|
||||
bool fileExists = SD_MMC.exists(bufferFilename);
|
||||
bool fileExists = SD_MMC.exists(bufferFilename);
|
||||
#endif
|
||||
|
||||
if (!fileExists) {
|
||||
// ESP_LOGD(TAG, "SD: file does not exist: opening");
|
||||
// file not exists, create it
|
||||
if (!fileExists) {
|
||||
ESP_LOGD(TAG, "SD: file not found, creating...");
|
||||
|
||||
#if HAS_SDCARD == 1
|
||||
fileSDCard = SD.open(bufferFilename, FILE_WRITE);
|
||||
fileSDCard = SD.open(bufferFilename, FILE_WRITE);
|
||||
#elif HAS_SDCARD == 2
|
||||
fileSDCard = SD_MMC.open(bufferFilename, FILE_WRITE);
|
||||
fileSDCard = SD_MMC.open(bufferFilename, FILE_WRITE);
|
||||
#endif
|
||||
|
||||
if (fileSDCard) {
|
||||
ESP_LOGD(TAG, "SD: name opened: <%s>", bufferFilename);
|
||||
fileSDCard.print(SDCARD_FILE_HEADER);
|
||||
#if (COUNT_ENS)
|
||||
fileSDCard.print(SDCARD_FILE_HEADER_CWA); // for Corona-data (CWA)
|
||||
if (fileSDCard) {
|
||||
ESP_LOGD(TAG, "SD: name opened: <%s>", bufferFilename);
|
||||
fileSDCard.print(SDCARD_FILE_HEADER);
|
||||
#if (defined BAT_MEASURE_ADC || defined HAS_PMU)
|
||||
fileSDCard.print(SDCARD_FILE_HEADER_VOLTAGE); // for battery level data
|
||||
#endif
|
||||
#if (HAS_SDS011)
|
||||
fileSDCard.print(SDCARD_FILE_HEADER_SDS011);
|
||||
fileSDCard.print(SDCARD_FILE_HEADER_SDS011);
|
||||
#endif
|
||||
fileSDCard.println();
|
||||
useSDCard = true;
|
||||
break;
|
||||
}
|
||||
fileSDCard.println();
|
||||
useSDCard = true;
|
||||
}
|
||||
}
|
||||
|
||||
// file exists, append data
|
||||
else {
|
||||
ESP_LOGD(TAG, "SD: file found, opening...");
|
||||
|
||||
#if HAS_SDCARD == 1
|
||||
fileSDCard = SD.open(bufferFilename, FILE_APPEND);
|
||||
#elif HAS_SDCARD == 2
|
||||
fileSDCard = SD_MMC.open(bufferFilename, FILE_APPEND);
|
||||
#endif
|
||||
|
||||
if (fileSDCard) {
|
||||
ESP_LOGD(TAG, "SD: name opened: <%s>", bufferFilename);
|
||||
useSDCard = true;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+22
-21
@@ -10,35 +10,37 @@ static const char TAG[] = __FILE__;
|
||||
#if (HAS_IF482)
|
||||
#error cannot use IF482 together with SDS011 (both use UART#2)
|
||||
#endif
|
||||
// UART(2) is unused in this project
|
||||
static HardwareSerial sdsSerial(2); // so we use it here
|
||||
static SDS011 sdsSensor; // fine dust sensor
|
||||
|
||||
// the results of the sensor:
|
||||
static float pm10, pm25;
|
||||
boolean isSDS011Active;
|
||||
SdsDustSensor sds(Serial2);
|
||||
|
||||
bool isSDS011Active = false;
|
||||
static float pm10 = 0.0, pm25 = 0.0;
|
||||
|
||||
// init
|
||||
bool sds011_init() {
|
||||
pm25 = pm10 = 0.0;
|
||||
sdsSensor.begin(&sdsSerial, SDS_RX, SDS_TX);
|
||||
sds011_sleep(); // we do sleep/wakup by ourselves
|
||||
sds.begin(9600, SERIAL_8N1, SDS_RX, SDS_TX);
|
||||
sds011_wakeup();
|
||||
ESP_LOGI(TAG, "SDS011: %s", sds.queryFirmwareVersion().toString().c_str());
|
||||
sds.setQueryReportingMode();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// reading data:
|
||||
void sds011_loop() {
|
||||
if (isSDS011Active) {
|
||||
int sdsErrorCode = sdsSensor.read(&pm25, &pm10);
|
||||
if (sdsErrorCode) {
|
||||
pm25 = pm10 = 0.0;
|
||||
ESP_LOGI(TAG, "SDS011 error: %d", sdsErrorCode);
|
||||
PmResult pm = sds.queryPm();
|
||||
if (!pm.isOk()) {
|
||||
ESP_LOGE(TAG, "SDS011: query error %s", pm.statusToString().c_str());
|
||||
pm10 = pm25 = 0.0;
|
||||
} else {
|
||||
ESP_LOGI(TAG, "fine-dust-values: %5.1f,%4.1f", pm10, pm25);
|
||||
ESP_LOGI(TAG, "SDS011: %s", pm.toString().c_str());
|
||||
pm10 = pm.pm10;
|
||||
pm25 = pm.pm25;
|
||||
}
|
||||
ESP_LOGD(TAG, "SDS011: go to sleep");
|
||||
sds011_sleep();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// retrieving stored data:
|
||||
@@ -49,17 +51,16 @@ void sds011_store(sdsStatus_t *sds_store) {
|
||||
|
||||
// putting the SDS-sensor to sleep
|
||||
void sds011_sleep(void) {
|
||||
sdsSensor.sleep();
|
||||
isSDS011Active = false;
|
||||
WorkingStateResult state = sds.sleep();
|
||||
isSDS011Active = state.isWorking();
|
||||
}
|
||||
|
||||
// start the SDS-sensor
|
||||
// needs 30 seconds for warming up
|
||||
void sds011_wakeup() {
|
||||
if (!isSDS011Active) {
|
||||
sdsSensor.wakeup();
|
||||
isSDS011Active = true;
|
||||
}
|
||||
WorkingStateResult state = sds.wakeup();
|
||||
isSDS011Active = state.isWorking();
|
||||
ESP_LOGD(TAG, "SDS011: %s", state.toString().c_str());
|
||||
}
|
||||
|
||||
#endif // HAS_SDS011
|
||||
+33
-28
@@ -5,6 +5,8 @@ void setSendIRQ(TimerHandle_t xTimer) {
|
||||
xTaskNotify(irqHandlerTask, SENDCYCLE_IRQ, eSetBits);
|
||||
}
|
||||
|
||||
void setSendIRQ(void) { setSendIRQ(NULL); }
|
||||
|
||||
void initSendDataTimer(uint8_t sendcycle) {
|
||||
static TimerHandle_t SendDataTimer = NULL;
|
||||
|
||||
@@ -65,31 +67,26 @@ void SendPayload(uint8_t port) {
|
||||
mqtt_enqueuedata(&SendBuffer);
|
||||
#endif
|
||||
|
||||
// write data to sdcard, if present
|
||||
#if (HAS_SDCARD)
|
||||
if (port == COUNTERPORT) {
|
||||
sdcardWriteData(libpax_macs_wifi, libpax_macs_ble
|
||||
#if (COUNT_ENS)
|
||||
,
|
||||
cwa_report()
|
||||
#endif
|
||||
);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // SendPayload
|
||||
|
||||
// interrupt triggered function to prepare payload to send
|
||||
// timer triggered function to prepare payload to send
|
||||
void sendData() {
|
||||
|
||||
uint8_t bitmask = cfg.payloadmask;
|
||||
uint8_t mask = 1;
|
||||
|
||||
#if (HAS_GPS)
|
||||
gpsStatus_t gps_status;
|
||||
#endif
|
||||
#if (HAS_SDS011)
|
||||
sdsStatus_t sds_status;
|
||||
#endif
|
||||
#if ((WIFICOUNTER) || (BLECOUNTER))
|
||||
struct count_payload_t count =
|
||||
count_from_libpax; // copy values from global libpax var
|
||||
ESP_LOGD(TAG, "Sending count results: pax=%d / wifi=%d / ble=%d", count.pax,
|
||||
count.wifi_count, count.ble_count);
|
||||
#endif
|
||||
|
||||
while (bitmask) {
|
||||
switch (bitmask & mask) {
|
||||
@@ -97,14 +94,14 @@ void sendData() {
|
||||
#if ((WIFICOUNTER) || (BLECOUNTER))
|
||||
case COUNT_DATA:
|
||||
payload.reset();
|
||||
|
||||
#if !(PAYLOAD_OPENSENSEBOX)
|
||||
ESP_LOGI(TAG, "Sending libpax wifi count: %d", libpax_macs_wifi);
|
||||
payload.addCount(libpax_macs_wifi, MAC_SNIFF_WIFI);
|
||||
payload.addCount(count.wifi_count, MAC_SNIFF_WIFI);
|
||||
if (cfg.blescan) {
|
||||
ESP_LOGI(TAG, "Sending libpax ble count: %d", libpax_macs_ble);
|
||||
payload.addCount(libpax_macs_ble, MAC_SNIFF_BLE);
|
||||
payload.addCount(count.ble_count, MAC_SNIFF_BLE);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if (HAS_GPS)
|
||||
if (GPSPORT == COUNTERPORT) {
|
||||
// send GPS position only if we have a fix
|
||||
@@ -115,23 +112,35 @@ void sendData() {
|
||||
ESP_LOGD(TAG, "No valid GPS position");
|
||||
}
|
||||
#endif
|
||||
|
||||
#if (PAYLOAD_OPENSENSEBOX)
|
||||
ESP_LOGI(TAG, "Sending libpax wifi count: %d", libpax_macs_wifi);
|
||||
payload.addCount(libpax_macs_wifi, MAC_SNIFF_WIFI);
|
||||
payload.addCount(count.wifi_count, MAC_SNIFF_WIFI);
|
||||
if (cfg.blescan) {
|
||||
ESP_LOGI(TAG, "Sending libpax ble count: %d", libpax_macs_ble);
|
||||
payload.addCount(libpax_macs_ble, MAC_SNIFF_BLE);
|
||||
payload.addCount(count.ble_count, MAC_SNIFF_BLE);
|
||||
#endif
|
||||
|
||||
#if (HAS_SDS011)
|
||||
sds011_store(&sds_status);
|
||||
payload.addSDS(sds_status);
|
||||
#endif
|
||||
SendPayload(COUNTERPORT);
|
||||
|
||||
#ifdef HAS_DISPLAY
|
||||
dp_plotCurve(libpax_macs_ble + libpax_macs_wifi, true);
|
||||
dp_plotCurve(count.pax, true);
|
||||
#endif
|
||||
break;
|
||||
|
||||
#if (HAS_SDCARD)
|
||||
sdcardWriteData(count.wifi_count, count.ble_count
|
||||
#if (defined BAT_MEASURE_ADC || defined HAS_PMU)
|
||||
,
|
||||
read_voltage()
|
||||
#endif
|
||||
);
|
||||
#endif // HAS_SDCARD
|
||||
|
||||
SendPayload(COUNTERPORT);
|
||||
break; // case COUNTDATA
|
||||
|
||||
#endif // ((WIFICOUNTER) || (BLECOUNTER))
|
||||
|
||||
#if (HAS_BME)
|
||||
case MEMS_DATA:
|
||||
@@ -162,10 +171,6 @@ void sendData() {
|
||||
payload.reset();
|
||||
payload.addSensor(sensor_read(1));
|
||||
SendPayload(SENSOR1PORT);
|
||||
#if (COUNT_ENS)
|
||||
if (cfg.countermode != 1)
|
||||
cwa_clear();
|
||||
#endif
|
||||
break;
|
||||
#endif
|
||||
#if (HAS_SENSOR_2)
|
||||
|
||||
@@ -2,12 +2,6 @@
|
||||
#include "globals.h"
|
||||
#include "sensor.h"
|
||||
|
||||
#if (COUNT_ENS)
|
||||
#include "payload.h"
|
||||
#include "corona.h"
|
||||
extern PayloadConvert payload;
|
||||
#endif
|
||||
|
||||
// Local logging tag
|
||||
static const char TAG[] = __FILE__;
|
||||
|
||||
@@ -53,16 +47,10 @@ uint8_t *sensor_read(uint8_t sensor) {
|
||||
case 1:
|
||||
|
||||
// insert user specific sensor data frames here
|
||||
// note: Sensor1 fields are used for ENS count, if ENS detection enabled
|
||||
#if (COUNT_ENS)
|
||||
if (cfg.enscount)
|
||||
payload.addCount(cwa_report(), MAC_SNIFF_BLE_ENS);
|
||||
#else
|
||||
buf[0] = length;
|
||||
buf[1] = 0x01;
|
||||
buf[2] = 0x02;
|
||||
buf[3] = 0x03;
|
||||
#endif
|
||||
break;
|
||||
case 2:
|
||||
|
||||
|
||||
+58
-54
@@ -19,9 +19,9 @@ static const char TAG[] = __FILE__;
|
||||
// G = GPS / R = RTC / L = LORA / * = no sync / ? = never synced
|
||||
const char timeSetSymbols[] = {'G', 'R', 'L', '*', '?'};
|
||||
|
||||
bool volatile TimePulseTick = false;
|
||||
DRAM_ATTR bool TimePulseTick = false;
|
||||
DRAM_ATTR unsigned long lastPPS = millis();
|
||||
timesource_t timeSource = _unsynced;
|
||||
time_t _COMPILETIME = compileTime(__DATE__);
|
||||
TaskHandle_t ClockTask = NULL;
|
||||
hw_timer_t *ppsIRQ = NULL;
|
||||
|
||||
@@ -40,8 +40,7 @@ Ticker timesyncer;
|
||||
void setTimeSyncIRQ() { xTaskNotify(irqHandlerTask, TIMESYNC_IRQ, eSetBits); }
|
||||
|
||||
void calibrateTime(void) {
|
||||
ESP_LOGD(TAG, "[%0.3f] calibrateTime, timeSource == %d", _seconds(),
|
||||
timeSource);
|
||||
|
||||
time_t t = 0;
|
||||
uint16_t t_msec = 0;
|
||||
|
||||
@@ -57,7 +56,7 @@ void calibrateTime(void) {
|
||||
|
||||
// has RTC -> fallback to RTC time
|
||||
#ifdef HAS_RTC
|
||||
t = get_rtctime();
|
||||
t = get_rtctime(&t_msec);
|
||||
// set time from RTC - method will check if time is valid
|
||||
setMyTime((uint32_t)t, t_msec, _rtc);
|
||||
#endif
|
||||
@@ -101,26 +100,27 @@ void IRAM_ATTR setMyTime(uint32_t t_sec, uint16_t t_msec,
|
||||
vTaskDelay(pdMS_TO_TICKS(1000 - t_msec % 1000));
|
||||
}
|
||||
|
||||
// from here on we are on top of next second
|
||||
|
||||
tv.tv_sec = time_to_set;
|
||||
tv.tv_usec = 0;
|
||||
sntp_sync_time(&tv);
|
||||
|
||||
ESP_LOGI(TAG, "[%0.3f] UTC time: %d.000 sec", _seconds(), time_to_set);
|
||||
|
||||
// if we have a software pps timer, shift it to top of second
|
||||
if (ppsIRQ != NULL) {
|
||||
|
||||
timerWrite(ppsIRQ, 0); // reset pps timer
|
||||
CLOCKIRQ(); // fire clock pps, this advances time 1 sec
|
||||
}
|
||||
|
||||
// if we have got an external timesource, set RTC time and shift RTC_INT pulse
|
||||
// to top of second
|
||||
// if we have a precise time timesource, set RTC time and shift RTC_INT
|
||||
// pulse to top of second
|
||||
#ifdef HAS_RTC
|
||||
if ((mytimesource == _gps) || (mytimesource == _lora))
|
||||
set_rtctime(time_to_set);
|
||||
#endif
|
||||
|
||||
// if we have a software pps timer, shift it to top of second
|
||||
if (ppsIRQ != NULL) {
|
||||
timerWrite(ppsIRQ, 0); // reset pps timer
|
||||
CLOCKIRQ(); // fire clock pps to advance wall clock by 1 sec
|
||||
}
|
||||
|
||||
timeSource = mytimesource; // set global variable
|
||||
|
||||
timesyncer.attach(TIME_SYNC_INTERVAL * 60, setTimeSyncIRQ);
|
||||
@@ -132,7 +132,7 @@ void IRAM_ATTR setMyTime(uint32_t t_sec, uint16_t t_msec,
|
||||
"[%0.3f] Failed to synchronise time from source %c | unix sec "
|
||||
"obtained from source: %d | unix sec at program compilation: %d",
|
||||
_seconds(), timeSetSymbols[mytimesource], time_to_set,
|
||||
_COMPILETIME);
|
||||
compileTime());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,29 +140,26 @@ void IRAM_ATTR setMyTime(uint32_t t_sec, uint16_t t_msec,
|
||||
uint8_t timepulse_init() {
|
||||
|
||||
// set esp-idf API sntp sync mode
|
||||
//sntp_init();
|
||||
// sntp_init();
|
||||
sntp_set_sync_mode(SNTP_SYNC_MODE_IMMED);
|
||||
|
||||
// use time pulse from GPS as time base with fixed 1Hz frequency
|
||||
// if we have, use PPS time pulse from GPS for syncing time on top of second
|
||||
#ifdef GPS_INT
|
||||
|
||||
// setup external interupt pin for rising edge GPS INT
|
||||
// setup external interupt pin for rising edge of GPS PPS
|
||||
pinMode(GPS_INT, INPUT_PULLDOWN);
|
||||
// setup external rtc 1Hz clock as pulse per second clock
|
||||
ESP_LOGI(TAG, "Timepulse: external (GPS)");
|
||||
return 1; // success
|
||||
attachInterrupt(digitalPinToInterrupt(GPS_INT), GPSIRQ, RISING);
|
||||
#endif
|
||||
|
||||
// use pulse from on board RTC chip as time base with fixed frequency
|
||||
#elif defined RTC_INT
|
||||
// if we have, use pulse from on board RTC chip as time base for calendar time
|
||||
#if defined RTC_INT
|
||||
|
||||
// setup external interupt pin for falling edge RTC INT
|
||||
pinMode(RTC_INT, INPUT_PULLUP);
|
||||
|
||||
// setup external rtc 1Hz clock as pulse per second clock
|
||||
// setup external rtc 1Hz clock pulse
|
||||
if (I2C_MUTEX_LOCK()) {
|
||||
Rtc.SetSquareWavePinClockFrequency(DS3231SquareWaveClock_1Hz);
|
||||
Rtc.SetSquareWavePin(DS3231SquareWavePin_ModeClock);
|
||||
I2C_MUTEX_UNLOCK();
|
||||
pinMode(RTC_INT, INPUT_PULLUP);
|
||||
attachInterrupt(digitalPinToInterrupt(RTC_INT), CLOCKIRQ, FALLING);
|
||||
ESP_LOGI(TAG, "Timepulse: external (RTC)");
|
||||
return 1; // success
|
||||
} else {
|
||||
@@ -172,33 +169,39 @@ uint8_t timepulse_init() {
|
||||
return 1; // success
|
||||
|
||||
#else
|
||||
// use ESP32 hardware timer as time base with adjustable frequency
|
||||
// use ESP32 hardware timer as time base for calendar time
|
||||
ppsIRQ = timerBegin(1, 8000, true); // set 80 MHz prescaler to 1/10000 sec
|
||||
timerAlarmWrite(ppsIRQ, 10000, true); // 1000ms
|
||||
timerAttachInterrupt(ppsIRQ, &CLOCKIRQ, true);
|
||||
timerAlarmEnable(ppsIRQ);
|
||||
ESP_LOGI(TAG, "Timepulse: internal (ESP32 hardware timer)");
|
||||
return 1; // success
|
||||
|
||||
#endif
|
||||
} // timepulse_init
|
||||
|
||||
void timepulse_start(void) {
|
||||
#ifdef GPS_INT // start external clock gps pps line
|
||||
attachInterrupt(digitalPinToInterrupt(GPS_INT), CLOCKIRQ, RISING);
|
||||
#elif defined RTC_INT // start external clock rtc
|
||||
attachInterrupt(digitalPinToInterrupt(RTC_INT), CLOCKIRQ, FALLING);
|
||||
#else // start internal clock esp32 hardware timer
|
||||
timerAttachInterrupt(ppsIRQ, &CLOCKIRQ, true);
|
||||
timerAlarmEnable(ppsIRQ);
|
||||
#endif
|
||||
// start cyclic time sync
|
||||
timesyncer.attach(TIME_SYNC_INTERVAL * 60, setTimeSyncIRQ);
|
||||
|
||||
// get time if we don't have one
|
||||
if (timeSource != _set)
|
||||
setTimeSyncIRQ(); // init systime by RTC or GPS or LORA
|
||||
// start cyclic time sync
|
||||
timesyncer.attach(TIME_SYNC_INTERVAL * 60, setTimeSyncIRQ);
|
||||
|
||||
} // timepulse_init
|
||||
|
||||
// interrupt service routine triggered by GPS PPS
|
||||
void IRAM_ATTR GPSIRQ(void) {
|
||||
|
||||
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
|
||||
|
||||
// take timestamp
|
||||
lastPPS = millis(); // last time of pps
|
||||
|
||||
// yield only if we should
|
||||
if (xHigherPriorityTaskWoken)
|
||||
portYIELD_FROM_ISR();
|
||||
}
|
||||
|
||||
// interrupt service routine triggered by either pps or esp32 hardware timer
|
||||
// interrupt service routine triggered by esp32 hardware timer
|
||||
void IRAM_ATTR CLOCKIRQ(void) {
|
||||
|
||||
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
|
||||
@@ -211,9 +214,7 @@ void IRAM_ATTR CLOCKIRQ(void) {
|
||||
|
||||
// flip time pulse ticker, if needed
|
||||
#ifdef HAS_DISPLAY
|
||||
#if (defined GPS_INT || defined RTC_INT)
|
||||
TimePulseTick = !TimePulseTick; // flip pulse ticker
|
||||
#endif
|
||||
TimePulseTick = !TimePulseTick; // flip global variable pulse ticker
|
||||
#endif
|
||||
|
||||
// yield only if we should
|
||||
@@ -224,7 +225,9 @@ void IRAM_ATTR CLOCKIRQ(void) {
|
||||
// helper function to check plausibility of a given epoch time
|
||||
bool timeIsValid(time_t const t) {
|
||||
// is t a time in the past? we use compile time to guess
|
||||
return (t > _COMPILETIME);
|
||||
// compile time is some local time, but we do not know it's time zone
|
||||
// thus, we go 1 full day back to be sure to catch a time in the past
|
||||
return (t > (compileTime() - 86400));
|
||||
}
|
||||
|
||||
// helper function to calculate serial transmit time
|
||||
@@ -252,7 +255,7 @@ void clock_init(void) {
|
||||
"clockloop", // name of task
|
||||
3072, // stack size of task
|
||||
(void *)1, // task parameter
|
||||
4, // priority of the task
|
||||
6, // priority of the task
|
||||
&ClockTask, // task handle
|
||||
1); // CPU core
|
||||
|
||||
@@ -283,7 +286,7 @@ void clock_loop(void *taskparameter) { // ClockTask
|
||||
// set calendar time for next second of clock output
|
||||
tt = (time_t)(current_time + 1);
|
||||
localtime_r(&tt, &t);
|
||||
mktime(&t);
|
||||
tt = mktime(&t);
|
||||
|
||||
#if defined HAS_IF482
|
||||
|
||||
@@ -293,13 +296,13 @@ void clock_loop(void *taskparameter) { // ClockTask
|
||||
if (xTaskNotifyWait(0x00, ULONG_MAX, ¤t_time, txDelay) == pdTRUE) {
|
||||
tt = (time_t)(current_time + 1);
|
||||
localtime_r(&tt, &t);
|
||||
mktime(&t);
|
||||
tt = mktime(&t);
|
||||
}
|
||||
|
||||
// send IF482 telegram
|
||||
IF482.print(IF482_Frame(t)); // note: telegram is for *next* second
|
||||
IF482.print(IF482_Frame(tt)); // note: telegram is for *next* second
|
||||
|
||||
ESP_LOGD(TAG, "[%0.3f] IF482: %s", _seconds(), IF482_Frame(t));
|
||||
ESP_LOGD(TAG, "[%0.3f] IF482: %s", _seconds(), IF482_Frame(tt).c_str());
|
||||
|
||||
#elif defined HAS_DCF77
|
||||
|
||||
@@ -340,7 +343,7 @@ void clock_loop(void *taskparameter) { // ClockTask
|
||||
} // clock_loop()
|
||||
|
||||
// we use compile date to create a time_t reference "in the past"
|
||||
time_t compileTime(const String compile_date) {
|
||||
time_t compileTime(void) {
|
||||
|
||||
char s_month[5];
|
||||
int year;
|
||||
@@ -353,10 +356,11 @@ time_t compileTime(const String compile_date) {
|
||||
if (secs == -1) {
|
||||
|
||||
// determine date
|
||||
// we go one day back to bypass unknown timezone of local time
|
||||
sscanf(compile_date.c_str(), "%s %d %d", s_month, &t.tm_mday - 1, &year);
|
||||
sscanf(__DATE__, "%s %d %d", s_month, &t.tm_mday, &year);
|
||||
t.tm_mon = (strstr(month_names, s_month) - month_names) / 3;
|
||||
t.tm_year = year - 1900;
|
||||
// determine time
|
||||
sscanf(__TIME__, "%d:%d:%d", &t.tm_hour, &t.tm_min, &t.tm_sec);
|
||||
|
||||
// convert to secs local time
|
||||
secs = mktime(&t);
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ void timesync_init(void) {
|
||||
"timesync_proc", // name of task
|
||||
4096, // stack size of task
|
||||
(void *)1, // task parameter
|
||||
3, // priority of the task
|
||||
7, // priority of the task
|
||||
&timeSyncProcTask, // task handle
|
||||
1); // CPU core
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user