基本逻辑完成

This commit is contained in:
2024-11-01 17:40:34 +08:00
commit f9b9beea4b
40 changed files with 1869 additions and 0 deletions

4
.cobra.yaml Normal file
View File

@@ -0,0 +1,4 @@
author: 慕枫Go <mapleafgo@163.com>
year: 2024
license: none
useViper: true

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

9
.idea/game-driver.iml generated Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/game-driver.iml" filepath="$PROJECT_DIR$/.idea/game-driver.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

BIN
531241201.mp3 Normal file

Binary file not shown.

0
LICENSE Normal file
View File

67
cmd/root.go Normal file
View File

@@ -0,0 +1,67 @@
/*
Copyright © 2024 慕枫Go <mapleafgo@163.com>
*/
package cmd
import (
"fmt"
"game-driver/config"
"game-driver/internal"
"log"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var cfgFile string
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "game-driver",
Version: "0.0.1",
Short: "A brief description of your application",
Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
// Uncomment the following line if your bare application
// has an action associated with it:
Run: func(cmd *cobra.Command, args []string) {
internal.Run()
},
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
cobra.OnInitialize(initConfig)
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 {
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
}
err := viper.Unmarshal(&config.C)
if err != nil {
log.Panicln("unmarshal config failed: ", err)
}
}

11
config.yml Normal file
View File

@@ -0,0 +1,11 @@
location: wushan
point: 0
relay: COM3
mqtt:
url: mqtt://36.138.38.16:1883
aliyun:
appKey: U7pipZG2pfCp1XJo
token: 21a1e37546404bdab318890648972416
timeout: 10 # 单位 s
game:
maxTimeout: 60 # 单位 s

26
config/config.go Normal file
View File

@@ -0,0 +1,26 @@
package config
type config struct {
Location string
Point int
Relay string
Mqtt MqttConfig
Aliyun AliyunConfig
Game GameConfig
}
type MqttConfig struct {
Url string
}
type AliyunConfig struct {
AppKey string
Token string
Timeout int
}
type GameConfig struct {
MaxTimeout int
}
var C config

0
game.md Normal file
View File

49
go.mod Normal file
View File

