That’s a way of doing and the one we went with, but it’s not really necessary. You may create a whole new application that gets the message either from the http integration or by subscribing to the broker, without ever needing to touch lora-app-server’s source code. You may even use lora-app-server’s encoding/decoding features so that you get the real decoded data at your end as to only handle storing and serving data. Also, you could interact with lora-app-server through its API and never even bother with the included frontend. There are lot of ways achieve what you are looking for.
In my mosquitto auth plugin I wrote some instructions for building mosquitto, including websockets support. Check it here.
Sure, here’s the function that handles a message and stores it at our DB. Have in mind that it deals with a whole lot more than just storing a simple message (it checks different data types, a preamble byte for our custom encoding, fires alerts depending on the data, etc.), but it could give you an idea of what you could end up doing when customizing lora-app-server.
//CreateRxMessage stores a received device message.
//It creates an rx_mesage and all children data.
//Return an error and a stuct containing a message to be published to the client to render on the front-end.
func CreateRxMessage(db sqlx.Ext, spClient *sp.Client, rxMessage *RxMessage) (ClientData, error) {
now := time.Now()
err := sqlx.Get(db, &rxMessage.ID, `
insert into rx_message (
created_at,
updated_at,
dev_eui,
message,
rssi,
snr,
gateway_mac
) values ($1, $2, $3, $4, $5, $6, $7) returning id`,
now,
now,
rxMessage.DevEUI[:],
rxMessage.Message,
rxMessage.Rssi,
rxMessage.Snr,
rxMessage.GatewayMAC[:],
)
if err != nil {
return ClientData{}, InsertionError(err)
}
//Get device
device, devErr := GetDevice(db, rxMessage.DevEUI)
if devErr != nil {
return ClientData{}, devErr
}
/*
* Now create message data and a ClientData to be sent.
*/
clientData := ClientData{
ReceivedAt: now,
DevEUI: rxMessage.DevEUI,
RSSI: rxMessage.Rssi,
SNR: rxMessage.Snr,
}
//Try to retrieve gateway (may not exist).
gateway, gErr := GetGateway(db, rxMessage.GatewayMAC, false)
if gErr != nil {
clientData.GatewayMAC = "NA"
clientData.GatewayName = "NA"
} else {
clientData.GatewayMAC = gateway.MAC.String()
clientData.GatewayName = gateway.Name
}
//Decode base64 message
decodedData, decodeErr := base64.StdEncoding.DecodeString(rxMessage.Message)
if decodeErr != nil {
return ClientData{}, errors.Wrap(decodeErr, "decode message error")
}
//Extract preamble from decodedData.
preambleByte := decodedData[0]
log.Infof("Read preamble byte: %v", preambleByte)
//////////
// TEST //
//////////
//Assume there's fix, no panic and dataGroup is 0
gpsFix := true //(preambleByte & 128) != 0
devicePanic := false //(preambleByte & 64) != 0
dataGroup := int32(0) //int32(preambleByte & 0x0F)
log.Infof("Preamble: fix is %t, panic is %t and group is %d.", gpsFix, devicePanic, dataGroup)
//Adjust decodedData
//////////
// TEST //
//////////
//Assume there's no preamble byte and group is 0
decodedData = decodedData[:]
/*
* Check if message length equals expected length.
* If not, discard the message and send an error.
*/
//Get data bytes count.
dBytes, dbErr := DataBytes(db, rxMessage.DevEUI, dataGroup)
if dbErr != nil {
return ClientData{}, errors.Wrapf(dbErr, "couldn't get bytes amount")
}
if len(decodedData) != dBytes {
return ClientData{}, errors.New(fmt.Sprintf("message has a different number of bytes than expected - got %d but wanted %d", len(decodedData), dBytes))
}
//Loop through data descriptors to save data.
//Check if it has location in order to treat them separately.
dataDescriptors, ndtErr := GetDataDescriptorsByGroup(db, rxMessage.DevEUI, dataGroup)
if ndtErr != nil {
return ClientData{}, errors.Wrap(ndtErr, "GetDataDescriptors error")
}
hasLocation, locationErr := HasGroupLocation(db, rxMessage.DevEUI, dataGroup)
if locationErr != nil {
log.WithField("locError", locationErr).Info("Error")
return ClientData{}, errors.Wrap(locationErr, "has_location error")
}
//Create locationDatum in case the device has location.
locationDatum := LocationDatum{
Lat: 0.0,
Lng: 0.0,
RxMessageID: rxMessage.ID,
}
var arrayIndex int32
arrayIndex = 0
/*
* Make a map to hold values.
*/
dataValues := make(map[string]interface{})
dataAlarms := make(map[string]AlarmMap)
for _, dataDescriptor := range dataDescriptors {
//Get the underlying data type
dataType, dtErr := GetDataTypeForDataDescriptor(db, dataDescriptor.ID)
if dtErr != nil {
log.WithField("dtErr", dtErr).Info("dtErr")
return ClientData{}, errors.Wrap(dtErr, "GetDataTypeForDataDescriptor error")
}
//Move the arrayIndex by ndt's left offset and get the subslice.
arrayIndex += dataDescriptor.LeftOffset
dataArray := decodedData[arrayIndex:(arrayIndex + dataType.NumBytes)]
log.WithField("dataType", dataType).Info("Data type")
if dataType.ValueType == "integer" {
//Try to convert the subslice to an int.
intValue, intErr := ReadIntData(dataType, dataArray)
if intErr != nil {
log.WithField("Err", intErr).Info("IntErr")
return ClientData{}, errors.Wrap(intErr, "ReadIntData error")
}
intDatum := IntDatum{
DataDescriptorID: dataDescriptor.ID,
Value: intValue,
RxMessageID: rxMessage.ID,
}
indErr := CreateIntDatum(db, &intDatum)
if indErr != nil {
log.WithField("indErr", indErr).Info("indErr")
return ClientData{}, errors.Wrap(indErr, "int_datum creation error")
}
hasDataAlarm, showDataAlarm, daErr := CheckDataAlarm(db, spClient, dataDescriptor.ID, rxMessage.ID, float64(intValue), rxMessage.CreatedAt)
if daErr != nil {
log.Errorf("error on check data alarm %v", daErr)
}
dataValues[dataDescriptor.Name] = intDatum
//Assign alarm if there was one
alarmMessage := ""
alarmType := "none"
if showDataAlarm {
if hasDataAlarm {
alarmMessage = fmt.Sprintf("Alert: received %s with value %d for device %s (EUI: %s).", dataDescriptor.Name, intValue, device.Name, device.DevEUI.String())
alarmType = "on"
} else {
alarmMessage = fmt.Sprintf("Alarm defused for device %s (EUI: %s): received %s with value %d.", device.Name, device.DevEUI.String(), dataDescriptor.Name, intValue)
alarmType = "off"
}
}
dataAlarms[dataDescriptor.Name] = AlarmMap{
Triggered: hasDataAlarm,
Show: showDataAlarm,
Message: alarmMessage,
Type: alarmType,
}
} else if dataType.ValueType == "float" {
floatValue, floatErr := ReadFloatData(dataType, dataArray)
if floatErr != nil {
log.WithField("floatErr", floatErr).Info("floatErr")
return ClientData{}, errors.Wrap(floatErr, "ReadFloatData error")
}
floatDatum := FloatDatum{
DataDescriptorID: dataDescriptor.ID,
Value: floatValue,
RxMessageID: rxMessage.ID,
}
fndErr := CreateFloatDatum(db, &floatDatum)
if fndErr != nil {
log.WithField("fndErr", fndErr).Info("fndErr")
return ClientData{}, errors.Wrap(fndErr, "float_datum creation error")
}
if hasLocation && (dataType.Name == latConst || dataType.Name == lngConst) {
if dataType.Name == latConst {
locationDatum.Lat = floatValue
} else {
locationDatum.Lng = floatValue
}
} else {
hasDataAlarm, showDataAlarm, daErr := CheckDataAlarm(db, spClient, dataDescriptor.ID, rxMessage.ID, float64(floatValue), rxMessage.CreatedAt)
if daErr != nil {
log.Errorf("error on check data alarm %v", daErr)
}
dataValues[dataDescriptor.Name] = floatDatum
//Assign alarm if there was one
alarmMessage := ""
alarmType := "none"
if showDataAlarm {
if hasDataAlarm {
alarmMessage = fmt.Sprintf("Alert: received %s with value %d for device %s (EUI: %s).", dataDescriptor.Name, floatValue, device.Name, device.DevEUI.String())
alarmType = "on"
} else {
alarmMessage = fmt.Sprintf("Alarm defused for device %s (EUI: %s): received %s with value %d.", device.Name, device.DevEUI.String(), dataDescriptor.Name, floatValue)
alarmType = "off"
}
}
dataAlarms[dataDescriptor.Name] = AlarmMap{
Triggered: hasDataAlarm,
Show: showDataAlarm,
Message: alarmMessage,
Type: alarmType,
}
}
} else if dataType.ValueType == "string" {
strValue, strErr := ReadStringData(dataType, dataArray)
if strErr != nil {
log.WithField("strErr", strErr).Info("strErr")
return ClientData{}, errors.Wrap(strErr, "ReadStringData error")
}
stringDatum := StringDatum{
DataDescriptorID: dataDescriptor.ID,
Value: strValue,
RxMessageID: rxMessage.ID,
}
sndErr := CreateStringDatum(db, &stringDatum)
if sndErr != nil {
log.WithField("sndErr", sndErr).Info("sndErr")
return ClientData{}, errors.Wrap(sndErr, "string_datum creation error")
}
dataValues[dataDescriptor.Name] = stringDatum
} else {
log.WithField("WTF", "AAHHH").Info("Weird")
}
//Move arrayIndex according to num bytes and right offset.
arrayIndex += dataType.NumBytes + dataDescriptor.RightOffset
}
//Create locationDatum
if hasLocation {
locAlarm, showLocAlarm, locErr := CreateLocationDatum(db, spClient, &locationDatum, rxMessage.DevEUI)
if locErr != nil {
log.WithField("locErr", locErr).Info("locErr")
return ClientData{}, errors.Wrap(locErr, "location_datum creation error")
}
/*locationMap := make(map[string]float64)
locationMap["Lat"] = locationDatum.Lat
locationMap["Lng"] = locationDatum.Lng
jsonLocation, _ := json.Marshal(locationMap)*/
dataValues["Location"] = locationDatum
alarmMessage := ""
alarmType := "none"
if showLocAlarm {
if locAlarm {
alarmMessage = fmt.Sprintf("Alert: a location alarm was triggered for device %s (EUI: %s).", device.Name, device.DevEUI.String())
alarmType = "on"
} else {
alarmMessage = fmt.Sprintf("A location alarm has stopped for device %s (EUI: %s).", device.Name, device.DevEUI.String())
alarmType = "off"
}
}
dataAlarms["Location"] = AlarmMap{
Triggered: locAlarm,
Show: showLocAlarm,
Message: alarmMessage,
Type: alarmType,
}
}
/*
* Done creating data.
*/
clientData.Data = dataValues
clientData.Alarms = dataAlarms
rxMessage.CreatedAt = now
rxMessage.UpdatedAt = now
//Check for timeAlarm
timeAlarm, naErr := GetTimeAlarmForDevice(db, rxMessage.DevEUI)
if naErr == nil {
if timeAlarm.AlarmSent {
err := SendTimeAlarmDefuse(db, spClient, &timeAlarm, rxMessage.CreatedAt)
if err != nil {
log.Errorf("Error on rx_message creation on alarm defuse attempt: %v", err)
}
}
}
log.WithFields(log.Fields{
"id": rxMessage.ID,
"devEUI": rxMessage.DevEUI[:],
}).Info("rx_message created")
return clientData, nil
}