package lc import ( "bytes" "encoding/binary" "encoding/hex" "errors" "fmt" "io" "sync" "time" "github.com/tarm/serial" "golang.org/x/text/encoding/simplifiedchinese" "golang.org/x/text/transform" ) // Loudspeaker 扬声器接口 type Loudspeaker interface { Speak(txt string) } // IpCast 串口语音播报实现(核心:播放中丢弃请求+仅空闲时接收+无缓存) type IpCast struct { Port string // 串口端口(外部传入) Baud int // 波特率(固定为115200) serialPort *serial.Port // 串口连接实例 mu sync.Mutex // 全局互斥锁(保护isPlaying/串口操作) idleChan chan struct{} // 芯片空闲通知通道 isClosed bool // 串口是否已关闭 isPlaying bool // 标记是否正在播放语音(核心控制字段) } // NewIpCast 初始化串口语音播报实例 // prot: 串口端口(如"/dev/ttyUSB0") func NewIpCast(prot string) *IpCast { s := &IpCast{ Port: prot, Baud: 115200, idleChan: make(chan struct{}, 1), // 缓冲通道避免阻塞 isClosed: false, isPlaying: false, // 初始为未播放状态 } // 初始化串口 if err := s.Reconnect(); err != nil { fmt.Printf("串口初始化失败: %v\n", err) } // 启动后台监听协程 go s.listenSerialResponse() return s } // Reconnect 重连串口(加锁保护+标记串口状态) func (ip *IpCast) Reconnect() error { ip.mu.Lock() defer ip.mu.Unlock() // 标记串口为关闭状态 ip.isClosed = true if ip.serialPort != nil { _ = ip.serialPort.Close() ip.serialPort = nil } // 配置串口参数 cfg := &serial.Config{ Name: ip.Port, Baud: ip.Baud, Size: 8, Parity: serial.ParityNone, StopBits: 1, ReadTimeout: time.Millisecond * 200, // 减少EOF报错频率 } port, err := serial.OpenPort(cfg) if err != nil { return fmt.Errorf("打开串口失败: %w", err) } ip.serialPort = port ip.isClosed = false // 标记串口可用 fmt.Println("串口重连成功") return nil } // clearIdleChan 清空空闲通道所有残留信号(避免旧信号干扰) func (ip *IpCast) clearIdleChan() { for { select { case <-ip.idleChan: default: return } } } // listenSerialResponse 后台监听串口返回(仅处理0x41/0x4F,清空残留信号) func (ip *IpCast) listenSerialResponse() { buf := make([]byte, 64) for { // 串口未就绪时低频轮询 if ip.isClosed || ip.serialPort == nil { time.Sleep(time.Second * 1) continue } n, err := ip.serialPort.Read(buf) // 忽略EOF和读取超时(正常无数据场景) if err != nil { switch { case err == io.EOF: continue case err.Error() == "serial: read timed out": continue default: fmt.Printf("串口读取异常: %v\n", err) ip.mu.Lock() ip.isClosed = true ip.mu.Unlock() } continue } if n == 0 { continue } // 解析返回字节 recvBytes := buf[:n] fmt.Printf("串口返回原始字节(十六进制): %s\n", hex.EncodeToString(recvBytes)) for _, b := range recvBytes { switch b { case 0x41: fmt.Println("<---- 41 接收成功") case 0x4F: fmt.Println("<---- 4F 芯片空闲") // 清空残留信号后发送新的空闲标记 ip.clearIdleChan() ip.idleChan <- struct{}{} } } } } // Speak 发送语音指令(核心逻辑:播放中丢弃请求+仅空闲时接收+无缓存) func (ip *IpCast) Speak(txt string) { // 1. 加锁检查播放状态,核心控制逻辑 ip.mu.Lock() // 若正在播放,直接丢弃当前雷达触发请求 if ip.isPlaying { fmt.Printf("语音正在播放中,丢弃雷达触发请求:%s\n", txt) ip.mu.Unlock() return } // 标记为正在播放(后续只有收到0x4F才会重置) ip.isPlaying = true ip.mu.Unlock() // 2. 函数退出时保证重置播放状态(无论成功/失败/超时) defer func() { ip.mu.Lock() ip.isPlaying = false ip.mu.Unlock() fmt.Println("语音播放流程结束,恢复接收雷达信号") }() // 3. 校验串口状态 ip.mu.Lock() isClosed := ip.isClosed serialPort := ip.serialPort ip.mu.Unlock() if isClosed || serialPort == nil { fmt.Println("串口未连接,尝试重连...") if err := ip.Reconnect(); err != nil { fmt.Printf("串口重连失败,无法发送指令: %v\n", err) return } // 重连后重新获取串口实例 ip.mu.Lock() serialPort = ip.serialPort ip.mu.Unlock() } // 4. 清空空闲通道残留信号(避免旧信号干扰) ip.clearIdleChan() // 5. 文本转GBK编码 GBKBytes, err := convertToGBK(txt) if err != nil { fmt.Printf("文本转GBK失败: %v\n", err) return } // 6. 构造语音帧数据(匹配官方格式) dataAreaLen := uint16(1 + 1 + len(GBKBytes)) // 命令字+编码格式+文本长度 frameBuf := bytes.NewBuffer([]byte{0xFD}) // 帧头FD _ = binary.Write(frameBuf, binary.BigEndian, dataAreaLen) frameBuf.Write([]byte{0x01, 0x01}) // 命令字01 + 编码格式01(GBK) frameBuf.Write(GBKBytes) // 调试打印帧数据 //frameHex := hex.EncodeToString(frameBuf.Bytes()) // 7. 发送帧数据到串口 _, err = serialPort.Write(frameBuf.Bytes()) if err != nil { fmt.Printf("串口发送失败: %v\n", err) ip.mu.Lock() ip.isClosed = true ip.mu.Unlock() _ = ip.Reconnect() return } // 8. 等待芯片空闲(0x4F),仅收到空闲信号才允许下一次播放 select { case <-ip.idleChan: case <-time.After(time.Second * 10): // 超时延长至10秒,适配长语音 } } // CorrectTime 保留原有方法(空实现) func (ip IpCast) CorrectTime() {} // convertToGBK 文本转GBK编码(保留原有逻辑) func convertToGBK(s string) ([]byte, error) { if s == "" { return nil, errors.New("文本不能为空") } encoder := simplifiedchinese.GBK.NewEncoder() reader := transform.NewReader(bytes.NewReader([]byte(s)), encoder) result, err := io.ReadAll(reader) if err != nil { return nil, fmt.Errorf("编码转换失败: %w", err) } fmt.Printf("文本「%s」的GBK编码(十六进制): %s\n", s, hex.EncodeToString(result)) if len(result) == 0 { return nil, errors.New("GBK转换结果为空") } return result, nil } // Close 手动关闭串口(清理资源) func (ip *IpCast) Close() { ip.mu.Lock() defer ip.mu.Unlock() ip.isClosed = true ip.isPlaying = false // 强制重置播放状态 ip.clearIdleChan() // 清空通道 if ip.serialPort != nil { _ = ip.serialPort.Close() ip.serialPort = nil } fmt.Println("串口已手动关闭") }