LoRaWAN Duress Button System
Step-by-step build guide: 7x wireless duress buttons integrating with a Bosch Solution 6000 alarm panel
ℹ About this guide
This is a first-timer walkthrough. If you've never built a LoRaWAN system before, follow it in order — each phase builds on the one before it. Don't skip ahead. Every callout box is there because something will go wrong if you ignore it.
1. System Overview & How It Works
Project: 7x wireless duress buttons feeding into an existing Bosch Solution 6000 alarm panel.
Use case: Mining camp, one button per laundry, roughly 100–500m range across the park.
Target panel: Bosch Solution 6000 (existing, with CM705B expander, 4G failover, network backhaul).
Back end: Milesight UC501 LoRaWAN IoT Controllers → Milesight UG67 outdoor gateway → Raspberry Pi (Node-RED + MQTT) → 8-channel relay board → Bosch CM705B zones.
1.1 Architecture at a glance
┌─────────────────────────────────────┐
│ Laundry 1..7 (each) │
│ │
│ [Red 40mm E-Stop Button] ◄─┐ │
│ │ │ │
│ ▼ │ │
│ [Tamper Lid Switch] ────────┤ │
│ │ │ │
│ ▼ │ │
│ ┌──────────────────┐ │ │
│ │ Milesight UC501 │ ◄──────┘ │
│ │ (IP67 LoRaWAN) │ │
│ │ GPIO_1: Duress │ │
│ │ GPIO_2: Tamper │ │
│ │ Battery/Solar │ │
│ └─────┬────────────┘ │
│ │ LoRaWAN AU915 │
└────────┼────────────────────────────┘
│
│ (100m – 2km LoRaWAN link)
▼
┌──────────────────────────────────────┐
│ HEAD OFFICE │
│ │
│ ┌────────────────────┐ │
│ │ Milesight UG67 │ (IP67, │
│ │ Outdoor Gateway │ mast-mount) │
│ │ Built-in ChirpStack│ │
│ └─────┬──────────────┘ │
│ │ PoE + MQTT │
│ ▼ │
│ ┌────────────────────┐ │
│ │ Raspberry Pi 4 │ │
│ │ Mosquitto MQTT │ │
│ │ Node-RED flows │ │
│ │ GPIO → Relay board │ │
│ └─────┬──────────────┘ │
│ │ GPIO (3.3V logic) │
│ ▼ │
│ ┌────────────────────┐ │
│ │ 8-ch Opto Relay │ │
│ │ Board (12V) │ │
│ │ Ch1-7 = 7 zones │ │
│ │ Ch8 = Supervision │ │
│ └─────┬──────────────┘ │
│ │ Dry contacts + EOL │
│ ▼ │
│ ┌────────────────────┐ │
│ │ Bosch CM705B │ │
│ │ Expander (existing)│ │
│ │ 7 zones 24hr Duress│ │
│ │ 1 zone Trouble │ │
│ └─────┬──────────────┘ │
│ │ LAN bus │
│ ▼ │
│ ┌────────────────────┐ │
│ │ Bosch Solution 6000│ │
│ │ + 4G failover │ │
│ │ → Monitoring stn │ │
│ └────────────────────┘ │
└──────────────────────────────────────┘
1.2 How it works, step by step
- Staff member presses the 40mm red e-stop button inside a laundry
- Button pulls UC501
GPIO_1 low (dry contact closed)
- UC501 detects the DI change and sends a LoRaWAN uplink (Class A, confirmed, up to 3 retries)
- UG67 at head office receives the uplink over AU915
- UG67 publishes an MQTT message to the local Mosquitto broker on the Pi
- Node-RED flow decodes the payload, matches the device EUI to a button/zone mapping, and pulses the matching GPIO high for 3 seconds
- Opto relay closes the dry contact — Bosch zone sees alarm
- Bosch Solution 6000 raises a 24-hour Duress alarm, reported to monitoring via IP + 4G failover
- Node-RED also publishes a push notification (optional) so head office staff see which laundry triggered
1.3 Supervision — how you know nothing is broken
- UC501 heartbeat: every 6 hours sends battery, GPIO state, RSSI
- Node-RED watchdog: if any UC501 misses two consecutive heartbeats (12+ hours silent), the Pi pulses the "Trouble" relay → Bosch sees a trouble zone → monitoring alerted
- UG67 gateway watchdog: Pi pings gateway every 60s. If unreachable for 5 min, Trouble relay fires
- Pi watchdog: systemd watchdog restarts Mosquitto/Node-RED if they crash. Bosch sees the Trouble relay in safe state if Pi is totally down
- Confirmed uplinks: UC501 set to Class A with ACK. If the first attempt fails, it retries up to 3 times within ~15 seconds. This is the fix for the false-positives and drops of the old P2P setup.
💡 Why this architecture
The Bosch panel doesn't need to know anything about LoRaWAN — from its perspective it's just getting dry-contact zone triggers like any other hardwired duress button. All the wireless magic happens upstream on the Pi. This means you don't have to reprogram the Bosch in any meaningful way, and if the whole LoRaWAN stack dies, the panel still works for everything else.
2. Bill of Materials
Three groups of parts: per-laundry kit (you need 7 of these), head office kit (1 of these), and test bench gear (should already have most of it).
2.1 Per-laundry kit (×7)
| Qty | Item | Part No. | ~$AU ex-GST | Supplier |
| 7 | Milesight UC501 LoRaWAN IoT Controller (AU915, solar rechargeable, external antenna version) | UC501-915M-EA | $249 | IoT Store AU |
| 7 | 40mm Red Mushroom E-Stop Pushbutton, IP65, push-to-lock / twist-release, 1NO contact, 22mm mount | IEC 60947-5-5 | ~$25 | RS Components / element14 |
| 7 | IP66 ABS enclosure ~160×120×80mm, hinged lid with key lock (or captive screws) | Jaycar HB6131 | ~$25 | Jaycar |
| 7 | Tamper switch (momentary microswitch, lever, 1NC) | Jaycar SM1039 | ~$3 | Jaycar |
| 7 | Cable gland, M20, IP68 | Generic | ~$2 | Jaycar / Bunnings |
| 7 | External 3–5 dBi LoRa antenna, N-type or SMA, for UC501-EA model | Milesight stock | $15–30 | IoT Store AU |
| 7 | Mounting hardware: wall plugs, screws, anti-vandal Torx bolts | — | ~$5 | Bunnings |
| 7 | Warning decal "EMERGENCY DURESS — PRESS TO ACTIVATE" | Custom vinyl | ~$5 | Local signwriter |
Per-laundry subtotal: ~$329 ex-GST × 7 = ~$2,303
2.2 Head office kit (×1)
| Qty | Item | Part No. | ~$AU ex-GST | Supplier |
| 1 | Milesight UG67 Outdoor LoRaWAN Gateway (AU915, 8-CH, PoE, IP67, built-in ChirpStack) | UG67-L04EU-915M / UG67-L00EU-915M | $749–849 | IoT Store AU |
| 1 | External high-gain omni antenna, 5–8 dBi, N-type, outdoor rated | Generic | $80–120 | IoT Store AU / RF Industries |
| 1 | N-type pigtail / jumper cable, 50cm (LMR-240) | LMR-240 | $25 | RF Industries |
| 1 | PoE injector, Gigabit, 48V | Ubiquiti POE-48-24W | $25 | Scorptec / Mwave |
| 1 | Raspberry Pi 4 Model B, 4GB | Official | $100 | Core Electronics |
| 1 | Raspberry Pi 4 case with fan | Argon Neo / Flirc | $40 | Core Electronics |
| 1 | Official Pi 4 PSU 5V 3A USB-C | Official | $25 | Core Electronics |
| 1 | SanDisk Industrial microSD 32GB (A1, U3) | SDSDQAF3-032G | $30 | Core Electronics |
| 1 | 8-channel 12V opto-isolated relay board (SPDT contacts) | Generic | $20–30 | Core Electronics / Jaycar |
| 1 | 12V 1A regulated DC plugpack | Jaycar MP3307 | $25 | Jaycar |
| 1 | DIN rail + terminal blocks + 6-way cage clamp | Phoenix Contact | $60 | element14 |
| 1 | Wall-mount enclosure ~300×200×100mm for Pi + relay board | Jaycar HB6308 | $60 | Jaycar |
| — | Cat6 outdoor UV cable to gateway (~20m) | — | $40 | Bunnings |
| — | EOL resistors (3.74kΩ 1%, 6.8kΩ 1% — confirm with Bosch config) | — | $5 | Jaycar |
| — | Duplex hookup wire, ferrules, heatshrink | — | $30 | Jaycar |
Head office subtotal: ~$1,290 ex-GST
2.3 Test bench gear
| Qty | Item | Purpose |
| 1 | Spare Bosch Solution 6000 panel + CM705B (or dev panel) | End-to-end test target |
| 1 | 12V 7Ah SLA battery + charger | Bench power for Bosch + relays |
| 1 | Android phone with NFC | Milesight ToolBox app config for UC501/WS101 |
| 1 | Multimeter | Relay / zone verification |
| 1 | Ethernet switch + laptop | Gateway/Pi access |
| 1 | Second laptop with Wireshark | Verify LoRa MQTT messages |
⚠ Total estimated cost
~$3,600 ex-GST for one full 7-button system (no labour, no shipping). Budget ~$4,500 landed. Add freight surcharges for any site deliveries and a buffer for one spare UC501 + one spare e-stop.
3. Pre-Build: Supplier Setup
Do these four things before you order anything.
1
Create an account with IoT Store AU
iot-store.com.au — they're the main AU Milesight distributor at retail. Ask for a trade or reseller account. You'll likely get 10–20% off list.
2
Contact Tekdis AU
tekdis.com.au — the official Milesight distributor for AU/NZ. Better pricing on volume (5+ UC501s) and access to project pricing on the UG67. This is where you'd buy for a real client deployment.
3
Register for a Milesight IoT Developer account
Sign up at cloud.milesight-iot.com. It's free and lets you test their cloud if you ever want a secondary dashboard alongside ChirpStack.
4
Install the Milesight ToolBox app
Free on the Google Play Store. This is how you configure UC501 and WS101 devices over NFC. Get this on the Android phone you'll be using in the field.
ℹ iOS doesn't work
ToolBox is Android-only because iPhones don't expose NFC the right way for writing to peripheral devices. Borrow an Android if needed.
4. Phase 1 — Head Office Hub (The Brain)
Build this first, fully on your bench. It's the core of the system. Don't try to build at the site — build it here, test it here, then bring a working unit to install.
4.1 Flash the Raspberry Pi
1
Download Raspberry Pi Imager
2
Choose Raspberry Pi OS Lite (64-bit)
No desktop, headless. You'll only ever SSH into this thing.
3
Set the bootstrap options before writing
Click the gear icon in Imager and set:
- Hostname:
lora-bridge
- Enable SSH, public-key auth (paste your key)
- Username:
cameron / password of choice
- Wi-Fi for initial bootstrap if needed
- Locale:
en_AU.UTF-8, Australia/Brisbane
4
Flash, boot, and SSH in
Flash the 32GB SD, boot the Pi on the bench with Ethernet, then:
ssh cameron@lora-bridge.local
sudo apt update && sudo apt upgrade -y
sudo apt install -y git curl vim htop build-essential python3-pip python3-rpi.gpio
4.2 Install Mosquitto (MQTT broker)
sudo apt install -y mosquitto mosquitto-clients
sudo systemctl enable mosquitto
sudo systemctl start mosquitto
Lock it down with a username and password:
sudo mosquitto_passwd -c /etc/mosquitto/passwd lora
sudo tee /etc/mosquitto/conf.d/auth.conf > /dev/null <<'EOF'
listener 1883
allow_anonymous false
password_file /etc/mosquitto/passwd
EOF
sudo systemctl restart mosquitto
Test that auth works:
mosquitto_sub -h localhost -t '#' -u lora -P '<password>' -v
mosquitto_pub -h localhost -t test -m hello -u lora -P '<password>'
⚠ Don't leave it anonymous
If allow_anonymous is left true, anyone on the LAN can publish fake duress events. Always enforce auth on MQTT brokers, even on a "trusted" network.
4.3 Install Node-RED
bash <(curl -sL https://raw.githubusercontent.com/node-red/linux-installers/master/deb/update-nodejs-and-nodered)
sudo systemctl enable nodered.service
sudo systemctl start nodered.service
Verify it's running by browsing to http://lora-bridge.local:1880 in your browser.
Install the required palette nodes via Manage Palette → Install:
node-red-contrib-rpi-gpio
node-red-node-pi-gpio (usually pre-installed)
node-red-contrib-buffer-parser (for decoding hex payloads)
node-red-dashboard (optional, for a local status UI)
4.4 Wire up the 8-channel relay board
Typical 5V/12V opto-isolated 8-channel relay board pinout:
VCC → Pi 5V (pin 2)
GND → Pi GND (pin 6)
JD-VCC → 12V+ (external PSU). Remove the VCC↔JD-VCC jumper for proper isolation.
GND2 → 12V−
IN1..IN8 → Pi GPIOs (see mapping table below)
| Relay Ch | Pi GPIO (BCM) | Physical Pin | Maps to |
| IN1 | GPIO 5 | 29 | Laundry 1 Duress |
| IN2 | GPIO 6 | 31 | Laundry 2 Duress |
| IN3 | GPIO 13 | 33 | Laundry 3 Duress |
| IN4 | GPIO 19 | 35 | Laundry 4 Duress |
| IN5 | GPIO 26 | 37 | Laundry 5 Duress |
| IN6 | GPIO 16 | 36 | Laundry 6 Duress |
| IN7 | GPIO 20 | 38 | Laundry 7 Duress |
| IN8 | GPIO 21 | 40 | System Trouble / Supervision |
⚠ Active-LOW relays
Most cheap Chinese opto relay boards are active LOW — driving GPIO low energises the relay. Node-RED flows need to account for this, and the GPIO should initialise high on boot (relay off). Get this wrong and every zone on the Bosch will trip the instant the Pi powers up.
Each relay has NC / COM / NO terminals. Wire COM + NO to the Bosch zone so that when the relay energises, the zone shorts through whatever EOL config Bosch expects.
4.5 Bosch zone wiring (reuse existing CM705B zones)
The existing CM705B in the head office already has 7 zones connected to the broken P2P relay rig. Disconnect those and re-terminate to the new 8-channel board.
Typical Bosch Solution 6000 single-EOL wiring
ZONE+ ─────┬───── NO (relay)
│
3.74kΩ
│
ZONE- ─────┴───── COM (relay)
- Relay open (normal): resistor in circuit, zone reads "sealed"
- Relay closed (alarm): resistor shorted, zone reads "alarm"
⚠ Check the EOL value first
Bosch 6000 supports No EOL, Single EOL (3.74kΩ default), or Dual EOL (3.74k seal / 6.8k alarm). Use whatever the existing zones are already programmed as. Don't reprogram the panel if you can avoid it. Change the wiring to match the panel, not the other way around.
Zone programming (only if it must change)
- Zone Type: 24-Hour Silent Duress or 24-Hour Audible Duress (client preference)
- Area: none (always armed)
- Pulse Count: 1
- Resistor: match physical wiring
- Zone Name: "LAUNDRY 1 DURESS", "LAUNDRY 2 DURESS", etc.
- Zone 8 (trouble): 24-Hour Tamper or Monitor Point named "DURESS SYSTEM TROUBLE"
4.6 Head office enclosure layout
┌──────────────────────────────────────────────┐
│ HB6308 Wall Enclosure (300×200×100mm) │
│ │
│ ┌───────┐ ┌────────────────────┐ │
│ │Pi 4 │ │ 8-CH Relay Board │ │
│ │+case │ │ │ │
│ └───┬───┘ └────────┬───────────┘ │
│ │ │ │
│ │ GPIO ribbon │ │
│ └────────────────┘ │
│ │
│ ┌────────────────────┐ │
│ │ 12V 1A PSU │ ┌───────────────┐ │
│ │ + DIN rail │ │ Terminal │ │
│ │ + terminal block │ │ block (zones) │ │
│ └────────────────────┘ └───────────────┘ │
│ │
│ Cable gland ↓ Cable gland ↓ (to Bosch) │
│ (Ethernet from (7x pair + 1 pair) │
│ UG67 PoE) │
└──────────────────────────────────────────────┘
Mount on the wall next to the Bosch panel. Power the Pi and relay board from the 12V rail so they share a common ground with the Bosch zones.
5. Phase 2 — Gateway (UG67)
The UG67 is the outdoor LoRaWAN gateway with ChirpStack network server built in. That means you don't need a separate network server, which simplifies everything.
5.1 Initial gateway bring-up
1
Plug in and connect a laptop
Plug the UG67 into the PoE injector, laptop on the LAN side.
2
Log into the default IP
Default IP is 192.168.1.1/24, login admin/password.
⚠ Change the default password immediately
First thing you do, before plugging it into a shared network. Every internet-facing UG67 with default creds gets owned inside a week.
3
Set hostname, timezone, NTP
System → General: hostname ug67-camp, timezone Brisbane, NTP to pool.ntp.org.
4
Set a static LAN IP
Network → Interface: set a static IP on your site LAN (e.g. 192.168.1.250).
5
Enable the built-in ChirpStack network server
System → Packet Forwarder: enable Built-in Network Server. This is the ChirpStack instance that ships with the UG67.
6
Configure radio for AU915 sub-band 2
Packet Forwarder → Radios: frequency AU915, sub-band 2 (channels 8–15 + 65).
7
Update firmware
System → Upgrade: make sure it's 60.0.0.43 or later.
5.2 ChirpStack built-in network server setup
UG67 ships with ChirpStack baked in. In the ChirpStack UI:
- Network Server → General: Enable.
- Applications → Add: name
duress-system, payload codec Custom Javascript codec functions (we'll paste a decoder in Phase 4).
- Profiles → Device Profile → Add:
- Name:
UC501-Duress
- LoRaWAN MAC Version: 1.0.3
- Regional Parameters: RP002-1.0.3
- Class: Class A
- Max EIRP: 16 dBm
- ADR: Enabled
- Integration → MQTT:
- Broker:
tcp://lora-bridge.local:1883 (the Pi)
- Username:
lora / password as set earlier
- Topic prefix:
application/duress/
5.3 Mounting and antenna
- Mount UG67 as high as practical — roof, mast, parapet. Line of sight to the laundries is the goal.
- Mount the external high-gain omni antenna on a short pole above the UG67 body, connected via the LMR-240 jumper.
- Use an N-type coaxial surge arrestor inline if the antenna is on a mast (grounded to mast ground).
- Run Cat6 PoE back to the head office switch and the Pi enclosure.
💡 Gateway placement wins or loses this job
Do a site survey with a spectrum analyser or a UC501 in scanner mode before committing. Spend real time finding the highest point with the best line of sight to all 7 laundries. If the gateway is on the ground with a single-storey building in the way, no amount of software tuning will save you.
6. Phase 3 — Field Nodes (UC501 + Button)
Do all 7 UC501s on the bench, before any enclosure work. Label everything as you go.
6.1 UC501 base configuration (via NFC)
1
Install Milesight ToolBox on your Android phone
2
Power on the UC501
Flick the internal switch and make sure the battery is charged.
3
Connect over NFC
In ToolBox: Device, then tap the back of your phone against the UC501 NFC logo. If it doesn't connect first go, try moving the phone around — NFC coupling is fiddly.
4
Note the Device EUI
Go to the Status page. Confirm firmware version, battery %, and write the Device EUI on a sticker. You'll need it to map buttons to zones.
5
Configure LoRaWAN settings
Settings → LoRaWAN:
- Work Mode: OTAA
- Confirmed Mode: On, Retransmissions: 3
- Class: Class A
- Spread Factor: SF10 (range/battery balance) or SF7 if close to gateway
- TX Power: 16 dBm (max for AU915)
- Channel Mode: Sub-Band 2 (must match the UG67 sub-band)
- App Key / App EUI: generate or use your own standard keys
- Uplink Interval: 360 min (6 hours) — idle heartbeat. DI changes are event-triggered on top of this.
6
Configure GPIO inputs
Settings → Interface → GPIO:
- GPIO_1 (duress button): mode = Digital Input, trigger = falling edge, debounce = 50 ms
- GPIO_2 (tamper): mode = Digital Input, trigger = rising edge, debounce = 200 ms
7
Save, label, photograph
Tap Save — ToolBox writes over NFC, you'll hear a confirmation beep. Label the UC501 with its Device EUI and Laundry number (1–7). Take a photo for records. Repeat for all 7.
6.2 Field enclosure assembly (per unit)
Prep the enclosure (Jaycar HB6131 or similar)
- Drill a 22mm hole dead centre of the lid for the e-stop button
- Drill one M20 gland hole on the bottom for the antenna cable
- Drill one 12mm hole for the tamper lever switch (internal, against lid)
- Apply a thin bead of silicone around the button bezel after mounting
Mount the components inside
- UC501 screwed to the rear wall of the enclosure (use the mounting ears)
- Tamper microswitch hot-glued or screwed so the lever touches the lid when closed. When the lid opens, the lever releases and the NC contact opens.
- E-stop button threaded through the lid, bezel nut tight, silicone seal
Wiring
E-stop NO contact ─┬─ UC501 GPIO_1
└─ UC501 GND
Tamper NC contact ─┬─ UC501 GPIO_2
└─ UC501 GND
- Use 24AWG stranded wire, ferruled ends into UC501's screw terminals
- Keep wiring neat, zip-tie to internal standoffs
Antenna
- External antenna mounts through the top of the enclosure via the M20 gland
- Use an N-type bulkhead adapter with O-ring for water sealing
- Antenna body outside the enclosure, cable inside to the UC501 SMA port
Final seal
- Close the lid, capture screws (or key lock), apply silicone around any gaps
- Optional: drop a desiccant sachet in the base to manage humidity in the laundry environment
- Apply the "EMERGENCY DURESS — PRESS TO ACTIVATE" vinyl decal to the lid
6.3 Wall mounting position (install guide for the site installer)
- Wall mount the enclosure 1200–1400mm above finished floor level — wheelchair + standard reach
- Not behind doors, machines, or inside cupboards
- Within 2m of the main laundry entry/exit path
- Visible and clearly labelled
- Antenna as high as possible, never pointing directly into a metal washing machine
7. Phase 4 — ChirpStack Device Registration
7.1 Add each UC501 as a device
For each UC501, in the UG67 ChirpStack UI:
- Applications → duress-system → Devices → Create:
- Name:
laundry-01 through laundry-07
- Device EUI: the EUI you wrote on the sticker
- Device Profile:
UC501-Duress
- Save
- On the device page → Keys (OTAA): paste the App Key you set in ToolBox (must match exactly). Save.
- Power cycle the UC501. It should JOIN within 30 seconds.
- Watch the Device Data tab — you should see a join accept, then uplinks as you press the button.
7.2 Payload codec
UC501 uplinks are TLV-encoded binary. Paste this codec into the device profile or application:
function decodeUplink(input) {
var bytes = input.bytes;
var data = {};
var i = 0;
while (i < bytes.length) {
var channel = bytes[i++];
var type = bytes[i++];
if (channel === 0x01 && type === 0x75) {
data.battery = bytes[i];
i += 1;
}
else if (channel === 0x03 && type === 0x00) {
data.gpio_1 = bytes[i] === 1 ? "active" : "inactive";
i += 1;
}
else if (channel === 0x04 && type === 0x00) {
data.gpio_2 = bytes[i] === 1 ? "tamper_open" : "tamper_sealed";
i += 1;
}
else {
i += 1;
}
}
return { data: data };
}
⚠ Verify channel/type bytes against the actual uplink
Milesight can change TLV channel/type values between firmware versions. Before you trust the decoder, watch raw MQTT messages and compare the bytes to what this code expects. The UC50x Communication Protocol PDF in the references section is the source of truth.
7.3 Verify the MQTT stream
SSH into the Pi and subscribe:
mosquitto_sub -h localhost -t 'application/duress/#' -u lora -P '<password>' -v
Press the e-stop button — you should see an MQTT message within 2 seconds:
application/duress/device/<EUI>/event/up {"applicationID":"...","devEUI":"...","object":{"battery":100,"gpio_1":"active"},...}
8. Phase 5 — Node-RED Flow
8.1 Device mapping
Create /home/cameron/duress-mapping.json on the Pi:
{
"a84041xxxxxxxxx1": { "name": "Laundry 1", "zone_gpio": 5 },
"a84041xxxxxxxxx2": { "name": "Laundry 2", "zone_gpio": 6 },
"a84041xxxxxxxxx3": { "name": "Laundry 3", "zone_gpio": 13 },
"a84041xxxxxxxxx4": { "name": "Laundry 4", "zone_gpio": 19 },
"a84041xxxxxxxxx5": { "name": "Laundry 5", "zone_gpio": 26 },
"a84041xxxxxxxxx6": { "name": "Laundry 6", "zone_gpio": 16 },
"a84041xxxxxxxxx7": { "name": "Laundry 7", "zone_gpio": 20 }
}
Replace the EUIs with your actual UC501 EUIs from the stickers.
8.2 Node-RED flows (high level)
Flow 1 — Duress trigger
[MQTT in: application/duress/+/device/+/event/up]
│
▼
[JSON parse]
│
▼
[Function: lookup device EUI in mapping, extract gpio_1 state]
│
▼
[Switch: is gpio_1 === "active"?]
│ yes
▼
[Function: set GPIO pin, msg.topic = mapping[EUI].zone_gpio]
│
▼
[rpi-gpio out: Output, BCM, Active Low, initial high]
│
▼
[Delay 3 seconds]
│
▼
[rpi-gpio out: reset pin high (relay off)]
│
▼
[Debug + Log to file /var/log/duress.log]
Flow 2 — Watchdog (heartbeat supervision)
[Interval trigger every 60s]
│
▼
[Function: check last-seen timestamp for each device in context store]
│
▼
[Switch: any device silent > 13 hours?]
│ yes
▼
[rpi-gpio out: GPIO 21 low (Trouble relay energised)]
│
▼
[Log + Push notification: "Duress button X offline"]
Flow 3 — Heartbeat recorder
[MQTT in: application/duress/+/device/+/event/up]
│
▼
[Function: update context store with last-seen timestamp for EUI]
Flow 4 — Gateway liveness watchdog
[Interval 60s]
│
▼
[Exec: ping -c 1 -W 2 192.168.1.250 (UG67 IP)]
│
▼
[Switch: fail count > 5?]
│ yes
▼
[rpi-gpio out: GPIO 21 low (Trouble)]
8.3 Reference flow JSON
A complete pre-built flow JSON will live alongside this guide as duress-flow.json. Import into Node-RED via Menu → Import → paste JSON → Deploy.
8.4 Active-low relay configuration
In each rpi-gpio out node in Node-RED:
- Pin: the BCM number from the mapping table
- Type: Digital Output
- Initialise pin state: Yes → Initial level High (off for active-low)
- Tick Active Low if your relay board is active-low (most cheap ones are)
💡 Test flow before you wire the Bosch
Before hooking the relay outputs into the Bosch zones, run everything with the relay contacts floating and a multimeter across them. Verify each button press closes the correct relay for the correct duration. Only THEN wire into the Bosch and cause real alarms.
9. Testing Protocol
Run through this checklist on your bench before anything goes to site.
9.1 Component tests
- Pi boots, SSH accessible, time sync OK
- Mosquitto running, auth enforced,
mosquitto_pub / sub work
- Node-RED reachable on :1880, flows deployed, no errors
- UG67 accessible on LAN IP, firmware up to date
- ChirpStack network server running, gateway state "Online" with recent uplinks
- All 7 UC501s joined to the application and sending heartbeats
- Relay board wired, all 8 relays fire individually from Node-RED test inject
9.2 End-to-end tests (per button)
- Press button 1 → MQTT observed → Node-RED logs "Laundry 1 duress" → Relay 1 clicks → Bosch test zone 1 shows alarm → panel dials monitoring (or observe in engineer mode)
- Repeat for buttons 2–7
- Release button → relay auto-releases after 3 seconds → Bosch zone returns to sealed
- Open enclosure lid → tamper triggers → separate zone activates (if allocated) or log entry
- Pull UC501 battery → wait 13 hours → watchdog fires Trouble zone → Bosch trouble alarm
- Reconnect UC501 → next heartbeat clears the watchdog → Trouble relay releases
- Unplug UG67 Ethernet → 5 min later Trouble relay fires
- Reboot Pi → Mosquitto + Node-RED auto-start → system recovers
9.3 Range testing
- Put a UC501 at expected max range (500m, line-of-sight-ish) → press button → confirm uplink arrives within 2 seconds
- Check RSSI / SNR in ChirpStack — aim for RSSI > −120 dBm, SNR > −5 dB for reliable operation
- Test inside a metal-clad laundry shell if possible — worst-case environment
9.4 Stress tests
- Press 3 buttons within 1 second → all 3 should register (LoRaWAN may queue)
- Hold a button down for 30 seconds → fires alarm on press, no extra events, releases cleanly
- Rapid-fire the same button 10 times in 10 seconds → all 10 hit the Bosch log
9.5 Before handover
- Document all device EUIs, App Keys, and zone mappings in a handoff doc
- Back up UG67 config, Pi SD image (
dd), Node-RED flows (flows.json), Mosquitto config
- Print the "quick reference card" — what each zone means, who to call, how to silence
10. Deployment Notes
- Do a site survey first. Walk the park with a UC501 and a portable gateway (or use a Milesight test device in scanner mode). Log RSSI at every intended button location BEFORE committing mounting points.
- Gateway placement wins or loses this job. Spend real time finding the highest point with the best line of sight. Don't shortcut.
- Keep a spare UC501 and a spare e-stop button onsite. Mining camps are hard on gear.
- Plan the Bosch zone programming change carefully. Do it out of business hours, and have the client confirm monitoring knows you're testing so every duress alarm doesn't dispatch response.
- Label everything. Enclosures, cables, zones, relay channels. Future-Cameron will thank you.
⚠ Monitoring station coordination
Before any live test, email the monitoring company with: the site address, the test window, and the zones you'll be triggering. Otherwise your first test dispatches a response unit to a mining camp at 2am.
11. Open Questions & Risks
ℹ UC501 GPIO TLV channel numbers
The included decoder is based on the UC50x protocol doc, but firmware versions differ. Verify against your UC501's actual uplinks by watching raw MQTT and adjust the decoder bytes if needed.
ℹ Sub-band 2 vs Sub-band 8
Confirm your UG67 and UC501 are in the same sub-band. Default is sub-band 2 for AU915, but it's worth double-checking — if they're mismatched the devices will never join, and the failure is silent.
ℹ Tamper DI wiring
Milesight UC501 GPIOs are typically pulled up internally. NC contact to ground should give the correct edge trigger but confirm in ToolBox after first install.
ℹ Power at each laundry
UC501 on battery is good for 2–5 years with a 6-hour heartbeat. If you want mains backup, feed 5–24V DC to the UC501's power input and it will trickle-charge the built-in cell.
⚠ Lightning / surge
In a mining camp environment, consider gas discharge arrestors on the UG67 antenna line and surge protection on the Pi power. Mining camps see real lightning, and losing the whole system to one strike is an expensive lesson.
12. Appendix — Files & References
Files in this project folder
Located at /mnt/shared/research/lorawan-duress/:
LoRaWAN-Duress-Build-Guide.md — the source markdown for this guide
duress-flow.json — Node-RED flow export (to be created)
uc501-decoder.js — standalone codec file (to be created)
device-mapping.template.json — mapping template (to be created)
bosch-zone-programming.md — Bosch 6000 config changes (to be created)
handoff-template.md — final client handover doc template (to be created)
Useful references
| Resource | URL |
| Milesight UC50x User Guide | resource.milesight.com/milesight/iot/document/uc50x-series-user-guide-en.pdf |
| Milesight UC50x Communication Protocol | resource.milesight.com/milesight/iot/document/uc50x-series-communication-protocol-en.pdf |
| Milesight UG67 User Guide | resource.milesight.com/milesight/iot/document/ug67-user-guide-en.pdf |
| ChirpStack Node-RED integration | chirpstack.io/docs/guides/node-red-integration.html |
| Milesight ChirpStack V4 Integration | support.milesight-iot.com/support/solutions/articles/73000610017 |
| Bosch Solution 6000 Installation Manual | manualslib.com/manual/1210378/Bosch-Solution-6000.html |
| LoRa Alliance AU915 regional parameters | lora-alliance.org/resource_hub/rp002-1-0-4-regional-parameters/ |
Supplier contact list
| Supplier | Website | Notes |
| IoT Store AU | iot-store.com.au | Retail + trade, Milesight + Dragino in stock, fast AU dispatch |
| Tekdis AU | tekdis.com.au | Official Milesight distributor AU/NZ, project pricing for volume |
| Powertec | powertec.com.au | Alternative Milesight stockist |
| Core Electronics | core-electronics.com.au | Raspberry Pi + accessories, Australian |
| Jaycar | jaycar.com.au | Enclosures, pushbuttons, basic electronics |
| RS Components AU | au.rs-online.com | Industrial pushbuttons, Phoenix Contact terminals |
| element14 AU | au.element14.com | Same as RS, different stock/pricing |
13. Quick Build Order
Shortest path from zero to working prototype:
- Order parts from the BOM: UC501 ×1 first for testing, UG67 ×1, Pi kit, relay board, enclosures, buttons, antennas
- While parts are shipping: flash Pi, install Mosquitto + Node-RED on the bench
- Configure UG67 on the bench, join it to ChirpStack built-in network server
- Join one UC501 via NFC, watch MQTT uplinks arrive
- Wire one e-stop button to that UC501, test end-to-end: MQTT → Node-RED → GPIO → multimeter on relay
- Wire to a test Bosch panel (or the CM705B expander from the actual job), verify zone fires
- Once one works, order the remaining 6 UC501s + enclosures, build in parallel
- Full bench test all 7 with the testing protocol in section 9
- Deploy to site using the deployment notes in section 10
💡 Realistic timeline
Expected bench build time: 2–3 days for the first working prototype once parts arrive. 1 day to build out the other 6 once the template is dialled in. Then 1 full day onsite for deployment + commissioning. Plan for double that on your first one.
Version History
| Version | Date | Author | Changes |
| 1.0 | 11 April 2026 | Cameron Pollack | Initial HTML release, converted from markdown build guide dated 8 April 2026 |