package lc import ( "encoding/hex" "fmt" "github.com/sirupsen/logrus" "golang.org/x/text/encoding/simplifiedchinese" "lc-smartX/bx" "net" "strconv" "time" "unicode/utf8" ) // Screener 屏接口 type Screener interface { Display(Color, RunMode, DisplayMode, int, int, string) } type Screen struct { Name string Addr string conn net.Conn liveState bool StateInfo *bx.StateInfo //状态信息 Params *bx.Params //屏参 sendQueue *CommandQueue // 发送队列,用于串行处理Send操作 } // 初始化屏幕时创建并启动发送队列 func NewScreen(name string, ip, port string) *Screen { s := &Screen{ Name: name, Addr: fmt.Sprintf("%s:%s", ip, port), StateInfo: &bx.StateInfo{}, Params: &bx.Params{}, sendQueue: NewCommandQueue(100), // 缓冲区大小100,可调整 } // 启动队列,传入实际发送处理函数 s.sendQueue.Start(s.handleSend) s.Reconnect() return s } // 实际执行发送的处理函数(由队列goroutine调用) func (s *Screen) handleSend(data []byte) { if !s.getLiveState() || s.conn == nil { return } _, err := s.conn.Write(data) if err != nil { logrus.WithFields(map[string]interface{}{"设备名": s.Name}).Error("tcp write error:", err) s.liveState = false } //resp := s.ReadResp() //if !resp.IsAck() { // logrus.Error("设备拒绝写文件! error:", resp.Error().Description) // return //} } // 修改Send方法,将数据添加到队列而非直接发送 func (s *Screen) Send(data []byte) { if !s.getLiveState() { return } // 将发送任务加入队列,由队列串行处理 s.sendQueue.AddTask(data) } // 添加Close方法,用于释放队列资源 func (s *Screen) Close() { if s.sendQueue != nil { s.sendQueue.Stop() } if s.conn != nil { s.conn.Close() } } func (s *Screen) Display(color Color, runMode RunMode, displayMode DisplayMode, isProgram, speed int, text string) { if !s.getLiveState() { return } s.SendRam(color, runMode, displayMode, isProgram, speed, text) } // Correct 校正时间 func (s *Screen) Correct() { if !s.getLiveState() { return } now := time.Now() cmd := bx.NewBxCmdSystemClockCorrect(now) data := bx.NewBxDataPackCmd(cmd) s.Send(data.Pack()) } // Reconnect方法中添加连接关闭时的队列处理(可选增强) func (s *Screen) Reconnect() { if s.getLiveState() { return } // 关闭旧连接(如果存在) if s.conn != nil { s.conn.Close() } conn, err := net.DialTimeout("tcp", s.Addr, 5*time.Second) if err != nil { logrus.Error(s.Name, "-", s.Addr, "[屏]重连接失败! error:", err) return } logrus.Info(s.Name, "-", s.Addr, "[屏]连接成功!") s.setConn(conn) //读取屏信息 state := s.State() s.StateInfo.Parse(state.Data) params := s.Param() s.Params.Parse(params.Data) } func (s *Screen) getLiveState() bool { return s.liveState } func (s *Screen) setConn(conn net.Conn) { s.conn = conn s.liveState = true } //// 给屏发送数据 //func (s *Screen) Send(data []byte) { // if !s.getLiveState() { // return // } // _, err := s.conn.Write(data) // if err != nil { // logrus.WithFields(map[string]interface{}{"设备名": s.Name}).Error("tcp write error:", err) // s.liveState = false // } //} //以下对协议进行封装 //↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ type Color byte const ( Default Color = iota Red Green Yellow Blue LightBlue LightPurple White ) const ( Normal = 0 Warn = 1 ) // 发送动态区节目 0正常页面 1红色提醒页面 func (s *Screen) SendRam(color Color, runMode RunMode, displayMode DisplayMode, isProgram, speed int, text string) { if isProgram == 0 { file1 := FlashFile{} file1.SetMsg(FormatSpeed(speed), color) file1.SetMode(runMode, displayMode) file1.SetOrigin(0, true, 32) file1.SetArea(128, true, 96) s.TextRam(file1, false) } else if isProgram == 1 { //file1 := FlashFile{} //file1.SetMsg(text, color) //file1.SetMode(runMode, displayMode) //file1.SetOrigin(32, true, 4) //file1.SetArea(96, true, 32) //s.TextFlash([]FlashFile{file1}, "P000", false, false) } else if isProgram == 2 { file1 := FlashFile{} file1.SetMsg(text, color) file1.SetMode(runMode, displayMode) file1.SetOrigin(0, true, 0) file1.SetArea(128, true, 32) s.TextRam(file1, true) } } func FormatSpeed(speed int) string { // 校验输入范围(仅处理0-99的整数) if speed < 0 || speed > 99 { return "00" } // 处理个位数(0-9) if speed < 10 { speedStr := strconv.Itoa(speed) return fmt.Sprintf("0%s", speedStr) } // 处理十位数(10-99) return fmt.Sprintf("%d", speed) } // TextRam 发送动态区节目实现 func (s *Screen) TextRam(ff FlashFile, isNumber bool) { if !s.getLiveState() { return } var areas []bx.BxArea encoder := simplifiedchinese.GB18030.NewEncoder() var bytes []byte var id = 0 var alignment = 0x00 if ff.color == Default && isNumber == false { bytes, _ = encoder.Bytes([]byte(ff.msg)) } else if isNumber == true { province, number := ParsePlateUTF8(ff.msg) logrus.Info("车牌号", ff.msg) logrus.Info("车牌号", "号码", number, "省份", province) bytes, _ = encoder.Bytes([]byte("\\FO000\\C" + strconv.Itoa(int(ff.color)) + province + "\\FE000\\C" + strconv.Itoa(int(Green)) + number)) id = 1 } else if isNumber == false { bytes, _ = encoder.Bytes([]byte("\\FE001\\C" + strconv.Itoa(int(ff.color)) + ff.msg)) alignment = 0x0a } area := bx.NewBxAreaProgram(byte(id), byte(ff.runMode), byte(ff.dispMode), byte(alignment), ff.originX, ff.originY, ff.width, ff.height, bytes, false) areas = append(areas, area) // cmd := bx.NewBxCmdSendDynamicArea(areas) pack := bx.NewBxDataPackCmd(cmd) pack.SetDisplayType(1) //动态显示模式 d := pack.Pack() if isNumber { logrus.Info(hex.EncodeToString(d)) } s.Send(d) resp := s.ReadResp() if !resp.IsAck() { logrus.Error("设备拒绝写文件! error:", resp.Error().Description) return } s.StateInfo.DynaAreaNum++ } // DelRamText 删除动态区,不传删除所有 func (s *Screen) DelRamText(numbers ...byte) { if !s.getLiveState() { return } cmd := bx.NewCmdDelDynamicArea(numbers) pack := bx.NewBxDataPackCmd(cmd) s.Send(pack.Pack()) s.ReadResp() if len(numbers) == 0 { s.StateInfo.DynaAreaNum = 0 } else { s.StateInfo.DynaAreaNum-- } } type RunMode byte const ( Loop RunMode = iota //循环 LoopAndStayAtEnd //循环直到最后,停留在最后一个动态区 LoopAndTimeoutOff //循环直到超时,超时后未更新不在显示 LoopAndStayAtLogo //循环完后,停留显示LOGO LoopAndOff //循环完后不在显示 LoopAndCountOff //循环设定次数后不在显示 DefaultRunMode RunMode = Loop //默认 ) type DisplayMode byte const ( _ DisplayMode = iota Static //0x01——静止显示 QuickPunch //0x02——快速打出 MoveLeft //0x03——向左移动 MoveRight //0x04——向右移动 MoveUp //0x05——向上移动 MoveDown //0x06——向下移动 Flicker //0x07——闪烁 DefaultDisplayMode DisplayMode = Static ) type FlashFile struct { msg string soundData string color Color runMode RunMode dispMode DisplayMode originX uint16 originY uint16 width uint16 height uint16 } func (ft *FlashFile) SetMsg(msg string, color Color) { ft.msg = msg ft.color = color } func (ft *FlashFile) SetMode(runMode RunMode, displayMode DisplayMode) { ft.runMode = runMode ft.dispMode = displayMode } func (ft *FlashFile) SetSoundData(soundData string) { ft.soundData = soundData } // SetOrigin xIsPixel=true表示x坐标为像素单位, =false表示以字节(8像素)为单位;y只有像素单位 func (ft *FlashFile) SetOrigin(x uint16, xIsPixel bool, y uint16) { ft.originY = y if xIsPixel { ft.originX = 0x8000 | x return } ft.originX = x } // SetArea yIsPixel=true表示像素单位, =false表示以字节(8像素)为单位 func (ft *FlashFile) SetArea(w uint16, yIsPixel bool, h uint16) { ft.height = h if yIsPixel { ft.width = 0x8000 | w return } ft.width = w } // TextFlash 发送静态文件节目,掉电保存,文件名格式"P000","P001" func (s *Screen) TextFlash(ft []FlashFile, name string, isNumber, isLogo bool) { if !s.getLiveState() { return } encoder := simplifiedchinese.GB18030.NewEncoder() if isLogo { name = "LOGO" } var areas []bx.BxArea for _, i := range ft { var bytes []byte if i.color == Default && isNumber == false { bytes, _ = encoder.Bytes([]byte(i.msg)) } else if isNumber == true { province, number := ParsePlateUTF8(i.msg) logrus.Info("车牌号", i.msg) logrus.Info("车牌号", "号码", number, "省份", province) bytes, _ = encoder.Bytes([]byte("\\FO000\\C" + strconv.Itoa(int(i.color)) + province + "\\FE001\\C" + strconv.Itoa(int(Green)) + number)) } else if isNumber == false { bytes, _ = encoder.Bytes([]byte("\\FO000\\C" + strconv.Itoa(int(i.color)) + i.msg)) } area := bx.NewBxAreaProgram(0xff, byte(i.runMode), byte(i.dispMode), 0x00, i.originX, i.originY, i.width, i.height, bytes, false) areas = append(areas, area) } file := bx.NewBxFile(name, "", areas) cmd := file.NewCmdWriteFile() pack := bx.NewBxDataPackCmd(cmd) data := pack.Pack() if isNumber { logrus.Info(hex.EncodeToString(data)) } s.Send(data) resp := s.ReadResp() if !resp.IsAck() { logrus.Error("设备拒绝写文件! error:", resp.Error().Description) return } //pack1 := bx.NewBxDataPackCmd(cmd) //data1 := pack1.Pack() //s.Send(data1) //resp1 := s.ReadResp() //if resp1.NoError() { // s.StateInfo.ProgramNum++ //} } func ParsePlateUTF8(rawPlate string) (province, number string) { firstRune, firstLen := utf8.DecodeRuneInString(rawPlate) province = string(firstRune) number = rawPlate[firstLen:] return } // Bitmap 发送自定义位图节目 func (s *Screen) Bitmap(name string, bitmap []byte) { file := bx.NewBitmapFile(name, bitmap) cmd := file.NewCmd() pack := bx.NewBxDataPackCmd(cmd) data := pack.Pack() s.Send(data) resp := s.ReadResp() if !resp.IsAck() { logrus.Error("设备拒绝写文件! error:", resp.Error().Description) return } pack1 := bx.NewBxDataPackCmd(cmd) data1 := pack1.Pack() s.Send(data1) fmt.Printf("写图文件数据:% 02x\n", data1) s.ReadResp() } // Lock 锁定状态:0x00——解锁状态,0x01——锁定状态 func (s *Screen) Lock(flag byte, name string) { cmd := bx.NewCmdLock(flag, name) pack := bx.NewBxDataPackCmd(&cmd) s.Send(pack.Pack()) resp := s.ReadResp() if !resp.IsAck() { logrus.Error("设备拒绝写文件! error:", resp.Error().Description) return } } // Brightness 设置亮度 func (s *Screen) Brightness(brightnessType byte, currentBrightness byte, brightnessValue []byte) { cmd := bx.NewCmdBrightness(brightnessType, currentBrightness, brightnessValue) pack := bx.NewBxDataPackCmd(&cmd) fmt.Println(hex.EncodeToString(pack.Pack())) s.Send(pack.Pack()) resp := s.ReadResp() if !resp.IsAck() { logrus.Error("设备拒绝写文件! error:", resp.Error().Description) return } } // DelFile 删除静态文件节目 func (s *Screen) DelFile(delFiles ...string) { cmd := bx.NewCmdDeleteFile(delFiles) pack := bx.NewBxDataPackCmd(cmd) s.Send(pack.Pack()) s.ReadResp() if len(delFiles) == 0 { s.StateInfo.ProgramNum = 0 } else { s.StateInfo.ProgramNum-- } } // DelText 删除动态区域节目 func (s *Screen) DelText(delIds []byte) { cmd := bx.NewBxCmdSendDynamicArea(nil) cmd.SetDelAreaIds(delIds) pack := bx.NewBxDataPackCmd(cmd) s.Send(pack.Pack()) } func (s *Screen) TurnOnOff(onOff bool) { if !s.getLiveState() { return } cmd := bx.NewBxCmdTurnOnOff(onOff) pack := bx.NewBxDataPackCmd(cmd) s.Send(pack.Pack()) } // TimingSwitch 定时开关屏 // 13:49 开, 13:55 关,最多设置3组 // // onOffSet := [][2]uint64{ // {1349, 1355}, // } func (s *Screen) TimingSwitch(onOffSet [][2]uint64) { if !s.getLiveState() { return } cmd := bx.NewCmdTimingSwitch(onOffSet) pack := bx.NewBxDataPackCmd(cmd) s.Send(pack.Pack()) } func (s *Screen) CancelTimingSwitch() { if !s.getLiveState() { return } cmd := bx.NewCmdCancelTimingSwitch() pack := bx.NewBxDataPackCmd(cmd) s.Send(pack.Pack()) } func (s *Screen) State() *bx.BxResp { if !s.getLiveState() { return nil } cmd := bx.NewCmdState() pack := bx.NewBxDataPackCmd(cmd) s.Send(pack.Pack()) r := s.ReadResp() return &r } func (s *Screen) Param() *bx.BxResp { if !s.getLiveState() { return nil } cmd := bx.NewCmdReadParams() pack := bx.NewBxDataPackCmd(cmd) s.Send(pack.Pack()) r := s.ReadResp() return &r } func (s *Screen) Info() { s.StateInfo.Print(s.Name) } // ReadResp 读取响应 func (s *Screen) ReadResp() bx.BxResp { var resp = make([]byte, 1024) read, err := s.conn.Read(resp) if err != nil { logrus.Error("读数据错误:", err) s.liveState = false return bx.BxResp{} } var bxResp = bx.BxResp{} parse := bxResp.Parse(resp, read) //if parse.IsAck() { // fmt.Println("response ACK") // fmt.Printf("原始响应数据:% 0x\n", resp[:read]) // fmt.Println("解析响应数据:", parse) //} else if parse.IsInfo() { // fmt.Println("state ACK") // fmt.Printf("原始响应数据:% 0x\n", resp[:read]) // fmt.Println("解析响应数据:", parse) //} else { // fmt.Println("response") // fmt.Printf("原始响应数据:% 0x\n", resp[:read]) // fmt.Println("解析响应数据:", parse) //} return *parse }