diff --git a/README.md b/README.md index 2eb3d65..2720744 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,6 @@ log_level = "info" bandwith = 125 spread_factor = 10 bit_rate = 0 - BitRateS = "0" [rx_info] channel = 0 @@ -66,6 +65,11 @@ log_level = "info" [raw_payload] payload = "ff00" use_raw = false + script = "\n// Encode encodes the given object into an array of bytes.\n// - fPort contains the LoRaWAN fPort number\n// - obj is an object, e.g. {\"temperature\": 22.5}\n// The function must return an array of bytes, e.g. [225, 230, 255, 0]\nfunction Encode(fPort, obj) {\n\treturn [\n obj[\"Flags\"],\n obj[\"Battery\"],\n obj[\"Light\"],\n ];\n}\n" + use_encoder = true + max_exec_time = 500 + js_object = "{\n \"Flags\": 0,\n \"Battery\": 65,\n \"Light\": 54\n}" + fport = 2 [[encoded_type]] name = "Flags" @@ -102,7 +106,7 @@ When OTAA is set and the device is joined, uponinitialization the program will t ### Data -The data to be sent may be presented as a hex string representation of the raw bytes, or using our encoding method (which then needs to be decoded accordingly at `lora-app-server`). As a reference, this is how we encode our data: +The data to be sent may be presented as a hex string representation of the raw bytes, using a JS object and a decoding function to extract a bytes array from it, or using our encoding method (which then needs to be decoded accordingly at `lora-app-server`). As a reference, this is how we encode our data: ```go func GenerateFloat(originalFloat, maxValue float32, numBytes int32) []byte { @@ -143,7 +147,11 @@ func GenerateInt(originalInt, numBytes int32) []byte { } ``` -Values may be added using the `Add encoded type` button and setting the options. +When using our encoding method, values may be added using the `Add encoded type` button and setting the options. + +To use your own custom JS encoder, click the "Use encoder" checkbox and the "Open decoder" button to open the form: + +![](images/encoder.png?raw=true) #### MAC Commands diff --git a/control.go b/control.go index 9b9824f..84ba165 100644 --- a/control.go +++ b/control.go @@ -183,3 +183,16 @@ func beginFCtrl() { imgui.SameLine() imgui.Checkbox("FPending##FCtrl-FPending", &fCtrl.FPending) } + +func beginControl() { + //imgui.SetNextWindowPos(imgui.Vec2{X: 400, Y: 25}) + //imgui.SetNextWindowSize(imgui.Vec2{X: 780, Y: 250}) + imgui.Begin("Control") + imgui.Text("FCtrl") + imgui.Separator() + beginFCtrl() + imgui.Text("MAC Commands") + beginMACCommands() + imgui.Separator() + imgui.End() +} diff --git a/data.go b/data.go new file mode 100644 index 0000000..d2e8911 --- /dev/null +++ b/data.go @@ -0,0 +1,271 @@ +package main + +import ( + "encoding/json" + "fmt" + "reflect" + + "github.com/inkyblackness/imgui-go" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + + "time" + + "github.com/robertkrimen/otto" +) + +type encodedType struct { + Name string `toml:"name"` + Value float64 `toml:"value"` + MaxValue float64 `toml:"max_value"` + MinValue float64 `toml:"min_value"` + IsFloat bool `toml:"is_float"` + NumBytes int `toml:"num_bytes"` + //String representations. + ValueS string `toml:"-"` + MinValueS string `toml:"-"` + MaxValueS string `toml:"-"` + NumBytesS string `toml:"-"` +} + +//rawPayload holds optional raw bytes payload (hex encoded). +type rawPayload struct { + Payload string `toml:"payload"` + UseRaw bool `toml:"use_raw"` + Script string `toml:"script"` + UseEncoder bool `toml:"use_encoder"` + MaxExecTime int `toml:"max_exec_time"` + Obj string `toml:"js_object"` + FPort int `toml:"fport"` + FPortS string `toml:"-"` +} + +var openScript bool +var defaultScript = ` +// Encode encodes the given object into an array of bytes. +// - fPort contains the LoRaWAN fPort number +// - obj is an object, e.g. {"temperature": 22.5} +// The function must return an array of bytes, e.g. [225, 230, 255, 0] +function Encode(fPort, obj) { + return []; +} +` + +func beginDataForm() { + //imgui.SetNextWindowPos(imgui.Vec2{X: 400, Y: 285}) + //imgui.SetNextWindowSize(imgui.Vec2{X: 780, Y: 355}) + imgui.Begin("Data") + imgui.Text("Raw data") + imgui.PushItemWidth(150.0) + imgui.InputTextV("Raw bytes in hex", &config.RawPayload.Payload, imgui.InputTextFlagsCharsHexadecimal, nil) + imgui.SameLine() + imgui.Checkbox("Send raw", &config.RawPayload.UseRaw) + imgui.SameLine() + imgui.Checkbox("Use encoder", &config.RawPayload.UseEncoder) + imgui.SameLine() + if imgui.Button("Open encoder") { + openScript = true + } + imgui.InputTextV(fmt.Sprintf("fPort ##fport"), &config.RawPayload.FPortS, imgui.InputTextFlagsCharsDecimal|imgui.InputTextFlagsCallbackAlways, handleInt(config.RawPayload.FPortS, 10, &config.RawPayload.FPort)) + imgui.SliderInt("X", &interval, 1, 60) + imgui.SameLine() + imgui.Checkbox("Send every X seconds", &repeat) + if !running { + if imgui.Button("Send data") { + go run() + } + } + if repeat && running { + if imgui.Button("Stop") { + running = false + } + } + + imgui.Separator() + + imgui.Text("Encoded data") + if imgui.Button("Add encoded type") { + et := &encodedType{ + Name: "New type", + ValueS: "0", + MaxValueS: "0", + MinValueS: "0", + NumBytesS: "0", + } + config.EncodedType = append(config.EncodedType, et) + log.Println("added new type") + } + + for i := 0; i < len(config.EncodedType); i++ { + delete := false + imgui.Separator() + imgui.InputText(fmt.Sprintf("Name ##%d", i), &config.EncodedType[i].Name) + imgui.SameLine() + imgui.InputTextV(fmt.Sprintf("Bytes ##%d", i), &config.EncodedType[i].NumBytesS, imgui.InputTextFlagsCharsDecimal|imgui.InputTextFlagsCallbackAlways, handleInt(config.EncodedType[i].NumBytesS, 10, &config.EncodedType[i].NumBytes)) + imgui.SameLine() + imgui.Checkbox(fmt.Sprintf("Float##%d", i), &config.EncodedType[i].IsFloat) + imgui.SameLine() + if imgui.Button(fmt.Sprintf("Delete##%d", i)) { + delete = true + } + imgui.InputTextV(fmt.Sprintf("Value ##%d", i), &config.EncodedType[i].ValueS, imgui.InputTextFlagsCharsDecimal|imgui.InputTextFlagsCallbackAlways, handleFloat64(config.EncodedType[i].ValueS, &config.EncodedType[i].Value)) + imgui.SameLine() + imgui.InputTextV(fmt.Sprintf("Max value##%d", i), &config.EncodedType[i].MaxValueS, imgui.InputTextFlagsCharsDecimal|imgui.InputTextFlagsCallbackAlways, handleFloat64(config.EncodedType[i].MaxValueS, &config.EncodedType[i].MaxValue)) + imgui.SameLine() + imgui.InputTextV(fmt.Sprintf("Min value##%d", i), &config.EncodedType[i].MinValueS, imgui.InputTextFlagsCharsDecimal|imgui.InputTextFlagsCallbackAlways, handleFloat64(config.EncodedType[i].MinValueS, &config.EncodedType[i].MinValue)) + if delete { + if len(config.EncodedType) == 1 { + config.EncodedType = make([]*encodedType, 0) + } else { + copy(config.EncodedType[i:], config.EncodedType[i+1:]) + config.EncodedType[len(config.EncodedType)-1] = &encodedType{} + config.EncodedType = config.EncodedType[:len(config.EncodedType)-1] + } + } + } + imgui.Separator() + beginScript() + imgui.End() +} + +func beginScript() { + if openScript { + imgui.OpenPopup("JS encoder") + openScript = false + } + imgui.SetNextWindowPos(imgui.Vec2{X: (float32(windowWidth) / 2) - 370.0, Y: (float32(windowHeight) / 2) - 200.0}) + imgui.SetNextWindowSize(imgui.Vec2{X: 740, Y: 600}) + if imgui.BeginPopupModal("JS encoder") { + imgui.Text(`If "Use encoder" is checked, you may write a function that accepts a JS object`) + imgui.Text(`and returns a byte array that'll be used as the raw bytes when sending data.`) + imgui.Text(`The function must be named Encode and accept a port and JS object.`) + imgui.InputTextMultilineV("##encoder-function", &config.RawPayload.Script, imgui.Vec2{X: 710, Y: 300}, imgui.InputTextFlagsAllowTabInput, nil) + imgui.Separator() + imgui.Text("JS object:") + imgui.InputTextMultilineV("##encoder-object", &config.RawPayload.Obj, imgui.Vec2{X: 710, Y: 140}, 0, nil) + if imgui.Button("Clear##encoder-cancel") { + config.RawPayload.Script = defaultScript + imgui.CloseCurrentPopup() + } + imgui.SameLine() + if imgui.Button("Close##encoder-close") { + imgui.CloseCurrentPopup() + } + imgui.EndPopup() + } +} + +// EncodeToBytes encodes the payload to a slice of bytes. +// Taken from github.com/brocaar/lora-app-server. +func EncodeToBytes() (b []byte, err error) { + defer func() { + if caught := recover(); caught != nil { + err = fmt.Errorf("%s", caught) + } + }() + + script := config.RawPayload.Script + "\n\nEncode(fPort, obj);\n" + + vm := otto.New() + vm.Interrupt = make(chan func(), 1) + vm.SetStackDepthLimit(32) + var jsonData interface{} + err = json.Unmarshal([]byte(config.RawPayload.Obj), &jsonData) + if err != nil { + log.Errorf("couldn't unmarshal object: %s", err) + return nil, err + } + log.Debugf("JS object: %v", jsonData) + vm.Set("obj", jsonData) + vm.Set("fPort", config.RawPayload.FPort) + + go func() { + time.Sleep(time.Duration(config.RawPayload.MaxExecTime) * time.Millisecond) + vm.Interrupt <- func() { + panic(errors.New("execution timeout")) + } + }() + + var val otto.Value + val, err = vm.Run(script) + if err != nil { + return nil, errors.Wrap(err, "js vm error") + } + if !val.IsObject() { + return nil, errors.New("function must return an array") + } + + var out interface{} + out, err = val.Export() + if err != nil { + return nil, errors.Wrap(err, "export error") + } + + return interfaceToByteSlice(out) +} + +// Taken from github.com/brocaar/lora-app-server. +func interfaceToByteSlice(obj interface{}) ([]byte, error) { + if obj == nil { + return nil, errors.New("value must not be nil") + } + + if reflect.TypeOf(obj).Kind() != reflect.Slice { + return nil, errors.New("value must be an array") + } + + s := reflect.ValueOf(obj) + l := s.Len() + + var out []byte + for i := 0; i < l; i++ { + var b int64 + + el := s.Index(i).Interface() + switch v := el.(type) { + case int: + b = int64(v) + case uint: + b = int64(v) + case uint8: + b = int64(v) + case int8: + b = int64(v) + case uint16: + b = int64(v) + case int16: + b = int64(v) + case uint32: + b = int64(v) + case int32: + b = int64(v) + case uint64: + b = int64(v) + if uint64(b) != v { + return nil, fmt.Errorf("array value must be in byte range (0 - 255), got: %d", v) + } + case int64: + b = int64(v) + case float32: + b = int64(v) + if float32(b) != v { + return nil, fmt.Errorf("array value must be in byte range (0 - 255), got: %f", v) + } + case float64: + b = int64(v) + if float64(b) != v { + return nil, fmt.Errorf("array value must be in byte range (0 - 255), got: %f", v) + } + default: + return nil, fmt.Errorf("array value must be an array of ints or floats, got: %T", el) + } + + if b < 0 || b > 255 { + return nil, fmt.Errorf("array value must be in byte range (0 - 255), got: %d", b) + } + + out = append(out, byte(b)) + } + + return out, nil +} diff --git a/device.go b/device.go new file mode 100644 index 0000000..8dd7894 --- /dev/null +++ b/device.go @@ -0,0 +1,431 @@ +package main + +import ( + "encoding/hex" + "fmt" + "time" + + "github.com/brocaar/loraserver/api/gw" + "github.com/brocaar/lorawan" + "github.com/golang/protobuf/ptypes" + "github.com/iegomez/lds/lds" + "github.com/inkyblackness/imgui-go" + log "github.com/sirupsen/logrus" +) + +type device struct { + DevEUI string `toml:"eui"` + DevAddress string `toml:"address"` + NwkSEncKey string `toml:"network_session_encription_key"` + SNwkSIntKey string `toml:"serving_network_session_integrity_key"` //For Lorawan 1.0 this is the same as the NwkSEncKey + FNwkSIntKey string `toml:"forwarding_network_session_integrity_key"` //For Lorawan 1.0 this is the same as the NwkSEncKey + AppSKey string `toml:"application_session_key"` + Marshaler string `toml:"marshaler"` + NwkKey string `toml:"nwk_key"` //Network key, used to be called application key for Lorawan 1.0 + AppKey string `toml:"app_key"` //Application key, for Lorawan 1.1 + JoinEUI string `toml:"join_eui"` //JoinEUI for 1.1. (AppEUI on 1.0) + Major lorawan.Major `toml:"-"` + MACVersion lorawan.MACVersion `toml:"mac_version"` //Lorawan MAC version + MType lorawan.MType `toml:"-"` + Profile string `toml:"profile"` + Joined bool `toml:"joined"` + SkipFCntCheck bool `toml:"skip_fcnt_check"` +} + +func beginDeviceForm() { + //imgui.SetNextWindowPos(imgui.Vec2{X: 10, Y: 205}) + //imgui.SetNextWindowSize(imgui.Vec2{X: 380, Y: 435}) + imgui.Begin("Device") + imgui.PushItemWidth(250.0) + imgui.InputTextV("Device EUI", &config.Device.DevEUI, imgui.InputTextFlagsCharsHexadecimal|imgui.InputTextFlagsCallbackCharFilter, maxLength(config.Device.DevEUI, 16)) + imgui.InputTextV("Device address", &config.Device.DevAddress, imgui.InputTextFlagsCharsHexadecimal|imgui.InputTextFlagsCallbackCharFilter, maxLength(config.Device.DevAddress, 8)) + imgui.InputTextV("NwkSEncKey", &config.Device.NwkSEncKey, imgui.InputTextFlagsCharsHexadecimal|imgui.InputTextFlagsCallbackCharFilter, maxLength(config.Device.NwkSEncKey, 32)) + imgui.InputTextV("SNwkSIntkey", &config.Device.SNwkSIntKey, imgui.InputTextFlagsCharsHexadecimal|imgui.InputTextFlagsCallbackCharFilter, maxLength(config.Device.SNwkSIntKey, 32)) + imgui.InputTextV("FNwkSIntKey", &config.Device.FNwkSIntKey, imgui.InputTextFlagsCharsHexadecimal|imgui.InputTextFlagsCallbackCharFilter, maxLength(config.Device.FNwkSIntKey, 32)) + imgui.InputTextV("AppSKey", &config.Device.AppSKey, imgui.InputTextFlagsCharsHexadecimal|imgui.InputTextFlagsCallbackCharFilter, maxLength(config.Device.AppSKey, 32)) + imgui.InputTextV("NwkKey", &config.Device.NwkKey, imgui.InputTextFlagsCharsHexadecimal|imgui.InputTextFlagsCallbackCharFilter, maxLength(config.Device.NwkKey, 32)) + imgui.InputTextV("AppKey", &config.Device.AppKey, imgui.InputTextFlagsCharsHexadecimal|imgui.InputTextFlagsCallbackCharFilter, maxLength(config.Device.AppKey, 32)) + imgui.InputTextV("Join EUI", &config.Device.JoinEUI, imgui.InputTextFlagsCharsHexadecimal|imgui.InputTextFlagsCallbackCharFilter, maxLength(config.Device.JoinEUI, 16)) + if imgui.BeginCombo("Marshaler", config.Device.Marshaler) { + for _, marshaler := range marshalers { + if imgui.SelectableV(marshaler, marshaler == config.Device.Marshaler, 0, imgui.Vec2{}) { + config.Device.Marshaler = marshaler + } + } + imgui.EndCombo() + } + if imgui.BeginCombo("LoRaWAN major", majorVersions[config.Device.Major]) { + if imgui.SelectableV("LoRaWANRev1", config.Device.Major == 0, 0, imgui.Vec2{}) { + config.Device.MACVersion = 0 + } + imgui.EndCombo() + } + if imgui.BeginCombo("MAC Version", macVersions[config.Device.MACVersion]) { + + if imgui.SelectableV("LoRaWAN 1.0", config.Device.MACVersion == 0, 0, imgui.Vec2{}) { + config.Device.MACVersion = 0 + } + if imgui.SelectableV("LoRaWAN 1.1", config.Device.MACVersion == 1, 0, imgui.Vec2{}) { + config.Device.MACVersion = 1 + } + imgui.EndCombo() + } + if imgui.BeginCombo("MType", mTypes[config.Device.MType]) { + if imgui.SelectableV("UnconfirmedDataUp", config.Device.MType == lorawan.UnconfirmedDataUp || config.Device.MType == 0, 0, imgui.Vec2{}) { + config.Device.MType = lorawan.UnconfirmedDataUp + } + if imgui.SelectableV("ConfirmedDataUp", config.Device.MType == lorawan.ConfirmedDataUp, 0, imgui.Vec2{}) { + config.Device.MType = lorawan.ConfirmedDataUp + } + imgui.EndCombo() + } + if imgui.BeginCombo("Profile", config.Device.Profile) { + if imgui.SelectableV("OTAA", config.Device.Profile == "OTAA", 0, imgui.Vec2{}) { + config.Device.Profile = "OTAA" + } + if imgui.SelectableV("ABP", config.Device.Profile == "ABP", 0, imgui.Vec2{}) { + config.Device.Profile = "ABP" + } + imgui.EndCombo() + } + imgui.Checkbox("Disable frame counter validation", &config.Device.SkipFCntCheck) + if imgui.Button("Join") { + join() + } + imgui.SameLine() + if cDevice != nil { + if imgui.Button("Reset device") { + resetDevice = true + } + } + beginReset() + imgui.Separator() + if cDevice != nil { + imgui.Text(fmt.Sprintf("DlFCnt: %d - UlFCnt: %d", cDevice.DlFcnt, cDevice.UlFcnt)) + imgui.Text(fmt.Sprintf("DevNonce: %d - JoinNonce: %d", cDevice.DevNonce, cDevice.JoinNonce)) + } + imgui.End() +} + +func setDevice() { + //Build your node with known keys (ABP). + nwkSEncHexKey := config.Device.NwkSEncKey + sNwkSIntHexKey := config.Device.SNwkSIntKey + fNwkSIntHexKey := config.Device.FNwkSIntKey + appSHexKey := config.Device.AppSKey + devHexAddr := config.Device.DevAddress + devAddr, err := lds.HexToDevAddress(devHexAddr) + if err != nil { + log.Errorf("dev addr error: %s", err) + } + + nwkSEncKey, err := lds.HexToKey(nwkSEncHexKey) + if err != nil { + log.Errorf("nwkSEncKey error: %s", err) + } + + sNwkSIntKey, err := lds.HexToKey(sNwkSIntHexKey) + if err != nil { + log.Errorf("sNwkSIntKey error: %s", err) + } + + fNwkSIntKey, err := lds.HexToKey(fNwkSIntHexKey) + if err != nil { + log.Errorf("fNwkSIntKey error: %s", err) + } + + appSKey, err := lds.HexToKey(appSHexKey) + if err != nil { + log.Errorf("appskey error: %s", err) + } + + devEUI, err := lds.HexToEUI(config.Device.DevEUI) + if err != nil { + return + } + + nwkHexKey := config.Device.NwkKey + appHexKey := config.Device.AppKey + + appKey, err := lds.HexToKey(appHexKey) + if err != nil { + return + } + nwkKey, err := lds.HexToKey(nwkHexKey) + if err != nil { + return + } + joinEUI, err := lds.HexToEUI(config.Device.JoinEUI) + if err != nil { + return + } + + if cDevice == nil { + cDevice = &lds.Device{ + DevEUI: devEUI, + DevAddr: devAddr, + NwkSEncKey: nwkSEncKey, + SNwkSIntKey: sNwkSIntKey, + FNwkSIntKey: fNwkSIntKey, + AppSKey: appSKey, + AppKey: appKey, + NwkKey: nwkKey, + JoinEUI: joinEUI, + Major: lorawan.Major(config.Device.Major), + MACVersion: lorawan.MACVersion(config.Device.MACVersion), + SkipFCntCheck: config.Device.SkipFCntCheck, + } + } else { + cDevice.DevEUI = devEUI + cDevice.DevAddr = devAddr + cDevice.NwkSEncKey = nwkSEncKey + cDevice.SNwkSIntKey = sNwkSIntKey + cDevice.FNwkSIntKey = fNwkSIntKey + cDevice.AppSKey = appSKey + cDevice.AppKey = appKey + cDevice.NwkKey = nwkKey + cDevice.JoinEUI = joinEUI + cDevice.Major = lorawan.Major(config.Device.Major) + cDevice.MACVersion = lorawan.MACVersion(config.Device.MACVersion) + cDevice.SkipFCntCheck = config.Device.SkipFCntCheck + } + cDevice.SetMarshaler(config.Device.Marshaler) + //Get redis info. + if cDevice.GetInfo() { + config.Device.NwkSEncKey = lds.KeyToHex(cDevice.NwkSEncKey) + config.Device.FNwkSIntKey = lds.KeyToHex(cDevice.FNwkSIntKey) + config.Device.SNwkSIntKey = lds.KeyToHex(cDevice.SNwkSIntKey) + config.Device.AppSKey = lds.KeyToHex(cDevice.AppSKey) + config.Device.DevAddress = lds.DevAddressToHex(cDevice.DevAddr) + } else { + cDevice.Reset() + } +} + +func beginReset() { + if resetDevice { + imgui.OpenPopup("Reset device") + resetDevice = false + } + imgui.SetNextWindowPos(imgui.Vec2{X: float32(windowWidth-190) / 2, Y: float32(windowHeight-90) / 2}) + imgui.SetNextWindowSize(imgui.Vec2{X: 380, Y: 180}) + imgui.PushItemWidth(250.0) + if imgui.BeginPopupModal("Reset device") { + + imgui.PushTextWrapPos() + imgui.Text("This will delete saved devNonce, joinNonce, downlink and uplink frame counters, device address and device keys. Are you sure you want to proceed?") + imgui.Separator() + if imgui.Button("Cancel") { + imgui.CloseCurrentPopup() + } + imgui.SameLine() + if imgui.Button("Confirm") { + //Reset device. + err := cDevice.Reset() + if err != nil { + log.Errorln(err) + } else { + config.Device.NwkSEncKey = lds.KeyToHex(cDevice.NwkSEncKey) + config.Device.FNwkSIntKey = lds.KeyToHex(cDevice.FNwkSIntKey) + config.Device.SNwkSIntKey = lds.KeyToHex(cDevice.SNwkSIntKey) + config.Device.AppSKey = lds.KeyToHex(cDevice.AppSKey) + config.Device.DevAddress = lds.DevAddressToHex(cDevice.DevAddr) + log.Infoln("device was reset") + } + imgui.CloseCurrentPopup() + //Close popup. + } + imgui.EndPopup() + } +} + +func join() { + if mqttClient == nil { + err := connectClient() + if err != nil { + return + } + } else if !mqttClient.IsConnected() { + log.Errorln("mqtt client not connected") + } + + //Always set device to get any changes to the configuration. + setDevice() + + dataRate := &lds.DataRate{ + Bandwidth: config.DR.Bandwith, + Modulation: "LORA", + SpreadFactor: config.DR.SpreadFactor, + BitRate: config.DR.BitRate, + } + + rxInfo := &lds.RxInfo{ + Channel: config.RXInfo.Channel, + CodeRate: config.RXInfo.CodeRate, + CrcStatus: config.RXInfo.CrcStatus, + DataRate: dataRate, + Frequency: config.RXInfo.Frequency, + LoRaSNR: float32(config.RXInfo.LoRaSNR), + Mac: config.GW.MAC, + RfChain: config.RXInfo.RfChain, + Rssi: config.RXInfo.Rssi, + Time: time.Now().Format(time.RFC3339), + Timestamp: int32(time.Now().UnixNano() / 1000000000), + } + + gwID, err := lds.MACToGatewayID(config.GW.MAC) + if err != nil { + log.Errorf("gw mac error: %s", err) + return + } + + err = cDevice.Join(mqttClient, string(gwID), *rxInfo) + + if err != nil { + log.Errorf("join error: %s", err) + } else { + log.Println("join sent") + } + +} + +func run() { + + if mqttClient == nil { + err := connectClient() + if err != nil { + return + } + } else if !mqttClient.IsConnected() { + log.Errorln("mqtt client not connected") + } + + setDevice() + + dataRate := &lds.DataRate{ + Bandwidth: config.DR.Bandwith, + Modulation: "LORA", + SpreadFactor: config.DR.SpreadFactor, + BitRate: config.DR.BitRate, + } + + running = true + + for { + if stop { + stop = false + running = false + return + } + payload := []byte{} + var pErr error + + if config.RawPayload.UseRaw { + payload, pErr = hex.DecodeString(config.RawPayload.Payload) + if pErr != nil { + log.Errorf("couldn't decode hex payload: %s", pErr) + return + } + } else if config.RawPayload.UseEncoder { + payload, pErr = EncodeToBytes() + if pErr != nil { + log.Errorf("couldn't encode js object: %s", pErr) + return + } + } else { + for _, v := range config.EncodedType { + if v.IsFloat { + arr := lds.GenerateFloat(float32(v.Value), float32(v.MaxValue), int32(v.NumBytes)) + payload = append(payload, arr...) + } else { + arr := lds.GenerateInt(int32(v.Value), int32(v.NumBytes)) + payload = append(payload, arr...) + } + } + } + + //Construct DataRate RxInfo with proper values according to your band (example is for US 915). + + rxInfo := &lds.RxInfo{ + Channel: config.RXInfo.Channel, + CodeRate: config.RXInfo.CodeRate, + CrcStatus: config.RXInfo.CrcStatus, + DataRate: dataRate, + Frequency: config.RXInfo.Frequency, + LoRaSNR: float32(config.RXInfo.LoRaSNR), + Mac: config.GW.MAC, + RfChain: config.RXInfo.RfChain, + Rssi: config.RXInfo.Rssi, + Size: len(payload), + Time: time.Now().Format(time.RFC3339), + Timestamp: int32(time.Now().UnixNano() / 1000000000), + } + + ////// + + gwID, err := lds.MACToGatewayID(config.GW.MAC) + if err != nil { + log.Errorf("gw mac error: %s", err) + return + } + now := time.Now() + rxTime := ptypes.TimestampNow() + tsge := ptypes.DurationProto(now.Sub(time.Time{})) + + urx := gw.UplinkRXInfo{ + GatewayId: gwID, + Rssi: int32(rxInfo.Rssi), + LoraSnr: float64(rxInfo.LoRaSNR), + Channel: uint32(rxInfo.Channel), + RfChain: uint32(rxInfo.RfChain), + TimeSinceGpsEpoch: tsge, + Time: rxTime, + Timestamp: uint32(rxTime.GetSeconds()), + Board: 0, + Antenna: 0, + Location: nil, + FineTimestamp: nil, + FineTimestampType: gw.FineTimestampType_NONE, + } + + lmi := &gw.LoRaModulationInfo{ + Bandwidth: uint32(rxInfo.DataRate.Bandwidth), + SpreadingFactor: uint32(rxInfo.DataRate.SpreadFactor), + CodeRate: rxInfo.CodeRate, + } + + umi := &gw.UplinkTXInfo_LoraModulationInfo{ + LoraModulationInfo: lmi, + } + + utx := gw.UplinkTXInfo{ + Frequency: uint32(rxInfo.Frequency), + ModulationInfo: umi, + } + + var fOpts []*lorawan.MACCommand + for i := 0; i < len(macCommands); i++ { + if macCommands[i].Use { + fOpts = append(fOpts, &macCommands[i].MACCommand) + } + } + + //Now send an uplink + ulfc, err := cDevice.Uplink(mqttClient, config.Device.MType, uint8(config.RawPayload.FPort), &urx, &utx, payload, config.GW.MAC, config.Band.Name, *dataRate, fOpts, fCtrl) + if err != nil { + log.Errorf("couldn't send uplink: %s", err) + } else { + log.Infof("message sent, uplink framecounter is now %d", ulfc) + } + + if !repeat || !running { + stop = false + running = false + return + } + + time.Sleep(time.Duration(interval) * time.Second) + + } + +} diff --git a/example_conf.toml b/example_conf.toml index d60e1be..1ddb657 100644 --- a/example_conf.toml +++ b/example_conf.toml @@ -31,7 +31,6 @@ skip_fcnt_check=true bandwith = 125 spread_factor = 10 bit_rate = 0 - BitRateS = "0" [rx_info] channel = 0 @@ -45,6 +44,11 @@ skip_fcnt_check=true [raw_payload] payload = "ff00" use_raw = false + script = "\n// Encode encodes the given object into an array of bytes.\n// - fPort contains the LoRaWAN fPort number\n// - obj is an object, e.g. {\"temperature\": 22.5}\n// The function must return an array of bytes, e.g. [225, 230, 255, 0]\nfunction Encode(fPort, obj) {\n\treturn [\n obj[\"Flags\"],\n obj[\"Battery\"],\n obj[\"Light\"],\n ];\n}\n" + use_encoder = true + max_exec_time = 500 + js_object = "{\n \"Flags\": 0,\n \"Battery\": 65,\n \"Light\": 54\n}" + fport = 2 [[encoded_type]] name = "Flags" diff --git a/go.mod b/go.mod index 9a499c3..9ac37a5 100644 --- a/go.mod +++ b/go.mod @@ -17,8 +17,10 @@ require ( github.com/jacobsa/crypto v0.0.0-20180924003735-d95898ceee07 github.com/konsorten/go-windows-terminal-sequences v1.0.1 github.com/pkg/errors v0.8.1 + github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d github.com/sirupsen/logrus v1.2.0 golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a golang.org/x/sys v0.0.0-20181121002834-0cf1ed9e522b + gopkg.in/sourcemap.v1 v1.0.5 // indirect ) diff --git a/go.sum b/go.sum index 27c55cb..5193a3a 100644 --- a/go.sum +++ b/go.sum @@ -27,6 +27,8 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d h1:1VUlQbCfkoSGv7qP7Y+ro3ap1P1pPZxgdGVqiTVy5C4= +github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY= github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -41,3 +43,5 @@ golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181121002834-0cf1ed9e522b h1:fpg9kqwtLzitbbnpLJATV5Ty8sDv8sJ2ii9+e6fG89A= golang.org/x/sys v0.0.0-20181121002834-0cf1ed9e522b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI= +gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78= diff --git a/images/encoder.png b/images/encoder.png new file mode 100644 index 0000000..d4ccd99 Binary files /dev/null and b/images/encoder.png differ diff --git a/images/new-gui.png b/images/new-gui.png index 5a96a49..e1e8b3f 100644 Binary files a/images/new-gui.png and b/images/new-gui.png differ diff --git a/imgui.ini b/imgui.ini index b18b546..194d9be 100644 --- a/imgui.ini +++ b/imgui.ini @@ -4,7 +4,7 @@ Size=380,265 Collapsed=0 [Window][Output] -Pos=400,650 +Pos=401,650 Size=780,265 Collapsed=0 diff --git a/lora.go b/lora.go new file mode 100644 index 0000000..000908c --- /dev/null +++ b/lora.go @@ -0,0 +1,85 @@ +package main + +import ( + "strconv" + + lwband "github.com/brocaar/lorawan/band" + "github.com/inkyblackness/imgui-go" +) + +type band struct { + Name lwband.Name `toml:"name"` +} + +type dataRate struct { + Bandwith int `toml:"bandwith"` + SpreadFactor int `toml:"spread_factor"` + BitRate int `toml:"bit_rate"` + BitRateS string `toml:"-"` +} + +type rxInfo struct { + Channel int `toml:"channel"` + CodeRate string `toml:"code_rate"` + CrcStatus int `toml:"crc_status"` + Frequency int `toml:"frequency"` + LoRaSNR float64 `toml:"lora_snr"` + RfChain int `toml:"rf_chain"` + Rssi int `toml:"rssi"` + //String representations for numeric values so that we can manage them with input texts. + ChannelS string `toml:"-"` + CrcStatusS string `toml:"-"` + FrequencyS string `toml:"-"` + LoRASNRS string `toml:"-"` + RfChainS string `toml:"-"` + RssiS string `toml:"-"` +} + +func beginLoRaForm() { + //imgui.SetNextWindowPos(imgui.Vec2{X: 10, Y: 650}) + //imgui.SetNextWindowSize(imgui.Vec2{X: 380, Y: 265}) + imgui.Begin("LoRa Configuration") + imgui.PushItemWidth(250.0) + if imgui.BeginCombo("Band", string(config.Band.Name)) { + for _, band := range bands { + if imgui.SelectableV(string(band), band == config.Band.Name, 0, imgui.Vec2{}) { + config.Band.Name = band + } + } + imgui.EndCombo() + } + + if imgui.BeginCombo("Bandwidth", strconv.Itoa(config.DR.Bandwith)) { + for _, bandwidth := range bandwidths { + if imgui.SelectableV(strconv.Itoa(bandwidth), bandwidth == config.DR.Bandwith, 0, imgui.Vec2{}) { + config.DR.Bandwith = bandwidth + } + } + imgui.EndCombo() + } + + if imgui.BeginCombo("SpreadFactor", strconv.Itoa(config.DR.SpreadFactor)) { + for _, sf := range spreadFactors { + if imgui.SelectableV(strconv.Itoa(sf), sf == config.DR.SpreadFactor, 0, imgui.Vec2{}) { + config.DR.SpreadFactor = sf + } + } + imgui.EndCombo() + } + + imgui.InputTextV("Bit rate", &config.DR.BitRateS, imgui.InputTextFlagsCharsDecimal|imgui.InputTextFlagsCallbackAlways|imgui.InputTextFlagsCallbackCharFilter, handleInt(config.DR.BitRateS, 6, &config.DR.BitRate)) + + imgui.InputTextV("Channel", &config.RXInfo.ChannelS, imgui.InputTextFlagsCharsDecimal|imgui.InputTextFlagsCallbackAlways|imgui.InputTextFlagsCallbackCharFilter, handleInt(config.RXInfo.ChannelS, 10, &config.RXInfo.Channel)) + + imgui.InputTextV("CrcStatus", &config.RXInfo.CrcStatusS, imgui.InputTextFlagsCharsDecimal|imgui.InputTextFlagsCallbackAlways|imgui.InputTextFlagsCallbackCharFilter, handleInt(config.RXInfo.CrcStatusS, 10, &config.RXInfo.CrcStatus)) + + imgui.InputTextV("Frequency", &config.RXInfo.FrequencyS, imgui.InputTextFlagsCharsDecimal|imgui.InputTextFlagsCallbackAlways|imgui.InputTextFlagsCallbackCharFilter, handleInt(config.RXInfo.FrequencyS, 14, &config.RXInfo.Frequency)) + + imgui.InputTextV("LoRaSNR", &config.RXInfo.LoRASNRS, imgui.InputTextFlagsCharsDecimal|imgui.InputTextFlagsCallbackAlways, handleFloat64(config.RXInfo.LoRASNRS, &config.RXInfo.LoRaSNR)) + + imgui.InputTextV("RfChain", &config.RXInfo.RfChainS, imgui.InputTextFlagsCharsDecimal|imgui.InputTextFlagsCallbackAlways, handleInt(config.RXInfo.RfChainS, 10, &config.RXInfo.RfChain)) + + imgui.InputTextV("Rssi", &config.RXInfo.RssiS, imgui.InputTextFlagsCharsDecimal|imgui.InputTextFlagsCallbackAlways|imgui.InputTextFlagsCallbackCharFilter, handleInt(config.RXInfo.RssiS, 10, &config.RXInfo.Rssi)) + + imgui.End() +} diff --git a/main.go b/main.go index 65e82dd..ef7ed53 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,6 @@ package main import ( - "encoding/hex" "flag" "fmt" "io/ioutil" @@ -13,11 +12,9 @@ import ( "github.com/BurntSushi/toml" "github.com/atotto/clipboard" - "github.com/brocaar/loraserver/api/gw" "github.com/brocaar/lorawan" "github.com/go-gl/gl/v3.2-core/gl" "github.com/go-gl/glfw/v3.2/glfw" - "github.com/golang/protobuf/ptypes" "github.com/iegomez/lds/lds" "github.com/inkyblackness/imgui-go" @@ -26,83 +23,6 @@ import ( log "github.com/sirupsen/logrus" ) -type mqtt struct { - Server string `toml:"server"` - User string `toml:"user"` - Password string `toml:"password"` -} - -type gateway struct { - MAC string `toml:"mac"` -} - -type band struct { - Name lwband.Name `toml:"name"` -} - -type device struct { - DevEUI string `toml:"eui"` - DevAddress string `toml:"address"` - NwkSEncKey string `toml:"network_session_encription_key"` - SNwkSIntKey string `toml:"serving_network_session_integrity_key"` //For Lorawan 1.0 this is the same as the NwkSEncKey - FNwkSIntKey string `toml:"forwarding_network_session_integrity_key"` //For Lorawan 1.0 this is the same as the NwkSEncKey - AppSKey string `toml:"application_session_key"` - Marshaler string `toml:"marshaler"` - NwkKey string `toml:"nwk_key"` //Network key, used to be called application key for Lorawan 1.0 - AppKey string `toml:"app_key"` //Application key, for Lorawan 1.1 - JoinEUI string `toml:"join_eui"` //JoinEUI for 1.1. (AppEUI on 1.0) - Major lorawan.Major `toml:"-"` - MACVersion lorawan.MACVersion `toml:"mac_version"` //Lorawan MAC version - MType lorawan.MType `toml:"-"` - Profile string `toml:"profile"` - Joined bool `toml:"joined"` - SkipFCntCheck bool `toml:"skip_fcnt_check"` -} - -type dataRate struct { - Bandwith int `toml:"bandwith"` - SpreadFactor int `toml:"spread_factor"` - BitRate int `toml:"bit_rate"` - BitRateS string -} - -type rxInfo struct { - Channel int `toml:"channel"` - CodeRate string `toml:"code_rate"` - CrcStatus int `toml:"crc_status"` - Frequency int `toml:"frequency"` - LoRaSNR float64 `toml:"lora_snr"` - RfChain int `toml:"rf_chain"` - Rssi int `toml:"rssi"` - //String representations for numeric values so that we can manage them with input texts. - ChannelS string `toml:"-"` - CrcStatusS string `toml:"-"` - FrequencyS string `toml:"-"` - LoRASNRS string `toml:"-"` - RfChainS string `toml:"-"` - RssiS string `toml:"-"` -} - -type encodedType struct { - Name string `toml:"name"` - Value float64 `toml:"value"` - MaxValue float64 `toml:"max_value"` - MinValue float64 `toml:"min_value"` - IsFloat bool `toml:"is_float"` - NumBytes int `toml:"num_bytes"` - //String representations. - ValueS string `toml:"-"` - MinValueS string `toml:"-"` - MaxValueS string `toml:"-"` - NumBytesS string `toml:"-"` -} - -//rawPayload holds optional raw bytes payload (hex encoded). -type rawPayload struct { - Payload string `toml:"payload"` - UseRaw bool `toml:"use_raw"` -} - type redisConf struct { Addr string `toml:"addr"` Password string `toml:"password"` @@ -194,7 +114,9 @@ func importConf() { cRx := rxInfo{} - cPl := rawPayload{} + cPl := rawPayload{ + MaxExecTime: 100, + } et := []*encodedType{} @@ -241,6 +163,12 @@ func importConf() { config.RXInfo.RfChainS = strconv.Itoa(config.RXInfo.RfChain) config.RXInfo.RssiS = strconv.Itoa(config.RXInfo.Rssi) + //Set default script when it's not present. + if config.RawPayload.Script == "" { + config.RawPayload.Script = defaultScript + } + config.RawPayload.FPortS = strconv.Itoa(config.RawPayload.FPort) + //Set the device with the given options. setDevice() } @@ -265,397 +193,6 @@ func exportConf(filename string) { } -func writeHistory() { - f, err := os.Create(fmt.Sprintf("lds-%d.log", time.Now().UnixNano())) - if err != nil { - log.Errorf("export error: %s", err) - return - } - defer f.Close() - n, err := f.Write([]byte(ow.History)) - f.Sync() - log.Infof("wrote %d bytes to %s", n, f.Name()) -} - -func setLevel(level log.Level) { - log.SetLevel(level) -} - -func beginMQTTForm() { - //imgui.SetNextWindowPos(imgui.Vec2{X: 10, Y: 25}) - //imgui.SetNextWindowSize(imgui.Vec2{X: 380, Y: 170}) - imgui.Begin("MQTT & Gateway") - imgui.Separator() - imgui.PushItemWidth(250.0) - imgui.InputText("Server", &config.MQTT.Server) - imgui.InputText("User", &config.MQTT.User) - imgui.InputTextV("Password", &config.MQTT.Password, imgui.InputTextFlagsPassword, nil) - imgui.InputText("MAC", &config.GW.MAC) - if imgui.Button("Connect") { - connectClient() - } - if mqttClient != nil && mqttClient.IsConnected() { - if imgui.Button("Disconnect") { - mqttClient.Disconnect(200) - log.Infoln("mqtt client disconnected") - } - } - //Add popus for file administration. - beginOpenFile() - beginSaveFile() - imgui.End() -} - -func connectClient() error { - //Connect to the broker - opts := paho.NewClientOptions() - opts.AddBroker(config.MQTT.Server) - opts.SetUsername(config.MQTT.User) - opts.SetPassword(config.MQTT.Password) - opts.SetAutoReconnect(true) - - mqttClient = paho.NewClient(opts) - log.Infoln("connecting...") - if token := mqttClient.Connect(); token.Wait() && token.Error() != nil { - log.Errorf("connection error: %s", token.Error()) - return token.Error() - } - log.Infoln("connection established") - mqttClient.Subscribe(fmt.Sprintf("gateway/%s/tx", config.GW.MAC), 1, func(c paho.Client, msg paho.Message) { - if cDevice != nil { - dlMessage, err := cDevice.ProcessDownlink(msg.Payload(), cDevice.MACVersion) - //Update keys when necessary. - config.Device.AppSKey = lds.KeyToHex(cDevice.AppSKey) - config.Device.FNwkSIntKey = lds.KeyToHex(cDevice.FNwkSIntKey) - config.Device.NwkSEncKey = lds.KeyToHex(cDevice.NwkSEncKey) - config.Device.SNwkSIntKey = lds.KeyToHex(cDevice.SNwkSIntKey) - config.Device.DevAddress = lds.DevAddressToHex(cDevice.DevAddr) - config.Device.Joined = cDevice.Joined - if err != nil { - log.Errorf("downlink error: %s", err) - } else { - log.Infof("received message: %s", dlMessage) - } - //Get redis info. - cDevice.GetInfo() - } - }) - return nil -} - -func beginDeviceForm() { - //imgui.SetNextWindowPos(imgui.Vec2{X: 10, Y: 205}) - //imgui.SetNextWindowSize(imgui.Vec2{X: 380, Y: 435}) - imgui.Begin("Device") - imgui.PushItemWidth(250.0) - imgui.InputTextV("Device EUI", &config.Device.DevEUI, imgui.InputTextFlagsCharsHexadecimal|imgui.InputTextFlagsCallbackCharFilter, maxLength(config.Device.DevEUI, 16)) - imgui.InputTextV("Device address", &config.Device.DevAddress, imgui.InputTextFlagsCharsHexadecimal|imgui.InputTextFlagsCallbackCharFilter, maxLength(config.Device.DevAddress, 8)) - imgui.InputTextV("NwkSEncKey", &config.Device.NwkSEncKey, imgui.InputTextFlagsCharsHexadecimal|imgui.InputTextFlagsCallbackCharFilter, maxLength(config.Device.NwkSEncKey, 32)) - imgui.InputTextV("SNwkSIntkey", &config.Device.SNwkSIntKey, imgui.InputTextFlagsCharsHexadecimal|imgui.InputTextFlagsCallbackCharFilter, maxLength(config.Device.SNwkSIntKey, 32)) - imgui.InputTextV("FNwkSIntKey", &config.Device.FNwkSIntKey, imgui.InputTextFlagsCharsHexadecimal|imgui.InputTextFlagsCallbackCharFilter, maxLength(config.Device.FNwkSIntKey, 32)) - imgui.InputTextV("AppSKey", &config.Device.AppSKey, imgui.InputTextFlagsCharsHexadecimal|imgui.InputTextFlagsCallbackCharFilter, maxLength(config.Device.AppSKey, 32)) - imgui.InputTextV("NwkKey", &config.Device.NwkKey, imgui.InputTextFlagsCharsHexadecimal|imgui.InputTextFlagsCallbackCharFilter, maxLength(config.Device.NwkKey, 32)) - imgui.InputTextV("AppKey", &config.Device.AppKey, imgui.InputTextFlagsCharsHexadecimal|imgui.InputTextFlagsCallbackCharFilter, maxLength(config.Device.AppKey, 32)) - imgui.InputTextV("Join EUI", &config.Device.JoinEUI, imgui.InputTextFlagsCharsHexadecimal|imgui.InputTextFlagsCallbackCharFilter, maxLength(config.Device.JoinEUI, 16)) - if imgui.BeginCombo("Marshaler", config.Device.Marshaler) { - for _, marshaler := range marshalers { - if imgui.SelectableV(marshaler, marshaler == config.Device.Marshaler, 0, imgui.Vec2{}) { - config.Device.Marshaler = marshaler - } - } - imgui.EndCombo() - } - if imgui.BeginCombo("LoRaWAN major", majorVersions[config.Device.Major]) { - if imgui.SelectableV("LoRaWANRev1", config.Device.Major == 0, 0, imgui.Vec2{}) { - config.Device.MACVersion = 0 - } - imgui.EndCombo() - } - if imgui.BeginCombo("MAC Version", macVersions[config.Device.MACVersion]) { - - if imgui.SelectableV("LoRaWAN 1.0", config.Device.MACVersion == 0, 0, imgui.Vec2{}) { - config.Device.MACVersion = 0 - } - if imgui.SelectableV("LoRaWAN 1.1", config.Device.MACVersion == 1, 0, imgui.Vec2{}) { - config.Device.MACVersion = 1 - } - imgui.EndCombo() - } - if imgui.BeginCombo("MType", mTypes[config.Device.MType]) { - if imgui.SelectableV("UnconfirmedDataUp", config.Device.MType == lorawan.UnconfirmedDataUp || config.Device.MType == 0, 0, imgui.Vec2{}) { - config.Device.MType = lorawan.UnconfirmedDataUp - } - if imgui.SelectableV("ConfirmedDataUp", config.Device.MType == lorawan.ConfirmedDataUp, 0, imgui.Vec2{}) { - config.Device.MType = lorawan.ConfirmedDataUp - } - imgui.EndCombo() - } - if imgui.BeginCombo("Profile", config.Device.Profile) { - if imgui.SelectableV("OTAA", config.Device.Profile == "OTAA", 0, imgui.Vec2{}) { - config.Device.Profile = "OTAA" - } - if imgui.SelectableV("ABP", config.Device.Profile == "ABP", 0, imgui.Vec2{}) { - config.Device.Profile = "ABP" - } - imgui.EndCombo() - } - imgui.Checkbox("Disable frame counter validation", &config.Device.SkipFCntCheck) - if imgui.Button("Join") { - join() - } - imgui.SameLine() - if cDevice != nil { - if imgui.Button("Reset device") { - resetDevice = true - } - } - beginReset() - imgui.Separator() - if cDevice != nil { - imgui.Text(fmt.Sprintf("DlFCnt: %d - UlFCnt: %d", cDevice.DlFcnt, cDevice.UlFcnt)) - imgui.Text(fmt.Sprintf("DevNonce: %d - JoinNonce: %d", cDevice.DevNonce, cDevice.JoinNonce)) - } - imgui.End() -} - -func setDevice() { - //Build your node with known keys (ABP). - nwkSEncHexKey := config.Device.NwkSEncKey - sNwkSIntHexKey := config.Device.SNwkSIntKey - fNwkSIntHexKey := config.Device.FNwkSIntKey - appSHexKey := config.Device.AppSKey - devHexAddr := config.Device.DevAddress - devAddr, err := lds.HexToDevAddress(devHexAddr) - if err != nil { - log.Errorf("dev addr error: %s", err) - } - - nwkSEncKey, err := lds.HexToKey(nwkSEncHexKey) - if err != nil { - log.Errorf("nwkSEncKey error: %s", err) - } - - sNwkSIntKey, err := lds.HexToKey(sNwkSIntHexKey) - if err != nil { - log.Errorf("sNwkSIntKey error: %s", err) - } - - fNwkSIntKey, err := lds.HexToKey(fNwkSIntHexKey) - if err != nil { - log.Errorf("fNwkSIntKey error: %s", err) - } - - appSKey, err := lds.HexToKey(appSHexKey) - if err != nil { - log.Errorf("appskey error: %s", err) - } - - devEUI, err := lds.HexToEUI(config.Device.DevEUI) - if err != nil { - return - } - - nwkHexKey := config.Device.NwkKey - appHexKey := config.Device.AppKey - - appKey, err := lds.HexToKey(appHexKey) - if err != nil { - return - } - nwkKey, err := lds.HexToKey(nwkHexKey) - if err != nil { - return - } - joinEUI, err := lds.HexToEUI(config.Device.JoinEUI) - if err != nil { - return - } - - if cDevice == nil { - cDevice = &lds.Device{ - DevEUI: devEUI, - DevAddr: devAddr, - NwkSEncKey: nwkSEncKey, - SNwkSIntKey: sNwkSIntKey, - FNwkSIntKey: fNwkSIntKey, - AppSKey: appSKey, - AppKey: appKey, - NwkKey: nwkKey, - JoinEUI: joinEUI, - Major: lorawan.Major(config.Device.Major), - MACVersion: lorawan.MACVersion(config.Device.MACVersion), - SkipFCntCheck: config.Device.SkipFCntCheck, - } - } else { - cDevice.DevEUI = devEUI - cDevice.DevAddr = devAddr - cDevice.NwkSEncKey = nwkSEncKey - cDevice.SNwkSIntKey = sNwkSIntKey - cDevice.FNwkSIntKey = fNwkSIntKey - cDevice.AppSKey = appSKey - cDevice.AppKey = appKey - cDevice.NwkKey = nwkKey - cDevice.JoinEUI = joinEUI - cDevice.Major = lorawan.Major(config.Device.Major) - cDevice.MACVersion = lorawan.MACVersion(config.Device.MACVersion) - cDevice.SkipFCntCheck = config.Device.SkipFCntCheck - } - cDevice.SetMarshaler(config.Device.Marshaler) - //Get redis info. - if cDevice.GetInfo() { - config.Device.NwkSEncKey = lds.KeyToHex(cDevice.NwkSEncKey) - config.Device.FNwkSIntKey = lds.KeyToHex(cDevice.FNwkSIntKey) - config.Device.SNwkSIntKey = lds.KeyToHex(cDevice.SNwkSIntKey) - config.Device.AppSKey = lds.KeyToHex(cDevice.AppSKey) - config.Device.DevAddress = lds.DevAddressToHex(cDevice.DevAddr) - } else { - cDevice.Reset() - } -} - -func beginLoRaForm() { - //imgui.SetNextWindowPos(imgui.Vec2{X: 10, Y: 650}) - //imgui.SetNextWindowSize(imgui.Vec2{X: 380, Y: 265}) - imgui.Begin("LoRa Configuration") - imgui.PushItemWidth(250.0) - if imgui.BeginCombo("Band", string(config.Band.Name)) { - for _, band := range bands { - if imgui.SelectableV(string(band), band == config.Band.Name, 0, imgui.Vec2{}) { - config.Band.Name = band - } - } - imgui.EndCombo() - } - - if imgui.BeginCombo("Bandwidth", strconv.Itoa(config.DR.Bandwith)) { - for _, bandwidth := range bandwidths { - if imgui.SelectableV(strconv.Itoa(bandwidth), bandwidth == config.DR.Bandwith, 0, imgui.Vec2{}) { - config.DR.Bandwith = bandwidth - } - } - imgui.EndCombo() - } - - if imgui.BeginCombo("SpreadFactor", strconv.Itoa(config.DR.SpreadFactor)) { - for _, sf := range spreadFactors { - if imgui.SelectableV(strconv.Itoa(sf), sf == config.DR.SpreadFactor, 0, imgui.Vec2{}) { - config.DR.SpreadFactor = sf - } - } - imgui.EndCombo() - } - - imgui.InputTextV("Bit rate", &config.DR.BitRateS, imgui.InputTextFlagsCharsDecimal|imgui.InputTextFlagsCallbackAlways|imgui.InputTextFlagsCallbackCharFilter, handleInt(config.DR.BitRateS, 6, &config.DR.BitRate)) - - imgui.InputTextV("Channel", &config.RXInfo.ChannelS, imgui.InputTextFlagsCharsDecimal|imgui.InputTextFlagsCallbackAlways|imgui.InputTextFlagsCallbackCharFilter, handleInt(config.RXInfo.ChannelS, 10, &config.RXInfo.Channel)) - - imgui.InputTextV("CrcStatus", &config.RXInfo.CrcStatusS, imgui.InputTextFlagsCharsDecimal|imgui.InputTextFlagsCallbackAlways|imgui.InputTextFlagsCallbackCharFilter, handleInt(config.RXInfo.CrcStatusS, 10, &config.RXInfo.CrcStatus)) - - imgui.InputTextV("Frequency", &config.RXInfo.FrequencyS, imgui.InputTextFlagsCharsDecimal|imgui.InputTextFlagsCallbackAlways|imgui.InputTextFlagsCallbackCharFilter, handleInt(config.RXInfo.FrequencyS, 14, &config.RXInfo.Frequency)) - - imgui.InputTextV("LoRaSNR", &config.RXInfo.LoRASNRS, imgui.InputTextFlagsCharsDecimal|imgui.InputTextFlagsCallbackAlways, handleFloat64(config.RXInfo.LoRASNRS, &config.RXInfo.LoRaSNR)) - - imgui.InputTextV("RfChain", &config.RXInfo.RfChainS, imgui.InputTextFlagsCharsDecimal|imgui.InputTextFlagsCallbackAlways, handleInt(config.RXInfo.RfChainS, 10, &config.RXInfo.RfChain)) - - imgui.InputTextV("Rssi", &config.RXInfo.RssiS, imgui.InputTextFlagsCharsDecimal|imgui.InputTextFlagsCallbackAlways|imgui.InputTextFlagsCallbackCharFilter, handleInt(config.RXInfo.RssiS, 10, &config.RXInfo.Rssi)) - - imgui.End() -} - -func beginControl() { - //imgui.SetNextWindowPos(imgui.Vec2{X: 400, Y: 25}) - //imgui.SetNextWindowSize(imgui.Vec2{X: 780, Y: 250}) - imgui.Begin("Control") - imgui.Text("FCtrl") - imgui.Separator() - beginFCtrl() - imgui.Text("MAC Commands") - beginMACCommands() - imgui.Separator() - imgui.End() -} - -func beginDataForm() { - //imgui.SetNextWindowPos(imgui.Vec2{X: 400, Y: 285}) - //imgui.SetNextWindowSize(imgui.Vec2{X: 780, Y: 355}) - imgui.Begin("Data") - imgui.Text("Raw data") - imgui.PushItemWidth(150.0) - imgui.InputTextV("Raw bytes in hex", &config.RawPayload.Payload, imgui.InputTextFlagsCharsHexadecimal, nil) - imgui.SameLine() - imgui.Checkbox("Send raw", &config.RawPayload.UseRaw) - imgui.SliderInt("X", &interval, 1, 60) - imgui.SameLine() - imgui.Checkbox("Send every X seconds", &repeat) - if !running { - if imgui.Button("Send data") { - run() - } - } - if repeat { - if imgui.Button("Stop") { - running = false - } - } - - imgui.Separator() - - imgui.Text("Encoded data") - if imgui.Button("Add encoded type") { - et := &encodedType{ - Name: "New type", - ValueS: "0", - MaxValueS: "0", - MinValueS: "0", - NumBytesS: "0", - } - config.EncodedType = append(config.EncodedType, et) - log.Println("added new type") - } - - for i := 0; i < len(config.EncodedType); i++ { - delete := false - imgui.Separator() - imgui.InputText(fmt.Sprintf("Name ##%d", i), &config.EncodedType[i].Name) - imgui.SameLine() - imgui.InputTextV(fmt.Sprintf("Bytes ##%d", i), &config.EncodedType[i].NumBytesS, imgui.InputTextFlagsCharsDecimal|imgui.InputTextFlagsCallbackAlways, handleInt(config.EncodedType[i].NumBytesS, 10, &config.EncodedType[i].NumBytes)) - imgui.SameLine() - imgui.Checkbox(fmt.Sprintf("Float##%d", i), &config.EncodedType[i].IsFloat) - imgui.SameLine() - if imgui.Button(fmt.Sprintf("Delete##%d", i)) { - delete = true - } - imgui.InputTextV(fmt.Sprintf("Value ##%d", i), &config.EncodedType[i].ValueS, imgui.InputTextFlagsCharsDecimal|imgui.InputTextFlagsCallbackAlways, handleFloat64(config.EncodedType[i].ValueS, &config.EncodedType[i].Value)) - imgui.SameLine() - imgui.InputTextV(fmt.Sprintf("Max value##%d", i), &config.EncodedType[i].MaxValueS, imgui.InputTextFlagsCharsDecimal|imgui.InputTextFlagsCallbackAlways, handleFloat64(config.EncodedType[i].MaxValueS, &config.EncodedType[i].MaxValue)) - imgui.SameLine() - imgui.InputTextV(fmt.Sprintf("Min value##%d", i), &config.EncodedType[i].MinValueS, imgui.InputTextFlagsCharsDecimal|imgui.InputTextFlagsCallbackAlways, handleFloat64(config.EncodedType[i].MinValueS, &config.EncodedType[i].MinValue)) - if delete { - if len(config.EncodedType) == 1 { - config.EncodedType = make([]*encodedType, 0) - } else { - copy(config.EncodedType[i:], config.EncodedType[i+1:]) - config.EncodedType[len(config.EncodedType)-1] = &encodedType{} - config.EncodedType = config.EncodedType[:len(config.EncodedType)-1] - } - } - } - imgui.Separator() - - imgui.End() -} - -func beginOutput() { - //imgui.SetNextWindowPos(imgui.Vec2{X: 400, Y: 650}) - //imgui.SetNextWindowSize(imgui.Vec2{X: 780, Y: 265}) - imgui.Begin("Output") - imgui.PushTextWrapPos() - imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 0.1, Y: 0.8, Z: 0.1, W: 0.5}) - imgui.Text(ow.Text) - imgui.PopStyleColor() - imgui.End() -} - func beginMenu() { if imgui.BeginMainMenuBar() { if imgui.BeginMenu("File") { @@ -777,43 +314,6 @@ func beginSaveFile() { } } -func beginReset() { - if resetDevice { - imgui.OpenPopup("Reset device") - resetDevice = false - } - imgui.SetNextWindowPos(imgui.Vec2{X: float32(windowWidth-190) / 2, Y: float32(windowHeight-90) / 2}) - imgui.SetNextWindowSize(imgui.Vec2{X: 380, Y: 180}) - imgui.PushItemWidth(250.0) - if imgui.BeginPopupModal("Reset device") { - - imgui.PushTextWrapPos() - imgui.Text("This will delete saved devNonce, joinNonce, downlink and uplink frame counters, device address and device keys. Are you sure you want to proceed?") - imgui.Separator() - if imgui.Button("Cancel") { - imgui.CloseCurrentPopup() - } - imgui.SameLine() - if imgui.Button("Confirm") { - //Reset device. - err := cDevice.Reset() - if err != nil { - log.Errorln(err) - } else { - config.Device.NwkSEncKey = lds.KeyToHex(cDevice.NwkSEncKey) - config.Device.FNwkSIntKey = lds.KeyToHex(cDevice.FNwkSIntKey) - config.Device.SNwkSIntKey = lds.KeyToHex(cDevice.SNwkSIntKey) - config.Device.AppSKey = lds.KeyToHex(cDevice.AppSKey) - config.Device.DevAddress = lds.DevAddressToHex(cDevice.DevAddr) - log.Infoln("device was reset") - } - imgui.CloseCurrentPopup() - //Close popup. - } - imgui.EndPopup() - } -} - func main() { runtime.LockOSThread() @@ -880,184 +380,3 @@ func main() { <-time.After(time.Millisecond * 25) } } - -func join() { - if mqttClient == nil { - err := connectClient() - if err != nil { - return - } - } else if !mqttClient.IsConnected() { - log.Errorln("mqtt client not connected") - } - - //Always set device to get any changes to the configuration. - setDevice() - - dataRate := &lds.DataRate{ - Bandwidth: config.DR.Bandwith, - Modulation: "LORA", - SpreadFactor: config.DR.SpreadFactor, - BitRate: config.DR.BitRate, - } - - rxInfo := &lds.RxInfo{ - Channel: config.RXInfo.Channel, - CodeRate: config.RXInfo.CodeRate, - CrcStatus: config.RXInfo.CrcStatus, - DataRate: dataRate, - Frequency: config.RXInfo.Frequency, - LoRaSNR: float32(config.RXInfo.LoRaSNR), - Mac: config.GW.MAC, - RfChain: config.RXInfo.RfChain, - Rssi: config.RXInfo.Rssi, - Time: time.Now().Format(time.RFC3339), - Timestamp: int32(time.Now().UnixNano() / 1000000000), - } - - gwID, err := lds.MACToGatewayID(config.GW.MAC) - if err != nil { - log.Errorf("gw mac error: %s", err) - return - } - - err = cDevice.Join(mqttClient, string(gwID), *rxInfo) - - if err != nil { - log.Errorf("join error: %s", err) - } else { - log.Println("join sent") - } - -} - -func run() { - - if mqttClient == nil { - err := connectClient() - if err != nil { - return - } - } else if !mqttClient.IsConnected() { - log.Errorln("mqtt client not connected") - } - - setDevice() - - dataRate := &lds.DataRate{ - Bandwidth: config.DR.Bandwith, - Modulation: "LORA", - SpreadFactor: config.DR.SpreadFactor, - BitRate: config.DR.BitRate, - } - - for { - if stop { - stop = false - return - } - payload := []byte{} - - if config.RawPayload.UseRaw { - var pErr error - payload, pErr = hex.DecodeString(config.RawPayload.Payload) - if pErr != nil { - log.Errorf("couldn't decode hex payload: %s", pErr) - return - } - } else { - for _, v := range config.EncodedType { - if v.IsFloat { - arr := lds.GenerateFloat(float32(v.Value), float32(v.MaxValue), int32(v.NumBytes)) - payload = append(payload, arr...) - } else { - arr := lds.GenerateInt(int32(v.Value), int32(v.NumBytes)) - payload = append(payload, arr...) - } - } - } - - //Construct DataRate RxInfo with proper values according to your band (example is for US 915). - - rxInfo := &lds.RxInfo{ - Channel: config.RXInfo.Channel, - CodeRate: config.RXInfo.CodeRate, - CrcStatus: config.RXInfo.CrcStatus, - DataRate: dataRate, - Frequency: config.RXInfo.Frequency, - LoRaSNR: float32(config.RXInfo.LoRaSNR), - Mac: config.GW.MAC, - RfChain: config.RXInfo.RfChain, - Rssi: config.RXInfo.Rssi, - Size: len(payload), - Time: time.Now().Format(time.RFC3339), - Timestamp: int32(time.Now().UnixNano() / 1000000000), - } - - ////// - - gwID, err := lds.MACToGatewayID(config.GW.MAC) - if err != nil { - log.Errorf("gw mac error: %s", err) - return - } - now := time.Now() - rxTime := ptypes.TimestampNow() - tsge := ptypes.DurationProto(now.Sub(time.Time{})) - - urx := gw.UplinkRXInfo{ - GatewayId: gwID, - Rssi: int32(rxInfo.Rssi), - LoraSnr: float64(rxInfo.LoRaSNR), - Channel: uint32(rxInfo.Channel), - RfChain: uint32(rxInfo.RfChain), - TimeSinceGpsEpoch: tsge, - Time: rxTime, - Timestamp: uint32(rxTime.GetSeconds()), - Board: 0, - Antenna: 0, - Location: nil, - FineTimestamp: nil, - FineTimestampType: gw.FineTimestampType_NONE, - } - - lmi := &gw.LoRaModulationInfo{ - Bandwidth: uint32(rxInfo.DataRate.Bandwidth), - SpreadingFactor: uint32(rxInfo.DataRate.SpreadFactor), - CodeRate: rxInfo.CodeRate, - } - - umi := &gw.UplinkTXInfo_LoraModulationInfo{ - LoraModulationInfo: lmi, - } - - utx := gw.UplinkTXInfo{ - Frequency: uint32(rxInfo.Frequency), - ModulationInfo: umi, - } - - var fOpts []*lorawan.MACCommand - for i := 0; i < len(macCommands); i++ { - if macCommands[i].Use { - fOpts = append(fOpts, &macCommands[i].MACCommand) - } - } - - //Now send an uplink - ulfc, err := cDevice.Uplink(mqttClient, config.Device.MType, 1, &urx, &utx, payload, config.GW.MAC, config.Band.Name, *dataRate, fOpts, fCtrl) - if err != nil { - log.Errorf("couldn't send uplink: %s", err) - } else { - log.Infof("message sent, uplink framecounter is now %d", ulfc) - } - - if !repeat || !running { - stop = false - return - } - - time.Sleep(time.Duration(interval) * time.Second) - - } - -} diff --git a/mqtt.go b/mqtt.go new file mode 100644 index 0000000..38ab807 --- /dev/null +++ b/mqtt.go @@ -0,0 +1,82 @@ +package main + +import ( + "fmt" + + paho "github.com/eclipse/paho.mqtt.golang" + "github.com/iegomez/lds/lds" + "github.com/inkyblackness/imgui-go" + log "github.com/sirupsen/logrus" +) + +type mqtt struct { + Server string `toml:"server"` + User string `toml:"user"` + Password string `toml:"password"` +} + +type gateway struct { + MAC string `toml:"mac"` +} + +func beginMQTTForm() { + //imgui.SetNextWindowPos(imgui.Vec2{X: 10, Y: 25}) + //imgui.SetNextWindowSize(imgui.Vec2{X: 380, Y: 170}) + imgui.Begin("MQTT & Gateway") + imgui.Separator() + imgui.PushItemWidth(250.0) + imgui.InputText("Server", &config.MQTT.Server) + imgui.InputText("User", &config.MQTT.User) + imgui.InputTextV("Password", &config.MQTT.Password, imgui.InputTextFlagsPassword, nil) + imgui.InputText("MAC", &config.GW.MAC) + if imgui.Button("Connect") { + connectClient() + } + if mqttClient != nil && mqttClient.IsConnected() { + if imgui.Button("Disconnect") { + mqttClient.Disconnect(200) + log.Infoln("mqtt client disconnected") + } + } + //Add popus for file administration. + beginOpenFile() + beginSaveFile() + imgui.End() +} + +func connectClient() error { + //Connect to the broker + opts := paho.NewClientOptions() + opts.AddBroker(config.MQTT.Server) + opts.SetUsername(config.MQTT.User) + opts.SetPassword(config.MQTT.Password) + opts.SetAutoReconnect(true) + + mqttClient = paho.NewClient(opts) + log.Infoln("connecting...") + if token := mqttClient.Connect(); token.Wait() && token.Error() != nil { + log.Errorf("connection error: %s", token.Error()) + return token.Error() + } + log.Infoln("connection established") + mqttClient.Subscribe(fmt.Sprintf("gateway/%s/tx", config.GW.MAC), 1, func(c paho.Client, msg paho.Message) { + if cDevice != nil { + dlMessage, err := cDevice.ProcessDownlink(msg.Payload(), cDevice.MACVersion) + //Update keys when necessary. + config.Device.AppSKey = lds.KeyToHex(cDevice.AppSKey) + config.Device.FNwkSIntKey = lds.KeyToHex(cDevice.FNwkSIntKey) + config.Device.NwkSEncKey = lds.KeyToHex(cDevice.NwkSEncKey) + config.Device.SNwkSIntKey = lds.KeyToHex(cDevice.SNwkSIntKey) + config.Device.DevAddress = lds.DevAddressToHex(cDevice.DevAddr) + config.Device.Joined = cDevice.Joined + if err != nil { + log.Errorf("downlink error: %s", err) + } else { + log.Infof("received message: %s", dlMessage) + } + //Get redis info. + cDevice.GetInfo() + } + }) + return nil +} diff --git a/output.go b/output.go new file mode 100644 index 0000000..3407c9e --- /dev/null +++ b/output.go @@ -0,0 +1,37 @@ +package main + +import ( + "fmt" + "os" + "time" + + "github.com/inkyblackness/imgui-go" + log "github.com/sirupsen/logrus" +) + +func writeHistory() { + f, err := os.Create(fmt.Sprintf("lds-%d.log", time.Now().UnixNano())) + if err != nil { + log.Errorf("export error: %s", err) + return + } + defer f.Close() + n, err := f.Write([]byte(ow.History)) + f.Sync() + log.Infof("wrote %d bytes to %s", n, f.Name()) +} + +func setLevel(level log.Level) { + log.SetLevel(level) +} + +func beginOutput() { + //imgui.SetNextWindowPos(imgui.Vec2{X: 400, Y: 650}) + //imgui.SetNextWindowSize(imgui.Vec2{X: 780, Y: 265}) + imgui.Begin("Output") + imgui.PushTextWrapPos() + imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 0.1, Y: 0.8, Z: 0.1, W: 0.5}) + imgui.Text(ow.Text) + imgui.PopStyleColor() + imgui.End() +}