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
.
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 aleatorios generados de temperatura, humedad y presión, enviados por el dispositivo simulado:
Puede descargar el Dashboard desde el siguiente enlace: Simulación_ ChirpStack_Random.json