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.
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
.
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.
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:
Puede descargar el Dashboard desde el siguiente enlace: Simulación_ ChirpStack_OpenWeather.json