Explorar el Código

设备整合 0.7

xu hace 7 meses
padre
commit
8b02403a41
Se han modificado 10 ficheros con 398 adiciones y 122 borrados
  1. 36 7
      lc/IDevice.go
  2. 44 0
      lc/handler.go
  3. 1 1
      lc/radar_event.go
  4. 58 0
      lc/redis_process.go
  5. 22 0
      lc/routes.go
  6. 32 21
      lc/screen.go
  7. 33 14
      lc/server.go
  8. 166 73
      lc/speaker.go
  9. 1 1
      main.go
  10. 5 5
      program_test.go

+ 36 - 7
lc/IDevice.go

@@ -1,5 +1,23 @@
 package lc
 
+import "github.com/sirupsen/logrus"
+
+// 设备参数
+
+var DeviceInitData DeviceData
+
+type DeviceData struct {
+	//雷达
+	LowSpeed  int //低速
+	OverSpeed int //超速
+	// 屏幕
+	Brightness int // 亮度
+	//语音
+	Volume         int    // 声音大小
+	NormalVoice    string // 正常语音
+	OverSpeedVoice string // 超速语音
+}
+
 // 通知接口
 type Notifier interface {
 	Notify(text string, isProgram, speed int)
@@ -54,22 +72,33 @@ type SentinelDevice struct {
 func (s *SentinelDevice) Call(text string, isProgram, speed int) {
 	if s.Screen != nil {
 		if isProgram == 1 || isProgram == 0 {
-			s.Screen.Display(Red, DefaultRunMode, DefaultDisplayMode, isProgram, speed, text) // 显示警告
+			if speed <= DeviceInitData.OverSpeed { //小于等于超速显示绿色
+				s.Screen.Display(Green, DefaultRunMode, DefaultDisplayMode, isProgram, speed, text) // 显示警告
+			} else {
+				s.Screen.Display(Red, DefaultRunMode, DefaultDisplayMode, isProgram, speed, text) // 显示警告
+			}
+
 		} else if isProgram == 2 {
-			s.Screen.Display(Red, DefaultRunMode, MoveLeft, isProgram, speed, text) // 显示警告
+			s.Screen.Display(Red, LoopAndOff, MoveLeft, isProgram, speed, text) // 显示警告
+		}
+
+	}
+	if s.Speaker != nil {
+		if speed > DeviceInitData.OverSpeed {
+			logrus.Info("触发语音----------------------------------")
+			s.Speaker.Speak(DeviceInitData.OverSpeedVoice) // 语音提示
+		} else if speed > DeviceInitData.LowSpeed {
+			s.Speaker.Speak(DeviceInitData.NormalVoice) // 语音提示
 		}
 
 	}
-	//if s.Speaker != nil {
-	//	s.Speaker.Speak("主路来车,请减速") // 语音提示
-	//}
 }
 
 // 恢复初始状态
 func (s *SentinelDevice) Rollback() {
 	if s.Screen != nil {
-		s.Screen.Display(Red, DefaultRunMode, DefaultDisplayMode, 0, 0, "")        //恢复默认
-		s.Screen.Display(Yellow, DefaultRunMode, DefaultDisplayMode, 1, 0, "减速慢行") // 恢复默认显示
+		s.Screen.Display(Red, DefaultRunMode, DefaultDisplayMode, 0, 0, "") //恢复默认
+		//s.Screen.Display(Yellow, DefaultRunMode, DefaultDisplayMode, 1, 0, "减速慢行") // 恢复默认显示
 	}
 }
 

+ 44 - 0
lc/handler.go

@@ -0,0 +1,44 @@
+// handler.go - 专门实现接口业务逻辑
+package lc
+
+import (
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+// PlateDeviceHandler 业务处理结构体,持有哨兵服务实例(核心依赖)
+type PlateDeviceHandler struct {
+	sentinelServer *SentinelServer // 依赖哨兵服务实例,调用核心方法
+}
+
+// NewPlateDeviceHandler 创建业务处理实例(注入哨兵服务依赖)
+func NewPlateDeviceHandler(s *SentinelServer) *PlateDeviceHandler {
+	return &PlateDeviceHandler{
+		sentinelServer: s,
+	}
+}
+
+// 设置屏幕参数
+func (h *PlateDeviceHandler) SetDeviceData(c *gin.Context) {
+	var info DeviceData
+	err := c.ShouldBindJSON(&info)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+	}
+	err = h.sentinelServer.SetDeviceDataToRedis(info)
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+		return
+	}
+	c.JSON(http.StatusOK, gin.H{})
+}
+
+// 获取设备参数
+func (h *PlateDeviceHandler) GetDeviceData(c *gin.Context) {
+	data, err := h.sentinelServer.GetDeviceDataFromRedis()
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+		return
+	}
+	c.JSON(http.StatusOK, gin.H{"data": data})
+}

