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/cmd/root.go b/cmd/root.go index 4093eec..f1d4864 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,8 +4,11 @@ Copyright © 2024 慕枫Go package cmd import ( + "errors" "game-driver/config" + "game-driver/config/game" "game-driver/internal" + "io/fs" "log" "os" @@ -45,22 +48,35 @@ func Execute() { func init() { cobra.OnInitialize(initConfig) - rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "config.yml", "默认当前目录下的config.yml") + rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "config.yml", "默认当前目录下的 config.yml") } // initConfig reads in config file and ENV variables if set. func initConfig() { viper.SetConfigFile(cfgFile) - viper.AutomaticEnv() // read in environment variables that match - // If a config file is found, read it in. if err := viper.ReadInConfig(); err == nil { log.Printf("Using config file: %s", viper.ConfigFileUsed()) + } else if errors.Is(err, fs.ErrNotExist) { + log.Printf("无法找到主配置文件: %s", viper.ConfigFileUsed()) + os.Exit(1) + } else { + log.Panicln("read config file error: ", err) } - err := viper.Unmarshal(&config.C) if err != nil { log.Panicln("unmarshal config failed: ", err) } + + // 初始化游戏节点配置 + game.G = game.NewConfig(config.C.Point) + if game.G != nil { // 如果需要游戏配置 + err = viper.UnmarshalKey("game", &game.G) + if err != nil { + log.Panicln("unmarshal game config failed: ", err) + } + } else { + log.Panicln("game config not found") + } } diff --git a/config.yml b/config.yml index e4da68f..fb3c1cb 100755 --- a/config.yml +++ b/config.yml @@ -1,6 +1,7 @@ location: wushan -point: 0 -relay: COM7 +point: 4 +relay: /dev/ttyUSB1 +maxTimeout: 60 # 单位 s,必须大于 0 log: level: debug file: @@ -16,14 +17,16 @@ aliyun: accessKeySecret: appKey: timeout: 10 # 单位 s + voice: zhifeng_emo game: - maxTimeout: 60 # 单位 s - cardGroups: + # addr: /dev/ttyUSB0 # 点位 5 的串口地址 + pushGroups: # 点位 4 的发卡器配置 - name: gpiochip0 - outOK: 6 - lower: 13 - error: 19 - empty: 26 - push: 11 - reset: 22 - pull: 27 + outOK: 12 + lower: 1 + error: 7 + empty: 8 + push: 0 + reset: 5 + pull: 6 + readAddr: /dev/ttyUSB0 # 点位 4 的读卡器配置 \ No newline at end of file diff --git a/config/config.go b/config/config.go index b95bf02..3158ea1 100644 --- a/config/config.go +++ b/config/config.go @@ -1,7 +1,6 @@ package config import ( - "game-driver/internal/routes/play/card_device" "gopkg.in/natefinch/lumberjack.v2" ) @@ -14,11 +13,7 @@ type AliyunConfig struct { AccessKeySecret string AppKey string Timeout int -} - -type GameConfig struct { - MaxTimeout int - CardGroups []*card_device.LineGroup + Voice string } type Logger struct { @@ -27,13 +22,13 @@ type Logger struct { } type config struct { - Location string - Point int - Relay string - Log Logger - Mqtt MqttConfig - Aliyun AliyunConfig - Game GameConfig + Location string + Point int + Relay string + Log Logger + Mqtt MqttConfig + Aliyun AliyunConfig + MaxTimeout int } var C config diff --git a/config/game/game.go b/config/game/game.go new file mode 100644 index 0000000..e549618 --- /dev/null +++ b/config/game/game.go @@ -0,0 +1,16 @@ +package game + +type Config any + +func NewConfig(point int) Config { + switch point { + case 4: + return ConfigPush{} + case 5: + return ConfigWait{} + default: + return nil + } +} + +var G Config diff --git a/config/game/push_card.go b/config/game/push_card.go new file mode 100644 index 0000000..169fd7a --- /dev/null +++ b/config/game/push_card.go @@ -0,0 +1,8 @@ +package game + +import "game-driver/internal/routes/play/card_pusher" + +type ConfigPush struct { + PushGroups []*card_pusher.LineGroup + ReadAddr string +} diff --git a/config/game/wait_card.go b/config/game/wait_card.go new file mode 100644 index 0000000..9fd449d --- /dev/null +++ b/config/game/wait_card.go @@ -0,0 +1,5 @@ +package game + +type ConfigWait struct { + Addr string +} diff --git a/demo/gpio2/main.go b/demo/gpio2/main.go new file mode 100644 index 0000000..c675f19 --- /dev/null +++ b/demo/gpio2/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + "game-driver/internal/routes/play/card_pusher" + "game-driver/logger" + "github.com/warthog618/go-gpiocdev/device/rpi" + "time" +) + +func main() { + logger.DefaultLogger() + defer logger.Sync() + + device, err := card_pusher.New(&card_pusher.LineGroup{ + Name: "gpiochip0", + OutOK: rpi.GPIO6, + Lower: rpi.GPIO13, + Error: rpi.GPIO19, + Empty: rpi.GPIO26, + Push: rpi.GPIO11, + Reset: rpi.GPIO22, + Pull: rpi.GPIO27, + }) + if err != nil { + fmt.Println("打开 GPIO 设备失败:", err) + return + } + defer device.Close() + + for { + var userInput string + fmt.Println("按 o/p/r 发送信号") + _, _ = fmt.Scanln(&userInput) + + if userInput == "o" { + device.PushCard() + } + if userInput == "p" { + device.PullCard() + } + if userInput == "r" { + device.Reset() + } + + time.Sleep(1 * time.Second) + } +} diff --git a/demo/modbus2/main.go b/demo/modbus2/main.go new file mode 100644 index 0000000..21ac104 --- /dev/null +++ b/demo/modbus2/main.go @@ -0,0 +1,41 @@ +package main + +import ( + "context" + "fmt" + "game-driver/logger" + "game-driver/pkg/card_reader" + "go.uber.org/zap" + "os" + "os/signal" + "syscall" +) + +func main() { + logger.DefaultLogger() + defer logger.Sync() + + reader, err := card_reader.NewReader("/dev/ttyUSB0") + if err != nil { + zap.S().Panicln(err) + } + defer reader.Close() + + err = reader.Init() + if err != nil { + zap.S().Panicln(err) + } + + ctx, cancel := context.WithCancel(context.Background()) + go reader.OnCardInfo(ctx, func(info *card_reader.CardInfo) { + zap.S().Infow("Card info", "Type", fmt.Sprintf("%#x", info.Type), "ID", info.ID) + }) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGTERM) + + <-sig + zap.S().Infoln("接收到关闭命令 - 正在关闭程序") + cancel() + zap.S().Infoln("关闭完成") +} diff --git a/demo/relay/main.go b/demo/relay/main.go new file mode 100644 index 0000000..4d22735 --- /dev/null +++ b/demo/relay/main.go @@ -0,0 +1,41 @@ +package main + +import ( + "game-driver/logger" + "game-driver/pkg/relay" + "go.uber.org/zap" + "os" + "os/signal" + "syscall" + "time" +) + +func main() { + logger.DefaultLogger() + defer logger.Sync() + + r, err := relay.New("/dev/ttyUSB0") + if err != nil { + zap.S().Panicln(err) + } + defer r.Close() + + for i := 0; i < 4; i++ { + func(num int) { + r.On(num) + defer r.Off(num) + time.Sleep(1 * time.Second) + }(i) + time.Sleep(1 * time.Second) + } + + r.OnAll() + defer r.OffAll() + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGTERM) + + <-sig + zap.S().Infoln("接收到关闭命令 - 正在关闭程序") + zap.S().Infoln("关闭完成") +} diff --git a/docs/继电器LH-04.zip b/docs/继电器LH-04.zip new file mode 100755 index 0000000..4782d55 Binary files /dev/null and b/docs/继电器LH-04.zip differ 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 0de7655..66d37a8 100644 --- a/game.md +++ b/game.md @@ -22,13 +22,24 @@ ```json lines { // 播放文件地址,支持 file:// 本地文件地址、 http(s):// 远程文件地址 - "video": "", + "video": "" } ``` ## 神女除妖(3) -除妖处不需要任何额外的处理,待游戏自然完成即可 +参照 [请求响应文档](https://www.emqx.com/zh/blog/mqtt5-request-response), +发送附带`ResponseTopic`,并订阅`ResponseTopic`,才能接收到响应结果 + +### RequestPayload + +```json lines +{} +``` + +### ResponsePayload + +暂时还未对接除妖设备,不知道返回的数据结构是什么 ```json lines {} @@ -36,8 +47,8 @@ ## 神女授书(4) -参照 [请求响应文档](https://www.emqx.com/zh/blog/mqtt5-request-response),发送附带`ResponseTopic`,并订阅`ResponseTopic` -,才能接收到响应结果 +参照 [请求响应文档](https://www.emqx.com/zh/blog/mqtt5-request-response), +发送附带`ResponseTopic`,并订阅`ResponseTopic`,才能接收到响应结果 ### RequestPayload @@ -54,6 +65,8 @@ ```json lines { + // 发出的卡片ID + "card_id": "", // 空卡设备数量 "empty": 0, // 错误设备数量 @@ -71,6 +84,12 @@ ```json lines { + // 需要的卡片ID + "card_id": "", + // 卡片错误时的播报内容 + "card_error": "", + // 卡片正确时播报的恭喜通关内容 + "card_ok": "", // 等待插卡时间,单位秒 "wait_card": 0, // 插卡后持续时间,单位秒 @@ -78,4 +97,4 @@ } ``` -`wait_card` 为等待插卡的时长,超过时长将直接结束且没有任何现场效果。`wait` 为插卡后持续的现场效果时间。 \ No newline at end of file +`wait_card` 为等待插卡的时长,超过时长将直接结束且没有任何现场效果。`wait` 为插卡后持续的现场效果时间。 diff --git a/go.mod b/go.mod index 114267f..4e9e33c 100644 --- a/go.mod +++ b/go.mod @@ -6,21 +6,21 @@ 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/grid-x/modbus v0.0.0-20241004123532-f6c6fb5201b3 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 github.com/warthog618/go-gpiocdev v0.9.1 - go.bug.st/serial v1.6.2 go.uber.org/zap v1.27.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 ) require ( github.com/aliyun/alibaba-cloud-sdk-go v1.63.53 // indirect - github.com/creack/goselect v0.1.2 // indirect github.com/ebitengine/oto/v3 v3.3.1 // indirect github.com/ebitengine/purego v0.8.1 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect + github.com/grid-x/serial v0.0.0-20211107191517-583c7356b3aa // indirect github.com/hajimehoshi/go-mp3 v0.3.4 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect diff --git a/go.sum b/go.sum index 88d1877..56bc9df 100644 --- a/go.sum +++ b/go.sum @@ -3,15 +3,11 @@ 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= github.com/aliyun/alibabacloud-nls-go-sdk v1.1.1/go.mod h1:4BDMUKpEaP/Ct79w0ozR0nbnEj49g1k3mrgX/IKG5I4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= -github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -41,6 +37,10 @@ github.com/gopxl/beep/v2 v2.1.0/go.mod h1:sQvj2oSsu8fmmDWH3t0DzIe0OZzTW6/TJEHW4K github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grid-x/modbus v0.0.0-20241004123532-f6c6fb5201b3 h1:TfBJ561lUg0i0GLsxKeRaWoBGN8nyCLNt0OMGRx7R2M= +github.com/grid-x/modbus v0.0.0-20241004123532-f6c6fb5201b3/go.mod h1:WpbUAyptAAi0VAriSRopZa6uhiJOJCTz7KFvgGtNRXc= +github.com/grid-x/serial v0.0.0-20211107191517-583c7356b3aa h1:Rsn6ARgNkXrsXJIzhkE4vQr5Gbx2LvtEMv4BJOK4LyU= +github.com/grid-x/serial v0.0.0-20211107191517-583c7356b3aa/go.mod h1:kdOd86/VGFWRrtkNwf1MPk0u1gIjc4Y7R2j7nhwc7Rk= github.com/hajimehoshi/go-mp3 v0.3.4 h1:NUP7pBYH8OguP4diaTZ9wJbUbk3tC0KlfzsEpWmYj68= github.com/hajimehoshi/go-mp3 v0.3.4/go.mod h1:fRtZraRFcWb0pu7ok0LqyFhCUrPeMsGRSVop0eemFmo= github.com/hajimehoshi/oto/v2 v2.3.1/go.mod h1:seWLbgHH7AyUMYKfKYT9pg7PhUu9/SisyJvNTT+ASQo= @@ -126,8 +126,6 @@ github.com/warthog618/go-gpiocdev v0.9.1 h1:pwHPaqjJfhCipIQl78V+O3l9OKHivdRDdmgX github.com/warthog618/go-gpiocdev v0.9.1/go.mod h1:dN3e3t/S2aSNC+hgigGE/dBW8jE1ONk9bDSEYfoPyl8= github.com/warthog618/go-gpiosim v0.1.1 h1:MRAEv+T+itmw+3GeIGpQJBfanUVyg0l3JCTwHtwdre4= github.com/warthog618/go-gpiosim v0.1.1/go.mod h1:YXsnB+I9jdCMY4YAlMSRrlts25ltjmuIsrnoUrBLdqU= -go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8= -go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -193,4 +191,6 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw= +pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/internal/middleware/bgm.go b/internal/middleware/bgm.go index 6f3c7af..6841222 100644 --- a/internal/middleware/bgm.go +++ b/internal/middleware/bgm.go @@ -53,7 +53,7 @@ func PlayBgm() leaf.HandlerFunc { // } //}() } else { - zap.S().Errorln("背景音乐解析为空") + zap.S().Infoln("未解析到背景音乐") } c.Next() diff --git a/internal/middleware/relay.go b/internal/middleware/relay.go index ffee498..3c0436f 100644 --- a/internal/middleware/relay.go +++ b/internal/middleware/relay.go @@ -8,12 +8,12 @@ import ( ) // RelayMaster 继电器中间件 -func RelayMaster(r *relay.Device) leaf.HandlerFunc { +func RelayMaster(r relay.Relay) leaf.HandlerFunc { return func(c *leaf.Context) { pm := leaf.Value[*schema.PlayModal](c, PayloadJSONKey) if r != nil && pm.Power { - r.On(1) - defer r.Off(1) + r.On(0) + defer r.Off(0) zap.S().Infoln("开启电源") defer zap.S().Infoln("关闭电源") 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..1225fa2 100644 --- a/internal/routes/play/push_card.go +++ b/internal/routes/play/push_card.go @@ -3,32 +3,63 @@ package play import ( "context" "encoding/json" - "game-driver/config" + "errors" + "game-driver/config/game" "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/card_reader" "game-driver/pkg/utils" "github.com/eclipse/paho.golang/paho" "go.uber.org/zap" + "io/fs" + "os" "sync" "time" ) 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) - for _, group := range config.C.Game.CardGroups { + g := (game.G).(game.ConfigPush) + + // 开始初始化读卡器 + reader, err := card_reader.NewReader(g.ReadAddr) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + zap.S().Errorf("读卡器串口文件不存在: %s", g.ReadAddr) + os.Exit(1) + } + zap.S().Panicf("读卡器串口连接失败 [%T]: %q", err, err) + } + go func() { + <-ctx.Done() + _ = reader.Close() + }() + + // 开始初始化发卡器 + devices := make([]*card_pusher.Device, 0) + for i, group := range g.PushGroups { + // 对读卡器初始化配置 + reader.SetSlave(byte(i + 1)) + err = reader.Init() + if err != nil { + zap.S().Panicln("读卡器初始配置失败", err) + } + + // 解析发卡器配置 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) } @@ -81,6 +112,13 @@ func PushCard(ctx context.Context) leaf.HandlerFunc { // 延迟1秒获取结果并发送消息 time.AfterFunc(time.Second, func() { if body.num != 0 { + // 若卡片就位,读取卡片信息 + if devices[body.num-1].GetOutOk() == 1 { + reader.SetSlave(byte(body.num)) + if info := reader.GetCardInfo(); info != nil { + body.CardId = info.ID + } + } body.OutOk += devices[body.num-1].GetOutOk() } publishBody(ctx, c.Properties.ResponseTopic, body) diff --git a/internal/routes/play/wait_card.go b/internal/routes/play/wait_card.go index ff400f2..8d5ce41 100644 --- a/internal/routes/play/wait_card.go +++ b/internal/routes/play/wait_card.go @@ -1,43 +1,119 @@ package play import ( + "context" + "errors" + "game-driver/config/game" "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" + "io/fs" + "os" "sync" "time" ) -func WaitCard(c *leaf.Context) { - payload := leaf.Value[*schema.PlayModal](c, middleware.PayloadJSONKey) +func WaitCard(ctx context.Context) leaf.HandlerFunc { + g := (game.G).(game.ConfigWait) - var waitCard time.Duration - if a, ok := payload.Game["wait_card"]; ok { - if v, ok := a.(float64); ok { - waitCard = time.Duration(v) + reader, err := card_reader.NewReader(g.Addr) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + zap.S().Errorf("读卡器串口文件不存在: %s", g.Addr) + os.Exit(1) } + zap.S().Panicf("读卡器串口连接失败 [%T]: %q", err, err) } - - // 等待组 - var wait sync.WaitGroup - defer wait.Wait() - - a := make(chan string) - defer close(a) - - wait.Add(1) go func() { - defer wait.Done() - 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/internal/routes/wait.go b/internal/routes/wait.go index 430acf3..f3165e6 100644 --- a/internal/routes/wait.go +++ b/internal/routes/wait.go @@ -175,12 +175,12 @@ func relayAction(c *leaf.Context, item schema.WaitItemModel, root schema.TimeMod defer cancel() } - device, err := relay.New(item.Data, nil) + r, err := relay.New(item.Data) if err != nil { zap.S().Errorln("继电器初始化异常: ", err) return } - defer device.Close() + defer r.Close() select { case <-c.Done(): @@ -189,9 +189,9 @@ func relayAction(c *leaf.Context, item schema.WaitItemModel, root schema.TimeMod zap.S().Infoln("待机继电器供电") defer zap.S().Infoln("待机继电器断电") - device.On(1) + r.On(0) <-c.Done() - device.Off(1) + r.Off(0) } } } diff --git a/internal/server.go b/internal/server.go index a0f056d..edec86d 100644 --- a/internal/server.go +++ b/internal/server.go @@ -10,6 +10,7 @@ import ( "game-driver/internal/schema" "game-driver/leaf" "game-driver/logger" + "game-driver/pkg/relay" "game-driver/pkg/tts" "game-driver/pkg/utils" "github.com/eclipse/paho.golang/autopaho" @@ -108,13 +109,11 @@ func Run() { tts.DefaultTTS = tts.New(ctx, config.C.Aliyun) // 构建继电器对象 - //r, err := relay.New(config.C.Relay, func(msg string) { - // zap.S().Infoln("串口返回: ", msg) - //}) - //if err != nil { - // zap.S().Panicln("串口连接异常: ", err) - //} - //defer r.Close() + r, err := relay.New(config.C.Relay) + if err != nil { + zap.S().Panicln("串口连接异常: ", err) + } + defer r.Close() // 构建全局设备变量 device := common.DefaultDevice(ctx, publishTopic) @@ -129,7 +128,7 @@ func Run() { middleware.EmergencyStop(common.GlobalStopper), middleware.SoundStart(), middleware.RelayMaster(nil), - middleware.TimeoutOver(config.C.Game.MaxTimeout), + middleware.TimeoutOver(config.C.MaxTimeout), middleware.TickerAction(), middleware.PlayBgm(), routes.PlayRouter(ctx, config.C.Location, config.C.Point), diff --git a/pkg/card_reader/reader.go b/pkg/card_reader/reader.go new file mode 100644 index 0000000..3d658c8 --- /dev/null +++ b/pkg/card_reader/reader.go @@ -0,0 +1,155 @@ +package card_reader + +import ( + "context" + "fmt" + "github.com/grid-x/modbus" + "go.uber.org/zap" + "io" + "math" + "strings" + "time" +) + +type Reader interface { + io.Closer + // SetSlave 设置读卡器的 UnitID + SetSlave(byte) + // WriteUnitId 写入读卡器的 UnitID + WriteUnitId(uint16) error + // Init 初始化读卡器配置 + Init() error + // OnCardInfo 会在卡号信息发生变化时调用回调函数,直到上下文被取消,或者 Reader 被关闭。需要在 goroutine 中调用。 + OnCardInfo(context.Context, func(*CardInfo)) + GetCardInfo() *CardInfo +} + +type CardInfo struct { + Type []byte + ID string +} + +type reader struct { + h modbus.ClientHandler + c modbus.Client +} + +// WriteUnitId 写入读卡器的 UnitID +func (r *reader) WriteUnitId(id uint16) error { + v := id<<8 + 1 + zap.S().Infof("WriteRegister 40007 %#x", v) + _, err := r.c.WriteSingleRegister(6, v) + return err +} + +// Init 初始化读卡器配置 +func (r *reader) Init() error { + // 配置读卡间隔时间为 50 毫秒 + _, err := r.c.WriteSingleRegister(8, 0x0511) + if err != nil { + return err + } + // 由低到高输出 500 毫秒 + _, err = r.c.WriteSingleRegister(9, 0xF20A) + if err != nil { + return err + } + // 读取后自动清空,高字节:00 不清空 01 清空 + _, err = r.c.WriteSingleRegister(10, 0x0103) + if err != nil { + return err + } + // 自动读卡号,高字节高4位:0 读一次 1 连续读 + _, err = r.c.WriteSingleRegister(11, 0x0301) + if err != nil { + return err + } + return nil +} + +// GetCardInfo 获取卡号信息 +func (r *reader) GetCardInfo() *CardInfo { + // 读取状态寄存器确认是否信息已经准备好 + status, err := r.c.ReadHoldingRegisters(29, 1) + if err != nil { + zap.S().Errorln("ReadRegister 40030 error:", err) + return nil + } else if len(status) != 2 || status[1] == 0 { + return nil + } + + // 读取卡号长度 + cardLength, err := r.c.ReadHoldingRegisters(30, 1) + if err != nil { + zap.S().Errorln("ReadRegister 40031 error:", err) + return nil + } + dataLength := cardLength[0] + + // 读取卡类型 + cardType, err := r.c.ReadHoldingRegisters(31, 1) + if err != nil { + zap.S().Errorln("ReadRegister 40032 error:", err) + return nil + } + + // 读取卡号数据 + cardData, err := r.c.ReadHoldingRegisters(32, uint16(math.Round(float64(dataLength)/2))) + if err != nil { + zap.S().Errorln("ReadRegister 40033~ error:", err) + return nil + } + + s := make([]string, dataLength) + for i := 0; i < int(dataLength); i++ { + s[i] = fmt.Sprintf("%02X", cardData[i]) + } + + return &CardInfo{ + Type: cardType, + ID: strings.Join(s, ""), + } +} + +// OnCardInfo 会在卡号信息发生变化时调用回调函数,直到上下文被取消,或者 Reader 被关闭。需要在 goroutine 中调用。 +func (r *reader) OnCardInfo(ctx context.Context, f func(info *CardInfo)) { + for { + select { + case <-ctx.Done(): + return + case <-time.After(100 * time.Millisecond): + info := r.GetCardInfo() + if info != nil { + f(info) + } + } + } +} + +func (r *reader) SetSlave(unitID byte) { + r.h.SetSlave(unitID) +} + +func (r *reader) Close() error { + return r.h.Close() +} + +// NewReader 创建一个新的读卡器 +func NewReader(address string) (Reader, error) { + // 配置串口客户端 + h := modbus.NewRTUClientHandler(address) + h.SlaveID = 1 + h.BaudRate = 9600 + h.DataBits = 8 + h.Parity = "N" + h.StopBits = 1 + + // 连接串口 + if err := h.Connect(); err != nil { + return nil, err + } + // 创建 modbus 客户端 + c := modbus.NewClient(h) + + return &reader{h, 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), + } +} diff --git a/pkg/relay/portlist.go b/pkg/relay/portlist.go deleted file mode 100644 index 1a9d510..0000000 --- a/pkg/relay/portlist.go +++ /dev/null @@ -1,28 +0,0 @@ -package relay - -import ( - "fmt" - "log" - - "go.bug.st/serial/enumerator" -) - -func PrintPorts() { - ports, err := enumerator.GetDetailedPortsList() - if err != nil { - log.Fatal(err) - } - if len(ports) == 0 { - return - } - for _, port := range ports { - fmt.Printf("Port: %s\n", port.Name) - if port.Product != "" { - fmt.Printf(" Product Name: %s\n", port.Product) - } - if port.IsUSB { - fmt.Printf(" USB ID : %s:%s\n", port.VID, port.PID) - fmt.Printf(" USB serial : %s\n", port.SerialNumber) - } - } -} diff --git a/pkg/relay/relay.go b/pkg/relay/relay.go index 452f0d4..c027b60 100644 --- a/pkg/relay/relay.go +++ b/pkg/relay/relay.go @@ -1,46 +1,69 @@ package relay import ( - "bufio" - "fmt" - "go.bug.st/serial" + "github.com/grid-x/modbus" "io" ) -type Device struct { - port serial.Port +type Relay interface { + io.Closer + // SetSlave 设置继电器的 UnitID + SetSlave(byte) + // On 打开继电器 + On(int) error + // Off 关闭继电器 + Off(int) error + // OnAll 打开所有继电器 + OnAll() error + // OffAll 关闭所有继电器 + OffAll() error } -func (r *Device) Close() error { - return r.port.Close() +type device struct { + h modbus.ClientHandler + c modbus.Client } -func (r *Device) On(num int) error { - _, err := io.WriteString(r.port, fmt.Sprintf("AT+OUT%v+1=ON\r\n", num)) +func (r *device) SetSlave(slaveID byte) { + r.h.SetSlave(slaveID) +} + +func (r *device) Close() error { + return r.h.Close() +} + +func (r *device) OnAll() error { + _, err := r.c.WriteMultipleCoils(uint16(0), 16, []byte{0xFF, 0xFF}) return err } -func (r *Device) Off(num int) error { - _, err := io.WriteString(r.port, fmt.Sprintf("AT+OUT%v+1=OFF\r\n", num)) +func (r *device) OffAll() error { + _, err := r.c.WriteMultipleCoils(uint16(0), 16, []byte{0x00, 0x00}) return err } -func New(portName string, reader func(msg string)) (*Device, error) { - port, err := serial.Open(portName, &serial.Mode{ - BaudRate: 9600, - DataBits: 8, - }) - if err != nil { +func (r *device) On(num int) error { + _, err := r.c.WriteSingleCoil(uint16(num), 0xFF00) + return err +} + +func (r *device) Off(num int) error { + _, err := r.c.WriteSingleCoil(uint16(num), 0x0000) + return err +} + +func New(address string) (Relay, error) { + h := modbus.NewRTUClientHandler(address) + h.SlaveID = 1 + h.BaudRate = 9600 + h.DataBits = 8 + h.Parity = "N" + h.StopBits = 1 + + if err := h.Connect(); err != nil { return nil, err } - go func() { - for { - r := bufio.NewReader(port) - line, _, _ := r.ReadLine() - if reader != nil { - reader(string(line)) - } - } - }() - return &Device{port: port}, nil + c := modbus.NewClient(h) + + return &device{h, c}, nil } diff --git a/pkg/tts/aliyun.go b/pkg/tts/aliyun.go index 94d9985..1613d06 100644 --- a/pkg/tts/aliyun.go +++ b/pkg/tts/aliyun.go @@ -70,6 +70,7 @@ func (tts *AliTTS) getToken() error { func (tts *AliTTS) Get(text string) (io.Reader, error) { param := nls.DefaultSpeechSynthesisParam() param.Volume = 100 + param.Voice = tts.Voice err := tts.getToken() if err != nil {