@@ -0,0 +1,49 @@
module game-driver
go 1.23.2
require (
github.com/aliyun/alibabacloud-nls-go-sdk v1.1.1
github.com/eclipse/paho.golang v0.21.0
github.com/gopxl/beep/v2 v2.1.0
github.com/spf13/cobra v1.8.1
github.com/spf13/viper v1.19.0
go.bug.st/serial v1.6.2
)
require (
github.com/aliyun/alibaba-cloud-sdk-go v1.63.45 // 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/hajimehoshi/go-mp3 v0.3.4 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sagikazarmark/locafero v0.6.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/satori/go.uuid v1.2.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.uber.org/goleak v1.3.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.19.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

208
go.sum Normal file
View File

@@ -0,0 +1,208 @@
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
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.40 h1:WIALrTgfyI28BYluKFTWQ9sj2lQjgWunsTJCXheDjHA=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.40/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.45 h1:H74VbmrHgZcb7MN9ud8panaIXtY1nLgHZRWjJv2gyKU=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.45/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=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ebitengine/oto/v3 v3.2.0 h1:FuggTJTSI3/3hEYwZEIN0CZVXYT29ZOdCu+z/f4QjTw=
github.com/ebitengine/oto/v3 v3.2.0/go.mod h1:dOKXShvy1EQbIXhXPFcKLargdnFqH0RjptecvyAxhyw=
github.com/ebitengine/oto/v3 v3.3.1 h1:d4McwGQuXOT0GL7bA5g9ZnaUEIEjQvG3hafzMy+T3qE=
github.com/ebitengine/oto/v3 v3.3.1/go.mod h1:MZeb/lwoC4DCOdiTIxYezrURTw7EvK/yF863+tmBI+U=
github.com/ebitengine/purego v0.7.1 h1:6/55d26lG3o9VCZX8lping+bZcmShseiqlh2bnUDiPA=
github.com/ebitengine/purego v0.7.1/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/eclipse/paho.golang v0.21.0 h1:cxxEReu+iFbA5RrHfRGxJOh8tXZKDywuehneoeBeyn8=
github.com/eclipse/paho.golang v0.21.0/go.mod h1:GHF6vy7SvDbDHBguaUpfuBkEB5G6j0zKxMG4gbh6QRQ=
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopxl/beep/v2 v2.1.0 h1:Jv95iHw3aNWoAa/J78YyXvOvMHH2ZGeAYD5ug8tVt8c=
github.com/gopxl/beep/v2 v2.1.0/go.mod h1:sQvj2oSsu8fmmDWH3t0DzIe0OZzTW6/TJEHW4Ku+22o=
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/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=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
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/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=
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e h1:s2RNOM/IGdY0Y6qfTeUKhDawdHDpK9RGBdx80qN4Ttw=
github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e/go.mod h1:nBdnFKj15wFbf94Rwfq4m30eAcyY9V/IyKAGQFtqkW0=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk=
github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg=
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
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=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.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.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

72
internal/common/device.go Normal file
View File

@@ -0,0 +1,72 @@
package common
import (
"context"
"fmt"
"github.com/eclipse/paho.golang/autopaho"
"github.com/eclipse/paho.golang/paho"
"sync"
"sync/atomic"
)
type DeviceMan interface {
sync.Locker
Status() int
PublishStatus()
}
type Device struct {
mu sync.Mutex
C context.Context
cm *autopaho.ConnectionManager
topic string
status atomic.Int32
OnChange func()
}
func (d *Device) Lock() {
defer d.OnChange()
d.mu.Lock()
d.status.Store(1)
}
func (d *Device) Unlock() {
defer d.OnChange()
d.status.Store(0)
d.mu.Unlock()
}
func (d *Device) Status() int {
return int(d.status.Load())
}
// PublishStatus 推送设备状态
func (d *Device) PublishStatus() {
err := d.cm.AwaitConnection(d.C)
if err != nil {
return
}
_, _ = d.cm.Publish(d.C, &paho.Publish{
Topic: d.topic,
Payload: []byte(fmt.Sprint(d.Status())),
QoS: 1,
})
}
func DefaultDevice(ctx context.Context, cm *autopaho.ConnectionManager, topic string) *Device {
return &Device{
C: ctx,
cm: cm,
topic: topic,
}
}
func NewDevice(ctx context.Context, cm *autopaho.ConnectionManager, topic string, onChange func()) *Device {
return &Device{
C: ctx,
cm: cm,
topic: topic,
OnChange: onChange,
}
}

View File

@@ -0,0 +1,58 @@
package common
import (
"sync"
"sync/atomic"
)
type Stopper interface {
Reset()
Stop()
Done() <-chan struct{}
}
var closedchan = make(chan struct{})
func init() {
close(closedchan)
}
type simpleStopper struct {
mu sync.Mutex
done atomic.Value
}
func (g *simpleStopper) Reset() {
g.mu.Lock()
defer g.mu.Unlock()
g.done = atomic.Value{}
}
func (g *simpleStopper) Stop() {
g.mu.Lock()
defer g.mu.Unlock()
d, _ := g.done.Load().(chan struct{})
if d == nil {
g.done.Store(closedchan)
} else {
close(d)
}
}
func (g *simpleStopper) Done() <-chan struct{} {
d := g.done.Load()
if d != nil {
return d.(chan struct{})
}
g.mu.Lock()
defer g.mu.Unlock()
d = g.done.Load()
if d == nil {
d = make(chan struct{})
g.done.Store(d)
}
return d.(chan struct{})
}
// GlobalStopper 全局停止器
var GlobalStopper Stopper = &simpleStopper{}

View File

@@ -0,0 +1,65 @@
package middleware
import (
"bytes"
"game-driver/internal/schema"
"game-driver/leaf"
"game-driver/pkg/audio"
"io"
"log"
"net/http"
"net/url"
"os"
"strings"
)
func open(u string) io.ReadCloser {
p, _ := strings.CutPrefix(u, "file://")
f, e := os.Open(p)
if e != nil {
log.Printf("BGM 文件 [%v] 打开错误: %v\n", u, e)
return nil
}
return f
}
func get(u string) io.ReadCloser {
resp, e := http.Get(u)
if e != nil {
log.Printf("BGM 文件 [%v] 下载失败: %v\n", u, e)
return nil
}
return resp.Body
}
func toBuffer(b io.ReadCloser) io.ReadCloser {
data := &bytes.Buffer{}
_, _ = data.ReadFrom(b)
defer b.Close()
return io.NopCloser(data)
}
func PlayBgm() leaf.HandlerFunc {
return func(c *leaf.Context) {
pm := leaf.Value[*schema.PlayModal](c, PayloadJSONKey)
u, err := url.Parse(pm.BGM)
if err != nil {
log.Println("BGM URL 解析错误: ", err)
} else {
var bgm io.ReadCloser
if u.Scheme == "file" {
bgm = open(u.String())
} else if u.Scheme == "http" || u.Scheme == "https" {
bgm = get(u.String())
} else {
log.Printf("不支持的BGM文件协议: %v\n", u.String())
}
// 获取到了资源进行播放
if bgm != nil {
go audio.PlayMP3(c, toBuffer(bgm))
}
}
c.Next()
}
}

View File

@@ -0,0 +1,16 @@
package middleware
import (
"game-driver/internal/common"
"game-driver/leaf"
)
// DeviceLock 设备锁中间件
func DeviceLock(d *common.Device) leaf.HandlerFunc {
return func(c *leaf.Context) {
d.Lock()
defer d.Unlock()
c.Next()
}
}

View File

@@ -0,0 +1,25 @@
package middleware
import (
"encoding/json"
"game-driver/internal/schema"
"game-driver/leaf"
"log"
)
type JSONKey string
const PayloadJSONKey JSONKey = "payload_json"
// PayloadJSON 解析报文
func PayloadJSON() leaf.HandlerFunc {
return func(c *leaf.Context) {
pm := &schema.PlayModal{}
err := json.Unmarshal(c.Payload, pm)
if err != nil {
log.Panicf("报文解析错误: %v\n", err)
}
leaf.WithValue[*schema.PlayModal](c, PayloadJSONKey, pm)
c.Next()
}
}

View File

@@ -0,0 +1,19 @@
package middleware
import (
"game-driver/internal/schema"
"game-driver/leaf"
"game-driver/pkg/relay"
)
// RelayMaster 继电器中间件
func RelayMaster(r *relay.Device) 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)
}
c.Next()
}
}

