LoRaWAN

SF12 the sweet poison

Talk from Semtech about nbTrans: LoRaWAN and the IoT - Olivier Seller (Semtech) (7:20 to 22:00)

ADR

Meta study about confirmed traffic: A Survey on the Viability of Confirmed Traffic in a LoRaWAN | IEEE Journals & Magazine | IEEE Xplore

Devices / Sensors

Decoders and Firmware

My wishlist for a device firmware:

My plea to vendors for driver code:

Wall of shame

Seriously, don't do this (actual code from an actual professional "production-ready" driver):

var i = bytesToInt(byte);
var bm = ('00000000' + Number(i).toString(2)).substr(-8).split('').map(Number).map(Boolean);
return ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
	.reduce(function (obj, pos, index) {
		obj[pos] = bm[index];
		return obj;
	}, {});

Just do this:

return {
	"a": (byte & 0x80) > 0,
	"b": (byte & 0x40) > 0,
	"c": (byte & 0x20) > 0,
	"d": (byte & 0x10) > 0,
	"e": (byte & 0x08) > 0,
	"f": (byte & 0x04) > 0,
	"g": (byte & 0x02) > 0,
	"h": (byte & 0x01) > 0,
};
// you can even use binary literals (i.e. 0b01000000 insead of 0x40, etc.)
// if your environment supports ES6

The following is also pretty bad (I have removed some parts for brevity):

var sensorEuiLowBytes
var sensorEuiHighBytes
var realDataValue = isSpecialDataId(dataID) ? ttnDataSpecialFormat(dataID, dataValue) : ttnDataFormat(dataValue)

...
      switch (dataID) {
...
        case 2:
          // sensor eui, low bytes
          sensorEuiLowBytes = realDataValue
          break
        case 3:
          // sensor eui, high bytes
          sensorEuiHighBytes = realDataValue
          break
...
      }
...

  // if the complete id received, as "upload_sensor_id"
  if (sensorEuiHighBytes && sensorEuiLowBytes) {
    decoded.messages.unshift({
      type: 'upload_sensor_id', channel: 1, sensorId: (sensorEuiHighBytes + sensorEuiLowBytes).toUpperCase()
    })
  }

...

function ttnDataFormat (str) {
  var strReverse = littleEndianTransform(str)
  var str2 = toBinary(strReverse)
  if (str2.substring(0, 1) === '1') {
    var arr = str2.split('')
    var reverseArr = []
    for (var forArr = 0; forArr < arr.length; forArr++) {
      var item = arr[forArr]
      if (parseInt(item) === 1) {
        reverseArr.push(0)
      } else {
        reverseArr.push(1)
      }
    }
    var num = parseInt(reverseArr.join(''), 2) + 1
    return parseFloat('-' + num / 1000)
  }
  return parseInt(str2, 2) / 1000
}

// configurable
function ttnDataSpecialFormat (dataId, str) {
  var strReverse = littleEndianTransform(str)
  if (dataId === 2 || dataId === 3) {
    return strReverse.join('')
  }

  // handle unsigned number
  var str2 = toBinary(strReverse)
  var dataArray = []
  switch (dataId) {
...
    case 7:
      // battery && interval
      return {
        interval: parseInt(str2.substr(0, 16), 2), power: parseInt(str2.substr(-16, 16), 2)
      }
    case 9:
      let dataValue = {
        detectionType: parseInt(str2.substring(0, 8), 2),
        modelId: parseInt(str2.substring(8, 16), 2),
        modelVer: parseInt(str2.substring(16, 24), 2)
      }
      // 01010000
      return dataValue
  }
}

You have realDataValue, which can either be a Number, a string or two different types of structs! realDataValue is then copied to two other variables. So everywhere you want to work with any of these 3 vars, you would have to check which type you are working with! Which the code does not do: (sensorEuiHighBytes + sensorEuiLowBytes).toUpperCase(). At the moment this might be okay, since this codepath is only called with strings. But that is dangerous implicit knowledge with a high potential to explode at some point.
Don't merge codepaths that do different things! Instead of working with a mystery object, explicitly parse the data at the point where you know which data to expect (switch on type bytes, then do the conversion from bytes to whatever data you need).
Then there is also this gem:

var num = parseInt(reverseArr.join(''), 2) + 1
return parseFloat('-' + num / 1000)

Converting a string to an int, to convert it back to a string to negate it, to be converted into a float (which ironically can become NaN if num is already negative). A better solution is left as an exercise to the reader.

Installing The Things Stack on an RaspberryPi in 2025

This basically is an updated version of this "official" guide: Deploy The Things Stack
 in your local network

What do you need?

End result

In the end you will get a working open source installation of The Things Stack on your Raspberry Pi with a gateway connected to it for sending and receiving data. Everything is connected locally through WiFi or Ethernet and without TLS (so http only). You can use this as a private LoRaWAN setup (e.g. for testing).
This guide will focus on the open-source version.

