Demo con datos aleatorios

Esta demo emplea datos aleatorios para la generación del payload que es enviado por el dispositivo hacia el conjunto de servicios de ChirpStack.

Uso

Para poder utilizar este ejemplo se debe tener instalado docker y docker-compose. Así mismo, en el archivo de docker-compose.yml se debe especificar la dirección del servidor donde se encuentra ChirpStack, en la variable de entorno CHIRPSTACK_HOST. Luego, se puede desplegar empleando el siguiente comando:

docker-compose up -d --build

Mediante este comando se compila el simulador y se despliega en un contenedor enviando los mensajes hacia ChirpStack.

Simulador

El simulador está constituido por el siguiente programa:

package main

import (
	"context"
	"encoding/hex"
	"strconv"
	"sync"
	"time"

	"github.com/LuighiV/chirpstack-simulator/simulator"
	"github.com/LuighiV/payload-generator/generator"
	"github.com/brocaar/chirpstack-api/go/v3/common"
	"github.com/brocaar/chirpstack-api/go/v3/gw"
	"github.com/brocaar/lorawan"
	log "github.com/sirupsen/logrus"
	"github.com/spf13/viper"
)

// This example simulates an OTAA activation and the sending of a single uplink
// frame, after which the simulation terminates.
func main() {
	// Enable VIPER to read Environment Variables
	viper.AutomaticEnv()

	// Set undefined variables
	viper.SetDefault("CHIRPSTACK_HOST", "127.0.0.1")
	viper.SetDefault("CHIRPSTACK_PORT", "1883")
	viper.SetDefault("CHIRPSTACK_GW_EUI", "0101010101010101")
	viper.SetDefault("CHIRPSTACK_DEV_EUI", "0201010101010101")
	viper.SetDefault("CHIRPSTACK_APP_KEY", "03010101010101010101010101010101")
	viper.SetDefault("UPLINK_TIME", "10")
	viper.SetDefault("GENERATOR_TEMP_BASE", "38")
	viper.SetDefault("GENERATOR_HUM_BASE", "80")
	viper.SetDefault("GENERATOR_PRESS_BASE", "1000")
	viper.SetDefault("GENERATOR_TEMP_VAR", "0.5")
	viper.SetDefault("GENERATOR_HUM_VAR", "5")
	viper.SetDefault("GENERATOR_PRESS_VAR", "100")
	viper.SetDefault("LORA_FREQUENCY", "915200000")
	viper.SetDefault("LORA_BANDWIDTH", "125")
	viper.SetDefault("LORA_SF", "7")

	gwid_tmp, _ := hex.DecodeString(viper.Get("CHIRPSTACK_GW_EUI").(string))
	devid_tmp, _ := hex.DecodeString(viper.Get("CHIRPSTACK_DEV_EUI").(string))
	appkey_tmp, _ := hex.DecodeString(viper.Get("CHIRPSTACK_APP_KEY").(string))

	gatewayID := lorawan.EUI64{}
	copy(gatewayID[:], gwid_tmp[:8])
	devEUI := lorawan.EUI64{}
	copy(devEUI[:], devid_tmp[:8])
	appKey := lorawan.AES128Key{}
	copy(appKey[:], appkey_tmp[:16])

	var wg sync.WaitGroup
	ctx := context.Background()

	sgw, err := simulator.NewGateway(
		simulator.WithMQTTCredentials(viper.Get("CHIRPSTACK_HOST").(string)+":"+viper.Get("CHIRPSTACK_PORT").(string), "", ""),
		simulator.WithGatewayID(gatewayID),
		simulator.WithEventTopicTemplate("gateway/{{ .GatewayID }}/event/{{ .Event }}"),
		simulator.WithCommandTopicTemplate("gateway/{{ .GatewayID }}/command/{{ .Command }}"),
	)
	if err != nil {
		panic(err)
	}

	temp_base, _ := strconv.ParseFloat(viper.Get("GENERATOR_TEMP_BASE").(string), 64)
	hum_base, _ := strconv.ParseFloat(viper.Get("GENERATOR_HUM_BASE").(string), 64)
	press_base, _ := strconv.ParseFloat(viper.Get("GENERATOR_PRESS_BASE").(string), 64)
	temp_var, _ := strconv.ParseFloat(viper.Get("GENERATOR_TEMP_VAR").(string), 64)
	hum_var, _ := strconv.ParseFloat(viper.Get("GENERATOR_HUM_VAR").(string), 64)
	press_var, _ := strconv.ParseFloat(viper.Get("GENERATOR_PRESS_VAR").(string), 64)

	gen, err := generator.NewGenerator(
		generator.WithRandomBase(
			temp_base, hum_base, press_base,
			temp_var, hum_var, press_var,
		),
	)
	if err != nil {
		panic(err)
	}

	uplink_time, _ := strconv.Atoi(viper.Get("UPLINK_TIME").(string))
	lora_frequency, _ := strconv.Atoi(viper.Get("LORA_FREQUENCY").(string))
	lora_bandwidth, _ := strconv.Atoi(viper.Get("LORA_BANDWIDTH").(string))
	lora_sf, _ := strconv.Atoi(viper.Get("LORA_SF").(string))

	_, err = simulator.NewDevice(ctx, &wg,
		simulator.WithDevEUI(devEUI),
		simulator.WithAppKey(appKey),
		simulator.WithRandomDevNonce(),
		simulator.WithGenerator(gen, generator.Random),
		simulator.WithUplinkInterval(time.Duration(uplink_time)*time.Second),
		simulator.WithUplinkCount(0),
		simulator.WithUplinkPayload(true, 10, []byte{}),
		simulator.WithUplinkTXInfo(gw.UplinkTXInfo{
			Frequency:  uint32(lora_frequency),
			Modulation: common.Modulation_LORA,
			ModulationInfo: &gw.UplinkTXInfo_LoraModulationInfo{
				LoraModulationInfo: &gw.LoRaModulationInfo{
					Bandwidth:       uint32(lora_bandwidth),
					SpreadingFactor: uint32(lora_sf),
					CodeRate:        "3/4",
				},
			},
		}),
		simulator.WithGateways([]*simulator.Gateway{sgw}),
		simulator.WithDownlinkHandlerFunc(func(conf, ack bool, fCntDown uint32, fPort uint8, data []byte) error {
			log.WithFields(log.Fields{
				"ack":       ack,
				"fcnt_down": fCntDown,
				"f_port":    fPort,
				"data":      hex.EncodeToString(data),
			}).Info("WithDownlinkHandlerFunc triggered")

			return nil
		}),
	)
	if err != nil {
		panic(err)
	}

	wg.Wait()
}

