Skip to main content

LoRaWAN

The LoRaWAN page manages LoRaWAN gateways and sensor devices for long-range, low-power wireless communication with IoT sensors such as temperature probes, door contacts, water-leak detectors, and environmental monitors. GEM acts as the network server itself: a radio concentrator (an SX1302/SX1303 board) hears the sensors, a packet forwarder relays the frames to GEM over UDP, and GEM decrypts each frame and runs your payload decoder.

Open LoRaWAN

Licensing

LoRaWAN is a premium integration. Adding a lorawan_gateway device — and using this page — requires the LoRaWAN product in your license. Core drivers are always included, and existing devices keep running if a license later changes. See License.

Prerequisites

  • An SX1302 / SX1303 concentrator wired to the GEM server — either a USB corecell (enumerates as a serial port, typically /dev/ttyACM0) or an SPI board (SPI bus + a reset GPIO pin).
  • A packet forwarder posting frames to GEM over UDP. GEM can compile and supervise the forwarder for you (managed mode), or you can run your own forwarder pointed at GEM's UDP port (external mode).
  • The activation keys for each sensor, from its label or provisioning sheet:
    • OTAA sensors: DevEUI, AppKey, and (if used) JoinEUI/AppEUI.
    • ABP sensors: DevAddr, NwkSKey, and AppSKey.

Tabs

The page has four tabs: Gateways, Devices, Traffic, and Downlinks.

The Gateways tab collapses when there is only one gateway

Most sites run a single gateway, so the page adapts to that:

  • One gateway — the Gateways tab is hidden and the page opens on Devices. The gateway moves to a status strip above the tabs, visible from every tab, showing its name, Connected / Disconnected badge, radio count, uplinks in the last hour and last 24 hours, and a "fwd running" indicator when GEM is managing the packet forwarder. Click the name to open the gateway's record in a reference modal over the page — the join status, downlink queue and open traffic chart keep running behind it — falling back to the Devices page when the modal can't host the record. Gateway names in the Gateways table and sensor names on the Devices tab open the same way. The strip carries Reload and Settings buttons; Settings opens the full gateway management view (table, form, and New Gateway) in a dialog. The Gateway dropdowns on the Devices and Traffic tabs are hidden too — the one gateway is already selected.
  • Two or more gateways — the Gateways tab reappears in its usual place, the page opens on it, and the Gateway dropdowns return on the Devices and Traffic tabs.
  • No gateways — the page opens on the Gateways tab. The Devices and Traffic tabs show a prompt to add a gateway first.

Adding a second gateway brings the tab back immediately; deleting back down to one collapses it again. Nothing is lost in the collapsed state — everything the tab offered is reachable from Settings on the strip.


Gateways Tab

The Gateways tab lets you create and manage LoRaWAN gateway devices. A gateway receives radio transmissions from your LoRa sensors and forwards them to GEM. With a single gateway configured this tab is hidden; open the same view from Settings on the gateway strip.

When two or more gateways are configured, each also appears at the top of the tab as a status card showing its connection state (Connected / Disconnected), the number of radios reporting, the uplink count in the last hour and last 24 hours, and a "fwd running" indicator when GEM is managing the packet forwarder process. With one gateway the strip above the tabs carries the same information instead. The connection state updates live — the card, the strip, and the table's Status badge all flip to Connected or Disconnected as the gateway comes online or drops, with no page refresh needed.

Gateway Table

The table lists all configured gateways with the following columns:

ColumnDescription
NameThe gateway name. Click it to open this gateway's record on the Devices page.
UDP PortThe UDP port the gateway uses for the packet forwarder protocol (default: 1700).
ForwarderShows "Managed" if a binary path is configured (GEM compiles, spawns, and supervises the packet forwarder), or "External" if left empty (you run the forwarder separately).
StatusConnection status badge: Connected (green) or Disconnected (red).

Each row has these action buttons:

ButtonDescription
EditOpens the gateway form pre-filled with this gateway's settings.
ReloadRestarts the gateway device driver.
DeleteRemoves the gateway and all of its sensor zones (with confirmation).

Click New Gateway to add a new gateway.

Gateway Form

The gateway form has the following fields:

FieldDescription
NameRequired. The gateway name (used as the device name in GEM).
UDP PortThe UDP port for the packet forwarder protocol. Default: 1700.

Packet Forwarder Section

FieldDescription
Binary PathPath to the lora_pkt_fwd binary on the GEM server. Leave empty if you run the packet forwarder externally.
Compile / RecompileClones and compiles the sx1302_hal packet forwarder from source, pinned to release V2.1.0 (supports both SX1302 and SX1303 concentrators — including RAK2287/RAK7271 and RAK5146/RAK7371 USB gateways). This may take several minutes. Recompile refreshes the cached source to the pinned release first; if the site has no GitHub access, it compiles the cached source as-is. On success, the binary path and gateway EUI are populated automatically. The button reads Compile when no path is set and Recompile once one is.
Gateway EUI16 hex character unique gateway identifier (e.g., AA555A0000000000). Auto-detected from the attached concentrator whenever possible: GEM probes it automatically when you open the New Gateway form, during compilation, and via the Detect button next to this field. Detection requires a compiled packet forwarder (the chip_id helper it installs) and the concentrator wired to the server; if neither is present the field stays blank for manual entry, which is normal for external-forwarder setups. The field is only auto-filled while empty, so anything you type is never overwritten. When set, it is also enforced — see Which forwarder may register.

The following fields appear only when a Binary Path is configured (managed mode):

FieldDescription
Com TypeConnection type to the LoRa concentrator: USB or SPI. Changing this updates the Com Path default.
Com PathDevice path for the concentrator. Default: /dev/ttyACM0 for USB, /dev/spidev0.0 for SPI. When Com Type is USB, the gateway driver promotes this to a stable /dev/serial/by-id/... symlink at start-up so the dongle survives replug/reboot reordering. Set the usb_auto_discover device attribute to false (in the Attributes editor) to disable that promotion — recommended only on hosts running multiple identical concentrators. The stored by-id path is resolved back to its short /dev/tty* node each time the packet forwarder starts — the sx1302_hal binaries have a fixed-size path buffer and abort on the long by-id path, so the config file and chip_id probes always receive the short path.
Frequency PlanRegional frequency plan: US915, EU868, AU915, AS923, KR920, or IN865. Must match the sensors and your local regulations. Every plan ships a concentrator template, so all six work with the managed packet forwarder.
Reset GPIOGPIO pin number for the SPI concentrator reset line. Only shown when Com Type is SPI.

Radio Section

