You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

594 lines
16 KiB

5 years ago
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
}
}
}