View File

@@ -0,0 +1,26 @@
package middleware
import (
"game-driver/internal/schema"
"game-driver/leaf"
"game-driver/pkg/tts"
)
// SoundStart 开始词播报
func SoundStart(t *tts.AliTTS) leaf.HandlerFunc {
return func(c *leaf.Context) {
pm := leaf.Value[*schema.PlayModal](c, PayloadJSONKey)
t.Sound(pm.TTS.Start)
defer func() {
switch leaf.Value[leaf.EndType](c, leaf.EndKey) {
case leaf.EndTimer:
t.Sound(pm.TTS.End)
case leaf.EndStop:
t.Sound(pm.TTS.Stop)
}
}()
c.Next()
}
}

View File

@@ -0,0 +1,32 @@
package middleware
import (
"game-driver/internal/common"
"game-driver/leaf"
)
// EmergencyStop 紧急停止中间件
func EmergencyStop() leaf.HandlerFunc {
return func(c *leaf.Context) {
cancel := leaf.WithCancel(c)
defer common.GlobalStopper.Reset()
// 结束信号通道
a := make(chan struct{})
// 发送结束信号
defer close(a)
go func() {
select {
case <-a:
case <-common.GlobalStopper.Done():
{
cancel()
leaf.WithValue[leaf.EndType](c, leaf.EndKey, leaf.EndStop)
}
}
}()
c.Next()
}
}

View File

@@ -0,0 +1,80 @@
package middleware
import (
"game-driver/internal/schema"
"game-driver/leaf"
"game-driver/pkg/tts"
"sync"
"time"
)
// TimerAction 定时器中间件,用于定时触发屏幕打印和语音播报。 t 是语音播报实例
func TimerAction(t *tts.AliTTS, maxTimeout int) leaf.HandlerFunc {
return func(c *leaf.Context) {
pm := leaf.Value[*schema.PlayModal](c, PayloadJSONKey)
// 构建打印和语音播报的时间映射
printMap := make(map[int]schema.PrintModal, len(pm.Print))
for _, p := range pm.Print {
printMap[p.Time] = p
}
ttsMap := make(map[int]schema.TTSTimer, len(pm.TTS.Timer))
for _, t := range pm.TTS.Timer {
ttsMap[t.Time] = t
}
// 定时器
var timer *time.Timer
if pm.Timeout != 0 {
timer = time.NewTimer(time.Second * time.Duration(pm.Timeout))
} else {
timer = time.NewTimer(time.Second * time.Duration(maxTimeout))
}
defer timer.Stop()
// 等待组
var wait sync.WaitGroup
defer wait.Wait()
// 结束信号通道
a := make(chan struct{})
// 发送结束信号
defer close(a)
cancel := leaf.WithCancel(c)
go func() {
start := time.Now()
// 等待结束
wait.Add(1)
defer wait.Done()
// 定时器
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
// 结束标志
for over := false; !over; {
select {
case <-a:
over = true
case <-timer.C: // 定时器结束
{
cancel()
over = true
leaf.WithValue[leaf.EndType](c, leaf.EndKey, leaf.EndTimer)
}
case m := <-ticker.C:
{
s := int(m.Sub(start).Seconds())
if _, ok := printMap[s]; ok {
//TODO: 屏幕打印
}
if to, ok := ttsMap[s]; ok {
t.Sound(to.Value)
}
}
}
}
}()
c.Next()
}
}

View File

@@ -0,0 +1,21 @@
package routes
import (
"game-driver/internal/common"
"game-driver/leaf"
"log"
)
func Command(d *common.Device) leaf.HandlerFunc {
return func(c *leaf.Context) {
cmd := string(c.Payload)
switch cmd {
case "stop":
common.GlobalStopper.Stop()
case "status":
d.PublishStatus()
default:
log.Printf("接收到无效指令: %s\n", cmd)
}
}
}

30
internal/schema/play.go Normal file
View File

@@ -0,0 +1,30 @@
package schema
type TTSTimer struct {
Time int `json:"time"`
Value string `json:"value"`
}
type TTSModal struct {
Start string `json:"start"`
End string `json:"end"`
Stop string `json:"stop"`
Timer []TTSTimer `json:"timer"`
}
type PrintModal struct {
Time int `json:"time"`
Text string `json:"text"`
Duration int `json:"duration"`
}
type PlayModal struct {
Timeout int `json:"timeout"`
Power bool `json:"power"`
BGM string `json:"bgm"`
Volume float64 `json:"volume"`
DefaultPrint string `json:"default-print"`
TTS TTSModal `json:"tts"`
Print []PrintModal `json:"print"`
Game map[string]any `json:"game"`
}

