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 串口语音播报实现【最终极简版】 // 核心准则:✅ 仅雷达触发Speak播放 ✅ 播放中丢弃重复雷达信号 ✅ 无轮询 ✅ 无等待 ✅ 无超时 ✅ 无通道 type IpCast struct { Port string // 串口端口(外部传入) Baud int // 波特率(固定为115200) serialPort *serial.Port // 串口连接实例 mu sync.Mutex // 全局互斥锁(并发安全修改状态+串口读写) isClosed bool // 串口是否已关闭 isPlaying bool // 唯一播放状态:true=播放中(拒收雷达) false=空闲(可播放) } // NewIpCast 初始化串口语音播报实例 func NewIpCast(prot string) *IpCast { s := &IpCast{ Port: prot, Baud: 115200, isClosed: false, isPlaying: false, } if err := s.Reconnect(); err != nil { fmt.Printf("串口初始化失败: %v\n", err) return s } 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, } port, err := serial.OpenPort(cfg) if err != nil { return fmt.Errorf("打开串口失败: %w", err) } ip.serialPort = port ip.isClosed = false fmt.Println("串口重连成功") return nil } // listenSerialResponse 后台监听串口返回【核心不变,极简】 // 唯一逻辑:收到0x41=日志打印;收到0x4F=加锁改isPlaying=false(释放播放权限) func (ip *IpCast) listenSerialResponse() { buf := make([]byte, 64) for { ip.mu.Lock() isClosed := ip.isClosed serialPort := ip.serialPort ip.mu.Unlock() if isClosed || serialPort == nil { time.Sleep(time.Second * 1) continue } ip.mu.Lock() n, err := serialPort.Read(buf) ip.mu.Unlock() if err != nil { switch { case err == io.EOF, 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] for _, b := range recvBytes { switch b { case 0x41: fmt.Println("<---- 41 指令接收成功,芯片开始播放") case 0x4F: fmt.Println("<---- 4F 语音播放完成 ✅,释放播放权限") // 唯一操作:播放完成,解锁状态,让下次雷达信号可以触发 ip.mu.Lock() ip.isPlaying = false ip.mu.Unlock() } } } } // Speak 发送语音指令 【✅ 核心改版,彻底解决你的问题,极简逻辑】 // 唯一触发源:雷达信号调用 → 播放;无雷达调用 → 绝对不播放 // 播放中重复触发:直接return丢弃,无任何处理 // 发送指令后:立刻退出函数,无等待、无轮询、无超时,芯片独立播放 func (ip *IpCast) Speak(txt string) { // ========== 第一步:核心判断,播放中直接丢弃雷达信号【保留刚需】 ========== ip.mu.Lock() if ip.isPlaying { fmt.Printf("[丢弃雷达信号] 语音播放中,拒绝执行:%s\n", txt) ip.mu.Unlock() return } // 空闲状态,原子标记为播放中,并发安全,杜绝竞态 ip.isPlaying = true ip.mu.Unlock() // ========== 异常兜底:任何执行失败,都重置播放状态 ========== defer func() { ip.mu.Lock() if ip.isClosed { ip.isPlaying = false fmt.Println("[异常] 串口关闭,重置播放状态为空闲") } ip.mu.Unlock() }() // ========== 校验串口状态 ========== 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) ip.mu.Lock() ip.isPlaying = false ip.mu.Unlock() return } ip.mu.Lock() serialPort = ip.serialPort ip.mu.Unlock() } // ========== 文本转GBK编码 ========== GBKBytes, err := convertToGBK(txt) if err != nil { fmt.Printf("文本转GBK失败: %v\n", err) ip.mu.Lock() ip.isPlaying = false ip.mu.Unlock() return } // ========== 构造语音帧数据 ========== dataAreaLen := uint16(1 + 1 + len(GBKBytes)) frameBuf := bytes.NewBuffer([]byte{0xFD}) _ = binary.Write(frameBuf, binary.BigEndian, dataAreaLen) frameBuf.Write([]byte{0x01, 0x01}) frameBuf.Write(GBKBytes) // 保留你需要的500ms延迟 time.Sleep(500 * time.Millisecond) // ========== 发送语音指令给芯片 ========== ip.mu.Lock() _, err = serialPort.Write(frameBuf.Bytes()) ip.mu.Unlock() if err != nil { fmt.Printf("串口发送失败: %v\n", err) ip.mu.Lock() ip.isClosed = true ip.isPlaying = false ip.mu.Unlock() _ = ip.Reconnect() return } // ========== ✅ 核心修改:发送成功后,立刻打印日志,直接退出函数 ========== // 无等待、无轮询、无超时,芯片收到指令后独立播放,和程序解耦 // 雷达信号停止 → 不会再调用这里,自然不会有新的播放指令 fmt.Printf("[雷达触发成功] 语音指令已发送 → %s,芯片独立播放中\n", txt) } // 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 if ip.serialPort != nil { _ = ip.serialPort.Close() ip.serialPort = nil } fmt.Println("串口已关闭,播放状态已重置") } // VoiceParams 语音参数结构体 type VoiceParams struct { Speaker int Volume int Tone int Speed int } // DefaultVoiceParams 默认语音参数 var DefaultVoiceParams = VoiceParams{ Speaker: 3, Volume: 10, Tone: 10, Speed: 30, } // SetVoiceParams 独立设置语音参数 + 播放中禁止修改 func (ip *IpCast) SetVoiceParams(params VoiceParams) error { ip.mu.Lock() if ip.isPlaying { ip.mu.Unlock() return errors.New("语音播放中,禁止修改参数") } ip.mu.Unlock() if err := ip.validateVoiceParams(params); err != nil { return fmt.Errorf("参数校验失败: %w", err) } ip.mu.Lock() defer ip.mu.Unlock() if ip.isClosed || ip.serialPort == nil { fmt.Println("串口未连接,尝试重连...") if err := ip.Reconnect(); err != nil { return fmt.Errorf("串口重连失败: %w", err) } } paramStr := fmt.Sprintf("[m%d][v%d][t%d][s%d]", params.Speaker, params.Volume, params.Tone, params.Speed) fmt.Printf("设置语音参数:%s\n", paramStr) gbkBytes, err := convertToGBK(paramStr) if err != nil { return fmt.Errorf("参数字符串转GBK失败: %w", err) } dataLen := uint16(1 + 1 + len(gbkBytes)) frameBuf := bytes.NewBuffer([]byte{0xFD}) _ = binary.Write(frameBuf, binary.BigEndian, dataLen) frameBuf.Write([]byte{0x06, 0x01}) frameBuf.Write(gbkBytes) if _, err := ip.serialPort.Write(frameBuf.Bytes()); err != nil { ip.isClosed = true _ = ip.Reconnect() return fmt.Errorf("发送参数失败: %w", err) } respBuf := make([]byte, 16) timeout := time.After(time.Second * 3) for { select { case <-timeout: return errors.New("参数设置超时") default: n, err := ip.serialPort.Read(respBuf) if err != nil && err.Error() != "serial: read timed out" { return fmt.Errorf("读取参数响应失败: %w", err) } if bytes.Contains(respBuf[:n], []byte{0x41}) { fmt.Printf("参数设置成功: %+v\n", params) return nil } } } } // validateVoiceParams 参数校验 func (ip *IpCast) validateVoiceParams(params VoiceParams) error { validSpeakers := map[int]bool{3: true, 51: true, 52: true, 53: true, 54: true, 55: true, 56: true, 57: true} if !validSpeakers[params.Speaker] { return fmt.Errorf("发音人无效: %d", params.Speaker) } if params.Volume < 0 || params.Volume > 10 { return fmt.Errorf("音量无效: %d", params.Volume) } if params.Tone < 0 || params.Tone > 10 { return fmt.Errorf("语调无效: %d", params.Tone) } if params.Speed < 0 || params.Speed > 30 { return fmt.Errorf("语速无效: %d", params.Speed) } return nil }