LoRa Pulse Counter Demo
This is my LoRaWAN enabled business card project.
It’s a four-channel 2KHz impulse counter based on an STM32L051K8.
Hardware features include:
- STM32L051K8 (64K flash, 8K RAM, Cortex M0+)
- RFM95W (SX1276 based radio module from HopeRF)
- dual SMA/UFL footprint
- status button and LED indicator
- UART with level converter
- reverse cell protection
- power supply for accessories (via “V+”)
- surge protection
- four inputs suitable for open-collector or dry-contact
The firmware is currently occupying half of available flash. Features include:
- low power operation (~2uA in stop mode)
- bespoke LoRaWAN stack
- device failure alarms
- tickless application timers
- XTAL fail-safe
- 32 bit counters stored in no-init volatile memory
- count on rising/falling/both
- alert on rising/falling/both/none
- double-buffer configuration store with fail-safe defaults
- multi-function status button with LED indicator
- UART protocol
- local and remote configuration/control via a common register system
- hard fault diagnostics
Configuration items include:
- (per input)
- pull-up/pull-down
- polarity
- rise-time (250us to 10s)
- counter trigger (rising/falling/both)
- alert trigger (none/rising/falling/both)
- publish enable/disable
- publish interval
- LoRaWAN parameters
- radio enabled/disabled
- user defined serial number (device name)
- low-battery level
Pin Map
Inputs
pin | signal | comment |
---|---|---|
1 | V+ | weak power supply / reference |
2 | 1 | channel 1 |
3 | 2 | channel 2 |
4 | GND | |
5 | V+ | |
6 | 3 | channel 3 |
7 | 4 | channel 4 |
8 | GND |
UART
The pin layout is compatible with a large number of commercial USB-to-serial adapters:
pin | signal | comment |
---|---|---|
1 | GND | mandatory |
2 | CTS | connected to GND |
3 | VCC | mandatory 3V3 or 5V (this powers the level converter) |
4 | TXD | transmit line |
5 | RXD | receive line |
6 | DTR | not connected |
Status Button and LED
The button has two actions depending on how it is pressed:
- A short press will cause the status LED to blink the device status code.
- A long press (>5s) will enable or disable joining mode (i.e. toggle the setting)
The status codes are as follows:
number of flashes | status |
---|---|
0 | dead |
1 | joined and downlink received in interval |
2 | joined but no downlink received in interval |
3 | joining |
4 | not-joined |
LoRaWAN Message Format
direction | port | name |
---|---|---|
up | 1 | power-up |
up | 2 | config-publish |
up | 3 | input-publish |
up | 4 | input-alert |
down | 1 | remote-write-register |
Power-Up
Sent once immediately after the first join-accept is received after system start. If MCU state is available from the time of reset, this is included in the message.
Counter-Publish ::= SEQUENCE
{
device-reset-reason BIT STRING (SIZE(8)) {
wdt,
hard-fault,
power,
user,
reserved-5,
reserved-6,
reserved-7,
reserved-8,
},
device-alarms BIT STRING (SIZE(8)) {
clock-failure,
low-battery,
over-temperature,
reserved-4,
reserved-5,
reserved-6,
reserved-7,
reserved-8
},
mcu-state SEQUENCE
{
r0 INTEGER (0..4294967295),
r1 INTEGER (0..4294967295),
r2 INTEGER (0..4294967295),
r3 INTEGER (0..4294967295),
r12 INTEGER (0..4294967295),
lr INTEGER (0..4294967295),
pc INTEGER (0..4294967295),
sr INTEGER (0..4294967295)
} OPTIONAL
}
The encoding is Octet Encoding Rules X.696 with the following exceptions:
- OPTIONAL mcu-state presence is detected by size
Config-Publish
TBD.
Input-Publish
Sent every publish-interval. The abstract structure is as follows:
Counter-Publish ::= SEQUENCE
{
device-alarms BIT STRING (SIZE(8)) {
clock-failure,
low-battery,
over-temperature,
reserved-4,
reserved-5,
reserved-6,
reserved-7,
reserved-8
},
input-alarms BIT STRING (SIZE(8)) {
input-1-active,
input-2-active,
input-3-active,
input-4-active,
reserved1,
reserved2,
reserved3,
reserved4
},
counter-presence BIT STRING (SIZE(8)) {
input-1-present,
input-2-present,
input-3-present,
input-4-present,
reserved-5,
reserved-6,
reserved-7,
reserved-8,
},
counter1 INTEGER (0..4294967295) OPTIONAL,
counter2 INTEGER (0..4294967295) OPTIONAL,
counter3 INTEGER (0..4294967295) OPTIONAL,
counter4 INTEGER (0..4294967295) OPTIONAL
}
The encoding is Octet Encoding Rules X.696 with the following exceptions:
- OPTIONAL counter fields are included according to corresponding counter-presence bit being set
Input-Alert
TBD.
Remote-Write-Register
TBD.
UART Protocol
UART Settings
- 38400 baud
- 8 data bits, 1 stop bit, no parity
Framing
The SLIP framing format is used to transport the CRC Layer.
CRC Layer
The CRC Layer consists of n bytes of payload and two bytes of CRC:
<payload 0><payload 1>...<payload n><CRC(MSB)><CRC(LSB)>
The CRC is ordered most significant byte first and is calculated over all payload bytes.
CRC parameters are as follows:
name | CRC-16-CCITT |
polynomial | 0x1021 |
initial value | 0xffff |
reflect input | no |
reflect output | no |
xor output | no |
Message Format
Messages are encoded according to the Aloof protocol.
The key feature of this format is that information and control points are exposed as “objects” which may be read and/or written. The objects have human readable names which map to numerical codes.
Aloof Objects
Aloof objects are described in a JSON document that maps object name to a numerical OID and type.
{
"log_severity_level": {"oid":"0x0000", "type":"u8"},
"boot_version": {"oid":"0x0001", "type":"string"},
"app_version": {"oid":"0x0002", "type":"string"},
"factory_id": {"oid":"0x0003", "type":"blob"},
"vbat": {"oid":"0x0004", "type":"u32", "scale":-3, "unit":"V"},
"ambient": {"oid":"0x0005", "type":"i32", "unit":"degC"},
"vbat_low_threshold": {"oid":"0x0006", "type":"u16", "scale":-3},
"device_name": {"oid":"0x0007", "type":"string"},
"device_alarm": {"oid":"0x0008", "type":"u8"},
"ambient_offset": {"oid":"0x0009", "type":"i16", "unit":"degC"},
"reset": {"oid":"0x000a", "type":"u8"},
"reset_reason": {"oid":"0x000b", "type":"u8"},
"dev_eui": {"oid":"0x0100", "type":"blob", "size":8},
"app_eui": {"oid":"0x0101", "type":"blob", "size":8},
"app_key": {"oid":"0x0102", "type":"blob", "size":16},
"enc_app_key": {"oid":"0x0103", "type":"blob", "size":16},
"join_enabled": {"oid":"0x0104", "type":"bool"},
"rate": {"oid":"0x0105", "type":"u8"},
"power": {"oid":"0x0106", "type":"u8"},
"adr_enabled": {"oid":"0x0107", "type":"bool"},
"joined": {"oid":"0x0108", "type":"bool"},
"dev_nonce": {"oid":"0x0109", "type":"u32"},
"counter1": {"oid":"0x0200", "type":"u32"},
"counter2": {"oid":"0x0201", "type":"u32"},
"counter3": {"oid":"0x0202", "type":"u32"},
"counter4": {"oid":"0x0203", "type":"u32"},
"input1_polarity": {"oid":"0x0204", "type":"u8", "enum":{"low":0,"high":1}},
"input2_polarity": {"oid":"0x0205", "type":"u8", "enum":{"low":0,"high":1}},
"input3_polarity": {"oid":"0x0206", "type":"u8", "enum":{"low":0,"high":1}},
"input4_polarity": {"oid":"0x0207", "type":"u8", "enum":{"low":0,"high":1}},
"input1_pull": {"oid":"0x0208", "type":"bool", "enum":{"down":0,"up":1}},
"input2_pull": {"oid":"0x0209", "type":"bool", "enum":{"down":0,"up":1}},
"input3_pull": {"oid":"0x020a", "type":"bool", "enum":{"down":0,"up":1}},
"input4_pull": {"oid":"0x020b", "type":"bool", "enum":{"down":0,"up":1}},
"input1_rise_time": {"oid":"0x020c", "type":"u8", "enum":{"none":0,"25ms":1,"250ms":2,"1000ms":3,"5000ms":4,"10000ms":5}},
"input2_rise_time": {"oid":"0x020d", "type":"u8", "enum":{"none":0,"25ms":1,"250ms":2,"1000ms":3,"5000ms":4,"10000ms":5}},
"input3_rise_time": {"oid":"0x020e", "type":"u8", "enum":{"none":0,"25ms":1,"250ms":2,"1000ms":3,"5000ms":4,"10000ms":5}},
"input4_rise_time": {"oid":"0x020f", "type":"u8", "enum":{"none":0,"25ms":1,"250ms":2,"1000ms":3,"5000ms":4,"10000ms":5}},
"input1_alert_trigger": {"oid":"0x0210", "type":"u8", "enum":{"never":0, "rising":1, "falling":2, "rising_falling":3}},
"input2_alert_trigger": {"oid":"0x0211", "type":"u8", "enum":{"never":0, "rising":1, "falling":2, "rising_falling":3}},
"input3_alert_trigger": {"oid":"0x0212", "type":"u8", "enum":{"never":0, "rising":1, "falling":2, "rising_falling":3}},
"input4_alert_trigger": {"oid":"0x0213", "type":"u8", "enum":{"never":0, "rising":1, "falling":2, "rising_falling":3}},
"input1_lockout": {"oid":"0x0214", "type":"u8", "unit":"min"},
"input2_lockout": {"oid":"0x0215", "type":"u8", "unit":"min"},
"input3_lockout": {"oid":"0x0216", "type":"u8", "unit":"min"},
"input4_lockout": {"oid":"0x0217", "type":"u8", "unit":"min"},
"counter1_publish": {"oid":"0x0218", "type":"bool"},
"counter2_publish": {"oid":"0x0219", "type":"bool"},
"counter3_publish": {"oid":"0x021a", "type":"bool"},
"counter4_publish": {"oid":"0x021b", "type":"bool"},
"publish_interval": {"oid":"0x021c", "type":"u16", "unit":"min"},
"alert_interval": {"oid":"0x021d", "type":"u8", "unit":"min"},
"input1_state": {"oid":"0x021e", "type":"bool"},
"input2_state": {"oid":"0x021f", "type":"bool"},
"input3_state": {"oid":"0x0220", "type":"bool"},
"input4_state": {"oid":"0x0221", "type":"bool"},
"counter1_trigger": {"oid":"0x0222", "type":"u8", "enum":{"rising":0,"falling":1,"rising_falling":2}},
"counter2_trigger": {"oid":"0x0223", "type":"u8", "enum":{"rising":0,"falling":1,"rising_falling":2}},
"counter3_trigger": {"oid":"0x0224", "type":"u8", "enum":{"rising":0,"falling":1,"rising_falling":2}},
"counter4_trigger": {"oid":"0x0225", "type":"u8", "enum":{"rising":0,"falling":1,"rising_falling":2}}
}
Aloof Alerts
TBD.