diff --git a/.gitignore b/.gitignore index a93ee57..8a50848 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ /logs +/.idea +/.vscode *.mp3 game-driver* \ No newline at end of file diff --git a/config/config.go b/config/config.go index 1d7949b..d970ac1 100644 --- a/config/config.go +++ b/config/config.go @@ -1,7 +1,7 @@ package config import ( - "game-driver/internal/routes/play/card_device" + "game-driver/internal/routes/play/card_pusher" "gopkg.in/natefinch/lumberjack.v2" ) @@ -19,7 +19,7 @@ type AliyunConfig struct { type GameConfig struct { MaxTimeout int - CardGroups []*card_device.LineGroup + CardGroups []*card_pusher.LineGroup } type Logger struct { diff --git a/docs/读卡器YMC1701&1702资料V1.rar b/docs/读卡器YMC1701&1702资料V1.rar new file mode 100644 index 0000000..d75ba40 Binary files /dev/null and b/docs/读卡器YMC1701&1702资料V1.rar differ diff --git a/game.md b/game.md index a507947..66d37a8 100644 --- a/game.md +++ b/game.md @@ -65,6 +65,8 @@ ```json lines { + // 发出的卡片ID + "card_id": "", // 空卡设备数量 "empty": 0, // 错误设备数量 @@ -82,6 +84,12 @@ ```json lines { + // 需要的卡片ID + "card_id": "", + // 卡片错误时的播报内容 + "card_error": "", + // 卡片正确时播报的恭喜通关内容 + "card_ok": "", // 等待插卡时间,单位秒 "wait_card": 0, // 插卡后持续时间,单位秒 diff --git a/go.mod b/go.mod index 114267f..2d2fc6c 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/aliyun/alibabacloud-nls-go-sdk v1.1.1 github.com/eclipse/paho.golang v0.22.0 github.com/gopxl/beep/v2 v2.1.0 + github.com/munnik/modbus v1.6.6 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 github.com/warthog618/go-gpiocdev v0.9.1 diff --git a/go.sum b/go.sum index 88d1877..918d37b 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,6 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/aliyun/alibaba-cloud-sdk-go v1.61.1376/go.mod h1:9CMdKNL3ynIGPpfTcdwTvIm8SGuAZYYC4jFVSSvE1YQ= -github.com/aliyun/alibaba-cloud-sdk-go v1.63.52 h1:2qZQ6tiGuBqtaXd0rgsct29WxFzYyUKywg113mMP7QE= -github.com/aliyun/alibaba-cloud-sdk-go v1.63.52/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ= github.com/aliyun/alibaba-cloud-sdk-go v1.63.53 h1:I93ILTm5ytF4e5+lEQXSXcydS26D9eVyJ4H6z3rJqMA= github.com/aliyun/alibaba-cloud-sdk-go v1.63.53/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ= github.com/aliyun/alibabacloud-nls-go-sdk v1.1.1 h1:LjItoNZuu5xHlsByFo+kr3nGa4LRIESCGWhfurayxBg= @@ -75,6 +73,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnik/modbus v1.6.6 h1:QRAR+04bivKSzPN5qi5g9Wyzh+e3oQVFMUOhv35ya5Q= +github.com/munnik/modbus v1.6.6/go.mod h1:p8PIBjiZgsY82MiZPrkAGkrDomL1tBNGGQIwyAm4Vp0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A= @@ -160,6 +160,8 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/middleware/bgm.go b/internal/middleware/bgm.go index 785d238..57376a2 100644 --- a/internal/middleware/bgm.go +++ b/internal/middleware/bgm.go @@ -55,7 +55,7 @@ func PlayBgm() leaf.HandlerFunc { } }() } else { - zap.S().Errorln("背景音乐解析为空") + zap.S().Infoln("未解析到背景音乐") } c.Next() diff --git a/internal/routes/play.go b/internal/routes/play.go index 63d9204..3f3f11d 100644 --- a/internal/routes/play.go +++ b/internal/routes/play.go @@ -24,7 +24,7 @@ func switchPoint(ctx context.Context, point int) leaf.HandlerFunc { return play.PushCard(ctx) case 5: // 5号点位(等待插卡) - return play.WaitCard + return play.WaitCard(ctx) default: return play.Default } diff --git a/internal/routes/play/card_device/device.go b/internal/routes/play/card_pusher/device.go similarity index 99% rename from internal/routes/play/card_device/device.go rename to internal/routes/play/card_pusher/device.go index 9d62cbf..603d868 100644 --- a/internal/routes/play/card_device/device.go +++ b/internal/routes/play/card_pusher/device.go @@ -1,4 +1,4 @@ -package card_device +package card_pusher import ( "fmt" diff --git a/internal/routes/play/card_device/line_group.go b/internal/routes/play/card_pusher/line_group.go similarity index 96% rename from internal/routes/play/card_device/line_group.go rename to internal/routes/play/card_pusher/line_group.go index 92e07d0..caef059 100644 --- a/internal/routes/play/card_device/line_group.go +++ b/internal/routes/play/card_pusher/line_group.go @@ -1,4 +1,4 @@ -package card_device +package card_pusher type LineGroup struct { Name string diff --git a/internal/routes/play/card_device/status.go b/internal/routes/play/card_pusher/status.go similarity index 97% rename from internal/routes/play/card_device/status.go rename to internal/routes/play/card_pusher/status.go index bf9b1a7..f6a010e 100644 --- a/internal/routes/play/card_device/status.go +++ b/internal/routes/play/card_pusher/status.go @@ -1,4 +1,4 @@ -package card_device +package card_pusher import ( "strconv" diff --git a/internal/routes/play/push_card.go b/internal/routes/play/push_card.go index 32a9fbf..6df5d7e 100644 --- a/internal/routes/play/push_card.go +++ b/internal/routes/play/push_card.go @@ -5,7 +5,7 @@ import ( "encoding/json" "game-driver/config" "game-driver/internal/middleware" - "game-driver/internal/routes/play/card_device" + "game-driver/internal/routes/play/card_pusher" "game-driver/internal/schema" "game-driver/leaf" "game-driver/pkg/utils" @@ -16,19 +16,20 @@ import ( ) type ResponseBody struct { - Empty int `json:"empty"` - Error int `json:"error"` - OutOk int `json:"out_ok"` - num int + CardId string `json:"card_id"` + Empty int `json:"empty"` + Error int `json:"error"` + OutOk int `json:"out_ok"` + num int } func PushCard(ctx context.Context) leaf.HandlerFunc { - devices := make([]*card_device.Device, 0) + devices := make([]*card_pusher.Device, 0) for _, group := range config.C.Game.CardGroups { gv, _ := json.Marshal(group) zap.S().Info("发卡器指针:", string(gv)) - device, err := card_device.New(group) + device, err := card_pusher.New(group) if err != nil { zap.S().Panicln("初始化发卡器失败: ", err) } diff --git a/internal/routes/play/wait_card.go b/internal/routes/play/wait_card.go index 7b53bab..a0d6e68 100644 --- a/internal/routes/play/wait_card.go +++ b/internal/routes/play/wait_card.go @@ -1,44 +1,109 @@ package play import ( + "context" "game-driver/internal/middleware" "game-driver/internal/schema" "game-driver/leaf" + "game-driver/pkg/card_reader" + "game-driver/pkg/channel" + "game-driver/pkg/tts" + "go.uber.org/zap" "sync" "time" ) -func WaitCard(c *leaf.Context) { - payload := leaf.Value[*schema.PlayModal](c, middleware.PayloadJSONKey) - - var waitCard time.Duration - if a, ok := payload.Game["wait_card"]; ok { - if v, ok := a.(float64); ok { - waitCard = time.Duration(v) - } +func WaitCard(ctx context.Context) leaf.HandlerFunc { + reader, err := card_reader.NewReader("rtu:///dev/ttyUSB0") + if err != nil { + zap.S().Panicln("读卡器串口连接失败", err) } - - // 等待组 - var wait sync.WaitGroup - defer wait.Wait() - - a := make(chan string) - defer close(a) - - wait.Add(1) go func() { - defer wait.Done() - //TODO: 模拟卡片 3s 插入 - time.Sleep(3 * time.Second) - a <- "卡片数据" + <-ctx.Done() + _ = reader.Close() }() - select { - case <-c.Done(): - case <-time.After(waitCard * time.Second): - case _, ok := <-a: // 等待卡片插入 - if ok { // 非关闭信号 - Default(c) + err = reader.Init() + if err != nil { + zap.S().Panicln("读卡器初始化失败", err) + } + + return func(c *leaf.Context) { + payload := leaf.Value[*schema.PlayModal](c, middleware.PayloadJSONKey) + + // 读取卡片等待时间 + var waitCard time.Duration + if a, ok := payload.Game["wait_card"]; ok { + if v, ok := a.(float64); ok { + waitCard = time.Duration(v) + } + } + // 卡片ID预期值 + var cardId string + if a, ok := payload.Game["card_id"]; ok { + if v, ok := a.(string); ok { + cardId = v + } + } + // 卡片比对成功语音内容 + var cardOk string + if a, ok := payload.Game["card_ok"]; ok { + if v, ok := a.(string); ok { + cardOk = v + } + } + // 卡片比对失败语音内容 + var cardError string + if a, ok := payload.Game["card_error"]; ok { + if v, ok := a.(string); ok { + cardError = v + } + } + + // 等待组 + var wait sync.WaitGroup + defer wait.Wait() + + // 卡片信息通道 + cardInfo := channel.NewClosed[string]() + defer cardInfo.Close() + + // 结束信号通道 + cc, cancel := context.WithCancel(context.TODO()) + defer cancel() + + wait.Add(1) + go func() { + defer wait.Done() + reader.OnCardInfo(cc, func(info card_reader.CardInfo) { + cardInfo.Send(info.ID) + }) + }() + + // 多次读取,直到读取到正确的卡片 + for isNeed := true; isNeed; { + isNeed = false + + select { + case <-c.Done(): + case <-time.After(waitCard * time.Second): + case id, ok := <-cardInfo.Data(): // 等待卡片插入 + if ok { // 非关闭信号 + // 比对卡号是否正确,不正确则重新读取 + if cardId != id { + zap.S().Infof("读取到卡片数据%q,与预期卡片数据%q不一致", id, cardId) + // 播报错误提示 + tts.DefaultTTS.Sound(cardError) + isNeed = true + break + } + // 播报恭喜语音 + tts.DefaultTTS.Sound(cardOk) + //TODO: 打开炫酷光效 + zap.S().Infof("读取到卡片数据%q,开始打开炫酷光效", id) + Default(c) + } + } } } } diff --git a/pkg/card_reader/reader.go b/pkg/card_reader/reader.go new file mode 100644 index 0000000..793d839 --- /dev/null +++ b/pkg/card_reader/reader.go @@ -0,0 +1,137 @@ +package card_reader + +import ( + "context" + "fmt" + "github.com/munnik/modbus" + "go.uber.org/zap" + "io" + "math" + "strings" + "time" +) + +type Reader interface { + io.Closer + SetUnitID(uint8) + Init() error + // OnCardInfo 会在卡号信息发生变化时调用回调函数,直到上下文被取消,或者 Reader 被关闭。需要在 goroutine 中调用。 + OnCardInfo(context.Context, func(CardInfo)) +} + +type CardInfo struct { + Type uint16 + ID string +} + +type reader struct { + c *modbus.Client +} + +func (r *reader) Init() error { + // 配置读卡间隔时间为 50 毫秒 + err := r.c.WriteRegister(8, 0x0511) + if err != nil { + return err + } + // 由低到高输出 500 毫秒 + err = r.c.WriteRegister(9, 0xF20A) + if err != nil { + return err + } + // 读取后自动清空,高字节:00 不清空 01 清空 + err = r.c.WriteRegister(10, 0x0103) + if err != nil { + return err + } + // 自动读卡号,高字节高4位:0 读一次 1 连续读 + err = r.c.WriteRegister(11, 0x0301) + if err != nil { + return err + } + return nil +} + +func (r *reader) OnCardInfo(ctx context.Context, f func(info CardInfo)) { + for { + select { + case <-ctx.Done(): + return + case <-time.After(100 * time.Millisecond): + { + // 读取状态寄存器确认是否信息已经准备好 + status, err := r.c.ReadRegister(29, modbus.HoldingRegister) + if err != nil { + zap.S().Errorln("ReadRegister 40030 error:", err) + break + } else if status == 0x0000 { + break + } + + // 读取卡号长度 + cardLength, err := r.c.ReadRegister(30, modbus.HoldingRegister) + if err != nil { + zap.S().Errorln("ReadRegister 40031 error:", err) + break + } + dataLength := cardLength >> 8 + + // 读取卡类型 + cardType, err := r.c.ReadRegister(31, modbus.HoldingRegister) + if err != nil { + zap.S().Errorln("ReadRegister 40032 error:", err) + break + } + + // 读取卡号数据 + cardData, err := r.c.ReadRegisters(32, uint16(math.Round(float64(dataLength)/2)), modbus.HoldingRegister) + if err != nil { + zap.S().Errorln("ReadRegister 40033~ error:", err) + break + } + + s := make([]string, dataLength) + for i := 0; i < int(dataLength); i++ { + if i%2 == 0 { + s[i] = fmt.Sprintf("%02X", cardData[i/2]>>8) + } else { + s[i] = fmt.Sprintf("%02X", cardData[i/2]&0xFF) + } + } + + f(CardInfo{ + Type: cardType, + ID: strings.Join(s, ""), + }) + } + } + } +} + +func (r *reader) SetUnitID(unitID uint8) { + _ = r.c.SetUnitID(unitID) +} + +func (r *reader) Close() error { + return r.c.Close() +} + +// NewReader 创建一个新的读卡器 +func NewReader(URL string) (Reader, error) { + // 配置串口客户端 + c, _ := modbus.NewClient(&modbus.Configuration{ + URL: URL, + Speed: 9600, + DataBits: 8, + Timeout: 2 * time.Second, + Logger: zap.NewStdLog(zap.L()), + }) + + // 打开串口连接 + err := c.Open() + if err != nil { + return nil, err + } + + return &reader{c}, nil +} diff --git a/pkg/channel/channel.go b/pkg/channel/channel.go new file mode 100644 index 0000000..269879c --- /dev/null +++ b/pkg/channel/channel.go @@ -0,0 +1,54 @@ +package channel + +import "sync" + +// Closed 可包含关闭状态的通道 +type Closed[T any] struct { + ch chan T + closed bool + mu sync.RWMutex +} + +func (s *Closed[T]) Close() { + s.mu.Lock() + defer s.mu.Unlock() + + if s.closed { + return + } + + close(s.ch) + s.closed = true + + return +} + +func (s *Closed[T]) Data() <-chan T { + return s.ch +} + +func (s *Closed[T]) Send(data T) bool { + s.mu.RLock() + defer s.mu.RUnlock() + + if s.closed { + return false + } + + s.ch <- data + + return true +} + +func (s *Closed[T]) isClosed() bool { + s.mu.RLock() + defer s.mu.RUnlock() + + return s.closed +} + +func NewClosed[T any]() *Closed[T] { + return &Closed[T]{ + ch: make(chan T), + } +}