FieldDescription
TX Power (dBm)Transmit power requested for downlinks and join accepts, 12–27 dBm, capped at the frequency plan's legal ceiling — EU868 and KR920 are held to 14 dBm ERP, AS923 to 16, and a higher request is silently lowered rather than radiated. Leave it empty to take the plan's default, which the placeholder shows and which changes with the Frequency Plan dropdown: 20 dBm on US915/AU915, 14 on the other plans. Typing a value pins it for that gateway regardless of plan. Downlinks are the weak half of a LoRaWAN link, and 14 proved too quiet to commission a 500 kHz site — uplinks arrive cleanly while join accepts and queued downlinks are missed, which looks like nothing at all in the traffic log. See the asymmetric-link case. At 17 or below, the external power-amplifier entries are also stripped from the generated TX gain table, so no request can engage the PA at all. Applies to managed and external forwarders (the power request rides in each downlink packet).
Sub-bandUS915/AU915 only. The block of 8 uplink channels (1–8) the gateway listens on — it must match the sensors' channel plan. Most vendors ship sub-band 2 (channels 8–15, the default). The choice is also advertised to OTAA devices in the join accept, so joined sensors always transmit on the channels the gateway hears.
Antenna Gain (dBi)Managed forwarder only. Subtracted from each requested TX power by the packet forwarder so the radiated power (EIRP) stays within regulatory limits when using a high-gain antenna.
RX1 Data Rate OffsetSteps the RX1 downlink data rate below the rate the uplink implies. Default: 0, which answers at the fastest rate the region allows for that uplink and is what almost every deployment wants. Raise it only when sensors report a poor downlink margin while their uplinks arrive strongly — a slower downlink rate travels further. The value is advertised to OTAA devices in the join accept, so it takes effect at the next join.
Repeat Downlinks in RX2On by default. Transmits each Class A downlink a second time in the RX2 window, one second after RX1 — see How a downlink reaches the sensor. A sensor that decoded the RX1 copy has already closed its windows and ignores the repeat, so the only cost is airtime. Turn it off only if the doubled transmit burst destabilises a USB-powered concentrator.
Repeat Bare ACKs in RX2Off by default. Extends the repeat above to payload-less frames — a bare acknowledgement, or MAC answers on their own. Leave it off unless the fleet reports in a trickle and the Traffic tab shows sensors repeating one uplink at doubling intervals; see How a downlink reaches the sensor. Requires Repeat Downlinks in RX2, which remains the master switch.
Confirmed Downlink RetriesDefault 0. How many times to resend a confirmed downlink the sensor never acknowledged — see Confirmed downlinks. At 0 the missing acknowledgement is reported and nothing is resent, because the sensor may well have acted on the frame and only the acknowledgement went missing. Raise it only for idempotent payloads such as a state set.
Confirmed ACK Timeout (seconds)How long to wait for that acknowledgement before reporting it missing. Leave empty to use each sensor's Heartbeat Interval — the acknowledgement rides the sensor's next uplink, so a sensor that reports twice a day cannot be judged undelivered in a minute.
Field diagnosis: gateway resets on USB

A USB-powered concentrator on a weak port, unpowered hub, or long/thin cable can brown out and re-enumerate during transmit bursts — the current spike from the radio PA rides on top of the corecell's steady draw. Set TX Power to 12–17 dBm (which strips the PA entries from the gain table entirely) and prefer a short cable or powered hub. If the gateway resets even with no downlink traffic, the baseline draw is the problem and only the cabling/power fix will help.

This is worth checking after an upgrade on US915/AU915, where the default moved from 14 dBm to 20. A gateway that never had an explicit TX Power now transmits above 17 and so engages the PA where it previously could not. The symptom is unmistakable — the concentrator stops sending keepalives and the gateway shows as disconnected — and the fix is to set TX Power to 17 or lower for that gateway. Concentrators with their own power rail (mini-PCIe on a powered carrier, or a powered hub) are unaffected.

Field diagnosis: OTAA sensors join every day but never uplink

The traffic log shows nothing but join requests and accepts — each sensor rejoining roughly once a day, a minute or two later than the last, with no data uplinks ever, no MIC failures, and no forwarder TX errors. The daily drift is the giveaway that the sensor is unjoined and sweeping join attempts across sub-bands, landing in the gateway's sub-band about once a day.