Este ejemplo está basado en el ubicado en el fork del repositorio chirpstack-simulator https://github.com/LuighiV/chirpstack-simulator/blob/master/examples/single_uplink/main.go.

El ejemplo recibe como variables de entorno las siguientes:

  • Variables servidor ChirpStack Estas variables están asociadas a los servicios de ChirpStack:

    Table 1. Variables de entorno ChirpStack
    Nombre Valor por defecto

    CHIRPSTACK_HOST

    "127.0.0.1"

    CHIRPSTACK_PORT

    "1883"

    CHIRPSTACK_GW_EUI

    "0101010101010101"

    CHIRPSTACK_DEV_EUI

    "0201010101010101"

    CHIRPSTACK_APP_KEY

    "03010101010101010101010101010101"


  • Variable de simulador Variables relacionadas con aspectos propios de la simulación. En este caso, solo se tiene el intervalo de tiempo para el envío de los mensajes por parte del simulador.

    Table 2. Variables de entorno simulador
    Nombre Valor por defecto

    UPLINK_TIME

    "10"


  • Variables del generador de datos Están relacionadas con aspectos de la generación de datos aleatorios. En este caso, se debe escoger los valores base y los rangos de variación para la temperatura, presión y humedad.

    Table 3. Variables del generador de datos aleatorios
    Nombre Valor por defecto

    GENERATOR_TEMP_BASE

    "38"

    GENERATOR_HUM_BASE

    "80"

    GENERATOR_PRESS_BASE

    "1000"

    GENERATOR_TEMP_VAR

    "0.5"

    GENERATOR_HUM_VAR

    "5"

    GENERATOR_PRESS_VAR

    "100"


  • Variables de simulación RF Están relacionados con la simulación del protocolo LoRa, por lo que debe especificarse la frecuencia, ancho de banda y spreading factor. En este caso, la frecuencia debe coincidir con la frecuencia configurada en el servidor de red de ChirpStack.

    Table 4. Variables de simulación RF
    Nombre Valor por defecto

    LORA_FREQUENCY

    "915200000"

    LORA_BANDWIDTH

    "125"

    LORA_SF

    "7"


El simulador considera que en el servidor de aplicación de ChirpStack se encuentra creado un dispositivo con el CHIRPSTACK_DEV_EUI, un gateway con CHIRPSTACK_GW_EUI y que el dispositivo cuenta con una API key CHIRPSTACK_APP_KEY.

Compilación y despliegue

El programa al estar escrito en Go necesita ser compilado. Para ello, se hace uso de un archivo Makefile a través del cual se especifica el archivo a compilar y el ejecutable resultante.

.PHONY: build clean
VERSION := $(shell git describe --always |sed -e "s/^v//")

build:
	@echo "Compiling source"
	@mkdir -p build
	go build $(GO_EXTRA_BUILD_ARGS) -ldflags "-s -w -X main.version=$(VERSION)" -o build/demo main.go

clean:
	@echo "Cleaning up workspace"
	@rm -rf build
	@rm -rf dist
	@rm -rf docs/public

Para compilar el archivo se puede ejecutar lo siguiente:

make build

No obstante, para facilitar este proceso se considera efectuar el proceso de compilación y despliegue en un contenedor con go. Para ello se emplea el siguiente archivo Dockerfile:

FROM golang:1.13-alpine AS build_base

