Merge branch 'refs/heads/main' into clean_beep

This commit is contained in:
2024-12-19 10:54:06 +08:00
30 changed files with 661 additions and 149 deletions

2
.gitignore vendored
View File

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

View File

@@ -4,8 +4,11 @@ Copyright © 2024 慕枫Go <mapleafgo@163.com>
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")
}
}

View File

@@ -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 的读卡器配置

View File

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

16
config/game/game.go Normal file
View File

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

8
config/game/push_card.go Normal file
View File

@@ -0,0 +1,8 @@
package game
import "game-driver/internal/routes/play/card_pusher"
type ConfigPush struct {
PushGroups []*card_pusher.LineGroup
ReadAddr string
}

5
config/game/wait_card.go Normal file
View File

@@ -0,0 +1,5 @@
package game
type ConfigWait struct {
Addr string
}

48
demo/gpio2/main.go Normal file
View File

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

41
demo/modbus2/main.go Normal file
View File

@@ -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("关闭完成")
}

41
demo/relay/main.go Normal file
View File

@@ -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("关闭完成")
}

BIN
docs/继电器LH-04.zip Executable file

Binary file not shown.

Binary file not shown.

29
game.md
View File

@@ -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` 为插卡后持续的现场效果时间。
`wait_card` 为等待插卡的时长,超过时长将直接结束且没有任何现场效果。`wait` 为插卡后持续的现场效果时间。

4
go.mod
View File

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

12
go.sum
View File

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

View File

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

View File

@@ -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("关闭电源")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

View File

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

View File

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

View File

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