This commit is contained in:
Oliver Brandmueller 2018-06-22 22:03:42 +02:00
commit 08a0e2cf05
20 changed files with 813 additions and 213 deletions

View File

@ -20,23 +20,26 @@ This can all be done with a single small and cheap ESP32 board for less than $20
# Hardware # Hardware
Supported ESP32 based LoRa IoT boards: Supported ESP32 based LoRa IoT boards:
- **Heltec LoRa-32** *a)*
- **TTGOv1** *a)*
- **TTGOv2** *a,d)*
- **TTGOv2.1** *a),e)*
- **TTGO T-Beam** *d),e),f)*
- **Pycom LoPy** *b),f)*
- **Pycom LoPy4** *b),f)*
- **Pycom FiPy** *b),f)*
- **LoLin32** with [LoraNode32 shield](https://github.com/hallard/LoLin32-Lora) *b),c)*
- **LoLin32 Lite** with [LoraNode32-Lite shield](https://github.com/hallard/LoLin32-Lite-Lora) *b),c)*
a) on board OLED Display supported; - **Heltec LoRa-32**
b) on board RGB LED supported; - **TTGOv1**
c) on board Hardware unique DEVEUI supported; - **TTGOv2**
d) external wiring needed, see instructions in board.h file; - **TTGOv2.1**
e) battery voltage monitoring supported; - **TTGO T-Beam**
f) on board GPS supported (for Pycom PyTrack expansion needed) - **Pycom LoPy**
- **Pycom LoPy4**
- **Pycom FiPy**
- **LoLin32** + [LoraNode32 shield](https://github.com/hallard/LoLin32-Lora)
- **LoLin32 Lite** + [LoraNode32-Lite shield](https://github.com/hallard/LoLin32-Lite-Lora)
Depending on board hardware following features are supported:
- LED
- OLED Display
- RGB LED
- button
- silicon unique ID
- battery voltage monitoring
- GPS
Target platform must be selected in [platformio.ini](https://github.com/cyberman54/ESP32-Paxcounter/blob/master/platformio.ini).<br> Target platform must be selected in [platformio.ini](https://github.com/cyberman54/ESP32-Paxcounter/blob/master/platformio.ini).<br>
Hardware dependent settings (pinout etc.) are stored in board files in /hal directory.<br> Hardware dependent settings (pinout etc.) are stored in board files in /hal directory.<br>
@ -87,16 +90,16 @@ Note: If you use this software you do this at your own risk. That means that you
Paxcounter generates identifiers for sniffed MAC adresses and collects them temporary in the device's RAM for a configurable scan cycle time (default 240 seconds). After each scan cycle the collected identifiers are cleared. Identifiers are generated by salting and hashing MAC adresses. The random salt value changes after each scan cycle. Identifiers and MAC adresses are never transferred to the LoRaWAN network. No persistent storing of MAC adresses, identifiers or timestamps and no other kind of analytics than counting are implemented in this code. Wireless networks are not touched by this code, but MAC adresses from wireless devices as well within as not within wireless networks, regardless if encrypted or unencrypted, are sniffed and processed by this code. If the bluetooth option in the code is enabled, bluetooth MACs are scanned and processed by the included BLE stack, then hashed and counted by this code. Paxcounter generates identifiers for sniffed MAC adresses and collects them temporary in the device's RAM for a configurable scan cycle time (default 240 seconds). After each scan cycle the collected identifiers are cleared. Identifiers are generated by salting and hashing MAC adresses. The random salt value changes after each scan cycle. Identifiers and MAC adresses are never transferred to the LoRaWAN network. No persistent storing of MAC adresses, identifiers or timestamps and no other kind of analytics than counting are implemented in this code. Wireless networks are not touched by this code, but MAC adresses from wireless devices as well within as not within wireless networks, regardless if encrypted or unencrypted, are sniffed and processed by this code. If the bluetooth option in the code is enabled, bluetooth MACs are scanned and processed by the included BLE stack, then hashed and counted by this code.
# LED # LED blink pattern
Legend for mono color on board LED: **Mono color LED:**
- Single Flash (50ms): seen a new Wifi or BLE device - Single Flash (50ms): seen a new Wifi or BLE device
- Quick blink (20ms on each 1/5 second): joining LoRaWAN network in progress or pending - Quick blink (20ms on each 1/5 second): joining LoRaWAN network in progress or pending
- Small blink (10ms on each 1/2 second): LoRaWAN data transmit in progress or pending - Small blink (10ms on each 1/2 second): LoRaWAN data transmit in progress or pending
- Long blink (200ms on each 2 seconds): LoRaWAN stack error - Long blink (200ms on each 2 seconds): LoRaWAN stack error
Legend for RGB LED (LoPy/LoPy4/FiPy/Lolin32 only): **RGB LED:**
- Green each blink: seen a new Wifi device - Green each blink: seen a new Wifi device
- Magenta each blink: seen a new BLE device - Magenta each blink: seen a new BLE device
@ -104,7 +107,21 @@ Legend for RGB LED (LoPy/LoPy4/FiPy/Lolin32 only):
- Blue blink: LoRaWAN data transmit in progress or pending - Blue blink: LoRaWAN data transmit in progress or pending
- Red long blink: LoRaWAN stack error - Red long blink: LoRaWAN stack error
# Payload # Payload format
You can select different payload formats in [paxcounter.conf](src/paxcounter.conf#L40):
- ***Plain*** uses big endian format and generates json fields, e.g. useful for TTN console
- ***Packed*** uses little endian format and generates json fields
- [***CayenneLPP***](https://mydevices.com/cayenne/docs/lora/#lora-cayenne-low-power-payload-reference-implementation) generates MyDevices Cayenne readable fields
If you're using [TheThingsNetwork](https://www.thethingsnetwork.org/) (TTN) you may want to use a payload converter. Go to TTN Console - Application - Payload Formats and paste the code example below in tabs Decoder and Converter. This way your MQTT application can parse the fields `pax`, `ble` and `wifi`.
To track a paxcounter device with on board GPS and at the same time contribute to TTN coverage mapping, you simply activate the [TTNmapper integration](https://www.thethingsnetwork.org/docs/applications/ttnmapper/) in TTN Console. The formats *plain* and *packed* generate the fields `latitude`, `longitude` and `hdop` required by ttnmapper.
Hereafter described is the default *plain* format.
**LoRaWAN Port #1:** **LoRaWAN Port #1:**
@ -125,11 +142,7 @@ Legend for RGB LED (LoPy/LoPy4/FiPy/Lolin32 only):
- see remote control - - see remote control -
If you're using [TheThingsNetwork](https://www.thethingsnetwork.org/) (TTN) you may want to use a payload converter. Go to TTN Console - Application - Payload Formats and paste the code example below in tabs Decoder and Converter. Make sure that your application parses the fields `pax`, `ble` and `wifi`. [**plain_decoder.js**](src/TTN/plain_decoder.js)
To map a GPS capable paxcounter device and at the same time contribute to TTN coverage mapping, you simply activate the [TTNmapper integration](https://www.thethingsnetwork.org/docs/applications/ttnmapper/) in TTN Console. Paxcounter generates ttnmapper compatible data fields.
**Decoder:**
```javascript ```javascript
function Decoder(bytes, port) { function Decoder(bytes, port) {
@ -140,11 +153,11 @@ function Decoder(bytes, port) {
decoded.wifi = (bytes[i++] << 8) | bytes[i++]; decoded.wifi = (bytes[i++] << 8) | bytes[i++];
decoded.ble = (bytes[i++] << 8) | bytes[i++]; decoded.ble = (bytes[i++] << 8) | bytes[i++];
if (bytes.length > 4) { if (bytes.length > 4) {
decoded.latitude = ( (bytes[i++]) | (bytes[i++] << 8) | (bytes[i++] << 16) | bytes[i++] << 24 ); decoded.latitude = ( (bytes[i++] << 24) | (bytes[i++] << 16) | (bytes[i++] << 8) | bytes[i++] );
decoded.longitude = ( (bytes[i++]) | (bytes[i++] << 8) | (bytes[i++] << 16) | bytes[i++] << 24 ); decoded.longitude = ( (bytes[i++] << 24) | (bytes[i++] << 16) | (bytes[i++] << 8) | bytes[i++] );
decoded.sats = ( bytes[i++] | (bytes[i++] << 8) ); decoded.sats = ( bytes[i++] );
decoded.hdop = ( bytes[i++] | (bytes[i++] << 8) ); decoded.hdop = ( bytes[i++] << 8) | (bytes[i++] );
decoded.altitude = ( bytes[i++] | (bytes[i++] << 8) ); decoded.altitude = ( bytes[i++] << 8) | (bytes[i++] );
} }
} }
@ -152,7 +165,7 @@ function Decoder(bytes, port) {
} }
``` ```
**Converter:** [**plain_converter.js**](src/TTN/plain_converter.js)
```javascript ```javascript
function Converter(decoded, port) { function Converter(decoded, port) {
@ -264,7 +277,7 @@ Note: all settings are stored in NVRAM and will be reloaded when device starts.
0x80 get device configuration 0x80 get device configuration
device answers with it's current configuration. The configuration is a C structure declared in file [globals.h](src/globals.h#L32-L50) with the following definition: device answers with it's current configuration. The configuration is a C structure declared in file [main.h](src/main.h#L13-L31) with the following definition:
byte 1: Lora SF (7..12) [default 9] byte 1: Lora SF (7..12) [default 9]
byte 2: Lora TXpower (2..15) [default 15] byte 2: Lora TXpower (2..15) [default 15]
@ -283,25 +296,19 @@ device answers with it's current configuration. The configuration is a C structu
byte 16: GPS send data mode (1=on, 0=ff) [default 1] byte 16: GPS send data mode (1=on, 0=ff) [default 1]
bytes 17-27: Software version (ASCII format, terminating with zero) bytes 17-27: Software version (ASCII format, terminating with zero)
0x81 get device uptime 0x81 get device status
bytes 1-8: Uptime in seconds (little endian format) bytes 1-2: Battery voltage in millivolt, 0 if unreadable
bytes 3-10: Uptime in seconds
0x82 get device cpu temperature bytes 11-14: Chip temperature in degrees celsius
bytes 1-4: Chip temperature in degrees celsius (little endian format)
0x83 get device battery voltage
bytes 1-2: Battery voltage in millivolt, 0 if unreadable (little endian format)
0x84 get device GPS status 0x84 get device GPS status
bytes 1-4: Latitude bytes 1-4: Latitude
bytes 5-8: Longitude bytes 5-8: Longitude
byte 9-10: Number of satellites byte 9: Number of satellites
byte 11-12: HDOP byte 10-11: HDOP
bytes 13-14: altidute [meter] bytes 12-13: altidute [meter]
# License # License

View File

@ -27,6 +27,7 @@ description = Paxcounter is a proof-of-concept ESP32 device for metering passeng
[common_env_data] [common_env_data]
platform_espressif32 = espressif32@>=1.0.2 platform_espressif32 = espressif32@>=1.0.2
board_build.partitions = no_ota.csv board_build.partitions = no_ota.csv
lib_deps_all =
lib_deps_display = lib_deps_display =
U8g2@>=2.22.14 U8g2@>=2.22.14
lib_deps_rgbled = lib_deps_rgbled =
@ -55,6 +56,7 @@ board_build.partitions = ${common_env_data.board_build.partitions}
monitor_speed = 115200 monitor_speed = 115200
upload_speed = 115200 upload_speed = 115200
lib_deps = lib_deps =
${common_env_data.lib_deps_all}
${common_env_data.lib_deps_display} ${common_env_data.lib_deps_display}
build_flags = build_flags =
${common_env_data.build_flags} ${common_env_data.build_flags}
@ -68,6 +70,7 @@ board_build.partitions = ${common_env_data.board_build.partitions}
monitor_speed = 115200 monitor_speed = 115200
upload_speed = 115200 upload_speed = 115200
lib_deps = lib_deps =
${common_env_data.lib_deps_all}
${common_env_data.lib_deps_display} ${common_env_data.lib_deps_display}
build_flags = build_flags =
${common_env_data.build_flags} ${common_env_data.build_flags}
@ -81,6 +84,7 @@ board_build.partitions = ${common_env_data.board_build.partitions}
monitor_speed = 115200 monitor_speed = 115200
upload_speed = 921600 upload_speed = 921600
lib_deps = lib_deps =
${common_env_data.lib_deps_all}
${common_env_data.lib_deps_display} ${common_env_data.lib_deps_display}
build_flags = build_flags =
${common_env_data.build_flags} ${common_env_data.build_flags}
@ -94,6 +98,7 @@ board_build.partitions = ${common_env_data.board_build.partitions}
monitor_speed = 115200 monitor_speed = 115200
upload_speed = 921600 upload_speed = 921600
lib_deps = lib_deps =
${common_env_data.lib_deps_all}
${common_env_data.lib_deps_display} ${common_env_data.lib_deps_display}
build_flags = build_flags =
${common_env_data.build_flags} ${common_env_data.build_flags}
@ -107,19 +112,21 @@ board_build.partitions = ${common_env_data.board_build.partitions}
monitor_speed = 115200 monitor_speed = 115200
upload_speed = 921600 upload_speed = 921600
lib_deps = lib_deps =
${common_env_data.lib_deps_all}
${common_env_data.lib_deps_gps} ${common_env_data.lib_deps_gps}
build_flags = build_flags =
${common_env_data.build_flags} ${common_env_data.build_flags}
-include "src/hal/ttgobeam.h" -include "src/hal/ttgobeam.h"
[env:fipy] [env:fipy]
platform = espressif32@1.0.1 platform = ${common_env_data.platform_espressif32}
framework = arduino framework = arduino
board = esp32dev board = esp32dev
board_build.partitions = ${common_env_data.board_build.partitions} board_build.partitions = ${common_env_data.board_build.partitions}
monitor_speed = 115200 monitor_speed = 115200
upload_speed = 921600 upload_speed = 921600
lib_deps = lib_deps =
${common_env_data.lib_deps_all}
${common_env_data.lib_deps_rgbled} ${common_env_data.lib_deps_rgbled}
build_flags = build_flags =
${common_env_data.build_flags} ${common_env_data.build_flags}
@ -133,7 +140,9 @@ board_build.partitions = ${common_env_data.board_build.partitions}
monitor_speed = 115200 monitor_speed = 115200
upload_speed = 921600 upload_speed = 921600
lib_deps = lib_deps =
${common_env_data.lib_deps_all}
${common_env_data.lib_deps_rgbled} ${common_env_data.lib_deps_rgbled}
${common_env_data.lib_deps_gps}
build_flags = build_flags =
${common_env_data.build_flags} ${common_env_data.build_flags}
-include "src/hal/lopy.h" -include "src/hal/lopy.h"
@ -146,7 +155,9 @@ board_build.partitions = ${common_env_data.board_build.partitions}
monitor_speed = 115200 monitor_speed = 115200
upload_speed = 921600 upload_speed = 921600
lib_deps = lib_deps =
${common_env_data.lib_deps_all}
${common_env_data.lib_deps_rgbled} ${common_env_data.lib_deps_rgbled}
${common_env_data.lib_deps_gps}
build_flags = build_flags =
${common_env_data.build_flags} ${common_env_data.build_flags}
-include "src/hal/lopy4.h" -include "src/hal/lopy4.h"
@ -159,6 +170,7 @@ board_build.partitions = ${common_env_data.board_build.partitions}
monitor_speed = 115200 monitor_speed = 115200
upload_speed = 256000 upload_speed = 256000
lib_deps = lib_deps =
${common_env_data.lib_deps_all}
${common_env_data.lib_deps_rgbled} ${common_env_data.lib_deps_rgbled}
build_flags = build_flags =
${common_env_data.build_flags} ${common_env_data.build_flags}
@ -172,6 +184,7 @@ board_build.partitions = ${common_env_data.board_build.partitions}
monitor_speed = 115200 monitor_speed = 115200
upload_speed = 921600 upload_speed = 921600
lib_deps = lib_deps =
${common_env_data.lib_deps_all}
${common_env_data.lib_deps_rgbled} ${common_env_data.lib_deps_rgbled}
build_flags = build_flags =
${common_env_data.build_flags} ${common_env_data.build_flags}

View File

@ -0,0 +1,13 @@
// Converter for device payload encoder "PACKED"
// copy&paste to TTN Console -> Applications -> PayloadFormat -> Converter
function Converter(decoded, port) {
var converted = decoded;
if (port === 1) {
converted.pax = converted.ble + converted.wifi;
}
return converted;
}

167
src/TTN/packed_decoder.js Normal file
View File

@ -0,0 +1,167 @@
// Decoder for device payload encoder "PACKED"
// copy&paste to TTN Console -> Applications -> PayloadFormat -> Decoder
function Decoder(bytes, port) {
var decoded = {};
if (port === 1) {
// only counter data, no gps
if (bytes.length === 4) {
return decode(bytes, [uint16, uint16], ['wifi', 'ble']);
}
// combined counter and gps data
if (bytes.length === 17) {
return decode(bytes, [uint16, uint16, latLng, latLng, uint8, hdop, uint16], ['wifi', 'ble', 'latitude', 'longitude', 'sats', 'hdop', 'altitude']);
}
}
if (port === 2) {
// device status data
if (bytes.length === 12) {
return decode(bytes, [uint16, uptime, temperature], ['voltage', 'uptime', 'cputemp']);
}
// device config data
if (bytes.length === 8) {
return decode(bytes, [uint8, uint16, uint8, uint8, uint8, uint8, bitmap], ['lorasf', 'rssilimit', 'sendcycle', 'wifichancycle', 'blescantime', 'rgblum', 'flags']);
}
}
}
// ----- contents of /src/decoder.js --------------------------------------------
// https://github.com/thesolarnomad/lora-serialization/blob/master/src/decoder.js
var bytesToInt = function (bytes) {
var i = 0;
for (var x = 0; x < bytes.length; x++) {
i |= +(bytes[x] << (x * 8));
}
return i;
};
var uptime = function (bytes) {
if (bytes.length !== uptime.BYTES) {
throw new Error('uptime must have exactly 8 bytes');
}
return bytesToInt(bytes);
};
uptime.BYTES = 4;
var uint8 = function (bytes) {
if (bytes.length !== uint8.BYTES) {
throw new Error('int must have exactly 1 byte');
}
return bytesToInt(bytes);
};
uint8.BYTES = 1;
var uint16 = function (bytes) {
if (bytes.length !== uint16.BYTES) {
throw new Error('int must have exactly 2 bytes');
}
return bytesToInt(bytes);
};
uint16.BYTES = 2;
var latLng = function (bytes) {
if (bytes.length !== latLng.BYTES) {
throw new Error('Lat/Long must have exactly 4 bytes');
}
return bytesToInt(bytes) / 1e6;
};
latLng.BYTES = 4;
var hdop = function (bytes) {
if (bytes.length !== hdop.BYTES) {
throw new Error('hdop must have exactly 2 bytes');
}
return bytesToInt(bytes) / 100;
};
hdop.BYTES = 2;
var temperature = function (bytes) {
if (bytes.length !== temperature.BYTES) {
throw new Error('Temperature must have exactly 2 bytes');
}
var isNegative = bytes[0] & 0x80;
var b = ('00000000' + Number(bytes[0]).toString(2)).slice(-8)
+ ('00000000' + Number(bytes[1]).toString(2)).slice(-8);
if (isNegative) {
var arr = b.split('').map(function (x) { return !Number(x); });
for (var i = arr.length - 1; i > 0; i--) {
arr[i] = !arr[i];
if (arr[i]) {
break;
}
}
b = arr.map(Number).join('');
}
var t = parseInt(b, 2);
if (isNegative) {
t = -t;
}
return t / 1e2;
};
temperature.BYTES = 2;
var humidity = function (bytes) {
if (bytes.length !== humidity.BYTES) {
throw new Error('Humidity must have exactly 2 bytes');
}
var h = bytesToInt(bytes);
return h / 1e2;
};
humidity.BYTES = 2;
var bitmap = function (byte) {
if (byte.length !== bitmap.BYTES) {
throw new Error('Bitmap must have exactly 1 byte');
}
var i = bytesToInt(byte);
var bm = ('00000000' + Number(i).toString(2)).substr(-8).split('').map(Number).map(Boolean);
return ['adr', 'screensaver', 'screen', 'countermode', 'blescan', 'antenna', 'filter', 'gpsmode']
.reduce(function (obj, pos, index) {
obj[pos] = bm[index];
return obj;
}, {});
};
bitmap.BYTES = 1;
var decode = function (bytes, mask, names) {
var maskLength = mask.reduce(function (prev, cur) {
return prev + cur.BYTES;
}, 0);
if (bytes.length < maskLength) {
throw new Error('Mask length is ' + maskLength + ' whereas input is ' + bytes.length);
}
names = names || [];
var offset = 0;
return mask
.map(function (decodeFn) {
var current = bytes.slice(offset, offset += decodeFn.BYTES);
return decodeFn(current);
})
.reduce(function (prev, cur, idx) {
prev[names[idx] || idx] = cur;
return prev;
}, {});
};
if (typeof module === 'object' && typeof module.exports !== 'undefined') {
module.exports = {
uptime: uptime,
uint8: uint8,
uint16: uint16,
temperature: temperature,
humidity: humidity,
latLng: latLng,
hdop: hdop,
bitmap: bitmap,
decode: decode
};
}

View File

@ -0,0 +1,18 @@
// Converter for device payload encoder "PLAIN"
// copy&paste to TTN Console -> Applications -> PayloadFormat -> Converter
function Converter(decoded, port) {
var converted = decoded;
if (port === 1) {
converted.pax = converted.ble + converted.wifi;
if (converted.hdop) {
converted.hdop /= 100;
converted.latitude /= 1000000;
converted.longitude /= 1000000;
}
}
return converted;
}

23
src/TTN/plain_decoder.js Normal file
View File

@ -0,0 +1,23 @@
// Decoder for device payload encoder "PLAIN"
// copy&paste to TTN Console -> Applications -> PayloadFormat -> Decoder
function Decoder(bytes, port) {
var decoded = {};
if (port === 1) {
var i = 0;
decoded.wifi = (bytes[i++] << 8) | bytes[i++];
decoded.ble = (bytes[i++] << 8) | bytes[i++];
if (bytes.length > 4) {
decoded.latitude = ((bytes[i++] << 24) | (bytes[i++] << 16) | (bytes[i++] << 8) | bytes[i++]);
decoded.longitude = ((bytes[i++] << 24) | (bytes[i++] << 16) | (bytes[i++] << 8) | bytes[i++]);
decoded.sats = (bytes[i++]);
decoded.hdop = (bytes[i++] << 8) | (bytes[i++]);
decoded.altitude = (bytes[i++] << 8) | (bytes[i++]);
}
}
return decoded;
}

View File

@ -28,38 +28,7 @@
#include "rgb_led.h" #include "rgb_led.h"
#include "macsniff.h" #include "macsniff.h"
#include "main.h" #include "main.h"
#include "payload.h"
// Struct holding devices's runtime configuration
typedef struct {
uint8_t lorasf; // 7-12, lora spreadfactor
uint8_t txpower; // 2-15, lora tx power
uint8_t adrmode; // 0=disabled, 1=enabled
uint8_t screensaver; // 0=disabled, 1=enabled
uint8_t screenon; // 0=disabled, 1=enabled
uint8_t countermode; // 0=cyclic unconfirmed, 1=cumulative, 2=cyclic confirmed
int16_t rssilimit; // threshold for rssilimiter, negative value!
uint8_t sendcycle; // payload send cycle [seconds/2]
uint8_t wifichancycle; // wifi channel switch cycle [seconds/100]
uint8_t blescantime; // BLE scan cycle duration [seconds]
uint8_t blescan; // 0=disabled, 1=enabled
uint8_t wifiant; // 0=internal, 1=external (for LoPy/LoPy4)
uint8_t vendorfilter; // 0=disabled, 1=enabled
uint8_t rgblum; // RGB Led luminosity (0..100%)
uint8_t gpsmode; // 0=disabled, 1=enabled
char version[10]; // Firmware version
} configData_t;
#ifdef HAS_GPS
typedef struct {
uint32_t latitude;
uint32_t longitude;
uint8_t satellites;
uint16_t hdop;
uint16_t altitude;
} gpsStatus_t;
extern gpsStatus_t gps_status; // struct for storing gps data
extern TinyGPSPlus gps; // Make TinyGPS++ instance globally availabe
#endif
extern configData_t cfg; extern configData_t cfg;
extern uint64_t uptimecounter; extern uint64_t uptimecounter;
@ -69,6 +38,20 @@ extern int countermode, screensaver, adrmode, lorasf, txpower, rlim;
extern uint16_t macs_total, macs_wifi, macs_ble; // MAC counters extern uint16_t macs_total, macs_wifi, macs_ble; // MAC counters
extern std::set<uint16_t> macs; extern std::set<uint16_t> macs;
extern hw_timer_t extern hw_timer_t
*channelSwitch; // hardware timer used for wifi channel switching *channelSwitch; // hardware timer used for wifi channel switching
extern xref2u1_t rcmd_data; // buffer for rcommand results size
extern u1_t rcmd_data_size; // buffer for rcommand results size #ifdef HAS_GPS
extern gpsStatus_t gps_status; // struct for storing gps data
extern TinyGPSPlus gps; // Make TinyGPS++ instance globally availabe
#endif
// payload encoder
#if PAYLOAD_ENCODER == 1
extern TTNplain payload;
#elif PAYLOAD_ENCODER == 2
extern TTNpacked payload;
#elif PAYLOAD_ENCODER == 3
extern CayenneLPP payload;
#else
#error "No valid payload converter defined"
#endif

View File

@ -1,14 +1,15 @@
#ifdef HAS_GPS #ifdef HAS_GPS
#include "globals.h" #include "globals.h"
#include <Wire.h>
// Local logging tag // Local logging tag
static const char TAG[] = "main"; static const char TAG[] = "main";
// read GPS data and cast to global struct // read GPS data and cast to global struct
void gps_read() { void gps_read() {
gps_status.latitude = (uint32_t)(gps.location.lat() * 1000000); gps_status.latitude = (uint32_t)(gps.location.lat() * 1e6);
gps_status.longitude = (uint32_t)(gps.location.lng() * 1000000); gps_status.longitude = (uint32_t)(gps.location.lng() * 1e6);
gps_status.satellites = (uint8_t)gps.satellites.value(); gps_status.satellites = (uint8_t)gps.satellites.value();
gps_status.hdop = (uint16_t)gps.hdop.value(); gps_status.hdop = (uint16_t)gps.hdop.value();
gps_status.altitude = (uint16_t)gps.altitude.meters(); gps_status.altitude = (uint16_t)gps.altitude.meters();
@ -22,8 +23,8 @@ void gps_loop(void *pvParameters) {
// initialize and, if needed, configure, GPS // initialize and, if needed, configure, GPS
#if defined GPS_SERIAL #if defined GPS_SERIAL
HardwareSerial GPS_Serial(1); HardwareSerial GPS_Serial(1);
#elif defined GPS_I2C #elif defined GPS_QUECTEL_L76
// to be done Wire.begin(GPS_QUECTEL_L76, 400000); // I2C connect to GPS device with 400 KHz
#endif #endif
while (1) { while (1) {
@ -44,30 +45,29 @@ void gps_loop(void *pvParameters) {
// after GPS function was disabled, close connect to GPS device // after GPS function was disabled, close connect to GPS device
GPS_Serial.end(); GPS_Serial.end();
#elif defined GPS_I2C #elif defined GPS_QUECTEL_L76
// I2C connect to GPS device with 100 kHz Wire.beginTransmission(GPS_ADDR);
Wire.begin(GPS_I2C_PINS, 100000); Wire.write(0x00); // dummy write to start read
Wire.beginTransmission(GPS_I2C_ADDRESS_WRITE); Wire.endTransmission();
Wire.write(0x00);
i2c_ret == Wire.beginTransmission(GPS_I2C_ADDRESS_READ); Wire.beginTransmission(GPS_ADDR);
if (i2c_ret == 0) { // check if device seen on i2c bus while (cfg.gpsmode) {
while (cfg.gpsmode) { Wire.requestFrom(GPS_ADDR | 0x01, 32);
// feed GPS decoder with serial NMEA data from GPS device while (Wire.available()) {
while (Wire.available()) { gps.encode(Wire.read());
Wire.requestFrom(GPS_I2C_ADDRESS_READ, 255); vTaskDelay(1 / portTICK_PERIOD_MS); // polling mode: 500ms sleep
gps.encode(Wire.read());
vTaskDelay(1 / portTICK_PERIOD_MS); // reset watchdog
}
} }
// after GPS function was disabled, close connect to GPS device
Wire.endTransmission(); ESP_LOGI(TAG, "GPS NMEA data: passed %d / failed: %d / with fix: %d",
Wire.setClock(400000); // Set back to 400KHz to speed up OLED gps.passedChecksum(), gps.failedChecksum(),
gps.sentencesWithFix());
} }
// after GPS function was disabled, close connect to GPS device
#endif Wire.endTransmission();
#endif // GPS Type
} }
vTaskDelay(1 / portTICK_PERIOD_MS); // reset watchdog vTaskDelay(1 / portTICK_PERIOD_MS); // reset watchdog

View File

@ -5,13 +5,13 @@
#define HAS_RGB_LED GPIO_NUM_0 // WS2812B RGB LED on GPIO0 #define HAS_RGB_LED GPIO_NUM_0 // WS2812B RGB LED on GPIO0
// !!EXPERIMENTAL - not tested yet!! // !!EXPERIMENTAL - not tested yet!!
// uncomment this only if your LoPy lives on a Pytrack expansion board with GPS // uncomment this only if your LoPy runs on a Pytrack expansion board with GPS
// see http://www.quectel.com/UploadImage/Downlad/Quectel_L76-L_I2C_Application_Note_V1.0.pdf // see http://www.quectel.com/UploadImage/Downlad/Quectel_L76-L_I2C_Application_Note_V1.0.pdf
//#define HAS_GPS 1 //#define HAS_GPS 1
//#define GPS_I2C_PINS GPIO_NUM_9, GPIO_NUM_8 // SDA, SCL //#define GPS_QUECTEL_L76 GPIO_NUM_25, GPIO_NUM_26 // SDA (P22), SCL (P21)
//#define GPS_I2C_ADDRESS_READ 0x21 //#define GPS_ADDR 0x10
//#define GPS_I2C_ADDRESS_WRITE 0x20 //#define HAS_BUTTON GPIO_NUM_37 // (P14)
//#define HAS_BUTTON GPIO_NUM_4 //#define BUTTON_PULLUP 1 // Button need pullup instead of default pulldown
// Hardware pin definitions for Pycom LoPy board // Hardware pin definitions for Pycom LoPy board
#define PIN_SPI_SS GPIO_NUM_17 #define PIN_SPI_SS GPIO_NUM_17

View File

@ -4,14 +4,14 @@
#define HAS_LED NOT_A_PIN // LoPy4 has no on board LED, so we use RGB LED on LoPy4 #define HAS_LED NOT_A_PIN // LoPy4 has no on board LED, so we use RGB LED on LoPy4
#define HAS_RGB_LED GPIO_NUM_0 // WS2812B RGB LED on GPIO0 #define HAS_RGB_LED GPIO_NUM_0 // WS2812B RGB LED on GPIO0
// !!EXPERIMENTAL - not tested yet!!f // !!EXPERIMENTAL - not tested yet!!
// uncomment this only if your LoPy lives on a Pytrack expansion board with GPS // uncomment this only if your LoPy runs on a Pytrack expansion board with GPS
// see http://www.quectel.com/UploadImage/Downlad/Quectel_L76-L_I2C_Application_Note_V1.0.pdf // see http://www.quectel.com/UploadImage/Downlad/Quectel_L76-L_I2C_Application_Note_V1.0.pdf
//#define HAS_GPS 1 //#define HAS_GPS 1
//#define GPS_I2C_PINS GPIO_NUM_9, GPIO_NUM_8 // SDA, SCL //#define GPS_QUECTEL_L76 GPIO_NUM_25, GPIO_NUM_26 // SDA (P22), SCL (P21)
//#define GPS_I2C_ADDRESS_READ 0x21 //#define GPS_ADDR 0x10
//#define GPS_I2C_ADDRESS_WRITE 0x20 //#define HAS_BUTTON GPIO_NUM_37 // (P14)
//#define HAS_BUTTON GPIO_NUM_4 //#define BUTTON_PULLUP 1 // Button need pullup instead of default pulldown
// Hardware pin definitions for Pycom LoPy4 board // Hardware pin definitions for Pycom LoPy4 board
#define PIN_SPI_SS GPIO_NUM_18 #define PIN_SPI_SS GPIO_NUM_18

View File

@ -23,8 +23,8 @@
// Hardware pin definitions for TTGO V2 Board with OLED SSD1306 0,96" I2C Display // Hardware pin definitions for TTGO V2 Board with OLED SSD1306 0,96" I2C Display
#define OLED_RST U8X8_PIN_NONE // connected to CPU RST/EN #define OLED_RST U8X8_PIN_NONE // connected to CPU RST/EN
#define OLED_SDA GPIO_NUM_21 // ESP32 GPIO4 (Pin4) -- SD1306 D1+D2 #define OLED_SDA GPIO_NUM_21 // ESP32 GPIO21 -- SD1306 D1+D2
#define OLED_SCL GPIO_NUM_22 // ESP32 GPIO15 (Pin15) -- SD1306 D0 #define OLED_SCL GPIO_NUM_22 // ESP32 GPIO22 -- SD1306 D0
/* /*

View File

@ -1,12 +1,19 @@
// Hardware related definitions for TTGO V2.1 Board /* Hardware related definitions for TTGO V2.1 Board
/ ATTENTION: check your board version!
/ Different versions are on the market which need different settings in this file:
/ - without label -> "old"
/ - labelled v1.5 on pcb -> "old"
/ - labelled v1.6 on pcb -> "new"
*/
#define CFG_sx1276_radio 1 // HPD13A LoRa SoC #define CFG_sx1276_radio 1 // HPD13A LoRa SoC
#define HAS_DISPLAY U8X8_SSD1306_128X64_NONAME_HW_I2C #define HAS_DISPLAY U8X8_SSD1306_128X64_NONAME_HW_I2C
#define DISPLAY_FLIP 1 // rotated display #define DISPLAY_FLIP 1 // rotated display
#define HAS_LED GPIO_NUM_23 // green on board LED_G3 (not in initial board version) #define HAS_LED GPIO_NUM_23 // green on board LED (new board ONLY)
#define HAS_BATTERY_PROBE ADC1_GPIO35_CHANNEL // battery probe GPIO pin -> ADC1_CHANNEL_7 #define HAS_BATTERY_PROBE ADC1_GPIO35_CHANNEL // uses GPIO7 (new board ONLY)
#define BATT_FACTOR 2 // voltage divider 100k/100k on board #define BATT_FACTOR 2 // voltage divider 100k/100k on board (new board ONLY)
// re-define pin definitions of pins_arduino.h // re-define pin definitions of pins_arduino.h
#define PIN_SPI_SS GPIO_NUM_18 // ESP32 GPIO18 (Pin18) -- HPD13A NSS/SEL (Pin4) SPI Chip Select Input #define PIN_SPI_SS GPIO_NUM_18 // ESP32 GPIO18 (Pin18) -- HPD13A NSS/SEL (Pin4) SPI Chip Select Input
@ -15,12 +22,14 @@
#define PIN_SPI_SCK GPIO_NUM_5 // ESP32 GPIO5 (Pin5) -- HPD13A SCK (Pin5) SPI Clock Input #define PIN_SPI_SCK GPIO_NUM_5 // ESP32 GPIO5 (Pin5) -- HPD13A SCK (Pin5) SPI Clock Input
// non arduino pin definitions // non arduino pin definitions
#define RST LMIC_UNUSED_PIN // connected to ESP32 RST/EN #define RST LMIC_UNUSED_PIN // connected to ESP32 RST/EN (old board)
//#define RST GPIO_NUM_12 // (old board v1.5)
//#define RST GPIO_NUM_19 //(new board)
#define DIO0 GPIO_NUM_26 // ESP32 GPIO26 <-> HPD13A IO0 #define DIO0 GPIO_NUM_26 // ESP32 GPIO26 <-> HPD13A IO0
#define DIO1 GPIO_NUM_33 // ESP32 GPIO33 <-> HPDIO1 <-> HPD13A IO1 #define DIO1 GPIO_NUM_33 // ESP32 GPIO33 <-> HPDIO1 <-> HPD13A IO1
#define DIO2 GPIO_NUM_32 // ESP32 GPIO32 <-> HPDIO2 <-> HPD13A IO2 #define DIO2 GPIO_NUM_32 // ESP32 GPIO32 <-> HPDIO2 <-> HPD13A IO2
// Hardware pin definitions for TTGO V2 Board with OLED SSD1306 0,96" I2C Display // Hardware pin definitions for TTGO V2 Board with OLED SSD1306 0,96" I2C Display
#define OLED_RST U8X8_PIN_NONE // connected to CPU RST/EN #define OLED_RST U8X8_PIN_NONE // connected to CPU RST/EN
#define OLED_SDA GPIO_NUM_21 // ESP32 GPIO4 (Pin4) -- SD1306 D1+D2 #define OLED_SDA GPIO_NUM_21 // ESP32 GPIO21 -- SD1306 D1+D2
#define OLED_SCL GPIO_NUM_22 // ESP32 GPIO15 (Pin15) -- SD1306 D0 #define OLED_SCL GPIO_NUM_22 // ESP32 GPIO22 -- SD1306 D0

View File

@ -117,42 +117,21 @@ void do_send(osjob_t *j) {
return; return;
} }
// prepare payload with sum of unique WIFI MACs seen // Prepare payload with counter and, if applicable, gps data
static uint8_t mydata[4]; payload.reset();
payload.addCount(macs_wifi, cfg.blescan ? macs_ble : 0);
mydata[0] = (macs_wifi & 0xff00) >> 8;
mydata[1] = macs_wifi & 0xff;
if (cfg.blescan) {
// append sum of unique BLE MACs seen to payload
mydata[2] = (macs_ble & 0xff00) >> 8;
mydata[3] = macs_ble & 0xff;
} else {
mydata[2] = 0;
mydata[3] = 0;
}
#ifdef HAS_GPS #ifdef HAS_GPS
static uint8_t gpsdata[18]; if ((cfg.gpsmode) && (gps.location.isValid())) {
if (cfg.gpsmode && gps.location.isValid()) {
gps_read(); gps_read();
memcpy(gpsdata, mydata, 4); payload.addGPS(gps_status);
memcpy(gpsdata + 4, &gps_status, sizeof(gps_status));
ESP_LOGI(TAG, "lat=%.6f / lon=%.6f | %u Sats | HDOP=%.1f | Altitude=%u m",
gps_status.latitude / (float)1000000,
gps_status.longitude / (float)1000000, gps_status.satellites,
gps_status.hdop / (float)100, gps_status.altitude);
LMIC_setTxData2(COUNTERPORT, gpsdata, sizeof(gpsdata),
(cfg.countermode & 0x02));
ESP_LOGI(TAG, "%d bytes queued to send", sizeof(gpsdata));
} else {
#endif
LMIC_setTxData2(COUNTERPORT, mydata, sizeof(mydata),
(cfg.countermode & 0x02));
ESP_LOGI(TAG, "%d bytes queued to send", sizeof(mydata));
#ifdef HAS_GPS
} }
#endif #endif
// send payload
LMIC_setTxData2(COUNTERPORT, payload.getBuffer(), payload.getSize(),
(cfg.countermode & 0x02));
ESP_LOGI(TAG, "%d bytes queued to send", payload.getSize());
sprintf(display_lmic, "PACKET QUEUED"); sprintf(display_lmic, "PACKET QUEUED");
// clear counter if not in cumulative counter mode // clear counter if not in cumulative counter mode

View File

@ -76,7 +76,7 @@ bool mac_add(uint8_t *paddr, int8_t rssi, bool sniff_type) {
} }
// Log scan result // Log scan result
ESP_LOGI(TAG, ESP_LOGD(TAG,
"%s %s RSSI %ddBi -> MAC %s -> Hash %04X -> WiFi:%d BLTH:%d -> " "%s %s RSSI %ddBi -> MAC %s -> Hash %04X -> WiFi:%d BLTH:%d -> "
"%d Bytes left", "%d Bytes left",
added ? "new " : "known", added ? "new " : "known",

View File

@ -56,9 +56,7 @@ uint16_t LEDColor = COLOR_NONE; // state machine variable to set RGB LED color
hw_timer_t *displaytimer = hw_timer_t *displaytimer =
NULL; // configure hardware timer used for cyclic display refresh NULL; // configure hardware timer used for cyclic display refresh
hw_timer_t *channelSwitch = hw_timer_t *channelSwitch =
NULL; // configure hardware timer used for wifi channel switching NULL; // configure hardware timer used for wifi channel switching
xref2u1_t rcmd_data; // buffer for rcommand results size
u1_t rcmd_data_size; // buffer for rcommand results size
#ifdef HAS_GPS #ifdef HAS_GPS
gpsStatus_t gps_status; // struct for storing gps data gpsStatus_t gps_status; // struct for storing gps data
@ -72,6 +70,17 @@ portMUX_TYPE timerMux =
std::set<uint16_t> macs; // associative container holds total of unique MAC std::set<uint16_t> macs; // associative container holds total of unique MAC
// adress hashes (Wifi + BLE) // adress hashes (Wifi + BLE)
// initialize payload encoder
#if PAYLOAD_ENCODER == 1
TTNplain payload(PAYLOAD_BUFFER_SIZE);
#elif PAYLOAD_ENCODER == 2
TTNpacked payload(PAYLOAD_BUFFER_SIZE);
#elif PAYLOAD_ENCODER == 3
CayenneLPP payload(PAYLOAD_BUFFER_SIZE);
#else
#error "No valid payload converter defined"
#endif
// this variables will be changed in the ISR, and read in main loop // this variables will be changed in the ISR, and read in main loop
static volatile int ButtonPressedIRQ = 0, DisplayTimerIRQ = 0, static volatile int ButtonPressedIRQ = 0, DisplayTimerIRQ = 0,
ChannelTimerIRQ = 0; ChannelTimerIRQ = 0;
@ -459,6 +468,7 @@ void led_loop() {
* ------------------------------------------------------------ */ * ------------------------------------------------------------ */
void setup() { void setup() {
char features[64] = ""; char features[64] = "";
// disable brownout detection // disable brownout detection
@ -508,24 +518,24 @@ void setup() {
// initialize led if needed // initialize led if needed
#if (HAS_LED != NOT_A_PIN) #if (HAS_LED != NOT_A_PIN)
pinMode(HAS_LED, OUTPUT); pinMode(HAS_LED, OUTPUT);
strcat(features, " LED"); strcat_P(features, " LED");
#endif #endif
#ifdef HAS_RGB_LED #ifdef HAS_RGB_LED
rgb_set_color(COLOR_PINK); rgb_set_color(COLOR_PINK);
strcat(features, " RGB"); strcat_P(features, " RGB");
#endif #endif
// initialize button handling if needed // initialize button handling if needed
#ifdef HAS_BUTTON #ifdef HAS_BUTTON
strcat(features, " BTN_"); strcat_P(features, " BTN_");
#ifdef BUTTON_PULLUP #ifdef BUTTON_PULLUP
strcat(features, "PU"); strcat_P(features, "PU");
// install button interrupt (pullup mode) // install button interrupt (pullup mode)
pinMode(HAS_BUTTON, INPUT_PULLUP); pinMode(HAS_BUTTON, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(HAS_BUTTON), ButtonIRQ, RISING); attachInterrupt(digitalPinToInterrupt(HAS_BUTTON), ButtonIRQ, RISING);
#else #else
strcat(features, "PD"); strcat_P(features, "PD");
// install button interrupt (pulldown mode) // install button interrupt (pulldown mode)
pinMode(HAS_BUTTON, INPUT_PULLDOWN); pinMode(HAS_BUTTON, INPUT_PULLDOWN);
attachInterrupt(digitalPinToInterrupt(HAS_BUTTON), ButtonIRQ, FALLING); attachInterrupt(digitalPinToInterrupt(HAS_BUTTON), ButtonIRQ, FALLING);
@ -534,17 +544,17 @@ void setup() {
// initialize wifi antenna if needed // initialize wifi antenna if needed
#ifdef HAS_ANTENNA_SWITCH #ifdef HAS_ANTENNA_SWITCH
strcat(features, " ANT"); strcat_P(features, " ANT");
antenna_init(); antenna_init();
#endif #endif
// initialize gps if present // initialize gps if present
#ifdef HAS_GPS #ifdef HAS_GPS
strcat(features, " GPS"); strcat_P(features, " GPS");
#endif #endif
#ifdef HAS_DISPLAY #ifdef HAS_DISPLAY
strcat(features, " OLED"); strcat_P(features, " OLED");
// initialize display // initialize display
init_display(PROGNAME, PROGVERSION); init_display(PROGNAME, PROGVERSION);
DisplayState = cfg.screenon; DisplayState = cfg.screenon;
@ -580,8 +590,17 @@ void setup() {
timerAlarmWrite(channelSwitch, cfg.wifichancycle * 10000, true); timerAlarmWrite(channelSwitch, cfg.wifichancycle * 10000, true);
timerAlarmEnable(channelSwitch); timerAlarmEnable(channelSwitch);
// show payload encoder
#if PAYLOAD_ENCODER == 1
strcat_P(features, " PAYLOAD_PLAIN");
#elif PAYLOAD_ENCODER == 2
strcat_P(features, " PAYLOAD_PACKED");
#elif PAYLOAD_ENCODER == 3
strcat_P(features, " PAYLOAD_CAYENNE");
#endif
// show compiled features // show compiled features
ESP_LOGI(TAG, "Features %s", features); ESP_LOGI(TAG, "Features: %s", features);
// output LoRaWAN keys to console // output LoRaWAN keys to console
#ifdef VERBOSE #ifdef VERBOSE
@ -621,8 +640,7 @@ void setup() {
} }
#endif #endif
// if device has GPS and GPS function is enabled, start GPS reader task on core // if device has GPS and it is enabled, start GPS reader task on core 0
// 0
#ifdef HAS_GPS #ifdef HAS_GPS
if (cfg.gpsmode) { if (cfg.gpsmode) {
ESP_LOGI(TAG, "Starting GPS task on core 0"); ESP_LOGI(TAG, "Starting GPS task on core 0");
@ -630,8 +648,7 @@ void setup() {
} }
#endif #endif
// kickoff sendjob -> joins network and rescedules sendjob for cyclic // joins network and rescedules sendjob for cyclic transmitting payload
// transmitting payload
do_send(&sendjob); do_send(&sendjob);
} }
@ -674,11 +691,22 @@ void loop() {
} }
#ifdef HAS_GPS #ifdef HAS_GPS
// log NMEA status every 30 seconds, useful for debugging GPS connection // log NMEA status every 60 seconds, useful for debugging GPS connection
if ((uptime() % 30000) == 0) if ((uptime() % 60000) == 0) {
ESP_LOGI(TAG, "GPS NMEA data: passed %d / failed: %d / with fix: %d", ESP_LOGI(TAG, "GPS NMEA data: passed %d / failed: %d / with fix: %d",
gps.passedChecksum(), gps.failedChecksum(), gps.passedChecksum(), gps.failedChecksum(),
gps.sentencesWithFix()); gps.sentencesWithFix());
if ((cfg.gpsmode) && (gps.location.isValid())) {
gps_read();
ESP_LOGI(TAG,
"lat=%.6f | lon=%.6f | %u Sats | HDOP=%.1f | Altitude=%um",
gps_status.latitude / (float)1e6,
gps_status.longitude / (float)1e6, gps_status.satellites,
gps_status.hdop / (float)100, gps_status.altitude);
} else {
ESP_LOGI(TAG, "No valid GPS position or GPS disabled");
}
}
#endif #endif
vTaskDelay(1 / portTICK_PERIOD_MS); // reset watchdog vTaskDelay(1 / portTICK_PERIOD_MS); // reset watchdog

View File

@ -5,11 +5,43 @@
// program version - note: increment version after modifications to configData_t // program version - note: increment version after modifications to configData_t
// struct!! // struct!!
#define PROGVERSION "1.3.81" // use max 10 chars here! #define PROGVERSION "1.3.82" // use max 10 chars here!
#define PROGNAME "PAXCNT" #define PROGNAME "PAXCNT"
//--- Declarations --- //--- Declarations ---
// Struct holding devices's runtime configuration
typedef struct {
uint8_t lorasf; // 7-12, lora spreadfactor
uint8_t txpower; // 2-15, lora tx power
uint8_t adrmode; // 0=disabled, 1=enabled
uint8_t screensaver; // 0=disabled, 1=enabled
uint8_t screenon; // 0=disabled, 1=enabled
uint8_t countermode; // 0=cyclic unconfirmed, 1=cumulative, 2=cyclic confirmed
int16_t rssilimit; // threshold for rssilimiter, negative value!
uint8_t sendcycle; // payload send cycle [seconds/2]
uint8_t wifichancycle; // wifi channel switch cycle [seconds/100]
uint8_t blescantime; // BLE scan cycle duration [seconds]
uint8_t blescan; // 0=disabled, 1=enabled
uint8_t wifiant; // 0=internal, 1=external (for LoPy/LoPy4)
uint8_t vendorfilter; // 0=disabled, 1=enabled
uint8_t rgblum; // RGB Led luminosity (0..100%)
uint8_t gpsmode; // 0=disabled, 1=enabled
char version[10]; // Firmware version
} configData_t;
#ifdef HAS_GPS
typedef struct {
uint32_t latitude;
uint32_t longitude;
uint8_t satellites;
uint16_t hdop;
uint16_t altitude;
} gpsStatus_t;
extern gpsStatus_t gps_status; // struct for storing gps data
extern TinyGPSPlus gps; // Make TinyGPS++ instance globally availabe
#endif
enum led_states { LED_OFF, LED_ON }; enum led_states { LED_OFF, LED_ON };
#if defined(CFG_eu868) #if defined(CFG_eu868)
@ -35,4 +67,4 @@ void stop_BLEscan(void);
#ifdef HAS_GPS #ifdef HAS_GPS
void gps_read(void); void gps_read(void);
void gps_loop(void *pvParameters); void gps_loop(void *pvParameters);
#endif #endif

View File

@ -1,6 +1,8 @@
// ----- Paxcounter user config file ------ // ----- Paxcounter user config file ------
// //
// --> adapt to your needs and use case <-- // --> adapt to your needs and use case <--
//
// Note: After editing, before "build", use "clean" button in PlatformIO!
// Verbose enables serial output // Verbose enables serial output
#define VERBOSE 1 // comment out to silence the device, for mute use build option #define VERBOSE 1 // comment out to silence the device, for mute use build option
@ -34,16 +36,17 @@
#define WIFI_MY_COUNTRY "EU" // select locale for Wifi RF settings #define WIFI_MY_COUNTRY "EU" // select locale for Wifi RF settings
#define WIFI_CHANNEL_SWITCH_INTERVAL 50 // [seconds/100] -> 0,5 sec. #define WIFI_CHANNEL_SWITCH_INTERVAL 50 // [seconds/100] -> 0,5 sec.
// LoRa payload send cycle --> take care of duty cycle of LoRaWAN network! <-- // LoRa payload parameters
#define SEND_SECS 120 // [seconds/2] -> 240 sec. #define PAYLOAD_ENCODER 1 // select payload encoder: 1=Plain [default], 2=Packed, 3=CayenneLPP
#define SEND_SECS 120 // payload send cycle [seconds/2] -> 240 sec.
#define MEM_LOW 2048 // [Bytes] low memory threshold triggering a send cycle #define MEM_LOW 2048 // [Bytes] low memory threshold triggering a send cycle
#define RETRANSMIT_RCMD 5 // [seconds] wait time before retransmitting rcommand results #define RETRANSMIT_RCMD 5 // [seconds] wait time before retransmitting rcommand results
#define PAYLOAD_BUFFER_SIZE 51 // maximum size of payload block per transmit
// Default LoRa Spreadfactor // Default LoRa Spreadfactor
#define LORASFDEFAULT 9 // 7 ... 12 SF, according to LoRaWAN specs #define LORASFDEFAULT 9 // 7 ... 12 SF, according to LoRaWAN specs
#define MAXLORARETRY 500 // maximum count of TX retries if LoRa busy #define MAXLORARETRY 500 // maximum count of TX retries if LoRa busy
#define RCMDPORT 2 // LoRaWAN Port on which device listenes for remote commands #define RCMDPORT 2 // LoRaWAN Port on which device listenes for remote commands
#define GPSPORT 3 // LoRaWAN Port on which device sends gps data
#define COUNTERPORT 1 // LoRaWAN Port on which device sends counts #define COUNTERPORT 1 // LoRaWAN Port on which device sends counts
// Default RGB LED luminosity (in %) // Default RGB LED luminosity (in %)

246
src/payload.cpp Normal file
View File

@ -0,0 +1,246 @@
#include "globals.h"
#include "payload.h"
/* ---------------- plain format without special encoding ---------- */
TTNplain::TTNplain(uint8_t size) {
buffer = (uint8_t *)malloc(size);
cursor = 0;
}
TTNplain::~TTNplain(void) { free(buffer); }
void TTNplain::reset(void) { cursor = 0; }
uint8_t TTNplain::getSize(void) { return cursor; }
uint8_t *TTNplain::getBuffer(void) { return buffer; }
void TTNplain::addCount(uint16_t value1, uint16_t value2) {
buffer[cursor++] = value1 >> 8;
buffer[cursor++] = value1;
buffer[cursor++] = value2 >> 8;
buffer[cursor++] = value2;
}
#ifdef HAS_GPS
void TTNplain::addGPS(gpsStatus_t value) {
buffer[cursor++] = value.latitude >> 24;
buffer[cursor++] = value.latitude >> 16;
buffer[cursor++] = value.latitude >> 8;
buffer[cursor++] = value.latitude;
buffer[cursor++] = value.longitude >> 24;
buffer[cursor++] = value.longitude >> 16;
buffer[cursor++] = value.longitude >> 8;
buffer[cursor++] = value.longitude;
buffer[cursor++] = value.satellites;
buffer[cursor++] = value.hdop >> 8;
buffer[cursor++] = value.hdop;
buffer[cursor++] = value.altitude >> 8;
buffer[cursor++] = value.altitude;
}
#endif
void TTNplain::addConfig(configData_t value) {
buffer[cursor++] = value.lorasf;
buffer[cursor++] = value.adrmode;
buffer[cursor++] = value.screensaver;
buffer[cursor++] = value.screenon;
buffer[cursor++] = value.countermode;
buffer[cursor++] = value.rssilimit >> 8;
buffer[cursor++] = value.rssilimit;
buffer[cursor++] = value.sendcycle;
buffer[cursor++] = value.wifichancycle;
buffer[cursor++] = value.blescantime;
buffer[cursor++] = value.blescan;
buffer[cursor++] = value.wifiant;
buffer[cursor++] = value.vendorfilter;
buffer[cursor++] = value.rgblum;
buffer[cursor++] = value.gpsmode;
memcpy(buffer + cursor, value.version, 10);
cursor += 10;
}
void TTNplain::addStatus(uint16_t voltage, uint64_t uptime, float cputemp) {
buffer[cursor++] = voltage >> 8;
buffer[cursor++] = voltage;
buffer[cursor++] = uptime >> 56;
buffer[cursor++] = uptime >> 48;
buffer[cursor++] = uptime >> 40;
buffer[cursor++] = uptime >> 32;
buffer[cursor++] = uptime >> 24;
buffer[cursor++] = uptime >> 16;
buffer[cursor++] = uptime >> 8;
buffer[cursor++] = uptime;
buffer[cursor++] = (uint32_t)cputemp >> 24;
buffer[cursor++] = (uint32_t)cputemp >> 16;
buffer[cursor++] = (uint32_t)cputemp >> 8;
buffer[cursor++] = (uint32_t)cputemp;
}
/* ---------------- packed format with LoRa serialization Encoder ---------- */
// derived from
// https://github.com/thesolarnomad/lora-serialization/blob/master/src/LoraEncoder.cpp
TTNpacked::TTNpacked(uint8_t size) {
buffer = (uint8_t *)malloc(size);
cursor = 0;
}
TTNpacked::~TTNpacked(void) { free(buffer); }
void TTNpacked::reset(void) { cursor = 0; }
uint8_t TTNpacked::getSize(void) { return cursor; }
uint8_t *TTNpacked::getBuffer(void) { return buffer; }
void TTNpacked::addCount(uint16_t value1, uint16_t value2) {
writeUint16(value1);
writeUint16(value2);
}
#ifdef HAS_GPS
void TTNpacked::addGPS(gpsStatus_t value) {
writeLatLng(value.latitude, value.longitude);
writeUint8(value.satellites);
writeUint16(value.hdop);
writeUint16(value.altitude);
}
#endif
void TTNpacked::addConfig(configData_t value) {
writeUint8(value.lorasf);
writeUint16(value.rssilimit);
writeUint8(value.sendcycle);
writeUint8(value.wifichancycle);
writeUint8(value.blescantime);
writeUint8(value.rgblum);
writeBitmap(value.adrmode ? true : false, value.screensaver ? true : false,
value.screenon ? true : false, value.countermode ? true : false,
value.blescan ? true : false, value.wifiant ? true : false,
value.vendorfilter ? true : false, value.gpsmode ? true : false);
}
void TTNpacked::addStatus(uint16_t voltage, uint64_t uptime, float cputemp) {
writeUint16(voltage);
writeUptime(uptime);
writeTemperature(cputemp);
}
void TTNpacked::intToBytes(uint8_t pos, int32_t i, uint8_t byteSize) {
for (uint8_t x = 0; x < byteSize; x++) {
buffer[x + pos] = (byte)(i >> (x * 8));
}
cursor += byteSize;
}
void TTNpacked::writeUptime(uint64_t uptime) {
intToBytes(cursor, uptime, 8);
}
void TTNpacked::writeLatLng(double latitude, double longitude) {
intToBytes(cursor, latitude, 4);
intToBytes(cursor, longitude, 4);
}
void TTNpacked::writeUint16(uint16_t i) { intToBytes(cursor, i, 2); }
void TTNpacked::writeUint8(uint8_t i) { intToBytes(cursor, i, 1); }
void TTNpacked::writeHumidity(float humidity) {
int16_t h = (int16_t)(humidity * 100);
intToBytes(cursor, h, 2);
}
/**
* Uses a 16bit two's complement with two decimals, so the range is
* -327.68 to +327.67 degrees
*/
void TTNpacked::writeTemperature(float temperature) {
int16_t t = (int16_t)(temperature * 100);
if (temperature < 0) {
t = ~-t;
t = t + 1;
}
buffer[cursor++] = (byte)((t >> 8) & 0xFF);
buffer[cursor++] = (byte)t & 0xFF;
}
void TTNpacked::writeBitmap(bool a, bool b, bool c, bool d, bool e, bool f,
bool g, bool h) {
uint8_t bitmap = 0;
// LSB first
bitmap |= (a & 1) << 7;
bitmap |= (b & 1) << 6;
bitmap |= (c & 1) << 5;
bitmap |= (d & 1) << 4;
bitmap |= (e & 1) << 3;
bitmap |= (f & 1) << 2;
bitmap |= (g & 1) << 1;
bitmap |= (h & 1) << 0;
writeUint8(bitmap);
}
/* ---------------- Cayenne LPP format ---------- */
CayenneLPP::CayenneLPP(uint8_t size) {
buffer = (uint8_t *)malloc(size);
cursor = 0;
}
CayenneLPP::~CayenneLPP(void) { free(buffer); }
void CayenneLPP::reset(void) { cursor = 0; }
uint8_t CayenneLPP::getSize(void) { return cursor; }
uint8_t *CayenneLPP::getBuffer(void) { return buffer; }
void CayenneLPP::addCount(uint16_t value1, uint16_t value2) {
buffer[cursor++] = LPP_COUNT_WIFI_CHANNEL;
buffer[cursor++] = LPP_ANALOG_INPUT; // workaround, type meter not found?
buffer[cursor++] = value1 >> 8;
buffer[cursor++] = value1;
buffer[cursor++] = LPP_COUNT_BLE_CHANNEL;
buffer[cursor++] = LPP_ANALOG_INPUT; // workaround, type meter not found?
buffer[cursor++] = value1 >> 8;
buffer[cursor++] = value2;
}
#ifdef HAS_GPS
void CayenneLPP::addGPS(gpsStatus_t value) {
int32_t lat = value.latitude / 100;
int32_t lon = value.longitude / 100;
int32_t alt = value.altitude;
buffer[cursor++] = LPP_GPS_CHANNEL;
buffer[cursor++] = LPP_GPS;
buffer[cursor++] = lat >> 16;
buffer[cursor++] = lat >> 8;
buffer[cursor++] = lat;
buffer[cursor++] = lon >> 16;
buffer[cursor++] = lon >> 8;
buffer[cursor++] = lon;
buffer[cursor++] = alt >> 16;
buffer[cursor++] = alt >> 8;
buffer[cursor++] = alt;
}
#endif
void CayenneLPP::addConfig(configData_t value) {
buffer[cursor++] = LPP_ADR_CHANNEL;
buffer[cursor++] = LPP_DIGITAL_INPUT;
buffer[cursor++] = value.adrmode;
}
void CayenneLPP::addStatus(uint16_t voltage, uint64_t uptime, float cputemp) {
buffer[cursor++] = LPP_BATT_CHANNEL;
buffer[cursor++] = LPP_ANALOG_INPUT;
buffer[cursor++] = voltage >> 8;
buffer[cursor++] = voltage;
buffer[cursor++] = LPP_TEMP_CHANNEL;
buffer[cursor++] = LPP_TEMPERATURE;
buffer[cursor++] = (uint16_t)cputemp >> 8;
buffer[cursor++] = (uint16_t)cputemp;
}

95
src/payload.h Normal file
View File

@ -0,0 +1,95 @@
#ifndef _PAYLOAD_H_
#define _PAYLOAD_H_
#include <Arduino.h>
// MyDevices CayenneLPP channels
#define LPP_GPS_CHANNEL 20
#define LPP_COUNT_WIFI_CHANNEL 21
#define LPP_COUNT_BLE_CHANNEL 22
#define LPP_BATT_CHANNEL 23
#define LPP_ADR_CHANNEL 25
#define LPP_TEMP_CHANNEL 26
// MyDevices CayenneLPP types
#define LPP_GPS 136 // 3 byte lon/lat 0.0001 °, 3 bytes alt 0.01m
#define LPP_TEMPERATURE 103 // 2 bytes, 0.1°C signed
#define LPP_DIGITAL_INPUT 0 // 1 byte
#define LPP_DIGITAL_OUTPUT 1 // 1 byte
#define LPP_ANALOG_INPUT 2 // 2 bytes, 0.01 signed
#define LPP_LUMINOSITY 101 // 2 bytes, 1 lux unsigned
class TTNplain {
public:
TTNplain(uint8_t size);
~TTNplain();
void reset(void);
uint8_t getSize(void);
uint8_t *getBuffer(void);
void addCount(uint16_t value1, uint16_t value2);
void addConfig(configData_t value);
void addStatus(uint16_t voltage, uint64_t uptime, float cputemp);
#ifdef HAS_GPS
void addGPS(gpsStatus_t value);
#endif
private:
uint8_t *buffer;
uint8_t cursor;
};
class TTNpacked {
public:
TTNpacked(uint8_t size);
~TTNpacked();
void reset(void);
uint8_t getSize(void);
uint8_t *getBuffer(void);
void addCount(uint16_t value1, uint16_t value2);
void addConfig(configData_t value);
void addStatus(uint16_t voltage, uint64_t uptime, float cputemp);
#ifdef HAS_GPS
void addGPS(gpsStatus_t value);
#endif
private:
uint8_t *buffer;
uint8_t cursor;
void intToBytes(uint8_t pos, int32_t i, uint8_t byteSize);
void writeUptime(uint64_t unixtime);
void writeLatLng(double latitude, double longitude);
void writeUint16(uint16_t i);
void writeUint8(uint8_t i);
void writeHumidity(float humidity);
void writeTemperature(float temperature);
void writeBitmap(bool a, bool b, bool c, bool d, bool e, bool f, bool g,
bool h);
};
class CayenneLPP {
public:
CayenneLPP(uint8_t size);
~CayenneLPP();
void reset(void);
uint8_t getSize(void);
uint8_t *getBuffer(void);
void addCount(uint16_t value1, uint16_t value2);
void addConfig(configData_t value);
void addStatus(uint16_t voltage, uint64_t uptime, float cputemp);
#ifdef HAS_GPS
void addGPS(gpsStatus_t value);
#endif
private:
uint8_t *buffer;
uint8_t maxsize;
uint8_t cursor;
};
#endif // _PAYLOAD_H_

View File

@ -27,7 +27,7 @@ void antenna_select(const uint8_t _ant);
// function defined in adcread.cpp // function defined in adcread.cpp
#ifdef HAS_BATTERY_PROBE #ifdef HAS_BATTERY_PROBE
uint32_t read_voltage(void); uint16_t read_voltage(void);
#endif #endif
// function sends result of get commands to LoRaWAN network // function sends result of get commands to LoRaWAN network
@ -40,20 +40,13 @@ void do_transmit(osjob_t *j) {
os_setTimedCallback(&rcmdjob, os_getTime() + sec2osticks(RETRANSMIT_RCMD), os_setTimedCallback(&rcmdjob, os_getTime() + sec2osticks(RETRANSMIT_RCMD),
do_transmit); do_transmit);
} }
LMIC_setTxData2(RCMDPORT, rcmd_data, rcmd_data_size, // send payload
0); // send data unconfirmed on RCMD Port LMIC_setTxData2(RCMDPORT, payload.getBuffer(), payload.getSize(),
ESP_LOGI(TAG, "%d bytes queued to send", rcmd_data_size); (cfg.countermode & 0x02));
ESP_LOGI(TAG, "%d bytes queued to send", payload.getSize());
sprintf(display_lmic, "PACKET QUEUED"); sprintf(display_lmic, "PACKET QUEUED");
} }
// help function to transmit result of get commands, since callback function
// do_transmit() cannot have params
void transmit(xref2u1_t mydata, u1_t mydata_size) {
rcmd_data = mydata;
rcmd_data_size = mydata_size;
do_transmit(&rcmdjob);
}
// help function to assign LoRa datarates to numeric spreadfactor values // help function to assign LoRa datarates to numeric spreadfactor values
void switch_lora(uint8_t sf, uint8_t tx) { void switch_lora(uint8_t sf, uint8_t tx) {
if (tx > 20) if (tx > 20)
@ -292,41 +285,33 @@ void set_lorapower(uint8_t val) {
}; };
void get_config(uint8_t val) { void get_config(uint8_t val) {
ESP_LOGI(TAG, "Remote command: get configuration"); ESP_LOGI(TAG, "Remote command: get device configuration");
transmit((byte *)&cfg, sizeof(cfg)); payload.reset();
payload.addConfig(cfg);
do_transmit(&rcmdjob);
}; };
void get_uptime(uint8_t val) { void get_status(uint8_t val) {
ESP_LOGI(TAG, "Remote command: get uptime"); ESP_LOGI(TAG, "Remote command: get device status");
transmit((byte *)&uptimecounter, sizeof(uptimecounter));
};
void get_cputemp(uint8_t val) {
ESP_LOGI(TAG, "Remote command: get cpu temperature");
float temp = temperatureRead();
transmit((byte *)&temp, sizeof(temp));
};
void get_voltage(uint8_t val) {
ESP_LOGI(TAG, "Remote command: get battery voltage");
#ifdef HAS_BATTERY_PROBE #ifdef HAS_BATTERY_PROBE
uint16_t voltage = read_voltage(); uint16_t voltage = read_voltage();
#else #else
uint16_t voltage = 0; uint16_t voltage = 0;
#endif #endif
transmit((byte *)&voltage, sizeof(voltage)); payload.reset();
payload.addStatus(voltage, uptimecounter, temperatureRead());
do_transmit(&rcmdjob);
}; };
void get_gps(uint8_t val) { void get_gps(uint8_t val) {
ESP_LOGI(TAG, "Remote command: get gps status"); ESP_LOGI(TAG, "Remote command: get gps status");
#ifdef HAS_GPS #ifdef HAS_GPS
gps_read(); gps_read();
transmit((byte *)&gps_status, sizeof(gps_status)); payload.reset();
ESP_LOGI(TAG, "lat=%f / lon=%f | Sats=%u | HDOP=%u | Alti=%u", payload.addGPS(gps_status);
gps_status.latitude / 1000000, gps_status.longitude / 1000000, do_transmit(&rcmdjob);
gps_status.satellites, gps_status.hdop, gps_status.altitude);
#else #else
ESP_LOGE(TAG, "GPS not present"); ESP_LOGW(TAG, "GPS function not supported");
#endif #endif
}; };
@ -341,8 +326,7 @@ cmd_t table[] = {{0x01, set_rssi, true}, {0x02, set_countmode, true},
{0x0b, set_wifichancycle, true}, {0x0c, set_blescantime, true}, {0x0b, set_wifichancycle, true}, {0x0c, set_blescantime, true},
{0x0d, set_vendorfilter, false}, {0x0e, set_blescan, true}, {0x0d, set_vendorfilter, false}, {0x0e, set_blescan, true},
{0x0f, set_wifiant, true}, {0x10, set_rgblum, true}, {0x0f, set_wifiant, true}, {0x10, set_rgblum, true},
{0x80, get_config, false}, {0x81, get_uptime, false}, {0x80, get_config, false}, {0x81, get_status, false},
{0x82, get_cputemp, false}, {0x83, get_voltage, false},
{0x84, get_gps, false}}; {0x84, get_gps, false}};
// check and execute remote command // check and execute remote command