152
internal/server.go Normal file
View File

@@ -0,0 +1,152 @@
package internal
import (
"context"
"fmt"
"game-driver/config"
"game-driver/internal/common"
"game-driver/internal/middleware"
"game-driver/internal/routes"
"game-driver/leaf"
"game-driver/pkg/tts"
"github.com/eclipse/paho.golang/autopaho"
"github.com/eclipse/paho.golang/paho"
"log"
"net/url"
"os"
"os/signal"
"syscall"
"time"
)
func buildMqtt(c config.MqttConfig, r *leaf.Engine, subTopics ...string) autopaho.ClientConfig {
u, err := url.Parse(c.Url)
if err != nil {
log.Panicln("mqtt url parse error: ", err)
}
subscriptions := make([]paho.SubscribeOptions, 0)
for _, topic := range subTopics {
subscriptions = append(subscriptions, paho.SubscribeOptions{Topic: topic, QoS: 0})
}
mqttConfig := autopaho.ClientConfig{
ServerUrls: []*url.URL{u},
KeepAlive: 20,
CleanStartOnInitialConnection: false,
SessionExpiryInterval: 60,
OnConnectionUp: func(cm *autopaho.ConnectionManager, connAck *paho.Connack) {
log.Println("mqtt connection up")
if _, err := cm.Subscribe(context.Background(), &paho.Subscribe{
Subscriptions: subscriptions,
}); err != nil {
log.Printf("failed to subscribe (%s). This is likely to mean no messages will be received.", err)
return
}
log.Println("mqtt subscription made")
},
OnConnectError: func(err error) {
log.Printf("error whilst attempting connection: %s\n", err)
},
ClientConfig: paho.ClientConfig{
ClientID: "TestSubscriber",
OnPublishReceived: []func(paho.PublishReceived) (bool, error){
func(pr paho.PublishReceived) (bool, error) {
r.Route(pr.Packet.Packet())
return true, nil
},
},
OnClientError: func(err error) { fmt.Printf("client error: %s\n", err) },
OnServerDisconnect: func(d *paho.Disconnect) {
if d.Properties != nil {
fmt.Printf("server requested disconnect: %s\n", d.Properties.ReasonString)
} else {
fmt.Printf("server requested disconnect; reason code: %d\n", d.ReasonCode)
}
},
},
}
return mqttConfig
}
func Run() {
topicPrefix := fmt.Sprintf("server/%s/%v/", config.C.Location, config.C.Point)
publishTopic := fmt.Sprintf("device/%s/%v/status", config.C.Location, config.C.Point)
log.Println("topicPrefix: ", topicPrefix)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
router := leaf.Default(ctx)
router.DefaultHandler(func(c *leaf.Context) {
log.Println("接收到未处理消息: " + string(c.Payload))
})
// 构建 MQTT 连接
mqttBuild := buildMqtt(config.C.Mqtt, router, topicPrefix+"#")
// 连接 MQTT
cm, err := autopaho.NewConnection(ctx, mqttBuild)
if err != nil {
log.Panicln("连接 MQTT 异常: ", err)
}
// 构建语音合成对象
t := tts.New(ctx, config.C.Aliyun)
// 构建继电器对象
//r, err := relay.New(config.C.Relay, func(msg string) {
// log.Println("串口返回: ", msg)
//})
//if err != nil {
// log.Panicln("串口连接异常: ", err)
//}
//defer r.Close()
// 构建全局设备变量
device := common.DefaultDevice(ctx, cm, publishTopic)
// 设备状态变化
device.OnChange = func() { device.PublishStatus() }
// 处理启动报文
router.RegisterHandler(topicPrefix+"play",
middleware.PayloadJSON(),
middleware.DeviceLock(device),
middleware.EmergencyStop(),
middleware.SoundStart(t),
middleware.RelayMaster(nil),
middleware.TimerAction(t, config.C.Game.MaxTimeout),
middleware.PlayBgm(),
func(c *leaf.Context) {
log.Println("接收到启动消息: ", string(c.Payload))
select {
case <-c.Done():
log.Println("程序已关闭")
case <-time.After(10 * time.Second):
log.Println("10s 结束")
}
},
)
// 处理指令
router.RegisterHandler(topicPrefix+"command", routes.Command(device))
// 启动完成发送一次设备状态
device.PublishStatus()
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
<-sig
fmt.Println("接收到关闭命令 - 正在关闭程序")
ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := cm.Disconnect(ctx); err != nil {
fmt.Printf("断开连接异常: %s\n", err)
}
fmt.Println("关闭完成")
}

134
leaf/context.go Normal file
View File

