Skip to content

Commit c2951a9

Browse files
author
Ignacio Gómez
committed
Added custom JS decoder.
1 parent 06a6e20 commit c2951a9

File tree

14 files changed

+951
-695
lines changed

14 files changed

+951
-695
lines changed

README.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ log_level = "info"
5252
bandwith = 125
5353
spread_factor = 10
5454
bit_rate = 0
55-
BitRateS = "0"
5655

5756
[rx_info]
5857
channel = 0
@@ -66,6 +65,11 @@ log_level = "info"
6665
[raw_payload]
6766
payload = "ff00"
6867
use_raw = false
68+
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"
69+
use_encoder = true
70+
max_exec_time = 500
71+
js_object = "{\n \"Flags\": 0,\n \"Battery\": 65,\n \"Light\": 54\n}"
72+
fport = 2
6973

7074
[[encoded_type]]
7175
name = "Flags"
@@ -102,7 +106,7 @@ When OTAA is set and the device is joined, uponinitialization the program will t
102106

103107
### Data
104108

105-
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:
109+
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:
106110

107111
```go
108112
func GenerateFloat(originalFloat, maxValue float32, numBytes int32) []byte {
@@ -143,7 +147,11 @@ func GenerateInt(originalInt, numBytes int32) []byte {
143147
}
144148
```
145149

146-
Values may be added using the `Add encoded type` button and setting the options.
150+
When using our encoding method, values may be added using the `Add encoded type` button and setting the options.
151+
152+
To use your own custom JS encoder, click the "Use encoder" checkbox and the "Open decoder" button to open the form:
153+
154+
![](images/encoder.png?raw=true)
147155

148156
#### MAC Commands
149157

control.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,16 @@ func beginFCtrl() {
183183
imgui.SameLine()
184184
imgui.Checkbox("FPending##FCtrl-FPending", &fCtrl.FPending)
185185
}
186+
187+
func beginControl() {
188+
//imgui.SetNextWindowPos(imgui.Vec2{X: 400, Y: 25})
189+
//imgui.SetNextWindowSize(imgui.Vec2{X: 780, Y: 250})
190+
imgui.Begin("Control")
191+
imgui.Text("FCtrl")
192+
imgui.Separator()
193+
beginFCtrl()
194+
imgui.Text("MAC Commands")
195+
beginMACCommands()
196+
imgui.Separator()
197+
imgui.End()
198+
}