Setting up the Pi

Install the Raspberry Pi Operating System: https://www.raspberrypi.org/software

If you missed setting up WiFi or SSH in the imager:

Find out the IP of your Pi and connect to it via SSH

Installation

Update packages

Install docker

curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo \ "deb [arch=armhf signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin

If anything fails during this step, check the official guide: Debian | Docker Docs

Setup docker

Next, we'll install The Things Stack

sudo mkdir -p /app/the-things-stack

sudo chown username:username /app/the-things-stack 

cd /app/the-things-stack
curl -sSL -o docker-compose.yml https://www.thethingsindustries.com/docs/enterprise/docker/configuration/docker-compose-open-source.yml

Time to configure The Things Stack

HOSTNAME=192.168.178.43

HOSTNAME=demo.thethingsconference.com
CONSOLE_SECRET=$(openssl rand -hex 16)
docker compose run --rm stack config --env > stack.default.env

touch stack.env
grep LISTEN_TLS stack.default.env | sed -e "s/:888[0-9]//" >> stack.env

grep PUBLIC_TLS_ADDRESS stack.default.env | sed -e "s/localhost:888[0-9]//" >> stack.env
grep localhost:188[123] stack.default.env | sed -e "s/localhost/$HOSTNAME/" >> stack.env
grep localhost:1885 stack.default.env | sed -e "s/localhost:1885/$HOSTNAME/" >> stack.env
grep 's://localhost:888' stack.default.env | sed -e "s|s://localhost:888|://$HOSTNAME:188|" >> stack.env
echo TTN_LW_CONSOLE_OAUTH_CLIENT_SECRET=$CONSOLE_SECRET >> stack.env
echo TTN_LW_HTTP_COOKIE_BLOCK_KEY=$(openssl rand -hex 32) >> stack.env

echo TTN_LW_HTTP_COOKIE_HASH_KEY=$(openssl rand -hex 64) >> stack.env
sed -i'' -e 's/"//g' stack.env

Now let's initialize The Things Stack

docker compose run --rm stack is-db migrate
docker compose run --rm stack is-db create-admin-user --id admin --email your@email.com
docker compose run --rm stack is-db create-oauth-client --id cli --name "Command Line Interface" --owner admin --no-secret --redirect-uri "local-callback" --redirect-uri "code"

docker compose run --rm stack is-db create-oauth-client --id console --name "Console" --owner admin --secret "${CONSOLE_SECRET}" --redirect-uri "/console/oauth/callback" --logout-redirect-uri "/console"

Time to start The Things Stack

docker compose up -d stack
docker compose logs -f stack
sudo systemctl enable docker

Setup the CLI

ttn-lw-cli use YOUR_DOMAIN_OR_IP --grpc-port 1884

Gateway Installation

see Adding Gateways | The Things Stack for LoRaWAN and LoRa Basics™ Station | The Things Stack for LoRaWAN for adding Gateways.

Using Basic Station did not work with Tektelik Micro Gateways. There were TLS errors (probably because the connection to the PI is not secured) - even though TLS was disabled in the configuration on the gateway (maybe it's a gateway bug?).

Installation of the UDP Package Forwarders worked instead (Tektelik calls is "Kona Package Forwarder", installation instructions for TTN further down the page linked above). To setup you will need the KonaFT tool. Chose the "SNMP V2c" protocol. Username and password can be found on the "KONA Test Summary Certificate" which comes with the gateway.

Troubleshooting

If you are getting "OAuth" errors when trying to login to the ThingsStack GUI, there probably is a mismatch between the IP you are accessing the GUI on and the one setup for the auth-service in the stack.env file.
Solution: Confirm the IP of the Raspberry Pi is the same as the one in stack.env, change it if necessary and restart the docker container. See below for caveats.

Changing the IP

If the IP of the RaspberryPi ever changes, you need to change the IPs in the stack.env file, close the application with docker compose down (if it is running) and restart it with docker compose up -d stack (docker compose restart did not work for me).

IP changes

You should only do this while you have not registered any device yet. This is also why using a domain is preferred.

Applications and Devices are registered on a "cluster", which is determined by the IP of the Server (i.e. the RaspberryPi) at the time of creation. If the IP changes, you will need to change the "cluster" of the application using the CLI:

ttn-lw-cli applications set APP_ID --network-server-address NEW_IP --application-server-address NEW_IP --join-server-address NEW_IP

see for more info: ttn-lw-cli applications set | The Things Stack for LoRaWAN

Updating device cluster

Devices will also need to be updated. However I have not found a way to do this. Running equivalent commands only results in errors like Registered Network Server address of end device "xyz" does not match CLI configuration. I was also unable to delete the device. So beware!