A verifying MIC means the DevEUI and AppKey are right and the request was genuinely received, so the fault is on the way back. Work through these in order — the first three are content faults that no amount of radio tuning will fix:

  1. The JoinNonce stopped increasing. A device discards any join accept whose JoinNonce is not strictly greater than the highest it has ever received. Builds before this fix generated it randomly, which fails that test about half the time and — worse — pushes the device's stored watermark higher on every join that does succeed, until essentially no random value can clear it and the sensor is locked out for good. See The JoinNonce counter for the fix and the recovery path. This is the most likely cause of a sensor that used to join and now never does.
  2. The advertised RX2 data rate is wrong for the region. Fixed in GEM — the join accept now derives it from the plan's actual RX2 parameters. Older builds advertised DR0 on US915, which is outside the region's valid receive range entirely (US915 downlinks are DR8–DR13), so a conformant device rejects the accept.
  3. The sensor is LoRaWAN 1.0.2 and the CFList is unreadable to it. Only applies to genuinely 1.0.2 hardware — check the datasheet, since 1.0.3 and 1.0.4 devices parse it fine and benefit from it. Set lorawan_version to 1.0.2; see LoRaWAN version and the CFList.
  4. The accept is handed to the concentrator but never radiated. Some USB concentrators (notably the Seeed WM1302 US915 USB module, per Seeed's FAQ) need a longer just-in-time TX lead than the stock forwarder's 40 ms; the window is missed silently while the stats still count the packet as sent. GEM's compile step patches TX_JIT_DELAY to 120 ms automatically — sites compiled before that fix need a Recompile from the gateway's admin page. Two signals now catch this live: a tx buffer clobbered tx_error entry in the traffic log (a packet stuck TX_SCHEDULED was overwritten by the next one — the missed window's only trace), and a startup warning when the binary's build stamp predates the current source patches.
  5. The link is asymmetric. The gateway hears a sensor transmitting at 20 dBm into a good antenna; the sensor has to hear the gateway on a 500 kHz RX1 channel with a chip antenna. Raise TX Power, or raise RX1 Data Rate Offset so RX1 answers at a slower, longer-reaching rate.

The join_accept entry in the Traffic tab records exactly what went out — RX1 and RX2 frequency and data rate, the advertised DLSettings and RxDelay, whether a CFList was included, and the transmit power. Compare those against the sensor's datasheet before suspecting hardware.

Which forwarder may register

A packet forwarder announces itself with a keepalive that tells GEM where to send every downlink for that gateway. Nothing in the protocol authenticates it, and a gateway EUI is not a secret — it travels in the header of every uplink and is printed in the Traffic tab. Two controls decide which sender is believed:

  • Gateway EUI — when the field is set, a keepalive carrying any other EUI is refused and logged. This is the main protection and costs nothing, so fill it in.
  • Allowed Forwarder Hosts (forwarder_allowed_hosts, set in the Attributes editor) — an optional comma-separated list of IP addresses permitted to send packet-forwarder traffic. Leave it blank to accept any source whose EUI matches.

Both controls apply to uplink and keepalive traffic alike, and a refused datagram is not acknowledged. A refusal is logged once per sender and names which kind of datagram it was, so an allow-list that is missing an address shows up as refusing PUSH_DATA from disallowed host rather than as unexplained silence.

Two rules hold regardless of configuration, so a site that has set neither control still gets them:

  • Only a keepalive moves the downlink target. Uplink traffic refreshes a gateway's last-seen time but can never change the address GEM sends downlinks to. A legitimate address change (DHCP renewal, gateway restart) still takes effect promptly, because keepalives arrive every few seconds. An attempted change from uplink traffic is logged once.
  • Transmit verdicts must come from the gateway they were sent to. A TX_ACK is only accepted from the registered address for that EUI, so a forged verdict cannot mark an untransmitted frame as delivered or force a queued downlink to burn its retries.
Managed forwarders listen on loopback only

When a Binary Path is configured, GEM runs the forwarder on the same host and points it at 127.0.0.1, so the UDP port is bound to loopback and no other machine on the network can reach it. That is the whole exposure question settled for a managed deployment, with no configuration.

A gateway with no binary path (external mode) listens on all interfaces, because the forwarder is somewhere else — that is the deployment where the two controls above matter.

A wrong Gateway EUI stops downlinks

On an external-forwarder gateway, a Gateway EUI that does not match the EUI the forwarder actually sends means GEM never records where to send downlinks. Uplinks keep arriving normally, so the gateway looks healthy while every join accept and every queued downlink goes nowhere. If OTAA sensors stop completing joins after this field is filled in, check it against the EUI shown in the Traffic tab, or clear it.

Click Save to create or update the gateway. The gateway is automatically reloaded on save, so a managed packet forwarder restarts with the new radio settings (TX power, sub-band, antenna gain). Click Cancel to close the form.


Devices Tab

The Devices tab manages LoRa sensor nodes. Each sensor is represented as a GEM zone belonging to a gateway.

Gateway Selector and Grouping

Select a gateway from the Gateway dropdown to view and manage its sensors. With only one gateway configured the dropdown is hidden and that gateway is selected automatically — its name and status are shown in the strip above the tabs. Use the Group By dropdown to list sensors flat (None) or grouped by subsystem. The Fleet Alarm Presets buttons share this row — they apply to every LoRaWAN sensor on every gateway, so they don't depend on which gateway is selected here.

When a gateway is selected, a row of summary cards shows the sensor counts: Total Sensors, Online, Offline, Low Battery, and Weak Signal. Sensors that have never sent an uplink (Status "No data") are counted as neither online nor offline — the Offline card shows them as a "N never reported" note so the numbers visibly add up to the total.

Fleet Alarm Presets

Beside the Group By dropdown, a row of one-click buttons — Water Leak, Door Open, Temp High, Temp Low, Battery Low, Battery Voltage Low — each creates a single alarm definition scoped to every LoRaWAN sensor on every gateway (current and future) via a driver-scope condition. The alarm engine still tracks state per sensor, so each sensor raises its own alarm event. A button that has already been used is checked and disabled.

Definitions are created without notification steps; open the definition in Admin → Alarms to adjust thresholds, add notifications and escalation, or narrow the scope to specific devices — the hint under the preset buttons links straight to the Definitions tab, pre-filtered to the LoRaWAN category. A sensor that stops reporting raises the Zone Offline alarm on its own, without a preset — see Offline detection and alerts.

Sensor Table

The table lists all sensor zones for the selected gateway, sorted alphabetically by label (subsystem groups are also sorted alphabetically):

ColumnDescription
Zone IDThe zone record's id — the number macros, triggers, widgets and the REST API use to address this sensor.
LabelThe sensor display name. Click it to open this sensor's underlying zone record on the Zones page; clicking anywhere else in the row expands it in place.
DevAddrThe device address (shown in monospace). For OTAA sensors this is the current session DevAddr (rotates on every Join); for ABP sensors it is the static DevAddr programmed into the sensor.
ModeActivation mode: OTAA (green badge once joined, gray when not yet joined) or ABP.
StatusOnline (green), Offline (red), or No data (gray). A sensor is considered offline if no uplink has been received within its offline window (twice its heartbeat interval, or the offline_threshold attribute when set — see Offline detection and alerts).
BatteryBattery level if reported by the sensor — percentage or voltage, color-coded good/low/critical. When enough history exists, a ~Nd badge estimates the days of battery remaining.
Last UplinkTime since the last received packet (e.g., "5m ago", "2h ago"). Hover to see the full timestamp.
RSSISignal strength of the last received packet.

The sensor readings — Battery, Last Uplink, RSSI and the decoded values — refresh in place every 10 seconds while the tab is open, in a single read covering the whole gateway, so a large fleet costs one round trip per refresh rather than one per sensor. A refresh that is still in flight when the next one is due is skipped rather than stacked, which keeps a slow link (a site reached over the tunnel, say) from falling behind.

The battery ~Nd forecast badges are calculated separately, after the table has appeared — each one reads that sensor's history — so on a large fleet they fill in over the seconds following the first paint rather than holding the table back.

Click any sensor row to expand it. The expanded panel shows packet statistics for the last period — Packets/hr, Avg RSSI, Avg SNR, FCnt Gaps (missed frames), and Total Packets — a small activity sparkline, and the most recent Decoded Values the payload decoder produced.

The expanded panel also holds a Reset Frame Counters button, and shows a warning when recent uplinks have been rejected as frame-counter replays. Replay protection rejects any frame whose counter is equal to or lower than the last accepted one, so a sensor that was rebooted, rekeyed, or factory reset restarts at zero and looks dead while every uplink is discarded. Resetting clears the stored uplink/downlink counters (and, for OTAA sensors, the recorded DevNonces) so the device is accepted again.

The counter is restored from the sensor's stored frame_counter when the gateway starts, so replay protection survives a controller restart rather than resetting to zero and accepting one replayed frame per sensor.

The leftmost column of each row is a selection checkbox, and the checkbox in the header selects every sensor in that group. Above the table, a bar shows how many sensors are selected and offers Select All, Clear, and Bulk Downlink — see Bulk downlink.

Each row has these action buttons:

ButtonDescription
EditOpens the sensor form pre-filled with this sensor's settings.
DownlinkOpens the downlink form to send a command back to this sensor (see Sending Downlinks).
DuplicateOpens a new sensor form pre-filled with the same subsystem, payload decoder, and heartbeat interval — it clears the name and label and generates a fresh set of random ABP keys.
DeleteRemoves the sensor zone (with confirmation).

Below the table, the action bar has these buttons:

ButtonDescription
New SensorOpens the sensor form to add a sensor.
ProfilesOpens the Device Profiles panel for reusable decoder/heartbeat/downlink presets.
DiscoveredOpens the Discovered Devices panel, which lists sensors heard in the traffic log that don't yet have a zone.
ImportBulk-imports sensors from a .json or .csv file — see Bulk Import for the expected fields and format.
ExportDownloads every sensor on the selected gateway as a JSON file in the same format Import accepts — see Export.

Sensor Form

The sensor form opens in a modal dialog (via New Sensor, Edit, or Duplicate). Clicking outside the dialog or pressing Escape does not close it — only Save and Cancel do, so in-progress entries are never discarded accidentally.

FieldDescription
NameRequired. Lowercase zone name, no spaces (e.g., temp_sensor_1).
LabelDisplay name (e.g., Temperature Sensor 1). Auto-filled from the name when left blank (temp_sensor_1Temp Sensor 1).
SubsystemRequired. The GEM subsystem this sensor belongs to (e.g., sensors, climate, security).
ProfileOptional. Appears when at least one device profile exists. Pick a profile and click Apply to fill in the decoder, heartbeat interval, and downlink templates from that profile.

Activation

Choose the activation mode from the dropdown next to the Activation heading. It matches how the physical sensor is configured:

  • ABP (Activation By Personalization) — session keys and DevAddr are pre-programmed on the sensor and entered into GEM.
  • OTAA (Over-The-Air Activation) — the sensor joins the network by exchanging keys with GEM. DevAddr and session keys are derived on each Join and rotate automatically. New sensors default to OTAA.

The Generate button (next to the mode dropdown) fills in random keys for the currently selected mode: for ABP it generates DevAddr, NwkSKey, and AppSKey; for OTAA it generates DevEUI and AppKey. Use it when you are provisioning a sensor you can program yourself rather than entering keys from a label.

In OTAA mode, a Scan Label button lets you use the device camera to read a QR code or label and auto-fill the OTAA identifiers. If you already have a photo or screenshot of the label, use the Upload Image button to pick the file, or simply paste an image from the clipboard (Ctrl+V) while the sensor form is open — both feed the same scanner and auto-fill DevEUI, AppEUI/JoinEUI, and AppKey. The scanner reads the label with the AI assistant's Anthropic provider, so an Anthropic API key must be configured under Admin → AI Assistant (or in gem.json). The label is read twice and the passes are compared — if a field differs between reads, a warning asks you to verify that value against the physical label before saving.

ABP Fields
FieldDescription
DevAddrRequired. 4-byte (8 character) hex device address.
NwkSKeyRequired. 16-byte (32 character) hex network session key.
AppSKeyRequired. 16-byte (32 character) hex application session key.
OTAA Fields
FieldDescription
DevEUIRequired. 8-byte (16 character) hex unique device identifier. Static — programmed into the sensor at manufacture. Checked for duplicates on blur and again at save — a DevEUI (or ABP DevAddr) already provisioned as a sensor on any gateway shows "already provisioned on <gateway> as <sensor>" and blocks saving. The sensor named in that hint is a link that opens the conflicting sensor's zone record in a reference modal over the form, so you can inspect it without abandoning what you've typed; the gateway is a name only and stays plain text. When editing, the sensor's own address is exempt, and the save-time check only runs if the address was actually changed — editing other fields (decoder, heartbeat, mappings) never re-triggers it.
AppKeyRequired. 16-byte (32 character) hex application key used to derive session keys during Join.
JoinEUIOptional. 8-byte hex join server identifier. Defaults to zeros if omitted.

When editing an OTAA sensor, the form shows its join state — a Joined badge with the current DevAddr and join time, or Not joined.

info

For OTAA sensors, the DevEUI is the static identifier used to match incoming Join requests. After a successful Join, GEM derives the current DevAddr, NwkSKey, and AppSKey and stores them as zone attributes. The DevAddr column in the sensor table reflects the current session DevAddr; dev_addr is a read-only attribute that rotates on every re-join. The NwkSKey and AppSKey fields are not shown in the OTAA form — they are populated automatically.

Join accepts are transmitted twice: in the RX1 window and again in RX2 (6 seconds after the join request, on the region's fixed RX2 frequency and data rate). A device that decoded the RX1 accept has already closed its windows and ignores the duplicate; one that missed RX1 gets a second, more robust chance to complete the join.

The DevEUI, AppKey, and JoinEUI fields (and the ABP DevAddr/NwkSKey/AppSKey fields) validate live as you type: a red border and message flag a wrong length or non-hex characters, and separators (-, :, spaces) are stripped automatically when pasting values copied from a label or vendor portal. Save stays disabled while any field is invalid.

When you edit an OTAA sensor that has not joined yet, a diagnosis line appears under the "Not joined" badge explaining what the gateway has actually seen from that DevEUI — no join request at all (check power/range/DevEUI), a join request whose MIC failed (the AppKey doesn't match — re-check or re-scan the label), a DevNonce replay (factory-reset device — use Reset Frame Counters), or an accept the device never confirmed (missed RX window). It also counts join attempts and MIC failures over the last 24 hours.

Monitoring

FieldDescription
Heartbeat Interval (seconds)The cadence at which this sensor is expected to send uplinks. Default: 43200 (12 hours). A dropdown beside the field offers common intervals (15 minutes through 7 days) and fills the value in; any custom number of seconds can still be typed. A sensor quiet for twice this period raises the Zone Offline alarm (see Offline detection and alerts).
Device ClassA (default) or C. Class A devices only listen briefly after their own uplinks, so downlinks queue until the next uplink; Class C devices listen continuously and receive downlinks immediately. Must match how the physical sensor is configured — see Device class and RX timing.

The Downlink Command Mapping section maps standard GEM zone commands (on, off, open, close, stop, toggle) to downlink payloads, so a LoRa actuator — a valve, relay, or siren — can be controlled from macros, widgets, schedules, and voice like any other zone. Click Add Mapping, pick a verb, and enter the hex payload and fPort (or fill them from a profile's downlink template). Each mapping is stored as a <verb>_action override on the zone; when the command fires, GEM dispatches the mapped downlink through the gateway, resolving the sensor's current DevAddr at send time (so OTAA re-joins never break a mapping). Class C devices receive the command immediately; Class A devices on their next uplink. Removing a mapping row and saving clears the override.

Payload Decoder Section

FieldDescription
TemplateSelect a pre-built decoder template from the dropdown to populate the decoder text area. Templates cover generic shapes (Temperature + Humidity, Door/Window Contact, Water Leak, raw hex dump), the Cayenne LPP and Elsys generic TLV formats (each of which covers a whole family of devices), and specific sensor models: Dragino LHT65 / LDS02 / LWL02 / LDDS75 / LSE01, Milesight EM300-TH / EM310-UDL / AM103 / WS301 / WS101 / WS202, Sensative Strips (one template covering Drip / Comfort / Guard / Presence — the complete official TLV frame set, including battery, temp/humidity/lux, door, flood, tamper, presence/IR, and capacitance diagnostics; history replays and non-data ports are ignored), Browan TBWL100 / TBMS100, and Netvox R311A.
Device RepositoryFetch the device's official payload codec straight from the TTN Device Repository (~150 vendors with published device definitions, listed alphabetically). Click Browse TTN Device Repository, pick the vendor and device model, and click Fetch — the codec fills the decoder editor with a provenance header and is verified against the repository's own published test vectors (the toast reports how many passed). Fetches are strictly one device at a time, which is what the repository's license permits; nothing is bulk-mirrored. Requires internet access at configuration time only.
DecoderStandard LoRa Alliance TS013 payload codec: JavaScript that defines decodeUplink(input), receiving {bytes, fPort} (bytes is a Buffer, array-compatible), and returns {data: {...}} (or {errors: [...]} to reject a frame). Decoders published by sensor vendors, the TTN console, or ChirpStack paste in unchanged.

Example decoder:

function decodeUplink(input) {
return { data: { temperature: input.bytes.readInt16BE(0) / 100 } };
}

The returned data object is flattened into scalar zone attributes: nested objects join with underscores ({wind: {speed: 5}}wind_speed), camelCase keys become snake_case (batteryVoltagebattery_voltage), and the common {value: X} wrapper collapses onto its parent key ({temperature: {value: 25}}temperature: 25). Arrays are stored as JSON strings, and output is capped at 64 attributes per uplink.

Decoders must be synchronous

TS013 codecs return {data} directly, so async, await, Promise and queueMicrotask are rejected before the decoder is compiled, with decoder must be synchronous. An asynchronous decoder never produced a usable result anyway — the runner reads result.data off the returned promise and finds nothing — but it could also leave work running past the decoder's execution limit and stall the controller.

The check ignores comments and string literals, so a codec that merely mentions the word "async" in a header comment is fine. If a vendor codec genuinely awaits something, strip that section: everything a decoder needs is already in input.bytes and input.fPort.

The decoder's returned value is also size-capped (256 KB serialized) so one uplink cannot exhaust controller memory. Real payloads are a few hundred bytes.

Two helpers sit alongside the decoder:

  • Auto-detect (shown when editing a sensor that has no decoder yet) tests the built-in templates against the sensor's recent real payloads and lists the templates that decode cleanly, ranked by success rate — click Apply to use one.
  • Test lets you paste a hex payload and an fPort and run the decoder immediately, showing both the flattened attributes that would land on the zone and the codec's raw return value — or the error — so you can iterate before saving.

Click Save to create or update the sensor. The gateway device is automatically reloaded so it picks up the new or changed zone. Click Cancel to close the form.

Discovered Devices

Click Discovered to scan the gateway's recent traffic log for sensors that are transmitting but don't yet have a zone. Each unknown device is listed by its DevAddr/EUI with the packet count, last-seen time, and RSSI. Click Add to open a pre-filled sensor form for that device. Click Close to dismiss the panel.

Not everything heard here is yours — on a shared band the list also collects the neighbours' devices. Ignore drops that address before the traffic log, which removes it from this list and stops it filling the gateway's rolling buffer; see Ignoring a device.

On sites with more than one gateway, a device that is already provisioned on a different gateway shows an "on <gateway>" badge instead of an Add button — it is being heard here through overlapping radio coverage, and adding it again would create a duplicate. To move a sensor between gateways, delete it from the old gateway first.

Bulk Import

Click Import to add many sensors at once from a .json or .csv file. The imported sensors are attached to the gateway currently selected in the Gateway dropdown (the file does not specify a gateway), and each is created enabled.

Rows whose DevEUI or DevAddr is already provisioned — on any gateway, or earlier in the same file — are skipped and reported in the import results ("already provisioned on <gateway> as <sensor>"), so re-importing an export never creates duplicates.

The file type is chosen by its extension:

  • JSON — an array of sensor objects (a single bare object is also accepted). Use JSON whenever a value contains a comma — most importantly a payload_decoder, since decoder code almost always does.
  • CSV — the first line is a header row of field names; each following line is one sensor, with comma-separated values in the same column order. The CSV reader is deliberately simple: it splits on commas with no quoting or escaping, so any value that itself contains a comma will break the row. Don't import decoders via CSV — use JSON for those.

Each sensor entry recognizes the following fields. Only name is effectively required; every other field is optional and is skipped when empty, so a file can mix OTAA and ABP sensors and partial rows.

FieldDescription
nameLowercase zone name, no spaces (e.g., temp_sensor_1). Also used to label the row in any import error.
labelDisplay name. Defaults to name if omitted.
subsystem_idNumeric id of the GEM subsystem the sensor belongs to. Defaults to 0.
activation_modeOTAA or ABP.
dev_euiOTAA only. 16-character DevEUI — the identifier the Join request is matched against.
app_keyOTAA only. 32-character AppKey.
join_euiOTAA only, optional. 16-character JoinEUI/AppEUI. Defaults to zeros if omitted; not required for joining (matching is by DevEUI + AppKey).
dev_addrABP only. DevAddr. Leave blank for OTAA sensors — their DevAddr is assigned at Join and rotates.
nwk_s_keyABP only. NwkSKey.
app_s_keyABP only. AppSKey.
payload_decoderJavaScript decoder source for this sensor. JSON files only (contains commas).
heartbeat_intervalExpected uplink interval in seconds, used for offline detection.
device_classA or C — see Device class and RX timing.

Any other columns or keys are ignored. After the import, GEM reloads the gateway and a toast reports how many sensors were imported and how many failed.

JSON example:

[
{
"name": "soil_probe_01", "label": "Soil Probe 01", "subsystem_id": 11,
"activation_mode": "OTAA",
"dev_eui": "0011223344556677",
"app_key": "00112233445566778899AABBCCDDEEFF",
"heartbeat_interval": 7200,
"payload_decoder": "function decodeUplink(input){ return { data: { temp: input.bytes[0] } }; }"
},
{
"name": "door_contact_02", "label": "Door Contact 02", "subsystem_id": 11,
"activation_mode": "ABP",
"dev_addr": "26011F88",
"nwk_s_key": "0A1B2C3D4E5F60718293A4B5C6D7E8F9",
"app_s_key": "F9E8D7C6B5A4938271605F4E3D2C1B0A"
}
]

CSV example:

name,label,subsystem_id,activation_mode,dev_eui,app_key,heartbeat_interval
soil_probe_01,Soil Probe 01,11,OTAA,0011223344556677,00112233445566778899AABBCCDDEEFF,7200

Export

Click Export to download every sensor on the selected gateway as a JSON file. The file uses exactly the field names Bulk Import accepts — including activation_mode, activation keys, payload_decoder, heartbeat_interval, and device_class — so it round-trips: export from one gateway (or before a rebuild), import on another, and the fleet is reprovisioned. This matters especially for ABP sensors, whose session keys cannot be recovered from the hardware.

warning

The exported file contains the sensors' secret keys (AppKey, NwkSKey, AppSKey). Treat it like a credential file — store it encrypted and don't commit it anywhere.

Device Profiles

Click Profiles to manage reusable sensor presets. A profile bundles a payload decoder, a heartbeat interval, and a set of downlink templates so you can apply consistent settings across many identical sensors. Create a profile by giving it a name, pasting decoder code, setting the heartbeat, and adding any number of named downlink templates (name + hex payload + fPort). Existing profiles can be edited or deleted from the table. Apply a profile to a sensor with the Profile picker in the sensor form.

Click Downlink on a sensor row to open the downlink form for that sensor. It opens as a dialog over the sensor table, so the row you started from stays where it is:

FieldDescription
Payload (hex)The downlink bytes as a hex string (e.g., 0102FF). A warning appears under the field when the payload is larger than the frequency plan's RX2 window can carry (33 bytes on US915/AU915, 51 on the 125 kHz plans) — such a frame can only go out in RX1, with no second chance if the sensor misses it. See the RX2 size limit.
FPortThe downlink port (1–223).
TemplateOptional. Appears when downlink templates are available (from the device's profile). Selecting one fills in the payload and fPort.
Request ACKWhen checked, obliges the sensor to acknowledge the frame on its next uplink. The acknowledgement is reported in the traffic log; on its own it does not resend a frame the sensor never confirmed — see Confirmed downlinks.
Queue (Class A)When checked, the downlink is explicitly queued for the sensor's next uplink rather than attempting an immediate send.

Queued downlinks are listed under Pending Queue with a Cancel option. The button reads Queue Downlink when Queue is checked and Send Downlink otherwise.

Queueing the same payload and fPort twice does not queue it twice — an identical pending frame is collapsed onto the existing entry, so a double-tap can't make the sensor act on the command twice. A queue also has a depth limit of 20 frames per sensor; hitting it means the sensor isn't uplinking (or its downlinks aren't being transmitted), which is a fault to investigate rather than a backlog to grow.

Provisioning a site usually means sending the same frame to a whole batch of sensors — a reporting interval, a threshold, a mode. Select the sensors with the checkboxes in the sensor table (the header checkbox takes a whole group, Select All takes the gateway) and click Bulk Downlink.

The form opens as a dialog over the sensor table, so the selection it acts on stays visible behind it. It takes the same Payload (hex), FPort, Template and Request ACK fields as a single downlink, plus Queue all, which queues every frame instead of transmitting the Class C ones immediately. Above the button, GEM states what is about to happen — how many sensors receive the frame now and how many queue for their next uplink — and lists the selected sensors with their device class, so a fan-out that will sit in queues for hours is not mistaken for one that has already landed.

Each sensor is dispatched exactly as a single downlink would be, so device class, queueing, duplicate collapsing, frame counters and traffic logging behave identically. A few specifics:

  • One sensor failing never stops the rest. Every target gets a row in the result list — sent (with the window it went out in), queued, or failed with the reason. Each sensor's name in the list opens its zone record in a reference modal, so a failed row is one click from the sensor it names. Progress is reported while the run is in flight.
  • Class C sends are spaced by airtime. The concentrator has one transmitter, and an immediate downlink is never deferred to a free slot the way a scheduled one is, so GEM paces consecutive immediate frames by the time-on-air of one RX2 frame. Queued frames cost nothing here — they leave on each sensor's own next uplink.
  • A sensor selected twice is sent to once, and a sensor belonging to another gateway is reported as such rather than dispatched.
  • The payload is validated once, before anything is transmitted — a malformed payload in a fan-out is the same mistake repeated over the air N times.
  • At most 200 sensors in one bulk downlink.

Bulk downlink is subject to the same per-sensor queue depth of 20 and the same identical-frame collapsing as a single downlink, so repeating a bulk send to sensors that haven't uplinked yet doesn't stack duplicates. The dialog stays open while the run is in flight — the per-sensor report lives there, and the run continues regardless.

Gateway-wide queues — including frames stranded under a DevAddr no sensor uses any more — live on the Downlinks tab. The Pending Queue in this form only shows what the sensor's current DevAddr points at.

Request ACK sets the LoRaWAN confirmed-downlink bit: the sensor must acknowledge the frame on its next uplink, and GEM records that acknowledgement in the traffic log. If it never arrives within the Confirmed ACK Timeout (by default the sensor's Heartbeat Interval), the traffic log gets a no acknowledgement from device entry and a warning is written to the system log.

Whether a missing acknowledgement causes a resend is a separate, deliberate choice — the gateway's Confirmed Downlink Retries setting, which defaults to 0. A confirmed downlink that goes unacknowledged usually means one of two things happened, and the network cannot tell them apart: the frame never reached the sensor, or the sensor received and acted on it and the acknowledgement was lost on the way back. Resending covers the first case and repeats the actuation in the second. For an idempotent payload (set a state, set a threshold) that's harmless and worth enabling; for a payload that steps or toggles something, it isn't.

LoRaWAN sensors only listen for downlinks at specific times, so GEM schedules each frame into the right receive window based on the sensor's Device Class attribute (see Device class and RX timing):

  • Class A (default) — the sensor only opens a brief receive window right after it sends an uplink. GEM therefore queues the downlink and transmits it in the RX1 window of the next uplink, through the same gateway that heard that uplink. This happens whether or not you check Queue — sending to a Class A sensor with no uplink pending simply queues the frame. The confirmation message reads "downlink queued — sends in RX1 on the next uplink." For a battery sensor that uplinks every few hours, expect a corresponding delay before the command lands.
  • Class C — the sensor listens continuously on the RX2 channel, so GEM transmits the downlink immediately on the region's RX2 frequency. The confirmation message reads "downlink sent (RX2)."

Only one queued frame is delivered per uplink, and a queued frame is kept (not dropped) if the transmit fails — it retries on the following uplink. That covers both kinds of failure: GEM failing to schedule the frame, and the packet forwarder accepting it and then refusing to transmit it (see Downlink TX errors). A frame refused five times running is dropped and reported rather than riding every uplink forever.

Every Class A downlink that carries a payload is transmitted twice: once in RX1, and again one second later in RX2 on the region's fixed, most robust channel. A sensor that decoded the RX1 copy has already closed its windows and never sees the second; one that missed it gets another chance. On US915 this matters more than it sounds — RX1 is a 500 kHz channel derived from the uplink, which is the most fragile part of the downlink path, while RX2 is a fixed SF12 channel. Clear the gateway's Repeat Downlinks in RX2 setting to send RX1 only.

Three things are excluded from the repeat:

  • Bare ACKs and MAC-only answers, unless Repeat Bare ACKs in RX2 is set. A frame with no application payload is not duplicated by default — if the sensor misses it the protocol nominally covers the loss, since an unacknowledged confirmed uplink is retransmitted and an unanswered MAC command is re-asked. The duplicate is not free: an RX2 copy at SF12BW500 occupies the concentrator's single transmitter for the better part of a second, and that is what blocks the next downlink's RX1 window when the sensor uplinks again a second or two later.

    That default is worth revisiting on a site where the sensors send confirmed uplinks, because "the protocol covers it" is worth exactly what the sensor's retry schedule is worth. A Sensative Strips doubles its retry interval — "1, 2, 4, 8... minutes up to once per day" in Sensative's own documentation — and sends flood and tamper alarms as confirmed messages precisely so they are retried until answered, so a missed acknowledgement costs a day of resends on a battery meant to last years. A sensor stuck in that ladder never advances to its next report: it looks alive in the Traffic tab and yet sets no attributes, which reads as "connected but reporting nothing." The signature is one uplink payload repeating at doubling intervals, each paired with a downlink whose ack flag is set. Turn Repeat Bare ACKs in RX2 on there, and check the transmit power while you are at it — the acknowledgement is on the weak half of the link either way.

  • A window the transmitter is already committed to. The gateway can only transmit one packet at a time. GEM tracks what it has scheduled and drops the conflicting window rather than have it refused, keeping whichever window is clear — including sending a single-window frame in RX2 when its RX1 is blocked. See Why COLLISION_PACKET happens.

  • A window whose data rate cannot carry the payload. RX2 is the region's slowest, most robust rate, and on US915/AU915 that is SF12BW500 — which tops out at a 33-byte application payload. The 125 kHz plans (EU868, AS923, KR920, IN865) answer RX2 at SF12BW125 and carry 51 bytes. A larger frame rides RX1 (222 bytes on the US915 500 kHz block) but physically cannot go out in RX2, so GEM drops that leg rather than hand the forwarder a frame the rate cannot legally carry. The downlink's traffic entry shows an RX2 too large badge, and the downlink form warns before you queue it. This is worth avoiding rather than living with: the repeat exists precisely because RX1 is the fragile half, so the frames big enough to lose it are the ones that most need it. Split the payload into downlinks at or under the plan's RX2 size to keep both windows. A frame that fits no window is refused outright and stays queued — a Class C sensor, which has only RX2, gets an error rather than a silently half-delivered frame.

Device class and RX timing

The Device Class is set directly on the sensor form (Monitoring section): A (default) queues downlinks for the next uplink's RX1 window; C receives immediately on RX2. It must match how the physical sensor is configured.

One further advanced attribute can be set per sensor in the Attributes editor:

AttributeDescription
rx_delayRX1 delay in seconds — the gap between a Class A uplink and its RX1 receive window. Default 1, which suits most devices. OTAA sensors are told this value in the join accept, so changing it takes effect at the sensor's next join; an ABP sensor has no join, so the same value must be provisioned on the device itself.
lorawan_versionThe MAC version the sensor implements, from its datasheet — 1.0.2, 1.0.3 (default), 1.0.4, or 1.1.0. On US915/AU915 this decides whether the join accept carries a channel-mask CFList. See Sensors that join every day but never complete.

The JoinNonce counter

Every join accept carries a JoinNonce. The device stores the highest one it has ever received and discards any join accept whose JoinNonce is not strictly greater — that is what stops an attacker replaying a captured join accept. LoRaWAN 1.0.4 makes the check mandatory, and the LoRa Alliance recommends it for earlier versions too.

The value is allowed to be random, but only if it still increases, which a random draw does not. GEM now sends a per-sensor counter, stored in the join_nonce attribute and incremented on every join accept. It only ever goes up, and the frame-counter reset deliberately does not clear it — winding it backwards is precisely what locks a device out.

Sensors joined by an older build

Builds before this fix generated the JoinNonce randomly. Each successful join replaced the sensor's stored watermark with a fresh random draw, so the watermark climbed toward the top of the 24-bit range: after a handful of joins most new random values fell below it, joins became rare, and eventually stopped entirely. The symptom is a sensor that used to work and now sends join requests forever.

GEM recovers automatically where it can: every fresh counter starts at 16,711,681 rather than at 1 — above all but the unluckiest watermarks, with 65,535 joins of headroom left. This applies whether or not the sensor has joined this GEM before, because the watermark can come from a prior life GEM never saw: bench provisioning, a previous network server, or a zone that was deleted and re-created. A factory-fresh sensor accepts any first value, so starting high costs nothing.

This is best effort, not a guarantee: the watermark lives on the sensor and cannot be read back. If a sensor still will not join after the upgrade, raise its join_nonce attribute further (the ceiling is 16,777,215). If it reaches the ceiling, only a factory reset of the sensor — which clears its own stored copy — will recover it, and GEM refuses to wrap the counter rather than emit accepts the device is certain to discard.

LoRaWAN version and the CFList

On US915 and AU915 the join accept can carry a CFList — a 16-byte channel mask telling the sensor which sub-band to use, so it stops sweeping all 64 channels and converges on the ones the gateway listens to. That mask format was introduced in LoRaWAN 1.0.3, along with the type byte that identifies it.

A 1.0.2 sensor has no such byte in its parser. It reads the same 16 bytes as a list of five frequencies, decodes nonsense, and — depending on the stack — either configures unusable channels or discards the whole join accept. Either way the sensor never completes the join and keeps sending join requests, typically landing in the gateway's sub-band about once a day as its sweep drifts.

Set lorawan_version to 1.0.2 for those sensors and GEM omits the CFList entirely, matching what other network servers do. The trade-off is that the sensor keeps using its full channel plan, so only a fraction of its uplinks land in the gateway's sub-band until it is configured for that sub-band by other means.

The 125 kHz plans (EU868, AS923, IN865) use the older frequency-list CFList, which predates 1.0.3 and is always sent.

MAC commands

GEM answers the LoRaWAN MAC commands a sensor can send, piggybacked on the same downlink as any queued frame — a Class A uplink opens only one receive window, so everything owed to the sensor leaves together.

CommandBehaviour
LinkCheckReqAnswered with the uplink's margin over the demodulation floor for its spreading factor, and the number of gateways that heard it. This is how a sensor decides whether it is still in range.
DeviceTimeReqAnswered with the current GPS time.
DevStatusAnsRecorded as the battery_percent, battery_source, and device_margin attributes.
Confirmed uplinksAcknowledged. An unacknowledged confirmed uplink is retransmitted, and a sensor that exhausts its retries drops to a lower data rate and eventually rejoins.
ADRACKReqAnswered with a downlink. The sensor is asking whether the network still hears it; without a reply it demotes its data rate and ultimately rejoins.

GEM does not run an ADR algorithm — it never tells a sensor to change data rate or channels. Sensors keep the rate they choose, which is the conservative behaviour for battery devices whose link budget is set at install time.


Traffic Tab

The Traffic tab shows a live log of LoRa packets for a gateway.

Gateway Selector

Select a gateway to monitor. With only one gateway configured the dropdown is hidden and that gateway is monitored automatically. Packets appear in real time as they arrive — the tab subscribes to the gateway's live feed, so there is no fixed refresh interval. Click Refresh to reload the full stored log on demand.

Filters and Export

A filter bar lets you narrow the log:

  • Device — All Devices, or a specific sensor.
  • Direction — All, Uplink, Downlink, or Join.
  • MIC — All, MIC OK, or MIC Fail.
  • Time — All Time, Last 1 hour, Last 24 hours, or Last 7 days.

A packet count above the table shows how many packets match (and how many were filtered out). CSV and JSON buttons export the filtered log. The Ignored button opens the suppression list described below.

Ignoring a device

LoRa is a shared, unlicensed band, and the concentrator hears every device within range — including the neighbours'. Those frames match no sensor, fail their MIC check (GEM has none of their keys), and still consume the gateway's rolling 500-entry traffic log. One chatty stranger reporting every few seconds can evict your own sensors from the log entirely and bury them in the Discovered Devices list.

The last column of each traffic row carries an Ignore button for any frame that matches no sensor on this gateway. Clicking it suppresses that DevAddr (or DevEUI, for join requests): the frames are dropped before the traffic log, so they no longer fill the buffer, no longer arrive over the live feed, and no longer appear as discovered devices. Frames already in the log for that address are removed at the same time. The Discovered Devices panel offers the same action per row.

Ignored in the filter bar lists every suppressed address with how many frames each has dropped and when it was last heard, along with Remove per address, Clear All, and a field for typing an address in directly. Counts are since the gateway last started — the list itself is stored on the gateway (the ignored_dev_addrs attribute) and survives restarts.

Two properties make this safe to use freely:

  • Suppression never applies to a provisioned sensor. It is gated on the frame matching no sensor on this gateway, so if you later add a zone for an ignored address, its traffic reappears without touching the list. It is not possible to blind yourself to a device you own.
  • It is a capture filter, not a view filter. Suppressed frames are never recorded, so removing an address resumes logging from that moment — the frames dropped while it was ignored are gone. Nothing else is affected: suppression touches only the traffic log, never decoding, attributes, or alerts.

Suppressed frames are also dropped before the deduplication cache and MIC verification, so an ignored device costs the gateway nothing beyond parsing the frame header.

Channel Utilization

When uplinks have arrived in the last hour, a Channel Utilization table appears above the traffic log with one row per radio channel: the packet count, its share of all uplinks, the average RSSI, the estimated on-air time, and that airtime as a duty-cycle percentage of the hour. Airtime is estimated from each packet's spreading factor, bandwidth, and payload size. Use it to spot channels a noisy device is saturating, confirm sensors are hopping channels as expected, and — on EU868 sites — to keep an eye on the regulatory duty-cycle limits (0.1%–10% per sub-band depending on the band).

Traffic Table

ColumnDescription
TimeThe time the packet was received.
DirDirection arrow: ↑ uplink, ↓ downlink, J↑ join request, J↓ join accept, ↓✕ a downlink that was never transmitted (see Downlink TX errors).
DevAddrThe device address (or DevEUI for join requests) of the sending sensor, shown in monospace. For OTAA sensors this is the current session DevAddr assigned at Join.
ZoneThe GEM zone name matched to this device address, or "<unknown>" if no matching zone exists. A frame from a provisioned sensor links to that sensor's zone — click it to open the record in place, rather than losing the live log, its filters and the buffer it has collected.
RSSIReceived signal strength indicator.
SNRSignal-to-noise ratio.
FreqThe radio frequency used.
Data RateThe LoRa data rate (spreading factor and bandwidth).
FCntThe frame counter value. A -N badge flags a gap of N missing frames since the previous packet.
MICMessage Integrity Code verification: OK (green badge) or FAIL (red badge). A Replay badge appears when a frame is rejected for an equal-or-lower frame counter.
InfoContext for the row: decoded payload values for an uplink; fPort + payload and the ACK state (Awaiting ACK / ACK'd) for a downlink; the assigned DevAddr for a join accept; the forwarder's error for a refused downlink.

Click any packet row to expand it. The detail row shows which other gateways also heard the frame (with their per-gateway RSSI/SNR) and the full raw packet as JSON — useful when diagnosing multi-gateway coverage or a decoder problem.

The packet forwarder answers every downlink with a TX_ACK verdict. When that verdict carries an error — TOO_LATE, TOO_EARLY, TX_FREQ, TX_POWER, COLLISION_PACKET, GPS_UNLOCKED — the frame was not transmitted, and GEM surfaces the rejection instead of dropping it silently: a tx_error entry appears in the traffic log (expand it to see the gateway EUI and the error code) and a warning is written to the system log (lorawan: downlink rejected by forwarder). Persistent TOO_LATE errors mean the forwarder is missing its just-in-time TX window — the same failure mode the TX_JIT_DELAY compile patch addresses (see the field-diagnosis tips on the Gateways tab).

Each verdict is matched back to the downlink it answers, so a tx_error row names the sensor, zone, frame counter, and receive window rather than standing alone. Sending a downlink puts a datagram on the wire; the TX_ACK is what says it was transmitted — so a queued frame is only removed for good once a window actually carried it. A Class A frame goes out in both RX1 and RX2, and those are two separate verdicts for one delivery: the frame is put back on the queue only when every window was refused. A collision on RX1 that RX2 carried is a successful delivery and the frame stays gone. Frames put back are shown in the pending queue with a rejected transmit count; after five rejections the frame is dropped and logged (tx_dropped), because an error like TX_FREQ or TX_POWER is a misconfiguration that will be refused every time.

A forwarder that never answers with a TX_ACK at all is treated as success, not failure — silence is not a rejection, and resending on silence would double-send every downlink such a forwarder transmits.

TX buffer clobbers — the quieter failure

A TX_ACK error is the forwarder refusing a downlink. There is a quieter sibling: the forwarder accepts the packet and the concentrator destroys it. The SX1302 has a single TX command buffer, and a packet that lingers TX_SCHEDULED past its emission time (the missed-window failure described above) is silently overwritten by the next packet programmed — the stats still count it as sent, and the only trace is one WARNING: a downlink was already scheduled … overwritting it line on the forwarder's stdout. GEM watches for that line: each occurrence is counted (forwarder_tx_clobbers in gateway health), recorded as a tx buffer clobbered tx_error entry in the traffic log, and answered at most hourly with a system-log pointer at the usual fix — a Recompile, so the TX_JIT_DELAY patch applies.

Every compile also writes a build_info.json stamp next to the binary recording when it was built and which source patches it carries. At startup GEM compares the stamp against the patch level it expects and logs a warning when the binary is older — a forwarder is compiled once and cached forever, so without the stamp a site can keep radiating through pre-fix code long after the fix shipped. Gateway health shows the comparison as forwarder_build.

Why COLLISION_PACKET happens

COLLISION_PACKET is not an over-the-air event — nothing was jammed. The concentrator has one transmitter, and the packet forwarder's JIT queue refuses any packet whose transmit window overlaps one already queued. Each queued packet reserves

tmst − (TX_START_DELAY + TX_JIT_DELAY)tmst + time-on-air + TX_MARGIN_DELAY

Three things make that window wider than it looks:

  • The TX_JIT_DELAY patch. GEM's managed build raises it from the stock 40 ms to 120 ms for the Seeed WM1302 USB path (without it the module misses its window and reports a clean send), which triples the leading edge of every reservation.
  • RX2 airtime. A US915 RX2 downlink is SF12BW500 — roughly 290 ms on air. With the lead-in, one RX2 downlink owns about 400 ms of the transmitter.
  • The RX2 repeat. Each Class A downlink occupies two slots, one second apart. If a second sensor uplinks one second after the first, its RX1 window lands exactly on the first frame's RX2 repeat.

The most common shape is a single sensor uplinking twice in quick succession. A frame's RX2 copy reserves roughly 700 ms; if the sensor uplinks again 1.5 s later, that second uplink's RX1 window falls 500 ms after the pending RX2 — inside the reservation, and refused. Two different sensors uplinking within about a second of each other collide the same way, which is common because sensors on the same reporting interval drift into clusters.

GEM models the same occupancy the forwarder is about to enforce, tracking every downlink it schedules per gateway (including join accepts, which reserve 5–6 seconds ahead). Time-on-air is computed with the concentrator HAL's own formula rather than the datasheet's, because the JIT queue reserves with the HAL's number and the two disagree by 41 ms on exactly the frames involved in real collisions. When a window is already committed, GEM drops that leg instead of feeding the forwarder a packet certain to be refused — and when the primary window is the one that collides, the frame goes out in RX2 alone, which is clear. That converts a refused downlink into a delivered one.

Not repeating bare ACKs (above) removes the other half of the problem: in the sequence described, the frame that was refused was an empty ACK, and the frame that blocked it was the RX2 copy of a real config downlink. The ACK now takes one window, chosen to be a free one. Setting Repeat Bare ACKs in RX2 puts that half of the problem back, which is the trade it exists to let you make — take it only when missed acknowledgements are costing more than collisions are.

The model is deliberately timid, because it can be wrong (an external forwarder built with a different TX_JIT_DELAY, or traffic GEM never saw):

  • a window is only dropped if another window of the same frame survives — a frame is never reduced to nothing;
  • if every window looks occupied, all of them are sent anyway and the forwarder gets the final say;
  • Class C immediate downlinks are never gated, having no timestamp to model.

Skipped windows are noted in the system log (skipping RX1 … the transmitter is already committed) and on the downlink's traffic entry. If you see collisions persist, the levers are the sensors' reporting schedules (stagger them) and Repeat Downlinks in RX2 (turning it off halves the transmitter time each downlink claims, at the cost of RX1-only delivery).

A LoRaWAN downlink is transmitted without a physical-layer CRC — the MIC is the integrity check, and receive windows are configured for a CRC-less frame. The packet forwarder zeroes its transmit struct, so a txpk that omits the ncrc flag is transmitted with a CRC. GEM sets ncrc: true on every downlink and join accept.

Sensors already receiving downlinks are unaffected: the explicit LoRa header signals whether a CRC follows, and this is what every other network server sends, so it is the framing device vendors test against. It also shortens the most common downlink there is — a bare ACK is a 12-byte PHY payload, where the 16 CRC bits push the symbol count over a boundary and cost an extra 41 ms of SF12BW500 airtime.

When GEM is managing the packet forwarder, a Packet Forwarder Log section appears below the table with the forwarder's own stdout/stderr, plus Copy and Clear buttons.

tip

Use the Traffic tab to verify that sensors are communicating correctly. If you see packets with MIC FAIL, the NwkSKey or AppSKey on the sensor does not match what is configured in GEM. A Replay badge instead means the sensor's frame counter went backwards — typically after a reboot, rekey, or factory reset. Expand the sensor's row on the Devices tab and click Reset Frame Counters so new uplinks are accepted.

The frame counter a sensor sends is only 16 bits wide, but the integrity check covers a 32-bit value, so a sensor that passes 65,535 frames rolls over on the air while continuing to count internally. GEM reconstructs the upper half, so the rollover is invisible — a sensor uplinking every 10 minutes reaches it after about 15 months and keeps working.


The Downlinks tab is the gateway-wide view of what has been asked of the sensors but not yet delivered. It lists every queue on the gateway, not just one sensor's, and refreshes when you open the tab or press Refresh — queues drain on the sensors' own uplinks, so nothing polls behind you.

The tab follows the gateway chosen in the Gateway dropdown on the Devices tab (on a single-gateway site, the one gateway). Until a gateway is selected it prompts you to pick one there.

Each queue shows its sensor, DevAddr, and depth, with every pending frame beneath it — payload, fPort, an ack requested marker, how long it has been waiting, and a Cancel button. A frame the packet forwarder has rejected carries its attempt count and last error (see Downlink TX errors); one refused five times running is dropped and reported rather than riding every uplink forever.

The sensor name on a queue — and on an Awaiting Acknowledgement row — opens that sensor's zone record in a reference modal over the tab, so you can check the sensor's attributes without losing the queue view. An orphaned queue has no sensor left to open and stays plain text.

This is the only place a no matching sensor queue is visible. Queues are keyed by DevAddr, so when a sensor is deleted, re-addressed, or moved to another gateway, anything still queued under the old address can never be delivered — and the per-sensor list can't show it, because that resolves through the sensor's current DevAddr. Those frames are dropped automatically an hour after they were queued, on the next gateway reload, and what was dropped is written to the system log.

Below the queues, Awaiting Acknowledgement lists confirmed downlinks that have gone out but not been acknowledged, with an overdue badge once the sensor's expected uplink interval has passed — see Confirmed downlinks.

An empty tab is the normal state: a frame queued for a Class A sensor appears here only until that sensor's next uplink carries it.


Offline detection and alerts

A quiet sensor is handled by two mechanisms:

  • Offline window (badge + alarm) — a sensor counts as offline when no uplink has arrived within its offline window: the offline_threshold zone attribute (in seconds) when set, otherwise twice the sensor's Heartbeat Interval with a 15-minute floor (24 hours at the stock 12-hour heartbeat). The Devices tab Offline badge and the alarm below use this same window, so they always agree.
  • Zone Offline alarm — a sensor past its offline window raises the standard Zone Offline system alarm in the Alarm Center, one stateful event per sensor; a fresh uplink clears it. While the concentrator itself is down (that raises Device Offline for the gateway instead), and after a gateway or server restart, every sensor gets one full window to report before it can alarm — the listener was deaf, so silence proves nothing. Sensors that have never uplinked (status "No data") never raise it. To be notified about a stale sensor, add notification steps to the Zone Offline alarm definition in the Alarm Center — the alarm system is the single notification path, so acknowledgement, shelving, and digests all apply.

The heartbeat sweep is the only thing that decides whether a sensor is offline. GEM's generic connection monitor, which clears the Zone Offline alarm for any zone its transport reports as connected, stands aside on this driver: a Class A sensor sleeps between uplinks and holds no connection of its own, so the monitor would read every sensor as connected no matter how long it had been silent. Before that, a genuinely dead sensor produced a raise and a clear in the same minute, every minute — a "CLEARED + MEDIUM" notification pair on repeat for a device that was simply not there. Sensor liveness now comes from last_uplink and the offline window alone.

Both read one definition of the window, so the badge and the alarm always agree. The 2× margin is deliberate: LoRaWAN is lossy by design — a single uplink lost to an RF collision or deferred by duty-cycle backoff is routine, so the first missed interval is not an outage and does not page anyone.

Earlier versions also sent a direct email/SMS per silent sensor from the gateway itself (Alert Email / Alert SMS on the gateway form), repeating hourly for as long as the sensor stayed quiet. That path bypassed the alarm engine — no acknowledgement, no shelving, no digests — and has been removed; any addresses still stored in heartbeat_notify_email / heartbeat_notify_sms attributes are ignored. Use the Zone Offline alarm's notification steps instead.


  • Devices — Gateway device configuration
  • Zones — Sensor zone management
  • Attributes — Setting offline_threshold, usb_auto_discover, and ignored_dev_addrs
  • Alarms — Alarm definitions on sensor values, Zone Offline events
  • Triggers — Automation on sensor values