+ 1 - 1
lc/radar_event.go

@@ -59,7 +59,7 @@ func (s *RadarServer) CallbackWithData(radarData RadarData) {
 		return
 	}
 	// 自定义触发条件:速度>0时触发警告(可根据业务调整阈值,如>5)
-	if radarData.Valid && radarData.Speed > 10 {
+	if radarData.Valid && radarData.Speed > float64(DeviceInitData.LowSpeed) {
 		// 拼接方向和速度作为通知消息
 		s.Notifier.Notify("主路来车", 0, int(math.Round(radarData.Speed)))
 	}

+ 58 - 0
lc/redis_process.go

@@ -10,6 +10,12 @@ import (
 	"time"
 )
 
+// Redis基础配置(极简版,仅用于初始化连接)
+const (
+	RedisInitDevice = "init_device_data" // 仅用于标记Redis初始化完成的空Key
+	RedisKeyPrefix  = "plate_results"
+)
+
 // 启动定时任务:动态调整轮询间隔(未拿到车牌号3秒轮询,拿到则5秒轮询)
 func (s *SentinelServer) StartFetchingLatestPlate() {
 	go func() {
@@ -133,3 +139,55 @@ func parsePythonDictStr(pyStr string) (PlateData, error) {
 	}
 	return plateData, nil
 }
+
+// ========== 新增:存入 DeviceData 结构体到Redis ==========
+func (s *SentinelServer) SetDeviceDataToRedis(data DeviceData) error {
+	if s.rdb == nil {
+		return fmt.Errorf("redis客户端未初始化")
+	}
+	// 1. 将结构体序列化为JSON字节流
+	jsonData, err := json.Marshal(data)
+	if err != nil {
+		return fmt.Errorf("结构体序列化失败: %w", err)
+	}
+	// 2. 存入Redis (永久有效,重启不丢失)
+	err = s.rdb.Set(s.ctx, RedisInitDevice, jsonData, 0).Err()
+	if err != nil {
+		return fmt.Errorf("redis存入失败: %w", err)
+	}
+	logrus.Info("✅ 设备配置已存入Redis", "data", data)
+	return nil
+}
+
+// ========== 新增:从Redis读取 DeviceData 结构体 ==========
+func (s *SentinelServer) GetDeviceDataFromRedis() (DeviceData, error) {
+	// 默认值
+	data := DeviceData{
+		LowSpeed:       10,
+		OverSpeed:      60,
+		Brightness:     4,
+		Volume:         4,
+		NormalVoice:    "注意来车",
+		OverSpeedVoice: "您已超速",
+	}
+	if s.rdb == nil {
+		return data, fmt.Errorf("redis客户端未初始化")
+	}
+	// 1. 从Redis读取JSON字符串
+	jsonData, err := s.rdb.Get(s.ctx, RedisInitDevice).Result()
+	// 特殊处理:redis中无此key时,返回默认空结构体,不报错
+	if err == redis.Nil {
+		logrus.Warn("⚠️ Redis中暂无设备配置数据,返回默认值")
+		go s.SetDeviceDataToRedis(data)
+		return data, nil
+	}
+	if err != nil {
+		return data, fmt.Errorf("redis读取失败: %w", err)
+	}
+	// 2. JSON反序列化为结构体
+	err = json.Unmarshal([]byte(jsonData), &data)
+	if err != nil {
+		return data, fmt.Errorf("结构体反序列化失败: %w", err)
+	}
+	return data, nil
+}

+ 22 - 0
lc/routes.go

@@ -0,0 +1,22 @@
+// routes.go - 专门管理Gin路由注册
+package lc
+
+import "github.com/gin-gonic/gin"
+
+// RegisterRoutes 注册所有HTTP路由(仅路由规则,无业务逻辑)
+func RegisterRoutes(engine *gin.Engine, h *PlateDeviceHandler) {
+	// 分组管理接口(可选,规范接口路径)
+	apiGroup := engine.Group("/api")
+	{
+		//设置
+		setApi := apiGroup.Group("set")
+		{
+			setApi.POST("setDeviceInitData", h.SetDeviceData)
+		}
+		//获取
+		getApi := apiGroup.Group("get")
+		{
+			getApi.GET("getDeviceData", h.GetDeviceData)
+		}
+	}
+}

+ 32 - 21
lc/screen.go

@@ -169,22 +169,22 @@ func (s *Screen) SendRam(color Color, runMode RunMode, displayMode DisplayMode,
 		file1 := FlashFile{}
 		file1.SetMsg(FormatSpeed(speed), color)
 		file1.SetMode(runMode, displayMode)
-		file1.SetOrigin(8, true, 0)
-		file1.SetArea(16, true, 32)
+		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)
+		//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(32, true, 4)
-		file1.SetArea(96, true, 32)
+		file1.SetOrigin(0, true, 0)
+		file1.SetArea(128, true, 32)
 		s.TextRam(file1, true)
 	}
 
@@ -193,19 +193,17 @@ func (s *Screen) SendRam(color Color, runMode RunMode, displayMode DisplayMode,
 func FormatSpeed(speed int) string {
 	// 校验输入范围(仅处理0-99的整数)
 	if speed < 0 || speed > 99 {
-		return "0\\n0"
+		return "00"
 	}
 
 	// 处理个位数(0-9)
 	if speed < 10 {
 		speedStr := strconv.Itoa(speed)
-		return fmt.Sprintf("%s\\n0", speedStr)
+		return fmt.Sprintf("0%s", speedStr)
 	}
 
 	// 处理十位数(10-99)
-	tens := speed / 10  // 提取十位数字(如21→2)
-	units := speed % 10 // 提取个位数字(如21→1)
-	return fmt.Sprintf("%d\\n%d", units, tens)
+	return fmt.Sprintf("%d", speed)
 }
 
 // TextRam 发送动态区节目实现
@@ -218,26 +216,27 @@ func (s *Screen) TextRam(ff FlashFile, isNumber bool) {
 	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 + "\\FE001\\C" + strconv.Itoa(int(Green)) + number))
+		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("\\FE000\\C" + strconv.Itoa(int(ff.color)) + ff.msg))
+		bytes, _ = encoder.Bytes([]byte("\\FE001\\C" + strconv.Itoa(int(ff.color)) + ff.msg))
+		alignment = 0x0a
 	}
