package main import ( "bytes" "crypto/tls" "crypto/x509" "encoding/json" "fmt" mqtt "github.com/eclipse/paho.mqtt.golang" "github.com/lxn/walk" . "github.com/lxn/walk/declarative" "github.com/lxn/win" "go.bug.st/serial.v1" "io/ioutil" "log" "os" "sort" "strconv" "strings" "time" ) type DeviceInfo struct { ID int64 `json:"id"` DevEui string `json:"devEUI"` DevAddr string `json:"devAddr"` Username string `json:"username"` CreateAt string `json:"createAt"` } type Config struct { MQTT struct { Host string `json:"host"` Port int `json:"port"` Username string `json:"username"` Password string `json:"password"` SSL bool `json:"ssl"` CACert string `json:"ca_cert"` TLSCert string `json:"tls_cert"` TLSKey string `json:"tls_key"` } } type CSModel struct { walk.TableModelBase walk.SorterBase sortColumn int sortOrder walk.SortOrder items []*DeviceInfo } func (m *CSModel) RowCount() int { return len(m.items) } func (m *CSModel) Value(row, col int) interface{} { item := m.items[row] switch col { case 0: return item.ID case 1: return item.DevEui case 2: return item.DevAddr case 3: return item.Username case 4: return item.CreateAt } panic("unexpected col") } func (m *CSModel) Sort(col int, order walk.SortOrder) error { m.sortColumn, m.sortOrder = col, order sort.Stable(m) return m.SorterBase.Sort(col, order) } func (m *CSModel) Len() int { return len(m.items) } func (m *CSModel) Less(i, j int) bool { a, b := m.items[i], m.items[j] c := func(ls bool) bool { if m.sortOrder == walk.SortAscending { return ls } return !ls } switch m.sortColumn { case 0: return c(a.ID < b.ID) case 1: return c(a.DevEui < b.DevEui) case 2: return c(a.DevAddr < b.DevAddr) case 3: return c(a.Username < b.Username) default: return false } } func (m *CSModel) Swap(i, j int) { m.items[i], m.items[j] = m.items[j], m.items[i] } func NewCSModel() *CSModel { return new(CSModel) } type CSMainWindow struct { *walk.MainWindow mqttClient mqtt.Client serialPort serial.Port model *CSModel tv *walk.TableView host,username,password*walk.LineEdit port *walk.NumberEdit connect, disconnect,caConf,send,open,close *walk.PushButton ssl *walk.CheckBox msg,recvData,sendData *walk.TextEdit commPort,baudRate,dataBits,stopBits,parity *walk.ComboBox confFileName string conf Config icon *walk.Icon recvDatas,sendDatas string ch chan string serialOpen bool } func main() { mw := &CSMainWindow{model: NewCSModel(),ch:make(chan string,100),serialOpen:false} mw.icon,_ = walk.NewIconFromResourceId(3) maxWidth := int(win.GetSystemMetrics(win.SM_CXSCREEN)) - 200 maxHeight := int(win.GetSystemMetrics(win.SM_CYSCREEN)) - 100 err := MainWindow{ AssignTo: &mw.MainWindow, Title: "DeviceIDCS", Icon: mw.icon, Size: Size{maxWidth,maxHeight }, Layout: VBox{}, Children: []Widget{ Composite{ Layout: HBox{MarginsZero: true}, Children: []Widget{ Label{Text:"主机/IP:"}, LineEdit{AssignTo:&mw.host}, Label{Text:"端口:"}, NumberEdit{AssignTo:&mw.port,MinSize:Size{Width:50}}, Label{Text:"用户名:"}, LineEdit{AssignTo:&mw.username}, Label{Text:"密码:"}, LineEdit{AssignTo:&mw.password,PasswordMode:true}, PushButton{Text:"连接", AssignTo: &mw.connect,OnClicked:mw.Connect}, PushButton{Text:"断开连接", AssignTo:&mw.disconnect, Enabled:false, OnClicked: mw.Disconnect}, CheckBox{Text:"开启SSL/TLS",AssignTo:&mw.ssl,OnClicked:mw.SSL}, PushButton{Text:"证书配置",Enabled:false,AssignTo:&mw.caConf,OnClicked: mw.Config}, PushButton{Text:"清空数据", OnClicked: mw.Clean}, }, }, Composite{ Layout: HBox{}, Children: []Widget{ TableView{ AssignTo: &mw.tv, MinSize:Size{Width:maxWidth*3/5}, CheckBoxes: true, ColumnsOrderable: true, MultiSelection: true, Columns: []TableViewColumn{ {Title: "序号"}, {Title: "终端EUI"}, {Title: "终端Addr"}, {Title: "用户名"}, {Title: "创建时间"}, }, Model: mw.model, }, Composite{ Layout:VBox{}, Children:[]Widget{ GroupBox{ Title:"串口设置", Layout:VBox{SpacingZero:true}, Children:[]Widget{ Composite{ Layout: Grid{Columns:4}, Children: []Widget{ Label{Text:"端口号:"}, ComboBox{AssignTo:&mw.commPort}, Label{Text:"波特率:"}, ComboBox{AssignTo:&mw.baudRate,Model:[]string{"115200","57600","56000","38400","19200","14400","9600"}}, Label{Text:"数据位:"}, ComboBox{AssignTo:&mw.dataBits,Model:[]string{"8","7","6","5"}}, Label{Text:"停止位:"}, ComboBox{AssignTo:&mw.stopBits,Model:[]string{"1","1.5","2"}}, Label{Text:"奇偶校验:"}, ComboBox{AssignTo:&mw.parity,Model:[]string{"None","Odd","Even","Mark","Space"}}, HSpacer{}, HSpacer{}, HSpacer{}, HSpacer{}, PushButton{Text:"打开串口", AssignTo: &mw.open,OnClicked:mw.Open}, PushButton{Text:"关闭串口", AssignTo: &mw.close,Enabled:false,OnClicked:mw.Close1}, }, }, }, }, GroupBox{ Title:"串口发送", Layout:VBox{SpacingZero:true}, Children:[]Widget{ 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.recvData,ReadOnly:true,HScroll:true,VScroll:true}, TextEdit{AssignTo:&mw.sendData,ReadOnly:true,HScroll:true,VScroll:true}, }, }, }, }.Create() if err != nil { panic("DeviceIDCS窗口创建失败") } _ = mw.port.SetValue(1883) _ = mw.baudRate.SetCurrentIndex(0) _ = mw.dataBits.SetCurrentIndex(0) _ = mw.stopBits.SetCurrentIndex(0) _ = mw.parity.SetCurrentIndex(0) go func(mw *CSMainWindow) { for{ ports, _ := serial.GetPortsList() if (len(ports) > 0 && mw.commPort.Model() == nil) || (mw.commPort.Model() != nil && len(ports) != len(mw.commPort.Model().([]string))) { _ = mw.commPort.SetModel(ports) } time.Sleep(time.Second) } }(mw) dir,_ := os.Getwd() mw.confFileName = dir + "/config.json" data, err := ioutil.ReadFile(mw.confFileName) if err == nil { err = json.Unmarshal(data, &mw.conf) if err != nil { msg := "配置文件格式错误:" + err.Error() walk.MsgBox(mw, "错误", msg, walk.MsgBoxIconError) } else { _ = mw.host.SetText(mw.conf.MQTT.Host) _ = mw.port.SetValue(float64(mw.conf.MQTT.Port)) _ = mw.username.SetText(mw.conf.MQTT.Username) _ = mw.password.SetText(mw.conf.MQTT.Password) mw.ssl.SetChecked(mw.conf.MQTT.SSL) } } mw.Run() } func newTLSConfig(cafile, certFile, certKeyFile string) (*tls.Config, error) { if cafile == "" && certFile == "" && certKeyFile == "" { return nil, nil } tlsConfig := &tls.Config{} // Import trusted certificates from CAfile.pem. if cafile != "" { cacert, err := ioutil.ReadFile(cafile) if err != nil { return nil, err } certpool := x509.NewCertPool() certpool.AppendCertsFromPEM(cacert) tlsConfig.RootCAs = certpool // RootCAs = certs used to verify server cert. } // Import certificate and the key if certFile != "" && certKeyFile != "" { kp, err := tls.LoadX509KeyPair(certFile, certKeyFile) if err != nil { return nil, err } tlsConfig.Certificates = []tls.Certificate{kp} } return tlsConfig, nil } func (mw *CSMainWindow) Connect() { if !mw.serialOpen { msg := "串口未打开,请先打开串口" walk.MsgBox(mw, "错误", msg, walk.MsgBoxIconError) return } go func() { if mw.host.Text() != "" && mw.port.Value() > 0 { opts := mqtt.NewClientOptions() serverAddr := fmt.Sprintf("%s:%d",mw.host.Text(),int(mw.port.Value())) if mw.ssl.Checked() { if mw.conf.MQTT.CACert != "" { serverAddr = fmt.Sprintf("ssl://%s:%d",mw.host.Text(),int(mw.port.Value())) tlsconfig, err := newTLSConfig(mw.conf.MQTT.CACert, mw.conf.MQTT.TLSCert, mw.conf.MQTT.TLSKey) if err != nil { msg := "证书加载错误:" + err.Error() walk.MsgBox(mw, "错误", msg, walk.MsgBoxIconError) } if tlsconfig != nil { opts.SetTLSConfig(tlsconfig) } }else{ msg := "证书未配置,服务端单向认证只需配置CA证书,双向认证还需配置客户端证书及秘钥" walk.MsgBox(mw, "错误", msg, walk.MsgBoxIconError) return } } opts.AddBroker(serverAddr) opts.SetUsername(mw.username.Text()) opts.SetPassword(mw.password.Text()) opts.SetConnectTimeout(5 * time.Second) mw.mqttClient = mqtt.NewClient(opts) token := mw.mqttClient.Connect() if token.Wait() && token.Error() == nil { mw.conf.MQTT.Host = mw.host.Text() mw.conf.MQTT.Port = int(mw.port.Value()) mw.conf.MQTT.Username = mw.username.Text() mw.conf.MQTT.Password = mw.password.Text() var confData bytes.Buffer d,_ := json.Marshal(&mw.conf) _ = json.Indent(&confData, d, "", "\t") _ = ioutil.WriteFile(mw.confFileName,confData.Bytes(),0644) mw.host.SetEnabled(false) mw.port.SetEnabled(false) mw.username.SetEnabled(false) mw.password.SetEnabled(false) mw.disconnect.SetEnabled(true) mw.connect.SetEnabled(false) mw.ssl.SetEnabled(false) mw.caConf.SetEnabled(false) mw.send.SetEnabled(false) mw.msg.SetEnabled(false) topic := "device/id" token := mw.mqttClient.Subscribe(topic,0, func(client mqtt.Client, message mqtt.Message) { var dev DeviceInfo _ = json.Unmarshal(message.Payload(), &dev) const TIME_LAYOUT = "2006-01-02 15:04:05" if lt, err := time.ParseInLocation(time.RFC3339Nano, dev.CreateAt, time.UTC);err == nil { dev.CreateAt = lt.Local().Format(TIME_LAYOUT) } cmds := []string{"AT+DEUI=" + dev.DevEui + "\r\n","AT+DADDR=" + dev.DevAddr + "\r\n"} for _,cmd := range cmds{ _, err := mw.serialPort.Write([]byte(cmd)) if err != nil { mw.sendDatas += err.Error() + "\r\n" _ = mw.sendData.SetText(mw.sendDatas) continue } mw.sendDatas += cmd _ = mw.sendData.SetText(mw.sendDatas) mw.ch <- cmd } mw.model.items = append(mw.model.items, &dev) mw.model.PublishRowsReset() _ = mw.tv.SetSelectedIndexes([]int{}) }) if token.Wait() && token.Error() != nil { msg := "订阅失败:" + token.Error().Error() walk.MsgBox(mw, "错误", msg, walk.MsgBoxIconError) } }else{ msg := "连接失败:" + token.Error().Error() walk.MsgBox(mw, "错误", msg, walk.MsgBoxIconError) } }else{ msg := "主机/IP不能为空,端口不能为0" walk.MsgBox(mw, "错误", msg, walk.MsgBoxIconError) } }() } func (mw *CSMainWindow) Disconnect() { mw.connect.SetEnabled(true) mw.host.SetEnabled(true) mw.port.SetEnabled(true) mw.username.SetEnabled(true) mw.password.SetEnabled(true) mw.disconnect.SetEnabled(false) mw.ssl.SetEnabled(true) mw.caConf.SetEnabled(true) mw.mqttClient.Disconnect(0) mw.send.SetEnabled(true) mw.msg.SetEnabled(true) } func (mw *CSMainWindow) Clean() { mw.model.items = []*DeviceInfo{} mw.model.PublishRowsReset() _ = mw.tv.SetSelectedIndexes([]int{}) } func (mw *CSMainWindow) SSL() { mw.conf.MQTT.SSL = mw.ssl.Checked() if mw.ssl.Checked() { mw.caConf.SetEnabled(true) }else{ mw.caConf.SetEnabled(false) } } func (mw *CSMainWindow) Config() { var dlg *walk.Dialog var caCert,tlsCert,tlsKey *walk.LineEdit var acceptPB, cancelPB *walk.PushButton _ = Dialog{ Title: "配置", Layout: VBox{}, Icon: mw.icon, AssignTo: &dlg, DefaultButton: &acceptPB, CancelButton: &cancelPB, MinSize: Size{400, 200}, Children: []Widget{ Composite{ Layout:Grid{Columns: 3}, Children:[]Widget{ Label{Text:"CA证书:"}, LineEdit{Text:Bind("CACert"),AssignTo:&caCert}, PushButton{Text:"打开",OnClicked: func() { dlg := new(walk.FileDialog) dlg.Title = "请选择CA证书" dlg.Filter = "CA证书 (*.crt)|*.crt|所有文件 (*.*)|*.*" if ok, err := dlg.ShowOpen(mw); err != nil { return } else if !ok { return } _ = caCert.SetText(dlg.FilePath) }}, Label{Text:"客户端证书:"}, LineEdit{Text:Bind("TLSCert"),AssignTo:&tlsCert}, PushButton{Text:"打开",OnClicked: func() { dlg := new(walk.FileDialog) dlg.Title = "请选择客户端证书" dlg.Filter = "客户端证书 (*.crt)|*.crt|所有文件 (*.*)|*.*" if ok, err := dlg.ShowOpen(mw); err != nil { return } else if !ok { return } _ = tlsCert.SetText(dlg.FilePath) }}, Label{Text:"客户端证书秘钥:"}, LineEdit{Text:Bind("TLSKey"),AssignTo:&tlsKey}, PushButton{Text:"打开",OnClicked: func() { dlg := new(walk.FileDialog) dlg.Title = "请选择客户端证书秘钥" dlg.Filter = "客户端证书秘钥 (*.key)|*.key|所有文件 (*.*)|*.*" if ok, err := dlg.ShowOpen(mw); err != nil { return } else if !ok { return } _ = tlsKey.SetText(dlg.FilePath) }}, }, }, Composite{ Layout: HBox{}, Children: []Widget{ HSpacer{}, PushButton{ AssignTo: &acceptPB, Text: "确定", OnClicked: func() { mw.conf.MQTT.CACert = caCert.Text() mw.conf.MQTT.TLSCert = tlsCert.Text() mw.conf.MQTT.TLSKey = tlsKey.Text() dlg.Accept() }, }, PushButton{ AssignTo: &cancelPB, Text: "取消", OnClicked: func() { dlg.Cancel() }, }, }, }, }, }.Create(mw) _ = caCert.SetText(mw.conf.MQTT.CACert) _ = tlsCert.SetText(mw.conf.MQTT.TLSCert) _ = tlsKey.SetText(mw.conf.MQTT.TLSKey) dlg.Run() } func (mw *CSMainWindow) Open() { baudRate,err := strconv.Atoi(mw.baudRate.Text()) if err != nil { return } dataBits,err := strconv.Atoi(mw.dataBits.Text()) if err != nil { return } mode := &serial.Mode{ BaudRate: baudRate, DataBits: dataBits, StopBits: serial.StopBits(mw.stopBits.CurrentIndex()), Parity: serial.Parity(mw.parity.CurrentIndex()), } mw.serialPort,err = serial.Open(mw.commPort.Text(),mode) if err != nil { msg := "打开串口失败:" + err.Error() walk.MsgBox(mw, "错误", msg, walk.MsgBoxIconError) return } mw.open.SetEnabled(false) mw.close.SetEnabled(true) mw.serialOpen = true go RecvMsg(mw) } func (mw *CSMainWindow) Close1() { mw.open.SetEnabled(true) mw.close.SetEnabled(false) _ = mw.serialPort.Close() } func (mw *CSMainWindow) SendMsg() { if mw.serialPort == nil { msg := "请先打开串口" walk.MsgBox(mw, "错误", msg, walk.MsgBoxIconError) return } msg := mw.msg.Text() + "\r\n" _, err := mw.serialPort.Write([]byte(msg)) if err != nil { msg := "串口写入失败:" + err.Error() walk.MsgBox(mw, "错误", msg, walk.MsgBoxIconError) return } mw.ch <- msg mw.sendDatas += msg _ = mw.sendData.SetText(mw.sendDatas) } func RecvMsg(mw *CSMainWindow) { buff := make([]byte, 100) for { n, err := mw.serialPort.Read(buff) if err != nil { log.Println("读串口错误",err) break } msg := string(buff[:n]) dim := " ---- " cmd := <- mw.ch if strings.Contains(msg,"\r\n") { msg = strings.Trim(msg,"\r\n") } if !strings.Contains(cmd,"\r\n") { cmd += "\r\n" } mw.recvDatas += msg + dim + cmd _ = mw.recvData.SetText(mw.recvDatas) cmd = "" if n == 0 { fmt.Println("\nEOF") break } } }