ENV PROJECT_PATH=/chirpstack-simulator
ENV PATH=$PATH:$PROJECT_PATH/build
ENV CGO_ENABLED=0
ENV GO_EXTRA_BUILD_ARGS="-a -installsuffix cgo"

RUN apk add --no-cache ca-certificates tzdata make git bash

RUN mkdir -p $PROJECT_PATH
COPY . $PROJECT_PATH
WORKDIR $PROJECT_PATH

RUN make clean build

# Start fresh from a smaller image
FROM alpine:3.13
RUN apk add ca-certificates

COPY --from=build_base /chirpstack-simulator/build/demo /app/demo
CMD ["/app/demo"]

Este archivo efectúa una compilación multietapa, una para generar el ejecutable del simulador y otra para ponerlo en ejecución en una imagen de tamaño más reducido.

Finalmente, se emplea el siguiente archivo docker-compose.yml para la gestión del contenedor mediante Docker Compose:

version: "2"
services:
  chirpstack-simulator:
    build:
      context: .
      dockerfile: Dockerfile-devel
    environment:
      - CHIRPSTACK_HOST=192.168.27.51
      - CHIRPSTACK_GW_EUI=647fdafffe007f9f
    volumes:
      - ./:/chirpstack-simulator

En este caso, dentro de la sección environment se puede adicionar cualquiera de las variables consideradas en el programa en go, de tal manera que se especifique un valor, en lugar del valor por defecto.

Decodificador

El payload enviado está organizado en 12 bytes los cuales incluyen las representaciones en bytes de temperatura, humedad y presión, en ese orden. Cada valor es un dato entero multiplicado por 100 representados por 4 bytes cada uno:

[temperatura[0:8],humedad[4:8],presion[8:12]]

Cuando se desea decodificar este dato mediante el perfil del dispositivo, se puede incluir el siguiente código:

function apply_data_type(bytes, data_type) {
        output = 0;
        if (data_type === "unsigned") {
            for (var i = bytes.length-1; i >=0; i--) {
                output = (to_uint(output << 8)) | bytes[i];
            }
            return output;
        }

        if (data_type === "signed") {
            for (var j = bytes.length-1; j >=0; j--) {
                output = (output << 8) | bytes[j];
            }

            // Convert to signed, based on value size
            if (output > Math.pow(2, 8*bytes.length-1))
                output -= Math.pow(2, 8*bytes.length);
            return output;
        }

  		//https://www.thethingsnetwork.org/forum/t/decode-float-sent-by-lopy-as-node/8757/2
        if (data_type === "float") {

            var bits = bytes[3]<<24 | bytes[2]<<16 | bytes[1]<<8 | bytes[0];
            var sign = (bits>>>31 === 0) ? 1.0 : -1.0;
            var e = bits>>>23 & 0xff;
            var m = (e === 0) ? (bits & 0x7fffff)<<1 : (bits & 0x7fffff) | 0x800000;
            var output = sign * m * Math.pow(2, e - 150);
            return output;
        }

        // Incorrect data type
        return null;
    }

//https://stackoverflow.com/a/4081228
function to_uint(x) {
    return x >>> 0;
}
// Decode decodes an array of bytes into an object.
//  - fPort contains the LoRaWAN fPort number
//  - bytes is an array of bytes, e.g. [225, 230, 255, 0]
//  - variables contains the device variables e.g. {"calibration": "3.5"} (both the key / value are of type string)
// The function must return an object, e.g. {"temperature": 22.5}
function Decode(fPort, bytes, variables) {
  var myvalues = {};

  myvalues["temp"] = apply_data_type(bytes.slice(0,4),"signed");
  myvalues["hum"] = apply_data_type(bytes.slice(4,8),"signed");
  myvalues["press"] = apply_data_type(bytes.slice(8,12),"signed");

  return {length:bytes.length,data:myvalues};
}

Para añadir este decodificador se necesita ingresar a las propiedades del perfil de dispositivo, en la lista de perfiles y luego dirigirse a la pestaña Codec. Luego, se selecciona Custom JavaScript codec fuctions para ingresar una función de javascript específica. Finalmente, se hace clic en Update device-profile.

Adiciona decodificador random
Figure 1. Adiciona decodificador a perfil de dispositivo

Luego, para verificar que efectivamente se está efectuando la decodificación, se puede revisar los datos recibidos del dispositivo, en Device Data y luego en cada una de las entradas que aparecen al momento de recibir un mensaje, debe visualizarse un elemento objectJSON donde se muestren los valores decodificados.

Verifica decodificador random
Figure 2. Verificación de datos decodificados

Integración con ThingsBoard

Este demo también se integra con ThingsBoard, tal como se explica en la guía de Integración de esta documentación.

En este caso, la información se presenta en un Dashboard, mostrando los valores aleatorios generados de temperatura, humedad y presión, enviados por el dispositivo simulado:

Demo ChirpStack random
Figure 3. Dashboard con valores obtenidos de integración con ChirpStack.

Puede descargar el Dashboard desde el siguiente enlace: Simulación_ ChirpStack_Random.json