-	area := bx.NewBxAreaProgram(byte(id), 1, byte(ff.dispMode), ff.originX, ff.originY, ff.width,
+	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(0) //动态显示模式
+	pack.SetDisplayType(1) //动态显示模式
 	d := pack.Pack()
 	if isNumber {
 		logrus.Info(hex.EncodeToString(d))
@@ -359,7 +358,7 @@ func (s *Screen) TextFlash(ft []FlashFile, name string, isNumber, isLogo bool) {
 		} 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), i.originX, i.originY, i.width, i.height, bytes, false)
+		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)
 	}
 
@@ -415,6 +414,18 @@ func (s *Screen) Bitmap(name string, bitmap []byte) {
 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()

+ 33 - 14
lc/server.go

@@ -2,21 +2,17 @@ package lc
 
 import (
 	"context"
+	"github.com/gin-gonic/gin"
 	"github.com/sirupsen/logrus"
 	"lc-smartX/util"
 	"lc-smartX/util/gopool"
+	"log/slog"
 	"time"
 
 	// 仅引入Redis客户端核心依赖
 	"github.com/redis/go-redis/v9"
 )
 
-// Redis基础配置(极简版,仅用于初始化连接)
-const (
-	RedisInitKey   = "sentinel:init:flag" // 仅用于标记Redis初始化完成的空Key
-	RedisKeyPrefix = "plate_results"
-)
-
 // 单个哨兵服务(仅新增Redis客户端和上下文字段)
 type SentinelServer struct {
 	RadarServer  *RadarServer
@@ -29,6 +25,7 @@ type SentinelServer struct {
 	ctx         context.Context // 上下文(用于Redis操作)
 	fetchTicker *time.Ticker    // 定时任务ticker
 	stopChan    chan struct{}   // 停止信号通道
+	ginEngine   *gin.Engine
 }
 
 // 启动哨兵服务
@@ -42,6 +39,15 @@ func StartSentinelServer() {
 	// === 第一步:优先初始化Redis(极简版) ===
 	s.initRedis()
 
+	deviceData, err := s.GetDeviceDataFromRedis()
+	if err != nil {
+		return
+	}
+
+	DeviceInitData = deviceData
+
+	s.initGin()
+
 	// 原有逻辑:初始化设备
 	s.initDevice()
 
@@ -64,7 +70,12 @@ func StartSentinelServer() {
 	s.StartFetchingLatestPlate()
 
 	// 原有逻辑:启动主循环
-	s.Serve()
+	go s.Serve()
+
+	err = s.ginEngine.Run(":8080")
+	if err != nil {
+		return
+	}
 }
 
 // === 核心:极简Redis初始化(仅连接+标记初始化) ===
@@ -82,13 +93,6 @@ func (s *SentinelServer) initRedis() {
 		s.rdb = nil // 标记Redis不可用
 		return
 	}
-
-	// 3. 写入初始化标记Key(仅标记,无业务数据)
-	if err := s.rdb.Set(s.ctx, RedisInitKey, "initialized", 0).Err(); err != nil {
-		logrus.Warn("⚠️ Redis初始化标记写入失败", "err", err)
-	} else {
-		logrus.Info("✅ Redis初始化完成(仅连接+标记)")
-	}
 }
 
 // 原有逻辑:初始化本哨兵的设备(屏幕+喇叭)
@@ -116,6 +120,21 @@ func (s *SentinelServer) initDevice() {
 	}
 }
 
+// ========== 修改Gin初始化:整合handler和路由 ==========
+func (s *SentinelServer) initGin() {
+	// 1. 初始化Gin引擎
+	gin.SetMode(gin.DebugMode)
+	s.ginEngine = gin.Default()
+
+	// 2. 创建业务处理实例(注入哨兵服务依赖)
+	handler := NewPlateDeviceHandler(s)
+
+	// 3. 调用路由注册函数(路由与业务解耦)
+	RegisterRoutes(s.ginEngine, handler)
+
+	slog.Info("✅ Gin路由初始化成功")
+}
+
 // 原有逻辑:实现Notifier接口,处理雷达/摄像头事件
 func (s *SentinelServer) Notify(text string, isProgram, speed int) {
 	s.StateTicker.Reset(5 * time.Second) // 重置回滚定时器

+ 166 - 73
lc/speaker.go

@@ -20,32 +20,29 @@ type Loudspeaker interface {
 	Speak(txt string)
 }
 
-// IpCast 串口语音播报实现(核心:播放中丢弃请求+仅空闲时接收+无缓存)
+// IpCast 串口语音播报实现【最终极简版】
+// 核心准则:✅ 仅雷达触发Speak播放 ✅ 播放中丢弃重复雷达信号 ✅ 无轮询 ✅ 无等待 ✅ 无超时 ✅ 无通道
 type IpCast struct {
-	Port       string        // 串口端口(外部传入)
-	Baud       int           // 波特率(固定为115200)
-	serialPort *serial.Port  // 串口连接实例
-	mu         sync.Mutex    // 全局互斥锁(保护isPlaying/串口操作)
-	idleChan   chan struct{} // 芯片空闲通知通道
-	isClosed   bool          // 串口是否已关闭
-	isPlaying  bool          // 标记是否正在播放语音(核心控制字段)
+	Port       string       // 串口端口(外部传入)
+	Baud       int          // 波特率(固定为115200)
+	serialPort *serial.Port // 串口连接实例
+	mu         sync.Mutex   // 全局互斥锁(并发安全修改状态+串口读写)
+	isClosed   bool         // 串口是否已关闭
+	isPlaying  bool         // 唯一播放状态:true=播放中(拒收雷达) false=空闲(可播放)
 }
 
 // NewIpCast 初始化串口语音播报实例
-// prot: 串口端口(如"/dev/ttyUSB0")
 func NewIpCast(prot string) *IpCast {
 	s := &IpCast{
 		Port:      prot,
 		Baud:      115200,
-		idleChan:  make(chan struct{}, 1), // 缓冲通道避免阻塞
 		isClosed:  false,
-		isPlaying: false, // 初始为未播放状态
+		isPlaying: false,
 	}
-	// 初始化串口
 	if err := s.Reconnect(); err != nil {
 		fmt.Printf("串口初始化失败: %v\n", err)
+		return s
 	}
-	// 启动后台监听协程
 	go s.listenSerialResponse()
 	return s
 }
@@ -55,21 +52,19 @@ 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报错频率
+		ReadTimeout: time.Millisecond * 200,
 	}
 
 	port, err := serial.OpenPort(cfg)
@@ -78,39 +73,33 @@ func (ip *IpCast) Reconnect() error {
 	}
 
 	ip.serialPort = port
-	ip.isClosed = false // 标记串口可用
+	ip.isClosed = false
 	fmt.Println("串口重连成功")
 	return nil
 }
 