@@ -0,0 +1,134 @@
package leaf
import (
"context"
"github.com/eclipse/paho.golang/paho"
"math"
)
// abortIndex represents a typical value used in abort functions.
const abortIndex int8 = math.MaxInt8 >> 1
type endKeyType = string
const EndKey endKeyType = "end"
type EndType int
const (
EndTimer EndType = iota + 1
EndStop
)
type KeyValue struct {
parent *KeyValue
key any
value any
}
var rootKeyType = &KeyValue{}
type Context struct {
context.Context
*paho.Publish
engine *Engine
index int8
handlers HandlersChain
value *KeyValue
}
func WithLeafContext(c context.Context, p *paho.Publish, engine *Engine, handlers HandlersChain) *Context {
return &Context{
Context: c,
Publish: p,
engine: engine,
index: -1,
handlers: handlers,
value: rootKeyType,
}
}
// HandlerName returns the main handler's name. For example if the handler is "handleGetUsers()",
// this function will return "main.handleGetUsers".
func (c *Context) HandlerName() string {
return nameOfFunction(c.handlers.Last())
}
// HandlerNames returns a list of all registered handlers for this context in descending order,
// following the semantics of HandlerName()
func (c *Context) HandlerNames() []string {
hn := make([]string, 0, len(c.handlers))
for _, val := range c.handlers {
if val == nil {
continue
}
hn = append(hn, nameOfFunction(val))
}
return hn
}
// Handler returns the main handler.
func (c *Context) Handler() HandlerFunc {
return c.handlers.Last()
}
/************************************/
/*********** FLOW CONTROL ***********/
/************************************/
// Next should be used only inside middleware.
// It executes the pending handlers in the chain inside the calling handler.
// See example in GitHub.
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
if c.handlers[c.index] == nil {
continue
}
c.handlers[c.index](c)
c.index++
}
}
// IsAborted returns true if the current context was aborted.
func (c *Context) IsAborted() bool {
return c.index >= abortIndex
}
// Abort prevents pending handlers from being called. Note that this will not stop the current handler.
// Let's say you have an authorization middleware that validates that the current request is authorized.
// If the authorization fails (ex: the password does not match), call Abort to ensure the remaining handlers
// for this request are not called.
func (c *Context) Abort() {
c.index = abortIndex
}
func WithValue[T any](ctx *Context, k any, v T) {
ctx.value = &KeyValue{
parent: ctx.value,
key: k,
value: v,
}
}
func Value[T any](ctx *Context, k any) (v T) {
vo := ctx.value
for {
if vo.key == k {
v, _ = vo.value.(T)
break
} else if vo.parent == rootKeyType {
break
}
vo = vo.parent
}
return
}
func WithCancel(ctx *Context) context.CancelFunc {
c, cancel := context.WithCancel(ctx.Context)
ctx.Context = c
return cancel
}

144
leaf/leaf.go Normal file
View File

@@ -0,0 +1,144 @@
package leaf
import (
"context"
"github.com/eclipse/paho.golang/packets"
"github.com/eclipse/paho.golang/paho"
"github.com/eclipse/paho.golang/paho/log"
"sync"
)
type Router interface {
RegisterHandler(string, ...HandlerFunc)
UnregisterHandler(string)
Route(*packets.Publish)
SetDebugLogger(log.Logger)
Use(...HandlerFunc)
}
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)
// OptionFunc defines the function to change the default configuration
type OptionFunc func(*Engine)
// HandlersChain defines a HandlerFunc slice.
type HandlersChain []HandlerFunc
// Last returns the last handler in the chain. i.e. the last handler is the main one.
func (c HandlersChain) Last() HandlerFunc {
if length := len(c); length > 0 {
return c[length-1]
}
return nil
}
type Engine struct {
mu sync.RWMutex
ctx context.Context
Handlers HandlersChain
defaultHandler HandlersChain
subscriptions map[string]HandlersChain
aliases map[uint16]string
debug log.Logger
}
func New(ctx context.Context) *Engine {
return &Engine{
ctx: ctx,
Handlers: make(HandlersChain, 0),
subscriptions: make(map[string]HandlersChain),
aliases: make(map[uint16]string),
debug: log.NOOPLogger{},
}
}
func Default(ctx context.Context) *Engine {
engine := &Engine{
ctx: ctx,
Handlers: make(HandlersChain, 0),
subscriptions: make(map[string]HandlersChain),
aliases: make(map[uint16]string),
debug: log.NOOPLogger{},
}
engine.Use(Recovery())
return engine
}
func (e *Engine) RegisterHandler(topic string, handlers ...HandlerFunc) {
e.debug.Println("registering handler for:", topic)
e.mu.Lock()
defer e.mu.Unlock()
e.subscriptions[topic] = e.combineHandlers(handlers)
}
func (e *Engine) UnregisterHandler(topic string) {
e.debug.Println("unregistering handler for:", topic)
e.mu.Lock()
defer e.mu.Unlock()
delete(e.subscriptions, topic)
}
func (e *Engine) Route(pb *packets.Publish) {
e.debug.Println("routing message for:", pb.Topic)
e.mu.Lock()
defer e.mu.Unlock()
m := paho.PublishFromPacketPublish(pb)
var topic string
if pb.Properties.TopicAlias != nil {
e.debug.Println("message is using topic aliasing")
if pb.Topic != "" {
// Register new alias
e.debug.Printf("registering new topic alias '%d' for topic '%s'", *pb.Properties.TopicAlias, m.Topic)
e.aliases[*pb.Properties.TopicAlias] = pb.Topic
}
if t, ok := e.aliases[*pb.Properties.TopicAlias]; ok {
e.debug.Printf("aliased topic '%d' translates to '%s'", *pb.Properties.TopicAlias, m.Topic)
topic = t
}
} else {
topic = m.Topic
}
handlerCalled := false
for route, handlers := range e.subscriptions {
if match(route, topic) {
e.debug.Println("found handler for:", route)
go WithLeafContext(e.ctx, m, e, handlers).Next()
handlerCalled = true
}
}
if !handlerCalled && e.defaultHandler != nil {
go WithLeafContext(e.ctx, m, e, e.defaultHandler).Next()
}
}
func (e *Engine) SetDebugLogger(l log.Logger) {
e.debug = l
}
func (e *Engine) Use(middleware ...HandlerFunc) {
e.Handlers = append(e.Handlers, middleware...)
}
func (e *Engine) DefaultHandler(h HandlerFunc) {
e.debug.Println("registering default handler")
e.mu.Lock()
defer e.mu.Unlock()
e.defaultHandler = e.combineHandlers(HandlersChain{h})
}
func (e *Engine) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(e.Handlers) + len(handlers)
assert1(finalSize < int(abortIndex), "too many handlers")
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, e.Handlers)
copy(mergedHandlers[len(e.Handlers):], handlers)
return mergedHandlers
}

