Browse Source

调整布局,增加数据展示

master
lihua 5 years ago
parent
commit
4ee931fd63
  1. 204
      LoRaDTUMock.go
  2. 111
      jsonModel.go
  3. 1
      model.go
  4. 16
      utils.go

204
LoRaDTUMock.go

@ -13,9 +13,11 @@ import (
"github.com/lxn/walk" "github.com/lxn/walk"
. "github.com/lxn/walk/declarative" . "github.com/lxn/walk/declarative"
"github.com/lxn/win" "github.com/lxn/win"
"github.com/pkg/errors"
"io/ioutil" "io/ioutil"
"math/rand" "math/rand"
"os" "os"
"strings"
"time" "time"
) )
@ -24,11 +26,13 @@ type DTUMainWindow struct {
mqttClient paho.Client mqttClient paho.Client
model *DTUModel model *DTUModel
tv *walk.TableView tv *walk.TableView
jsonView *walk.TreeView
host,username,password *walk.LineEdit host,username,password *walk.LineEdit
port *walk.NumberEdit port,sendInterval *walk.NumberEdit
connect, disconnect,caConf,send *walk.PushButton connect, disconnect,caConf,send *walk.PushButton
msg *walk.TextEdit ascii,noAscii *walk.RadioButton
ssl *walk.CheckBox msg,data *walk.TextEdit
ssl,timeSend *walk.CheckBox
connConf ConnectConfig connConf ConnectConfig
dtuConf DTUConfig dtuConf DTUConfig
connConfFileName string connConfFileName string
@ -62,7 +66,7 @@ func main() {
CheckBox{Text:"开启SSL/TLS",AssignTo:&mw.ssl,OnClicked:mw.SSL}, CheckBox{Text:"开启SSL/TLS",AssignTo:&mw.ssl,OnClicked:mw.SSL},
PushButton{Text:"证书配置",Enabled:false,AssignTo:&mw.caConf,OnClicked: mw.ConnectConfig}, PushButton{Text:"证书配置",Enabled:false,AssignTo:&mw.caConf,OnClicked: mw.ConnectConfig},
PushButton{Text:"终端配置",OnClicked: mw.DTUConfig}, PushButton{Text:"终端配置",OnClicked: mw.DTUConfig},
PushButton{Text:"发送",AssignTo:&mw.send,OnClicked: mw.Send}, PushButton{Text:"清空数据",OnClicked: mw.Clean},
}, },
}, },
Composite{ Composite{
@ -70,7 +74,7 @@ func main() {
Children: []Widget{ Children: []Widget{
TableView{ TableView{
AssignTo: &mw.tv, AssignTo: &mw.tv,
CheckBoxes: true, MinSize:Size{Width:maxWidth*3/5},
ColumnsOrderable: true, ColumnsOrderable: true,
MultiSelection: true, MultiSelection: true,
Columns: []TableViewColumn{ Columns: []TableViewColumn{
@ -92,6 +96,45 @@ func main() {
Model: mw.model, Model: mw.model,
OnItemActivated: mw.tvItemActivated, OnItemActivated: mw.tvItemActivated,
}, },
GroupBox{
Title:"发送区",
Layout:VBox{SpacingZero:true},
Children:[]Widget{
Composite{
Layout:Grid{Columns:3},
Children:[]Widget{
Label{Text:"编码方式:"},
RadioButton{Text:"ASCII数据",AssignTo:&mw.ascii},
RadioButton{Text:"Hex数据",AssignTo:&mw.noAscii},
CheckBox{Text:"定时发送",AssignTo:&mw.timeSend,OnClicked:mw.TimeSend},
NumberEdit{AssignTo:&mw.sendInterval},
Label{Text:"ms/次"},
},
},
Composite{
Layout:VBox{},
Children:[]Widget{
Label{Text:"消息"},
TextEdit{AssignTo:&mw.msg},
},
},
Composite{
Layout:HBox{},
Children:[]Widget{
HSpacer{},
PushButton{Text:"发送",AssignTo:&mw.send,OnClicked: mw.SendMsg},
},
},
},
},
},
},
Composite{
Layout: HBox{},
Children: []Widget{
TextEdit{AssignTo:&mw.data,ReadOnly:true,HScroll:true,VScroll:true},
TreeView{AssignTo: &mw.jsonView},
}, },
}, },
}, },
@ -100,9 +143,10 @@ func main() {
panic("LoRaDTUMock窗口创建失败") panic("LoRaDTUMock窗口创建失败")
} }
_ = mw.port.SetValue(1883) _ = mw.port.SetValue(1883)
_ = mw.sendInterval.SetValue(1000)
mw.ascii.SetChecked(true)
dir,_ := os.Getwd() dir,_ := os.Getwd()
mw.connConfFileName = dir + "/LoRaDTUMock.json" mw.connConfFileName = dir + "/LoRaDTUMock.json"
mw.dtuConfFileName = dir + "/LoRaDTUConf.json"
data, err := ioutil.ReadFile(mw.connConfFileName) data, err := ioutil.ReadFile(mw.connConfFileName)
if err == nil { if err == nil {
err = json.Unmarshal(data, &mw.connConf) err = json.Unmarshal(data, &mw.connConf)
@ -116,6 +160,16 @@ func main() {
_ = mw.username.SetText(mw.connConf.Username) _ = mw.username.SetText(mw.connConf.Username)
_ = mw.password.SetText(mw.connConf.Password) _ = mw.password.SetText(mw.connConf.Password)
} }
mw.dtuConfFileName = dir + "/LoRaDTUConf.json"
data, err = ioutil.ReadFile(mw.dtuConfFileName)
if err == nil {
err = json.Unmarshal(data, &mw.dtuConf)
if err != nil {
msg := "配置文件格式错误:" + err.Error()
walk.MsgBox(mw, "错误", msg, walk.MsgBoxIconError)
return
}
}
mw.Run() mw.Run()
} }
@ -197,7 +251,6 @@ func (mw *DTUMainWindow) DTUConfig() {
var otaa *walk.CheckBox var otaa *walk.CheckBox
var gatewayId,devEUI,devAddr,appKey,appSKey,nwkSKey *walk.LineEdit var gatewayId,devEUI,devAddr,appKey,appSKey,nwkSKey *walk.LineEdit
var fPort,fCnt,freq *walk.NumberEdit var fPort,fCnt,freq *walk.NumberEdit
var msg *walk.TextEdit
var acceptPB, cancelPB *walk.PushButton var acceptPB, cancelPB *walk.PushButton
_ = Dialog{ _ = Dialog{
Title: "DTU配置", Title: "DTU配置",
@ -233,18 +286,16 @@ func (mw *DTUMainWindow) DTUConfig() {
LineEdit{AssignTo:&devAddr}, LineEdit{AssignTo:&devAddr},
Label{Text:"应用秘钥:"}, Label{Text:"应用秘钥:"},
LineEdit{AssignTo:&appKey,Enabled:false}, LineEdit{AssignTo:&appKey,Enabled:false},
Label{Text:"应用会话秘钥:"},
LineEdit{AssignTo:&appSKey},
Label{Text:"网络会话秘钥:"}, Label{Text:"网络会话秘钥:"},
LineEdit{AssignTo:&nwkSKey}, LineEdit{AssignTo:&nwkSKey},
Label{Text:"应用会话秘钥:"},
LineEdit{AssignTo:&appSKey},
Label{Text:"端口:"}, Label{Text:"端口:"},
NumberEdit{AssignTo:&fPort}, NumberEdit{AssignTo:&fPort},
Label{Text:"计数:"}, Label{Text:"计数:"},
NumberEdit{AssignTo:&fCnt}, NumberEdit{AssignTo:&fCnt},
Label{Text:"频率:"}, Label{Text:"频率:"},
NumberEdit{AssignTo:&freq,Decimals:2}, NumberEdit{AssignTo:&freq,Decimals:2},
Label{Text:"消息:"},
TextEdit{AssignTo:&msg},
}, },
}, },
Composite{ Composite{
@ -265,7 +316,6 @@ func (mw *DTUMainWindow) DTUConfig() {
mw.dtuConf.FPort = uint8(fPort.Value()) mw.dtuConf.FPort = uint8(fPort.Value())
mw.dtuConf.FCnt = uint32(fCnt.Value()) mw.dtuConf.FCnt = uint32(fCnt.Value())
mw.dtuConf.Freq = freq.Value() mw.dtuConf.Freq = freq.Value()
mw.dtuConf.msg = []byte(msg.Text())
var confData bytes.Buffer var confData bytes.Buffer
d,_ := json.Marshal(&mw.dtuConf) d,_ := json.Marshal(&mw.dtuConf)
@ -283,15 +333,7 @@ func (mw *DTUMainWindow) DTUConfig() {
}, },
}, },
}.Create(mw) }.Create(mw)
data, err := ioutil.ReadFile(mw.dtuConfFileName)
if err == nil {
err = json.Unmarshal(data, &mw.dtuConf)
if err != nil {
msg := "配置文件格式错误:" + err.Error()
walk.MsgBox(mw, "错误", msg, walk.MsgBoxIconError)
return
}
}
otaa.SetChecked(mw.dtuConf.OTAA) otaa.SetChecked(mw.dtuConf.OTAA)
_ = gatewayId.SetText(mw.dtuConf.GatewayId) _ = gatewayId.SetText(mw.dtuConf.GatewayId)
_ = devEUI.SetText(mw.dtuConf.DevEui) _ = devEUI.SetText(mw.dtuConf.DevEui)
@ -302,10 +344,14 @@ func (mw *DTUMainWindow) DTUConfig() {
_ = fPort.SetValue(float64(mw.dtuConf.FPort)) _ = fPort.SetValue(float64(mw.dtuConf.FPort))
_ = fCnt.SetValue(float64(mw.dtuConf.FCnt)) _ = fCnt.SetValue(float64(mw.dtuConf.FCnt))
_ = freq.SetValue(mw.dtuConf.Freq) _ = freq.SetValue(mw.dtuConf.Freq)
_ = msg.SetText(string(mw.dtuConf.msg))
dlg.Run() dlg.Run()
} }
func (mw *DTUMainWindow) Clean() {
mw.model.Items = []*DTU{}
mw.model.PublishRowsReset()
_ = mw.tv.SetSelectedIndexes([]int{})
}
func (mw *DTUMainWindow) SSL() { func (mw *DTUMainWindow) SSL() {
if mw.ssl.Checked() { if mw.ssl.Checked() {
mw.caConf.SetEnabled(true) mw.caConf.SetEnabled(true)
@ -399,22 +445,6 @@ func (mw *DTUMainWindow) ConnectConfig() {
dlg.Run() dlg.Run()
} }
func (mw *DTUMainWindow) BuildUpData() (*packets.PushDataPacket,*lorawan.PHYPayload,error) {
var fCtrl lorawan.FCtrl
_ = fCtrl.UnmarshalBinary([]byte{128})
return BuildUpData(mw.dtuConf.GatewayId,mw.dtuConf.DevAddr,mw.dtuConf.NwkSKey,
mw.dtuConf.FCnt,mw.dtuConf.FPort,5,2,mw.dtuConf.Freq,7,
lorawan.UnconfirmedDataUp,fCtrl,-51,mw.dtuConf.msg)
}
func (mw *DTUMainWindow) BuildJoin() (*packets.PushDataPacket,*lorawan.PHYPayload,error) {
mw.dtuConf.devNonce = lorawan.DevNonce(rand.Uint32())
appEui := "0807060504030201"
return BuildJoin(mw.dtuConf.GatewayId,appEui,mw.dtuConf.DevEui,mw.dtuConf.AppKey,
5,2,mw.dtuConf.Freq,7,-51, mw.dtuConf.devNonce)
}
func (mw *DTUMainWindow) HandleData(client paho.Client, message paho.Message){ func (mw *DTUMainWindow) HandleData(client paho.Client, message paho.Message){
var downlinkFrame gw.DownlinkFrame var downlinkFrame gw.DownlinkFrame
err := proto.Unmarshal(message.Payload(),&downlinkFrame) err := proto.Unmarshal(message.Payload(),&downlinkFrame)
@ -492,12 +522,16 @@ func (mw *DTUMainWindow) HandleJoinAccept(phy *lorawan.PHYPayload){
} }
func (mw *DTUMainWindow) HandleDataDown(phy *lorawan.PHYPayload){ func (mw *DTUMainWindow) HandleDataDown(phy *lorawan.PHYPayload){
key := mw.dtuConf.NwkSKey mpl := phy.MACPayload.(*lorawan.MACPayload)
key := mw.dtuConf.AppSKey
if mpl.FPort != nil && *mpl.FPort == 0 {
key = mw.dtuConf.NwkSKey
}
var aseKey lorawan.AES128Key var aseKey lorawan.AES128Key
_ = aseKey.UnmarshalText([]byte(key)) _ = aseKey.UnmarshalText([]byte(key))
err := phy.DecryptFRMPayload(aseKey) err := phy.DecryptFRMPayload(aseKey)
if err == nil { if err == nil {
mpl := phy.MACPayload.(*lorawan.MACPayload)
mw.dtuConf.DevAddr = mpl.FHDR.DevAddr.String() mw.dtuConf.DevAddr = mpl.FHDR.DevAddr.String()
} }
} }
@ -515,10 +549,17 @@ func (mw *DTUMainWindow) PushData(gatewayEUI string,event string, msg proto.Mess
fmt.Println("mqtt message error") fmt.Println("mqtt message error")
} }
} }
func (mw *DTUMainWindow) sendMsg() error{
func (mw *DTUMainWindow) Send(){ if mw.mqttClient == nil || !mw.mqttClient.IsConnected() {
msg := "请先连接服务器"
walk.MsgBox(mw, "错误", msg, walk.MsgBoxIconError)
return errors.New("请先连接服务器")
}
if mw.dtuConf.OTAA && mw.dtuConf.DevAddr == ""{ if mw.dtuConf.OTAA && mw.dtuConf.DevAddr == ""{
packet,phy,_ := mw.BuildJoin() mw.dtuConf.devNonce = lorawan.DevNonce(rand.Uint32())
appEui := "0807060504030201"
packet,phy,_ := BuildJoin(mw.dtuConf.GatewayId,appEui,mw.dtuConf.DevEui,mw.dtuConf.AppKey,
5,2,mw.dtuConf.Freq,7,-51, mw.dtuConf.devNonce)
var origData bytes.Buffer var origData bytes.Buffer
jsonData,_ := phy.MarshalJSON() jsonData,_ := phy.MarshalJSON()
_ = json.Indent(&origData, jsonData, "", " ") _ = json.Indent(&origData, jsonData, "", " ")
@ -549,10 +590,31 @@ func (mw *DTUMainWindow) Send(){
fmt.Println("join ok") fmt.Println("join ok")
}else{ }else{
fmt.Println("join failed") fmt.Println("join failed")
return return errors.New("join failed")
}
}
var bmsg []byte
var err error
if mw.ascii.Checked() {
bmsg = []byte(mw.msg.Text())
}else{
bmsg,err = hex.DecodeString(mw.msg.Text())
if err != nil {
msg := "hex数据格式错误:" + err.Error()
walk.MsgBox(mw, "错误", msg, walk.MsgBoxIconError)
return err
} }
} }
packet,phy,_ := mw.BuildUpData() var fCtrl lorawan.FCtrl
_ = fCtrl.UnmarshalBinary([]byte{128})
key := mw.dtuConf.NwkSKey
if mw.dtuConf.FPort == 0 {
key = mw.dtuConf.NwkSKey
}
packet,phy,_ := BuildUpData(mw.dtuConf.GatewayId,mw.dtuConf.DevAddr,key,
mw.dtuConf.FCnt,mw.dtuConf.FPort,5,2,mw.dtuConf.Freq,7,
lorawan.UnconfirmedDataUp,fCtrl,-51,bmsg)
var origData bytes.Buffer var origData bytes.Buffer
jsonData,_ := phy.MarshalJSON() jsonData,_ := phy.MarshalJSON()
_ = json.Indent(&origData, jsonData, "", " ") _ = json.Indent(&origData, jsonData, "", " ")
@ -568,8 +630,8 @@ func (mw *DTUMainWindow) Send(){
Frequency:packet.Payload.RXPK[0].Freq, Frequency:packet.Payload.RXPK[0].Freq,
FCnt:mw.dtuConf.FCnt, FCnt:mw.dtuConf.FCnt,
FPort:mw.dtuConf.FPort, FPort:mw.dtuConf.FPort,
HexData:hex.EncodeToString(mw.dtuConf.msg), HexData:hex.EncodeToString(bmsg),
AsciiData:BytesToString(mw.dtuConf.msg), AsciiData:BytesToString(bmsg),
Time:time.Now().Format("2006-01-02 15:04:05"), Time:time.Now().Format("2006-01-02 15:04:05"),
OrigData:origData.String(), OrigData:origData.String(),
} }
@ -585,12 +647,58 @@ func (mw *DTUMainWindow) Send(){
d,_ := json.Marshal(&mw.dtuConf) d,_ := json.Marshal(&mw.dtuConf)
_ = json.Indent(&confData, d, "", "\t") _ = json.Indent(&confData, d, "", "\t")
_ = ioutil.WriteFile(mw.dtuConfFileName,confData.Bytes(),0644) _ = ioutil.WriteFile(mw.dtuConfFileName,confData.Bytes(),0644)
return nil
} }
func (mw *DTUMainWindow) SendMsg() {
go mw.sendMsg()
}
func (mw *DTUMainWindow) SetSend() {
if mw.timeSend.Checked() {
mw.ascii.SetEnabled(false)
mw.noAscii.SetEnabled(false)
mw.sendInterval.SetEnabled(false)
mw.msg.SetEnabled(false)
mw.send.SetEnabled(false)
}else{
mw.ascii.SetEnabled(true)
mw.noAscii.SetEnabled(true)
mw.sendInterval.SetEnabled(true)
mw.msg.SetEnabled(true)
mw.send.SetEnabled(true)
}
}
func (mw *DTUMainWindow) TimeSend() {
if mw.sendInterval.Value() <= 0 {
msg := "时间间隔需大于0"
walk.MsgBox(mw, "错误", msg, walk.MsgBoxIconError)
return
}
mw.SetSend()
go func() {
for{
if !mw.timeSend.Checked() {
break
}
if err := mw.sendMsg();err != nil {
mw.timeSend.SetChecked(false)
mw.SetSend()
break
}
time.Sleep(time.Duration(mw.sendInterval.Value()) * time.Millisecond)
}
}()
}
func (mw *DTUMainWindow) tvItemActivated() { func (mw *DTUMainWindow) tvItemActivated() {
msg := "" msg := ""
for _, i := range mw.tv.SelectedIndexes() { for _, i := range mw.tv.SelectedIndexes() {
msg += mw.model.Items[i].OrigData + "\n" msg += mw.model.Items[i].OrigData + "\n"
} }
walk.MsgBox(mw, "原始数据", msg, walk.MsgBoxOK) _ = mw.data.SetText(strings.Replace(msg, "\n", "\r\n", -1))
m := make(map[string]interface{})
if err := json.Unmarshal([]byte(msg), &m); err == nil {
_ = mw.jsonView.SetModel(NewJSONModel(m))
}
} }