-// clearIdleChan 清空空闲通道所有残留信号(避免旧信号干扰)
-func (ip *IpCast) clearIdleChan() {
-	for {
-		select {
-		case <-ip.idleChan:
-		default:
-			return
-		}
-	}
-}
-
-// listenSerialResponse 后台监听串口返回(仅处理0x41/0x4F,清空残留信号)
+// listenSerialResponse 后台监听串口返回【核心不变,极简】
+// 唯一逻辑:收到0x41=日志打印;收到0x4F=加锁改isPlaying=false(释放播放权限)
 func (ip *IpCast) listenSerialResponse() {
 	buf := make([]byte, 64)
 	for {
-		// 串口未就绪时低频轮询
-		if ip.isClosed || ip.serialPort == nil {
+		ip.mu.Lock()
+		isClosed := ip.isClosed
+		serialPort := ip.serialPort
+		ip.mu.Unlock()
+
+		if isClosed || serialPort == nil {
 			time.Sleep(time.Second * 1)
 			continue
 		}
 
-		n, err := ip.serialPort.Read(buf)
-		// 忽略EOF和读取超时(正常无数据场景)
+		ip.mu.Lock()
+		n, err := serialPort.Read(buf)
+		ip.mu.Unlock()
+
 		if err != nil {
 			switch {
-			case err == io.EOF:
-				continue
-			case err.Error() == "serial: read timed out":
+			case err == io.EOF, err.Error() == "serial: read timed out":
 				continue
 			default:
 				fmt.Printf("串口读取异常: %v\n", err)
@@ -127,44 +116,48 @@ func (ip *IpCast) listenSerialResponse() {
 
 		// 解析返回字节
 		recvBytes := buf[:n]
-		fmt.Printf("串口返回原始字节(十六进制): %s\n", hex.EncodeToString(recvBytes))
 		for _, b := range recvBytes {
 			switch b {
 			case 0x41:
-				fmt.Println("<---- 41  接收成功")
+				fmt.Println("<---- 41  指令接收成功,芯片开始播放")
 			case 0x4F:
-				fmt.Println("<---- 4F  芯片空闲")
-				// 清空残留信号后发送新的空闲标记
-				ip.clearIdleChan()
-				ip.idleChan <- struct{}{}
+				fmt.Println("<---- 4F  语音播放完成 ✅,释放播放权限")
+				// 唯一操作:播放完成,解锁状态,让下次雷达信号可以触发
+				ip.mu.Lock()
+				ip.isPlaying = false
+				ip.mu.Unlock()
 			}
 		}
 	}
 }
 
-// Speak 发送语音指令(核心逻辑:播放中丢弃请求+仅空闲时接收+无缓存)
+// Speak 发送语音指令 【✅ 核心改版,彻底解决你的问题,极简逻辑】
+// 唯一触发源:雷达信号调用 → 播放;无雷达调用 → 绝对不播放
+// 播放中重复触发:直接return丢弃,无任何处理
+// 发送指令后:立刻退出函数,无等待、无轮询、无超时,芯片独立播放
 func (ip *IpCast) Speak(txt string) {
-	// 1. 加锁检查播放状态,核心控制逻辑
+	// ========== 第一步:核心判断,播放中直接丢弃雷达信号【保留刚需】 ==========
 	ip.mu.Lock()
-	// 若正在播放,直接丢弃当前雷达触发请求
 	if ip.isPlaying {
-		fmt.Printf("语音正在播放中,丢弃雷达触发请求:%s\n", txt)
+		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
+		if ip.isClosed {
+			ip.isPlaying = false
+			fmt.Println("[异常] 串口关闭,重置播放状态为空闲")
+		}
 		ip.mu.Unlock()
-		fmt.Println("语音播放流程结束,恢复接收雷达信号")
 	}()
 
-	// 3. 校验串口状态
+	// ========== 校验串口状态 ==========
 	ip.mu.Lock()
 	isClosed := ip.isClosed
 	serialPort := ip.serialPort
@@ -174,56 +167,61 @@ func (ip *IpCast) Speak(txt string) {
 		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()
 	}
 
-	// 4. 清空空闲通道残留信号(避免旧信号干扰)
-	ip.clearIdleChan()
-
-	// 5. 文本转GBK编码
+	// ========== 文本转GBK编码 ==========
 	GBKBytes, err := convertToGBK(txt)
 	if err != nil {
 		fmt.Printf("文本转GBK失败: %v\n", err)
+		ip.mu.Lock()
+		ip.isPlaying = false
+		ip.mu.Unlock()
 		return
 	}
 
-	// 6. 构造语音帧数据(匹配官方格式)
-	dataAreaLen := uint16(1 + 1 + len(GBKBytes)) // 命令字+编码格式+文本长度
-	frameBuf := bytes.NewBuffer([]byte{0xFD})    // 帧头FD
+	// ========== 构造语音帧数据 ==========
+	dataAreaLen := uint16(1 + 1 + len(GBKBytes))
+	frameBuf := bytes.NewBuffer([]byte{0xFD})
 	_ = binary.Write(frameBuf, binary.BigEndian, dataAreaLen)
-	frameBuf.Write([]byte{0x01, 0x01}) // 命令字01 + 编码格式01(GBK)
+	frameBuf.Write([]byte{0x01, 0x01})
 	frameBuf.Write(GBKBytes)
 
-	// 调试打印帧数据
-	//frameHex := hex.EncodeToString(frameBuf.Bytes())
+	// 保留你需要的500ms延迟
+	time.Sleep(500 * time.Millisecond)
 
-	// 7. 发送帧数据到串口
+	// ========== 发送语音指令给芯片 ==========
+	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
 	}
 
-	// 8. 等待芯片空闲(0x4F),仅收到空闲信号才允许下一次播放
-	select {
-	case <-ip.idleChan:
-	case <-time.After(time.Second * 10): // 超时延长至10秒,适配长语音
-	}
+	// ========== ✅ 核心修改:发送成功后,立刻打印日志,直接退出函数 ==========
+	// 无等待、无轮询、无超时,芯片收到指令后独立播放,和程序解耦
+	// 雷达信号停止 → 不会再调用这里,自然不会有新的播放指令
+	fmt.Printf("[雷达触发成功] 语音指令已发送 → %s,芯片独立播放中\n", txt)
 }
 
 // CorrectTime 保留原有方法(空实现)
 func (ip IpCast) CorrectTime() {}
 
-// convertToGBK 文本转GBK编码(保留原有逻辑)
+// convertToGBK 文本转GBK编码 【已修复语法错误】
 func convertToGBK(s string) ([]byte, error) {
 	if s == "" {
 		return nil, errors.New("文本不能为空")
@@ -236,7 +234,7 @@ func convertToGBK(s string) ([]byte, error) {
 		return nil, fmt.Errorf("编码转换失败: %w", err)
 	}
 
-	fmt.Printf("文本「%s」的GBK编码(十六进制): %s\n", s, hex.EncodeToString(result))
+	fmt.Printf("文本「%s」的GBK编码: %s\n", s, hex.EncodeToString(result))
 	if len(result) == 0 {
 		return nil, errors.New("GBK转换结果为空")
 	}
@@ -249,11 +247,106 @@ func (ip *IpCast) Close() {
 	defer ip.mu.Unlock()
 
 	ip.isClosed = true
-	ip.isPlaying = false // 强制重置播放状态
-	ip.clearIdleChan()   // 清空通道
+	ip.isPlaying = false
 	if ip.serialPort != nil {
 		_ = ip.serialPort.Close()
 		ip.serialPort = nil
 	}
-	fmt.Println("串口已手动关闭")
+	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
 }

+ 1 - 1
main.go

@@ -7,5 +7,5 @@ import (
 
 func main() {
 	gopool.SetCap(64)
-	lc.StartIntersectionServer()
+	lc.StartSentinelServer()
 }

+ 5 - 5
program_test.go

@@ -10,10 +10,10 @@ func TestProgram(t *testing.T) {
 	//先用以下示例将静态节目发送至屏,P000为初始节目,P001为来车警示节目,LOGO为开机节目
 	//单个屏设备连接不能并发发送数据,未作实现
 	client := lc.NewScreen("支路1屏", "192.168.1.200", "5005")
-	time.Sleep(2 * time.Second)
-	client.SendRam(0)
-	time.Sleep(2 * time.Second)
-	client.SendRam(1)
+	//time.Sleep(2 * time.Second)
+	//client.SendRam(0)
+	//time.Sleep(2 * time.Second)
+	//client.SendRam(1)
 	////清除所有文件
 
 	//time.Sleep(1*time.Second)
@@ -25,7 +25,7 @@ func TestProgram(t *testing.T) {
 	//file.SetMode(lc.DefaultRunMode, lc.DefaultDisplayMode)
 	//file.SetOrigin(0, true, 0)
 	//file.SetArea(64, true, 16)
-	//client.TextFlash([]lc.FlashFile{file}, true)
+	//client.TextFlash([]lc.FlashFile{file}, "LOGO", true)
 	//time.Sleep(1 * time.Second)
 
 	//发送flash文件节目 减速慢行