69
leaf/mode.go Normal file
View File

@@ -0,0 +1,69 @@
package leaf
import (
"flag"
"io"
"os"
"sync/atomic"
)
const EnvLeafMode = "LEAF_MODE"
const (
// DebugMode indicates gin mode is debug.
DebugMode = "debug"
// ReleaseMode indicates gin mode is release.
ReleaseMode = "release"
// TestMode indicates gin mode is test.
TestMode = "test"
)
const (
debugCode = iota
releaseCode
testCode
)
var DefaultWriter io.Writer = os.Stdout
var DefaultErrorWriter io.Writer = os.Stderr
var leafMode int32 = debugCode
var modeName atomic.Value
func init() {
mode := os.Getenv(EnvLeafMode)
SetMode(mode)
}
// SetMode sets gin mode according to input string.
func SetMode(value string) {
if value == "" {
if flag.Lookup("test.v") != nil {
value = TestMode
} else {
value = DebugMode
}
}
switch value {
case DebugMode, "":
atomic.StoreInt32(&leafMode, debugCode)
case ReleaseMode:
atomic.StoreInt32(&leafMode, releaseCode)
case TestMode:
atomic.StoreInt32(&leafMode, testCode)
default:
panic("leaf mode unknown: " + value + " (available mode: debug release test)")
}
modeName.Store(value)
}
func IsDebugging() bool {
return atomic.LoadInt32(&leafMode) == debugCode
}
// Mode returns current gin mode.
func Mode() string {
return modeName.Load().(string)
}

48
leaf/recovery.go Normal file
View File

@@ -0,0 +1,48 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package leaf
import (
"io"
"log"
)
// RecoveryFunc defines the function passable to CustomRecovery.
type RecoveryFunc func(c *Context, err any)
// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
func Recovery() HandlerFunc {
return RecoveryWithWriter(DefaultErrorWriter)
}
// CustomRecovery returns a middleware that recovers from any panics and calls the provided handle func to handle it.
func CustomRecovery(handle RecoveryFunc) HandlerFunc {
return RecoveryWithWriter(DefaultErrorWriter, handle)
}
// RecoveryWithWriter returns a middleware for a given writer that recovers from any panics and writes a 500 if there was one.
func RecoveryWithWriter(out io.Writer, recovery ...RecoveryFunc) HandlerFunc {
if len(recovery) > 0 {
return CustomRecoveryWithWriter(out, recovery[0])
}
return CustomRecoveryWithWriter(out, defaultHandleRecovery)
}
// CustomRecoveryWithWriter returns a middleware for a given writer that recovers from any panics and calls the provided handle func to handle it.
func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
return func(c *Context) {
defer func() {
if err := recover(); err != nil {
log.Println("执行出错: ", err)
handle(c, err)
}
}()
c.Next()
}
}
func defaultHandleRecovery(c *Context, _ any) {
c.Abort()
}

64
leaf/utils.go Normal file
View File

