初步完成龙台的读卡逻辑

This commit is contained in:
2024-12-09 18:28:36 +08:00
parent aa634c8860
commit 37fb40672a
15 changed files with 313 additions and 43 deletions

2
.gitignore vendored
View File

@@ -1,4 +1,6 @@
/logs /logs
/.idea
/.vscode
*.mp3 *.mp3
game-driver* game-driver*

View File

@@ -1,7 +1,7 @@
package config package config
import ( import (
"game-driver/internal/routes/play/card_device" "game-driver/internal/routes/play/card_pusher"
"gopkg.in/natefinch/lumberjack.v2" "gopkg.in/natefinch/lumberjack.v2"
) )
@@ -19,7 +19,7 @@ type AliyunConfig struct {
type GameConfig struct { type GameConfig struct {
MaxTimeout int MaxTimeout int
CardGroups []*card_device.LineGroup CardGroups []*card_pusher.LineGroup
} }
type Logger struct { type Logger struct {

Binary file not shown.

View File

@@ -65,6 +65,8 @@
```json lines ```json lines
{ {
// 发出的卡片ID
"card_id": "",
// 空卡设备数量 // 空卡设备数量
"empty": 0, "empty": 0,
// 错误设备数量 // 错误设备数量
@@ -82,6 +84,12 @@
```json lines ```json lines
{ {
// 需要的卡片ID
"card_id": "",
// 卡片错误时的播报内容
"card_error": "",
// 卡片正确时播报的恭喜通关内容
"card_ok": "",
// 等待插卡时间,单位秒 // 等待插卡时间,单位秒
"wait_card": 0, "wait_card": 0,
// 插卡后持续时间,单位秒 // 插卡后持续时间,单位秒

1
go.mod
View File

@@ -6,6 +6,7 @@ require (
github.com/aliyun/alibabacloud-nls-go-sdk v1.1.1 github.com/aliyun/alibabacloud-nls-go-sdk v1.1.1
github.com/eclipse/paho.golang v0.22.0 github.com/eclipse/paho.golang v0.22.0
github.com/gopxl/beep/v2 v2.1.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/cobra v1.8.1
github.com/spf13/viper v1.19.0 github.com/spf13/viper v1.19.0
github.com/warthog618/go-gpiocdev v0.9.1 github.com/warthog618/go-gpiocdev v0.9.1

6
go.sum
View File

@@ -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/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/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.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 h1:I93ILTm5ytF4e5+lEQXSXcydS26D9eVyJ4H6z3rJqMA=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.53/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ= 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= 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.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 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/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 h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 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= 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-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-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-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 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@@ -55,7 +55,7 @@ func PlayBgm() leaf.HandlerFunc {
} }
}() }()
} else { } else {
zap.S().Errorln("背景音乐解析为空") zap.S().Infoln("未解析到背景音乐")
} }
c.Next() c.Next()

View File

@@ -24,7 +24,7 @@ func switchPoint(ctx context.Context, point int) leaf.HandlerFunc {
return play.PushCard(ctx) return play.PushCard(ctx)
case 5: case 5:
// 5号点位(等待插卡) // 5号点位(等待插卡)
return play.WaitCard return play.WaitCard(ctx)
default: default:
return play.Default return play.Default
} }

View File

@@ -1,4 +1,4 @@
package card_device package card_pusher
import ( import (
"fmt" "fmt"

View File

@@ -1,4 +1,4 @@
package card_device package card_pusher
type LineGroup struct { type LineGroup struct {
Name string Name string

View File

@@ -1,4 +1,4 @@
package card_device package card_pusher
import ( import (
"strconv" "strconv"

View File

@@ -5,7 +5,7 @@ import (
"encoding/json" "encoding/json"
"game-driver/config" "game-driver/config"
"game-driver/internal/middleware" "game-driver/internal/middleware"
"game-driver/internal/routes/play/card_device" "game-driver/internal/routes/play/card_pusher"
"game-driver/internal/schema" "game-driver/internal/schema"
"game-driver/leaf" "game-driver/leaf"
"game-driver/pkg/utils" "game-driver/pkg/utils"
@@ -16,19 +16,20 @@ import (
) )
type ResponseBody struct { type ResponseBody struct {
Empty int `json:"empty"` CardId string `json:"card_id"`
Error int `json:"error"` Empty int `json:"empty"`
OutOk int `json:"out_ok"` Error int `json:"error"`
num int OutOk int `json:"out_ok"`
num int
} }
func PushCard(ctx context.Context) leaf.HandlerFunc { 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 { for _, group := range config.C.Game.CardGroups {
gv, _ := json.Marshal(group) gv, _ := json.Marshal(group)
zap.S().Info("发卡器指针:", string(gv)) zap.S().Info("发卡器指针:", string(gv))
device, err := card_device.New(group) device, err := card_pusher.New(group)
if err != nil { if err != nil {
zap.S().Panicln("初始化发卡器失败: ", err) zap.S().Panicln("初始化发卡器失败: ", err)
} }

View File

@@ -1,44 +1,109 @@
package play package play
import ( import (
"context"
"game-driver/internal/middleware" "game-driver/internal/middleware"
"game-driver/internal/schema" "game-driver/internal/schema"
"game-driver/leaf" "game-driver/leaf"
"game-driver/pkg/card_reader"
"game-driver/pkg/channel"
"game-driver/pkg/tts"
"go.uber.org/zap"
"sync" "sync"
"time" "time"
) )
func WaitCard(c *leaf.Context) { func WaitCard(ctx context.Context) leaf.HandlerFunc {
payload := leaf.Value[*schema.PlayModal](c, middleware.PayloadJSONKey) reader, err := card_reader.NewReader("rtu:///dev/ttyUSB0")
if err != nil {
var waitCard time.Duration zap.S().Panicln("读卡器串口连接失败", err)
if a, ok := payload.Game["wait_card"]; ok {
if v, ok := a.(float64); ok {
waitCard = time.Duration(v)
}
} }
// 等待组
var wait sync.WaitGroup
defer wait.Wait()
a := make(chan string)
defer close(a)
wait.Add(1)
go func() { go func() {
defer wait.Done() <-ctx.Done()
//TODO: 模拟卡片 3s 插入 _ = reader.Close()
time.Sleep(3 * time.Second)
a <- "卡片数据"
}() }()
select { err = reader.Init()
case <-c.Done(): if err != nil {
case <-time.After(waitCard * time.Second): zap.S().Panicln("读卡器初始化失败", err)
case _, ok := <-a: // 等待卡片插入 }
if ok { // 非关闭信号
Default(c) 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)
}
}
} }
} }
} }

137
pkg/card_reader/reader.go Normal file
View File

@@ -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
}

54
pkg/channel/channel.go Normal file
View File

@@ -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),
}
}