data.go

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"reflect"
7+
8+
"github.com/inkyblackness/imgui-go"
9+
"github.com/pkg/errors"
10+
log "github.com/sirupsen/logrus"
11+
12+
"time"
13+
14+
"github.com/robertkrimen/otto"
15+
)
16+
17+
type encodedType struct {
18+
Name string `toml:"name"`
19+
Value float64 `toml:"value"`
20+
MaxValue float64 `toml:"max_value"`
21+
MinValue float64 `toml:"min_value"`
22+
IsFloat bool `toml:"is_float"`
23+
NumBytes int `toml:"num_bytes"`
24+
//String representations.
25+
ValueS string `toml:"-"`
26+
MinValueS string `toml:"-"`
27+
MaxValueS string `toml:"-"`
28+
NumBytesS string `toml:"-"`
29+
}
30+
31+
//rawPayload holds optional raw bytes payload (hex encoded).
32+
type rawPayload struct {
33+
Payload string `toml:"payload"`
34+
UseRaw bool `toml:"use_raw"`
35+
Script string `toml:"script"`
36+
UseEncoder bool `toml:"use_encoder"`
37+
MaxExecTime int `toml:"max_exec_time"`
38+
Obj string `toml:"js_object"`
39+
FPort int `toml:"fport"`
40+
FPortS string `toml:"-"`
41+
}
42+
43+
var openScript bool
44+
var defaultScript = `
45+
// Encode encodes the given object into an array of bytes.
46+
// - fPort contains the LoRaWAN fPort number
47+
// - obj is an object, e.g. {"temperature": 22.5}
48+
// The function must return an array of bytes, e.g. [225, 230, 255, 0]
49+
function Encode(fPort, obj) {
50+
return [];
51+
}
52+
`
53+
54+
func beginDataForm() {
55+
//imgui.SetNextWindowPos(imgui.Vec2{X: 400, Y: 285})
56+
//imgui.SetNextWindowSize(imgui.Vec2{X: 780, Y: 355})
57+
imgui.Begin("Data")
58+
imgui.Text("Raw data")
59+
imgui.PushItemWidth(150.0)
60+
imgui.InputTextV("Raw bytes in hex", &config.RawPayload.Payload, imgui.InputTextFlagsCharsHexadecimal, nil)
61+
imgui.SameLine()
62+
imgui.Checkbox("Send raw", &config.RawPayload.UseRaw)
63+
imgui.SameLine()
64+
imgui.Checkbox("Use encoder", &config.RawPayload.UseEncoder)
65+
imgui.SameLine()
66+
if imgui.Button("Open encoder") {
67+
openScript = true
68+
}
69+
imgui.InputTextV(fmt.Sprintf("fPort ##fport"), &config.RawPayload.FPortS, imgui.InputTextFlagsCharsDecimal|imgui.InputTextFlagsCallbackAlways, handleInt(config.RawPayload.FPortS, 10, &config.RawPayload.FPort))
70+
imgui.SliderInt("X", &interval, 1, 60)
71+
imgui.SameLine()
72+
imgui.Checkbox("Send every X seconds", &repeat)
73+
if !running {
74+
if imgui.Button("Send data") {
75+
go run()
76+
}
77+
}
78+
if repeat && running {
79+
if imgui.Button("Stop") {
80+
running = false
81+
}
82+
}
83+
84+
imgui.Separator()
85+
86+
imgui.Text("Encoded data")
87+
if imgui.Button("Add encoded type") {
88+
et := &encodedType{
89+
Name: "New type",
90+
ValueS: "0",
91+
MaxValueS: "0",
92+
MinValueS: "0",
93+
NumBytesS: "0",
94+
}
95+
config.EncodedType = append(config.EncodedType, et)
96+
log.Println("added new type")
97+
}
98+
99+
for i := 0; i < len(config.EncodedType); i++ {
100+
delete := false
101+
imgui.Separator()
102+
imgui.InputText(fmt.Sprintf("Name ##%d", i), &config.EncodedType[i].Name)
103+
imgui.SameLine()
104+
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))
105+
imgui.SameLine()
106+
imgui.Checkbox(fmt.Sprintf("Float##%d", i), &config.EncodedType[i].IsFloat)
107+
imgui.SameLine()
108+
if imgui.Button(fmt.Sprintf("Delete##%d", i)) {
109+
delete = true
110+
}
111+
imgui.InputTextV(fmt.Sprintf("Value ##%d", i), &config.EncodedType[i].ValueS, imgui.InputTextFlagsCharsDecimal|imgui.InputTextFlagsCallbackAlways, handleFloat64(config.EncodedType[i].ValueS, &config.EncodedType[i].Value))
112+
imgui.SameLine()
113+
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))
114+
imgui.SameLine()
115+
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))
116+
if delete {
117+
if len(config.EncodedType) == 1 {
118+
config.EncodedType = make([]*encodedType, 0)
119+
} else {
120+
copy(config.EncodedType[i:], config.EncodedType[i+1:])
121+
config.EncodedType[len(config.EncodedType)-1] = &encodedType{}
122+
config.EncodedType = config.EncodedType[:len(config.EncodedType)-1]
123+
}
124+
}
125+
}
126+
imgui.Separator()
127+
beginScript()
128+
imgui.End()
129+
}
130+
131+
func beginScript() {
132+
if openScript {
133+
imgui.OpenPopup("JS encoder")
134+
openScript = false
135+
}
136+
imgui.SetNextWindowPos(imgui.Vec2{X: (float32(windowWidth) / 2) - 370.0, Y: (float32(windowHeight) / 2) - 200.0})
137+
imgui.SetNextWindowSize(imgui.Vec2{X: 740, Y: 600})
138+
if imgui.BeginPopupModal("JS encoder") {
139+
imgui.Text(`If "Use encoder" is checked, you may write a function that accepts a JS object`)
140+
imgui.Text(`and returns a byte array that'll be used as the raw bytes when sending data.`)
141+
imgui.Text(`The function must be named Encode and accept a port and JS object.`)
142+
imgui.InputTextMultilineV("##encoder-function", &config.RawPayload.Script, imgui.Vec2{X: 710, Y: 300}, imgui.InputTextFlagsAllowTabInput, nil)
143+
imgui.Separator()
144+
imgui.Text("JS object:")
145+
imgui.InputTextMultilineV("##encoder-object", &config.RawPayload.Obj, imgui.Vec2{X: 710, Y: 140}, 0, nil)
146+
if imgui.Button("Clear##encoder-cancel") {
147+
config.RawPayload.Script = defaultScript
148+
imgui.CloseCurrentPopup()
149+
}
150+
imgui.SameLine()
151+
if imgui.Button("Close##encoder-close") {
152+
imgui.CloseCurrentPopup()
153+
}
154+
imgui.EndPopup()
155+
}
156+
}
157+
158+
// EncodeToBytes encodes the payload to a slice of bytes.
159+
// Taken from github.com/brocaar/lora-app-server.
160+
func EncodeToBytes() (b []byte, err error) {
161+
defer func() {
162+
if caught := recover(); caught != nil {
163+
err = fmt.Errorf("%s", caught)
164+
}
165+
}()
166+
167+
script := config.RawPayload.Script + "\n\nEncode(fPort, obj);\n"
168+
169+
vm := otto.New()
170+
vm.Interrupt = make(chan func(), 1)
171+
vm.SetStackDepthLimit(32)
172+
var jsonData interface{}
173+
err = json.Unmarshal([]byte(config.RawPayload.Obj), &jsonData)
174+
if err != nil {
175+
log.Errorf("couldn't unmarshal object: %s", err)
176+
return nil, err
177+
}
178+
log.Debugf("JS object: %v", jsonData)
179+
vm.Set("obj", jsonData)
180+
vm.Set("fPort", config.RawPayload.FPort)
181+
182+
go func() {
183+
time.Sleep(time.Duration(config.RawPayload.MaxExecTime) * time.Millisecond)
184+
vm.Interrupt <- func() {
185+
panic(errors.New("execution timeout"))
186+
}
187+
}()
188+
189+
var val otto.Value
190+
val, err = vm.Run(script)
191+
if err != nil {
192+
return nil, errors.Wrap(err, "js vm error")
193+
}
194+
if !val.IsObject() {
195+
return nil, errors.New("function must return an array")
196+
}
197+
198+
var out interface{}
199+
out, err = val.Export()
200+
if err != nil {
201+
return nil, errors.Wrap(err, "export error")
202+
}
203+
204+
return interfaceToByteSlice(out)
205+
}
206+
207+
// Taken from github.com/brocaar/lora-app-server.
208+
func interfaceToByteSlice(obj interface{}) ([]byte, error) {
209+
if obj == nil {
210+
return nil, errors.New("value must not be nil")
211+
}
212+
213+
if reflect.TypeOf(obj).Kind() != reflect.Slice {
214+
return nil, errors.New("value must be an array")
215+
}
216+
217+
s := reflect.ValueOf(obj)
218+
l := s.Len()
219+
220+
var out []byte
221+
for i := 0; i < l; i++ {
222+
var b int64
223+
224+
el := s.Index(i).Interface()
225+
switch v := el.(type) {
226+
case int:
227+
b = int64(v)
228+
case uint:
229+
b = int64(v)
230+
case uint8:
231+
b = int64(v)
232+
case int8:
233+
b = int64(v)
234+
case uint16:
235+
b = int64(v)
236+
case int16:
237+
b = int64(v)
238+
case uint32:
239+
b = int64(v)
240+
case int32:
241+
b = int64(v)
242+
case uint64:
243+
b = int64(v)
244+
if uint64(b) != v {
245+
return nil, fmt.Errorf("array value must be in byte range (0 - 255), got: %d", v)
246+
}
247+
case int64:
248+
b = int64(v)
249+
case float32:
250+
b = int64(v)
251+
if float32(b) != v {
252+
return nil, fmt.Errorf("array value must be in byte range (0 - 255), got: %f", v)
253+
}
254+
case float64:
255+
b = int64(v)
256+
if float64(b) != v {
257+
return nil, fmt.Errorf("array value must be in byte range (0 - 255), got: %f", v)
258+
}
259+
default:
260+
return nil, fmt.Errorf("array value must be an array of ints or floats, got: %T", el)
261+
}
262+
263+
if b < 0 || b > 255 {
264+
return nil, fmt.Errorf("array value must be in byte range (0 - 255), got: %d", b)
265+
}
266+
267+
out = append(out, byte(b))
268+
}
269+
270+
return out, nil
271+
}

0 commit comments

Comments
 (0)