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
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
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
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.
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
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
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.