@@ -0,0 +1,64 @@
package leaf
import (
"reflect"
"runtime"
"strings"
)
func assert1(guard bool, text string) {
if !guard {
panic(text)
}
}
func nameOfFunction(f any) string {
return runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
}
func match(route, topic string) bool {
return route == topic || routeIncludesTopic(route, topic)
}
func matchDeep(route []string, topic []string) bool {
if len(route) == 0 {
return len(topic) == 0
}
if len(topic) == 0 {
return route[0] == "#"
}
if route[0] == "#" {
return true
}
if (route[0] == "+") || (route[0] == topic[0]) {
return matchDeep(route[1:], topic[1:])
}
return false
}
func routeIncludesTopic(route, topic string) bool {
return matchDeep(routeSplit(route), topicSplit(topic))
}
func routeSplit(route string) []string {
if len(route) == 0 {
return nil
}
var result []string
if strings.HasPrefix(route, "$share") {
result = strings.Split(route, "/")[2:]
} else {
result = strings.Split(route, "/")
}
return result
}
func topicSplit(topic string) []string {
if len(topic) == 0 {
return nil
}
return strings.Split(topic, "/")
}

11
main.go Normal file
View File

@@ -0,0 +1,11 @@
/*
Copyright © 2024 慕枫Go <mapleafgo@163.com>
*/
package main
import "game-driver/cmd"
func main() {
cmd.Execute()
}

80
pkg/audio/play.go Normal file
View File

@@ -0,0 +1,80 @@
package audio
import (
"context"
"github.com/gopxl/beep/v2"
"github.com/gopxl/beep/v2/mp3"
"github.com/gopxl/beep/v2/speaker"
"github.com/gopxl/beep/v2/wav"
"io"
"log"
"time"
)
var DefaultSampleRate = beep.SampleRate(44100)
func init() {
err := speaker.Init(DefaultSampleRate, DefaultSampleRate.N(time.Second/10))
if err != nil {
panic("扬声器初始化异常: " + err.Error())
}
log.Println("扬声器初始化完成")
}
func PlayWav(c context.Context, r io.Reader) {
streamer, format, err := wav.Decode(r)
if err != nil {
return
}
defer streamer.Close()
s := beep.Resample(4, format.SampleRate, DefaultSampleRate, streamer)
ctrl := &beep.Ctrl{Streamer: s}
done := make(chan struct{})
speaker.Play(beep.Seq(ctrl, beep.Callback(func() {
close(done)
})))
for {
select {
case <-done:
return
case <-c.Done():
{
speaker.Lock()
ctrl.Streamer = nil
speaker.Unlock()
}
}
}
}
func PlayMP3(c context.Context, r io.ReadCloser) {
streamer, format, err := mp3.Decode(r)
if err != nil {
return
}
defer streamer.Close()
s := beep.Resample(4, format.SampleRate, DefaultSampleRate, streamer)
ctrl := &beep.Ctrl{Streamer: s}
done := make(chan struct{})
speaker.Play(beep.Seq(ctrl, beep.Callback(func() {
close(done)
})))
for {
select {
case <-done:
return
case <-c.Done():
{
speaker.Lock()
ctrl.Streamer = nil
speaker.Unlock()
}
}
}
}

9
pkg/errorsx/error.go Normal file
View File

@@ -0,0 +1,9 @@
package errorsx
import "errors"
var DriverTimeoutErr = errors.New("处理超时")
var DriverCancelErr = errors.New("系统取消")
var ThirdPartyErr = errors.New("第三方请求异常")

7
pkg/ports.go Normal file
View File

@@ -0,0 +1,7 @@
package main
import "game-driver/pkg/relay"
func main() {
relay.PrintPorts()
}

28
pkg/relay/portlist.go Normal file
View File

@@ -0,0 +1,28 @@
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)
}
}
}

44
pkg/relay/relay.go Normal file
View File

@@ -0,0 +1,44 @@
package relay
import (
"bufio"
"fmt"
"go.bug.st/serial"
"io"
)
type Device struct {
port serial.Port
}
func (r *Device) Close() error {
return r.port.Close()
}
func (r *Device) On(num int) error {
_, err := io.WriteString(r.port, fmt.Sprintf("AT+OUT%v+1=ON\r\n", num))
return err
}
func (r *Device) Off(num int) error {
_, err := io.WriteString(r.port, fmt.Sprintf("AT+OUT%v+1=OFF\r\n", num))
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 {
return nil, err
}
go func() {
for {
r := bufio.NewReader(port)
line, _, _ := r.ReadLine()
reader(string(line))
}
}()
return &Device{port: port}, nil
}

101
pkg/tts/aliyun.go Normal file
View File

