| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259 |
- 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("串口已手动关闭")
- }
|