TTN Setup
Step-by-step guide to register and configure Multiflexmeter 3.7.0 devices on The Things Network (TTN).
Prerequisites
Section titled “Prerequisites”- TTN account (free): https://console.cloud.thethings.network/
- Multiflexmeter 3.7.0 hardware
- Internet connection
Understanding LoRaWAN Communication
Section titled “Understanding LoRaWAN Communication”Before setting up TTN, it’s important to understand how the Multiflexmeter communicates using LoRaWAN.
Uplinks: Device → Cloud
Section titled “Uplinks: Device → Cloud”Uplinks are messages sent from the Multiflexmeter device to The Things Network (and then to your backend/application).
In this project, uplinks are used for:
-
Poldermill Sensor Data (FPort 1)
- Sent every 5-15 minutes (configurable measurement interval)
- Contains: Module address, module type, operational status (spinning/pumping), and revolution count
- Payload format:
36 01 [flags] [revolutions](7 bytes) - Example:
36 01 03 00 00 00 32= Poldermill spinning + pumping, 50 revolutions this period
-
Device Version Info (FPort 2)
- Sent automatically after the device joins the network (OTAA)
- Contains: Firmware version and hardware version
- Payload format:
10 [fw_version] [hw_version](5 bytes) - Example:
10 03 07 02 08= Firmware v3.7, Hardware v2.8 - Helps you identify which devices are running which firmware in the field
Why uplinks matter:
- This is how you monitor your poldermills remotely - seeing if they’re spinning, pumping water, and how many revolutions they’ve made
- All historical data, charts, and alerts are based on uplink messages
- Uplinks consume airtime, so the measurement interval balances data freshness vs. battery life and TTN fair use limits
Downlinks: Cloud → Device
Section titled “Downlinks: Cloud → Device”Downlinks are messages sent from The Things Network to the Multiflexmeter device. These are commands that change device behavior.
In this project, downlinks are used for:
-
Change Measurement Interval (Command 0x10)
- Dynamically adjust how often the device takes measurements
- Payload format:
10 [interval_msb] [interval_lsb](3 bytes) - Example:
10 07 08= Set interval to 1800 seconds (30 minutes) - Use case: Increase frequency during windy periods, decrease to save battery in calm weather
-
Send Commands to Sensor Module (Command 0x11)
- Forward commands directly to the poldermill sensor module via I²C
- Payload format:
11 [module_addr] [command] [args...] - Example:
11 36 20 01= Send command 0x20 with argument 0x01 to sensor at address 0x36 - Use case: Configure sensor parameters, trigger calibration, or read diagnostic data
-
Force Device Reset (Command 0xDEAD)
- Emergency command to reboot the device and rejoin the network
- Payload format:
DE AD(2 bytes) - Use case: Recover from stuck states or network issues without physical access
Important LoRaWAN Class A Behavior:
- The Multiflexmeter is a Class A device (battery-optimized)
- Downlinks can only be sent after the device sends an uplink
- The device opens two short receive windows (RX1 at ~1s, RX2 at ~2s) after each uplink
- If you schedule a downlink, it will be delivered on the next uplink from that device
- This means there’s a delay between scheduling a downlink and the device receiving it (up to your measurement interval)
Example workflow:
- You notice a poldermill stopped spinning (from uplink data)
- You schedule a downlink to increase measurement frequency from 15 minutes to 5 minutes
- The device sends its next uplink (in up to 15 minutes)
- Immediately after that uplink, the device receives your downlink in the RX1/RX2 window
- The device processes the command and starts measuring every 5 minutes
- You now get updates 3× faster to monitor the situation
Step 1: Create TTN Application
Section titled “Step 1: Create TTN Application”1.1 Log In to TTN Console
Section titled “1.1 Log In to TTN Console”- Navigate to https://console.cloud.thethings.network/
- Log in with your account
- Select your region (Europe:
eu1.cloud.thethings.network)
1.2 Create Application
Section titled “1.2 Create Application”- Click “Create application”
- Fill in application details:
- Application ID:
multiflexmeter(must be unique) - Application name:
Multiflexmeter Deployment - Description:
IoT sensor network for environmental monitoring
- Application ID:
- Click “Create application”
Step 2: Register Device
Section titled “Step 2: Register Device”2.1 Add End Device
Section titled “2.1 Add End Device”- Open your application
- Click “Register end device”
- Select “Enter end device specifics manually”
2.2 Device Configuration
Section titled “2.2 Device Configuration”LoRaWAN Version:
- Select: LoRaWAN Specification 1.0.x
Regional Parameters:
- Select: PHY V1.0.2 REV B (or latest for EU868)
Frequency Plan:
- Select: Europe 863-870 MHz (SF9 for RX2 - recommended)
Activation Mode:
- Select: Over the air activation (OTAA)
2.3 Device Identifiers
Section titled “2.3 Device Identifiers”JoinEUI (AppEUI):
- Leave as default
0000000000000000OR - Use custom:
70B3D57ED00B1E57(example) - Format: 8 bytes hex (16 characters)
DevEUI:
- Click “Generate” for automatic generation OR
- Enter manually:
0004A30B00F8AC2D(example) - Format: 8 bytes hex, globally unique
- Note: Must match device EEPROM exactly
AppKey:
- Click “Generate” for secure random key
- Format: 16 bytes hex (32 characters)
- Important: Copy this key! You’ll need it for EEPROM configuration
End Device ID:
- Friendly name:
mfm-sensor-001 - Must be unique within application
2.4 Configure Device Location (Optional)
Section titled “2.4 Configure Device Location (Optional)”Before registering, you can set the device’s GPS location which will be included in webhook payloads:
- Scroll down to “Location” section
- Set location using one of these methods:
- Manual entry: Enter latitude, longitude, and altitude
- Map picker: Click on the map to set location visually
- Example for Mallemolen, Gouda:
- Latitude:
52.0116 - Longitude:
4.7104 - Altitude:
5(meters above sea level)
- Latitude:
- Click “Set location” to confirm
2.5 Complete Registration
Section titled “2.5 Complete Registration”Click “Register end device”
Step 3: Configure Payload Decoder
Section titled “Step 3: Configure Payload Decoder”3.1 Open Payload Formatters
Section titled “3.1 Open Payload Formatters”- In your device view, click “Payload formatters”
- Select “Uplink”
- Choose “Custom JavaScript formatter”
3.2 Add Decoder Function
Section titled “3.2 Add Decoder Function”Paste the following JavaScript decoder:
/** * TTN Payload Formatter - Decode Multiflexmeter uplink messages * * This decoder converts raw binary payloads from the Multiflexmeter device * into human-readable JSON objects displayed in TTN Console. * * SUPPORTED FPORTS: * - FPort 1: Poldermill sensor measurement data (7 bytes) * - FPort 2: Device version information (5 bytes) * * The firmware acts as a passthrough - it transmits raw sensor module data * without interpretation. This decoder extracts structured data from the * sensor module's binary format. * * @param {Object} input - TTN uplink input object * @param {number} input.fPort - LoRaWAN FPort (1 = measurements, 2 = version) * @param {Array<number>} input.bytes - Raw payload bytes (0-255) * @returns {Object} Decoded data or errors */function decodeUplink(input) { // FPort 1: Poldermill sensor measurement data (7 bytes) if (input.fPort === 1 && input.bytes.length >= 7) { // PAYLOAD FORMAT: <Module Addr> <Module Type> <Flags> <Revolutions (uint32 BE)> // Format specified in Multiflexmeter-3.7.0 firmware readme.md:64-66 // // The firmware reads this data from the sensor module at I²C address 0x36 // using SMBus Block Read, then transmits it unmodified via LoRaWAN.
// Byte 0: Module Address (0x36 = I²C address of poldermill sensor) // Identifies which sensor sent this data (supports multi-sensor setups) const moduleAddress = input.bytes[0];
// Byte 1: Module Type (0x01 = Poldermill sensor) // Allows backend to apply sensor-specific decoding logic const moduleType = input.bytes[1];
// Byte 2: Flags byte (operational status) // Bit 0 (LSB): Spinning (1 = rotor rotating via wind power, 0 = idle) // Bit 1: Pumping (1 = water pumping active, 0 = not pumping) // Bits 2-7: Reserved (unused, set to 0) const flags = input.bytes[2];
// Bytes 3-6: Revolution count (uint32 big-endian) // IMPORTANT: This is a PERIOD count, NOT a cumulative total! // The sensor counts revolutions during the measurement interval (e.g., 15 minutes), // transmits the count, then RESETS to 0 for the next period. // Example: Reading 1: 25 revs, Reading 2: 30 revs, Total: 25+30=55 revs const revolutions = (input.bytes[3] << 24) | // MSB (most significant byte) (input.bytes[4] << 16) | (input.bytes[5] << 8) | input.bytes[6]; // LSB (least significant byte)
return { data: { moduleAddress: moduleAddress, moduleType: moduleType, revolutions: revolutions, spinning: (flags & 0x01) !== 0, // Test bit 0 (LSB) pumping: (flags & 0x02) !== 0 // Test bit 1 } }; }
// FPort 2: Device version information (5 bytes) // Sent automatically after device joins network (OTAA) or on explicit request // // PAYLOAD FORMAT: <CMD> <FW_MSB> <FW_LSB> <HW_MSB> <HW_LSB> // CMD = 0x10: Version response indicator if (input.fPort === 2 && input.bytes.length === 5 && input.bytes[0] === 0x10) { // Bytes 1-2: Firmware version (uint16 big-endian, format: major.minor) // Example: 0x0307 = v3.7 // Bytes 3-4: Hardware version (uint16 big-endian, format: major.minor) // Example: 0x0208 = v2.8 return { data: { type: 'version', firmware: input.bytes[1] + '.' + input.bytes[2], // MSB.LSB hardware: input.bytes[3] + '.' + input.bytes[4] // MSB.LSB } }; }
// Unknown FPort or invalid payload return { errors: ["Unknown FPort or invalid payload length"] };}Click “Save changes”
3.3 Test Decoder
Section titled “3.3 Test Decoder”Use “Test” tab to verify:
Test Input (FPort 1 - Poldermill Sensor):
{ "bytes": [54, 1, 1, 0, 0, 0, 25], // Hex: 36 01 01 00 00 00 19 "fPort": 1}Expected Output:
{ "data": { "moduleAddress": 54, "moduleType": 1, "revolutions": 25, "spinning": true, "pumping": false }}Test Input (FPort 2 - Version):
{ "bytes": [16, 3, 7, 2, 8], // Hex: 10 03 07 02 08 "fPort": 2}Expected Output:
{ "data": { "type": "version", "firmware": "3.7", "hardware": "2.8" }}Step 4: Copy Device Credentials
Section titled “Step 4: Copy Device Credentials”You’ll need these values for EEPROM configuration:
From TTN Console:
Section titled “From TTN Console:”-
Application EUI (AppEUI)
- Location: Device overview → “Activation information”
- Format:
70B3D57ED00B1E57 - Copy: Click copy icon
-
Device EUI (DevEUI)
- Location: Device overview → “Activation information”
- Format:
0004A30B00F8AC2D - Copy: Click copy icon
-
Application Key (AppKey)
- Location: Device overview → “Activation information”
- Click eye icon to reveal
- Format:
5B7F1A2E3C9D8A6F4E0B2C5D8A3F1E9C - Copy: Click copy icon
Step 5: Configure EEPROM
Section titled “Step 5: Configure EEPROM”Use the copied credentials to configure your device EEPROM.
See: Configuration Guide for detailed instructions.
Step 6: Set Up Integrations
Section titled “Step 6: Set Up Integrations”6.1 MQTT Integration (Built-in)
Section titled “6.1 MQTT Integration (Built-in)”TTN provides MQTT access by default:
Connection Details:
- Server:
eu1.cloud.thethings.network(or your region) - Port: 1883 (MQTT) or 8883 (MQTTS)
- Username:
multiflexmeter@ttn(your app ID) - Password: API key (generate in console)
Topics:
# Uplink messagesv3/multiflexmeter@ttn/devices/mfm-sensor-001/up
# Downlink messagesv3/multiflexmeter@ttn/devices/mfm-sensor-001/down/push6.2 HTTP Integration
Section titled “6.2 HTTP Integration”- Go to “Integrations” → “Webhooks”
- Click “Add webhook”
- Select “Custom webhook”
- Configure:
- Webhook ID:
custom-backend - Webhook format: JSON
- Base URL:
https://your-server.com/api/uplink - Uplink message: Enabled
- Webhook ID:
6.3 Storage Integration
Section titled “6.3 Storage Integration”Enable “Storage Integration” (free tier: 24 hours retention):
- Go to “Integrations” → “Storage Integration”
- Click “Activate Storage Integration”
- Access data via API or TTN Console
Step 7: Send Downlink Commands
Section titled “Step 7: Send Downlink Commands”Downlink commands allow you to remotely control and configure your Multiflexmeter devices without physical access.
Example 1: Change Measurement Interval
Section titled “Example 1: Change Measurement Interval”Scenario: Windy weather approaching - increase measurement frequency from 15 minutes to 5 minutes to capture more data.
Via TTN Console:
- Open your device in TTN Console
- Go to “Messaging” → “Downlink”
- Configure:
- FPort:
1(any port works, but 1 is conventional) - Payload (hex):
10012C - Confirmed: ☐ (unchecked - no acknowledgment needed)
- FPort:
- Click “Schedule downlink”
Payload breakdown:
10 01 2C│ │ ││ └──┴─ Interval: 0x012C = 300 seconds (5 minutes)└────── Command: 0x10 (Change Interval)What happens:
- Downlink scheduled in TTN queue
- Device sends next uplink (within 15 minutes)
- Device receives downlink in RX1/RX2 window
- Device updates measurement interval to 300 seconds
- Device saves new interval to EEPROM (persistent across reboots)
- Next measurement happens in 5 minutes instead of 15
Via TTN API:
# Set measurement interval to 5 minutes (300 seconds = 0x012C)curl -X POST \ https://eu1.cloud.thethings.network/api/v3/as/applications/multiflexmeter/devices/mfm-sensor-001/down/push \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "downlinks": [{ "f_port": 1, "frm_payload": "EBIM", "priority": "NORMAL" }] }'Note: EBIM is the base64 encoding of hex 10 01 2C
Common intervals:
- 5 minutes:
10012C(300 seconds) - 10 minutes:
100258(600 seconds) - 15 minutes:
100384(900 seconds) - 30 minutes:
100708(1800 seconds) - 1 hour:
100E10(3600 seconds)
Example 2: Send Command to Poldermill Sensor Module
Section titled “Example 2: Send Command to Poldermill Sensor Module”Scenario: Trigger a sensor recalibration remotely.
Via TTN Console:
- Go to “Messaging” → “Downlink”
- Configure:
- FPort:
1 - Payload (hex):
11362001 - Confirmed: ☑ (check this to get acknowledgment)
- FPort:
- Click “Schedule downlink”
Payload breakdown:
11 36 20 01│ │ │ ││ │ │ └─ Argument: 0x01 (command-specific parameter)│ │ └──── Module Command: 0x20 (sensor-specific command)│ └─────── Module Address: 0x36 (poldermill sensor I²C address)└────────── Command: 0x11 (Forward to Module)What happens:
- Device receives downlink
- Device executes:
smbus_blockWrite(0x36, 0x20, [0x01], 1) - Sensor at address 0x36 receives command 0x20 with parameter 0x01
- Sensor performs its specific action (e.g., calibration, parameter change)
Via TTN API:
# Send command 0x20 with argument 0x01 to sensor at address 0x36curl -X POST \ https://eu1.cloud.thethings.network/api/v3/as/applications/multiflexmeter/devices/mfm-sensor-001/down/push \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "downlinks": [{ "f_port": 1, "frm_payload": "ETYgAQ==", "priority": "NORMAL", "confirmed": true }] }'Note: ETYgAQ== is the base64 encoding of hex 11 36 20 01
Example 3: Force Device Reset
Section titled “Example 3: Force Device Reset”Scenario: Device appears stuck or not responding normally - force a reboot and rejoin.
Via TTN Console:
- Go to “Messaging” → “Downlink”
- Configure:
- FPort:
1 - Payload (hex):
DEAD - Confirmed: ☑ (recommended)
- FPort:
- Click “Schedule downlink”
Payload breakdown:
DE AD│ ││ └─ Validation byte: 0xAD (must be exact)└──── Command: 0xDE (Reset command)What happens:
- Device receives
DE ADdownlink - Device validates second byte is
0xAD - Device schedules reset job with 5-second delay
- After 5 seconds, watchdog timer triggers MCU reset
- Device reboots, loads configuration from EEPROM
- Device performs OTAA rejoin procedure
- Device sends version info (FPort 2) after successful join
- Device resumes normal operation with configured interval
Via TTN API:
# Force device resetcurl -X POST \ https://eu1.cloud.thethings.network/api/v3/as/applications/multiflexmeter/devices/mfm-sensor-001/down/push \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "downlinks": [{ "f_port": 1, "frm_payload": "3q0=", "priority": "HIGH", "confirmed": true }] }'Note: 3q0= is the base64 encoding of hex DE AD
Monitoring Downlink Delivery
Section titled “Monitoring Downlink Delivery”Check downlink status:
- In TTN Console, go to “Live data” tab
- Look for downlink events after the next uplink:
downlink.scheduled- Downlink queueddownlink.sent- Downlink transmitted in RX1/RX2downlink.ack- Device acknowledged (if confirmed=true)downlink.failed- Delivery failed (check logs)
Successful delivery indicators:
- Device behavior changes as expected (e.g., interval updates)
- If confirmed downlink: You see
downlink.ackevent - Next uplink reflects the changes
Troubleshooting failed downlinks:
- Not delivered: Check if device sent an uplink after scheduling
- Device didn’t process: Check payload format (hex vs base64)
- No acknowledgment: Device may not support confirmed downlinks for that command
- Wrong timing: Downlink may have arrived during device sleep/wake transition
Monitoring
Section titled “Monitoring”Live Data
Section titled “Live Data”View incoming messages:
- Open device in TTN Console
- Go to “Live data” tab
- Watch for uplink messages
Typical Poldermill Uplink Message:
{ "end_device_ids": { "device_id": "mfm-sensor-001", "application_ids": { "application_id": "multiflexmeter" }, "dev_eui": "0004A30B00F8AC2D" }, "uplink_message": { "f_port": 1, "frm_payload": "NgEDAAAAMg==", "decoded_payload": { "moduleAddress": 54, "moduleType": 1, "revolutions": 50, "spinning": true, "pumping": true }, "rx_metadata": [ { "gateway_ids": { "gateway_id": "mallemolen-gateway" }, "rssi": -67, "snr": 8.5, "channel_rssi": -67, "uplink_token": "..." } ], "settings": { "data_rate": { "lora": { "bandwidth": 125000, "spreading_factor": 7 } }, "frequency": "868100000" }, "received_at": "2025-03-15T14:23:45.678Z" }, "locations": { "user": { "latitude": 52.0116, "longitude": 4.7104, "altitude": 5, "source": "SOURCE_REGISTRY" } }}What this tells you:
- Device: mfm-sensor-001 (DevEUI: 0004A30B00F8AC2D)
- Status: Poldermill is both spinning (wind-powered) and pumping water
- Revolutions: 50 revolutions during this measurement period (e.g., last 15 minutes)
- Signal quality: Good (RSSI -67 dBm, SNR 8.5 dB)
- Location: Mallemolen, Gouda (52.0116°N, 4.7104°E)
- Gateway: Received by mallemolen-gateway
- Data rate: SF7 (fastest spreading factor - short airtime)
Device Status
Section titled “Device Status”Check device health:
- Last seen: Timestamp of last uplink
- Uplink counter: Total messages received
- Downlink counter: Total messages sent
- RSSI: Signal strength (-120 to -30 dBm)
- SNR: Signal-to-noise ratio (>0 dB is good)
Fair Use Policy
Section titled “Fair Use Policy”TTN Fair Use Limits
Section titled “TTN Fair Use Limits”Free Tier:
- Uplink: 30 seconds airtime per device per day
- Downlink: 10 downlinks per device per day
Compliance
Section titled “Compliance”Typical SF7 message (16 bytes):
- Airtime: ~60ms
- Maximum messages: ~500/day
Recommended intervals:
- SF7: 2-5 minutes
- SF9: 5-10 minutes
- SF12: 15+ minutes
Enable Fair Use Policy in device EEPROM:
FAIR_USE = bytes([0x01]) # EnableTroubleshooting
Section titled “Troubleshooting”Device Not Joining
Section titled “Device Not Joining”Check:
- AppEUI, DevEUI, AppKey match exactly
- Device is in range of gateway
- Frequency plan matches region (EU868)
- Check gateway is online in TTN Console
Debug:
- Enable serial debug on device
- Check for “EV_JOINING” messages
- Verify LoRaWAN parameters in code
No Uplinks Received
Section titled “No Uplinks Received”Check:
- Device joined successfully (check “Last seen”)
- Gateway is receiving packets
- RSSI/SNR values are reasonable (-120 to -30 dBm)
- Device interval is configured correctly
Decoder Not Working
Section titled “Decoder Not Working”Check:
- FPort matches (1 for measurements, 2 for version)
- Payload length is correct (16 bytes for FPort 1)
- Test decoder with known values
- Check JavaScript console for errors
Next Steps
Section titled “Next Steps”- Configuration - Configure device EEPROM
- Field Deployment - Installation best practices
- Quick Start - Complete setup walkthrough