Demo con datos de OpenWeather

Esta demo emplea datos obtenidos del servicio gratuito OpenWeather para generar el 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. Adicionalmente, de forma obligatoria deb especificarse la API key provista por el servicio de OpenWeather. Esta clave se especifica en un archivo .env en la misma dirección donde se encuentra ubicado el archivo de Docker Compose.

Archivo .env
OW_API_KEY=<api_key_openweather>

Luego, se puede desplegar el simulador 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 a partir de datos obtenidos con la API de OpenWeather.

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", "0201010101010102")
	viper.SetDefault("CHIRPSTACK_APP_KEY", "03010101010101010101010101010102")
	viper.SetDefault("UPLINK_TIME", "20")
	viper.SetDefault("GENERATOR_OW_CITY", "London")
	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)
	}

	api_key := viper.Get("GENERATOR_OW_API_KEY").(string)
	city := viper.Get("GENERATOR_OW_CITY").(string)

	gen, err := generator.NewGenerator(
		generator.WithOWeatherConfig(
			api_key, city,
		),
	)
	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.OpenWeather),
		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á también basado en el ejemplo 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 del generador de datos Están relacionadas con aspectos de la generación del payload con datos obtenidos del API de OpenWeather. En este caso, como dato obligatorio se debe proporcionar el GENERATOR_OW_API_KEY.

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

    GENERATOR_OW_API_KEY

     — (obligatorio, ingresado por usuario)

    GENERATOR_OW_CITY

    "London"


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

    Table 2. Variables de entorno ChirpStack
    Nombre Valor por defecto

    CHIRPSTACK_HOST

    "127.0.0.1"

    CHIRPSTACK_PORT

    "1883"

    CHIRPSTACK_GW_EUI

    "0101010101010101"

    CHIRPSTACK_DEV_EUI

    "0201010101010102"

    CHIRPSTACK_APP_KEY

    "03010101010101010101010101010102"


  • 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. Tener en cuenta que el servicio de OpenWeather tiene limitaciones en cuanto a la cantidad de veces se le puede hacer peticiones por hora y por día. Un valor adecuado, para no sobrepasar estas limitaciones es 20 segundos o mayor a este.

    Table 3. Variables de entorno simulador
    Nombre Valor por defecto

    UPLINK_TIME

    "20"


  • 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

Al igual que en el demo con valores aleatorios, el programa se debe compilar, para lo cual se usa 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
      - GENERATOR_OW_API_KEY=${OW_API_KEY}
      - GENERATOR_OW_CITY=London
    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. Tener presente que en este caso se está empleando una variable OW_API_KEY la cual debe ser especificada en un archivo .env antes de desplegar el contenedor.

Decodificador

El payload enviado está organizado en 24 bytes los cuales incluyen las representaciones en bytes de temperatura, humedad, presión, velocidad del viento, latitud y longitud, en ese orden. Los valores meteorológicos son datos enteros multiplicados por 100 representados por 4 bytes cada uno, mientras que los valores de geoposicionamiento (latitud y longitud) se encuentran en formato flotante también de 4 bytes cada uno:

[temperatura[0:8],humedad[4:8],presion[8:12],velocidad_viento[12:16],latitud[16:20],longitud[20:24]]

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");

  if(bytes.length>12){
    myvalues["wind_speed"] = apply_data_type(bytes.slice(12,16),"signed");
    myvalues["latitude"] = apply_data_type(bytes.slice(16,20),"float");
    myvalues["longitude"] = apply_data_type(bytes.slice(20,24),"float");
  }

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

Para añadir este decodificador (esta versión puede ser empleada tanto por el simulador con valores aleatorios como el que obtiene sus datos de OpenWeather) se necesita realizar los mismos pasos que para el demo anterior, ingresando a las propiedades del perfil de dispositivo, en la lista de perfiles y luego dirigiéndose 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 OpenWeather
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 OpenWeather
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 obtenidos mediante la API de OpenWeather y enviados por el dispositivo simulado conectado con ChirpStack:

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

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