@@ -0,0 +1,101 @@
package tts
import (
"bytes"
"context"
"fmt"
"game-driver/config"
"game-driver/leaf"
"game-driver/pkg/audio"
"game-driver/pkg/errorsx"
"io"
"log"
"time"
nls "github.com/aliyun/alibabacloud-nls-go-sdk"
)
type AliTTS struct {
config.AliyunConfig
ctx context.Context
}
type result struct {
Data io.ReadWriter
Error error
}
// onTaskFailed 识别过程中的错误处理回调参数
func (tts *AliTTS) onTaskFailed(text string, param interface{}) {
p, _ := param.(*result)
p.Error = fmt.Errorf("语音合成异常: %v", text)
}
// onSynthesisResult 语音合成数据回调参数
func (tts *AliTTS) onSynthesisResult(data []byte, param interface{}) {
p, _ := param.(*result)
p.Data.Write(data)
}
func (tts *AliTTS) Sound(text string) {
if text == "" {
return
}
buf, err := tts.Get(text)
if err == nil && buf != nil {
audio.PlayWav(tts.ctx, buf)
} else {
log.Panicln("AliTTS 请求异常: ", err)
}
}
func (tts *AliTTS) Get(text string) (io.Reader, error) {
param := nls.DefaultSpeechSynthesisParam()
param.Volume = 100
connectConfig := nls.NewConnectionConfigWithToken(nls.DEFAULT_URL, tts.AppKey, tts.Token)
logger := nls.NewNlsLogger(leaf.DefaultWriter, "", log.LstdFlags|log.Ltime)
logger.SetLogSil(false)
logger.SetDebug(true)
ttsData := &result{
Data: &bytes.Buffer{},
}
synthesis, err := nls.NewSpeechSynthesis(
connectConfig, logger, false,
tts.onTaskFailed, tts.onSynthesisResult, nil,
nil, nil, ttsData,
)
if err != nil {
return ttsData.Data, err
}
defer synthesis.Shutdown()
ch, err := synthesis.Start(text, param, nil)
if err != nil {
return ttsData.Data, err
}
// 等待语音合成结束
select {
case done := <-ch:
{
if !done {
return ttsData.Data, errorsx.ThirdPartyErr
}
return ttsData.Data, nil
}
case <-time.After(time.Duration(tts.Timeout) * time.Second):
return ttsData.Data, errorsx.DriverTimeoutErr
case <-tts.ctx.Done():
return ttsData.Data, errorsx.DriverCancelErr
}
}
func New(ctx context.Context, config config.AliyunConfig) *AliTTS {
return &AliTTS{
ctx: ctx,
AliyunConfig: config,
}
}

26
puml/common-flow.puml Normal file
View File

@@ -0,0 +1,26 @@
@startuml 游戏通用逻辑
start
:接收开始指令-MQTT;
:设备状态锁定-设备锁;
:播放开始语音-TTS;
:设备供电-继电器;
fork
:倒计时开始-计时器;
:播放bgm-音频;
partition 设备游戏 {
:游戏进行中-状态;
}
:播放穿插语音-TTS;
:停止bgm-音频;
:计时结束-计时器;
:播放结束语音-TTS;
fork again
:等待终止指令-MQTT;
:终止设备;
:播放终止语音-TTS;
end fork
:结束供电-继电器;
:设备状态解锁-设备锁;
:发送结束状态-MQTT;
end
@enduml

26
puml/游戏.puml Normal file
View File

@@ -0,0 +1,26 @@
@startmindmap 游戏
+ 游戏
++ 入口
+++ 播放欢迎语音
++ 召唤神女
++ 神女低语(无)
+++ 按下按钮
+++ 播放语音
+++ 发送结果数据
++ 镇水神力
+++ 播放法阵视频
-- 镇收邪祟
--- 控制设备启动
--- 接收游戏结果
--- 发送结果数据
-- 流光寻踪(无)
-- 神女授书
--- 控制设备吐卡
--- 播放取卡提示语音
-- 青云龙台
--- 等待卡片插入
--- 播放恭喜语音
--- 屏幕恭喜文字
--- 等待拔卡
--- 结束屏幕提示
@endmindmap

86
readme.md Normal file
View File

@@ -0,0 +1,86 @@
# 边缘盒子
topic 中的 `location` 指景区编码, `point` 指景区内具体点位
## 1. 接收启动
Topic: `server/${location}/${point}/play`
Payload:
```json
{
"timeout": 30, // 设备超时时长(s)
"power": true, // 是否需要整体电源控制
"bgm": "", // 游戏中背景音乐(file://本地文件地址、http://远程文件地址)
"volume": 0.5, // 整体设备音量(0-1)
"default-print": "", // 屏幕默认打印(设备待机时展示文本)
"tts": { // 文本转语音整体控制
"start": "", // 开始语音
"end": "", // 结束语音
"stop": "", // 终止语音
"timer": [ // 固定节点语音
{
"time": 10, // 时间节点(s)
"value": "" // 语音文字
}
]
},
"print": [ // 屏幕打印控制
{
"time": 10, // 时间节点(s)
"text": "", // 展示文字
"duration": 10 // 持续时长(s)
}
],
"game": {} // 根据具体游戏特定
}
```
[游戏节点报文](./game.md)
#### 例: 入口欢迎播报
```json
{
"volume": 1,
"tts": {
"start": "欢迎前来挑战!"
}
}
```
## 2. 事件反馈
### 状态变更
Topic: `device/${location}/${point}/status`
Payload:
```bash
0 # 0 待机; 1 使用中; -1 状态异常
```
## 3. 接收指令
### 终止
Topic: `server/${location}/${point}/command`
Payload:
```bash
stop
```
### 查询状态
Topic: `server/${location}/${point}/command`
Payload:
```bash
status
```
设备接收到该指令会立即向 `device/${location}/${point}/status` 发送一次当前状态