111
jsonModel.go

@ -0,0 +1,111 @@
package main
import (
"fmt"
"github.com/lxn/walk"
)
func FormatBool(b bool) string {
if b {
return "true"
}
return "false"
}
type Node struct {
name string
value interface{}
parent *Node
children []*Node
}
func NewNode(name string,value interface{},parent *Node) *Node {
return &Node{name: name,value:value,parent: parent}
}
func (d *Node) Text() string {
txt := d.name
dim := " : "
switch d.value.(type) {
case int,int8,int16,int32,int64,uint,uint8,uint16,uint32,uint64:
txt += dim + fmt.Sprintf("%d",d.value)
case float32,float64:
txt += dim + fmt.Sprintf("%f",d.value)
case string:
txt += dim + "\"" + d.value.(string) + "\""
case bool:
txt += dim + FormatBool(d.value.(bool))
case map[string]interface{}:
txt += "(Object)"
case []interface{}:
txt += "(Array)"
}
return txt
}
func (d *Node) Parent() walk.TreeItem {
if d.parent == nil {
return nil
}
return d.parent
}
func (d *Node) ChildCount() int {
if d.children == nil {
d.ResetChildren();
}
return len(d.children)
}
func (d *Node) ChildAt(index int) walk.TreeItem {
return d.children[index]
}
func (d *Node) Image() interface{} {
icon,err := walk.NewIconFromResourceId(3)
if err != nil {
return ""
}
return icon
}
func (d *Node) ResetChildren() {
d.children = nil
switch d.value.(type) {
case map[string]interface{}:
m := d.value.(map[string]interface{})
for k,v := range m {
d.children = append(d.children, NewNode(k, v,d))
}
case []interface{}:
v := d.value.([]interface{})
for i,_ := range v {
d.children = append(d.children, NewNode(fmt.Sprintf("%d",i), v[i],d))
}
}
}
type JSONModel struct {
walk.TreeModelBase
roots []*Node
}
func NewJSONModel(m map[string]interface{}) *JSONModel {
model := new(JSONModel)
for k,v := range m {
model.roots = append(model.roots, NewNode(k, v,nil))
}
return model
}
func (*JSONModel) LazyPopulation() bool {
return true
}
func (m *JSONModel) RootCount() int {
return len(m.roots)
}
func (m *JSONModel) RootAt(index int) walk.TreeItem {
return m.roots[index]
}

1
model.go

@ -27,7 +27,6 @@ type DTUConfig struct {
FPort uint8 `json:"fPort"` FPort uint8 `json:"fPort"`
FCnt uint32 `json:"fCnt"` FCnt uint32 `json:"fCnt"`
Freq float64 `json:"freq"` Freq float64 `json:"freq"`
msg []byte
devNonce lorawan.DevNonce devNonce lorawan.DevNonce
} }

16
utils.go

@ -53,7 +53,7 @@ func newTLSConfig(cafile, certFile, certKeyFile string) (*tls.Config, error) {
return tlsConfig, nil return tlsConfig, nil
} }
func BuildUpData(gatewayId,devAddr,nwkSKey string, func BuildUpData(gatewayId,devAddr, key string,
fCnt uint32,fPort,dr,ch uint8,freq,lsnr float64, fCnt uint32,fPort,dr,ch uint8,freq,lsnr float64,
mType lorawan.MType,fCtrl lorawan.FCtrl,rssi int16, mType lorawan.MType,fCtrl lorawan.FCtrl,rssi int16,
userData []byte) (*packets.PushDataPacket,*lorawan.PHYPayload,error) { userData []byte) (*packets.PushDataPacket,*lorawan.PHYPayload,error) {
@ -81,15 +81,15 @@ func BuildUpData(gatewayId,devAddr,nwkSKey string,
} }
mac.FRMPayload = []lorawan.Payload{&dataPayload} mac.FRMPayload = []lorawan.Payload{&dataPayload}
phy.MACPayload = &mac phy.MACPayload = &mac
var aseKey lorawan.AES128Key var aesKey lorawan.AES128Key
if err := aseKey.UnmarshalText([]byte(nwkSKey));err != nil { if err := aesKey.UnmarshalText([]byte(key));err != nil {
return nil,nil,err return nil,nil,err
} }
if err := phy.EncryptFRMPayload(aseKey);err != nil { if err := phy.EncryptFRMPayload(aesKey);err != nil {
return nil,nil,err return nil,nil,err
} }
sf :=[...]string{"SF12","SF11","SF10","SF9","SF8","SF7"} sf :=[...]string{"SF12","SF11","SF10","SF9","SF8","SF7"}
if err := phy.SetUplinkDataMIC(lorawan.LoRaWAN1_0, 0, dr, ch, aseKey, aseKey);err != nil { if err := phy.SetUplinkDataMIC(lorawan.LoRaWAN1_0, 0, dr, ch, aesKey, aesKey);err != nil {
return nil,nil,err return nil,nil,err
} }
data,err := phy.MarshalBinary(); data,err := phy.MarshalBinary();
@ -154,12 +154,12 @@ func BuildJoin(gatewayId,appEui,devEui,appKey string,dr,ch uint8,
JoinEUI: joinEUI, JoinEUI: joinEUI,
DevNonce: devNonce, DevNonce: devNonce,
} }
var aseKey lorawan.AES128Key var aesKey lorawan.AES128Key
if err := aseKey.UnmarshalText([]byte(appKey));err != nil{ if err := aesKey.UnmarshalText([]byte(appKey));err != nil{
return nil,nil,err return nil,nil,err
} }
sf :=[...]string{"SF12","SF11","SF10","SF9","SF8","SF7"} sf :=[...]string{"SF12","SF11","SF10","SF9","SF8","SF7"}
if err := phy.SetUplinkJoinMIC(aseKey);err != nil{ if err := phy.SetUplinkJoinMIC(aesKey);err != nil{
return nil,nil,err return nil,nil,err
} }
data,err := phy.MarshalBinary() data,err := phy.MarshalBinary()

Loading…
Cancel
Save