commit f9b9beea4b1c145fe9e638d884930a387d4c895b Author: mapleafgo Date: Fri Nov 1 17:40:34 2024 +0800 基本逻辑完成 diff --git a/.cobra.yaml b/.cobra.yaml new file mode 100644 index 0000000..87daec6 --- /dev/null +++ b/.cobra.yaml @@ -0,0 +1,4 @@ +author: 慕枫Go +year: 2024 +license: none +useViper: true diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..3a86cee --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/game-driver.iml b/.idea/game-driver.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/game-driver.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..8269b9e --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/531241201.mp3 b/531241201.mp3 new file mode 100644 index 0000000..869fc24 Binary files /dev/null and b/531241201.mp3 differ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..5f1c9a5 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,67 @@ +/* +Copyright © 2024 慕枫Go +*/ +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) + } +} diff --git a/config.yml b/config.yml new file mode 100644 index 0000000..21a424e --- /dev/null +++ b/config.yml @@ -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 diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..4a97651 --- /dev/null +++ b/config/config.go @@ -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 diff --git a/game.md b/game.md new file mode 100644 index 0000000..e69de29 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..024c79d --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b60a77e --- /dev/null +++ b/go.sum @@ -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= diff --git a/internal/common/device.go b/internal/common/device.go new file mode 100644 index 0000000..a090cb3 --- /dev/null +++ b/internal/common/device.go @@ -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, + } +} diff --git a/internal/common/stopper.go b/internal/common/stopper.go new file mode 100644 index 0000000..5fde2a7 --- /dev/null +++ b/internal/common/stopper.go @@ -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{} diff --git a/internal/middleware/bgm.go b/internal/middleware/bgm.go new file mode 100644 index 0000000..cab233d --- /dev/null +++ b/internal/middleware/bgm.go @@ -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() + } +} diff --git a/internal/middleware/device.go b/internal/middleware/device.go new file mode 100644 index 0000000..8338634 --- /dev/null +++ b/internal/middleware/device.go @@ -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() + } +} diff --git a/internal/middleware/json.go b/internal/middleware/json.go new file mode 100644 index 0000000..ac040c6 --- /dev/null +++ b/internal/middleware/json.go @@ -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() + } +} diff --git a/internal/middleware/relay.go b/internal/middleware/relay.go new file mode 100644 index 0000000..64cdca1 --- /dev/null +++ b/internal/middleware/relay.go @@ -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() + } +} diff --git a/internal/middleware/sound_start.go b/internal/middleware/sound_start.go new file mode 100644 index 0000000..37c7c48 --- /dev/null +++ b/internal/middleware/sound_start.go @@ -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() + } +} diff --git a/internal/middleware/stop.go b/internal/middleware/stop.go new file mode 100644 index 0000000..40ac2e0 --- /dev/null +++ b/internal/middleware/stop.go @@ -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() + } +} diff --git a/internal/middleware/timer.go b/internal/middleware/timer.go new file mode 100644 index 0000000..bc0d0e0 --- /dev/null +++ b/internal/middleware/timer.go @@ -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() + } +} diff --git a/internal/routes/command.go b/internal/routes/command.go new file mode 100644 index 0000000..652babf --- /dev/null +++ b/internal/routes/command.go @@ -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) + } + } +} diff --git a/internal/schema/play.go b/internal/schema/play.go new file mode 100644 index 0000000..1462385 --- /dev/null +++ b/internal/schema/play.go @@ -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"` +} diff --git a/internal/server.go b/internal/server.go new file mode 100644 index 0000000..45edb00 --- /dev/null +++ b/internal/server.go @@ -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("关闭完成") +} diff --git a/leaf/context.go b/leaf/context.go new file mode 100644 index 0000000..0d2e728 --- /dev/null +++ b/leaf/context.go @@ -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 +} diff --git a/leaf/leaf.go b/leaf/leaf.go new file mode 100644 index 0000000..d63d2b4 --- /dev/null +++ b/leaf/leaf.go @@ -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 +} diff --git a/leaf/mode.go b/leaf/mode.go new file mode 100644 index 0000000..78ee05e --- /dev/null +++ b/leaf/mode.go @@ -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) +} diff --git a/leaf/recovery.go b/leaf/recovery.go new file mode 100644 index 0000000..b4d3958 --- /dev/null +++ b/leaf/recovery.go @@ -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() +} diff --git a/leaf/utils.go b/leaf/utils.go new file mode 100644 index 0000000..26e46c0 --- /dev/null +++ b/leaf/utils.go @@ -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, "/") +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..73925d9 --- /dev/null +++ b/main.go @@ -0,0 +1,11 @@ +/* +Copyright © 2024 慕枫Go + +*/ +package main + +import "game-driver/cmd" + +func main() { + cmd.Execute() +} diff --git a/pkg/audio/play.go b/pkg/audio/play.go new file mode 100644 index 0000000..0127e74 --- /dev/null +++ b/pkg/audio/play.go @@ -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() + } + } + } +} diff --git a/pkg/errorsx/error.go b/pkg/errorsx/error.go new file mode 100644 index 0000000..49f45e3 --- /dev/null +++ b/pkg/errorsx/error.go @@ -0,0 +1,9 @@ +package errorsx + +import "errors" + +var DriverTimeoutErr = errors.New("处理超时") + +var DriverCancelErr = errors.New("系统取消") + +var ThirdPartyErr = errors.New("第三方请求异常") diff --git a/pkg/ports.go b/pkg/ports.go new file mode 100644 index 0000000..4febd40 --- /dev/null +++ b/pkg/ports.go @@ -0,0 +1,7 @@ +package main + +import "game-driver/pkg/relay" + +func main() { + relay.PrintPorts() +} diff --git a/pkg/relay/portlist.go b/pkg/relay/portlist.go new file mode 100644 index 0000000..1a9d510 --- /dev/null +++ b/pkg/relay/portlist.go @@ -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) + } + } +} diff --git a/pkg/relay/relay.go b/pkg/relay/relay.go new file mode 100644 index 0000000..3302c8c --- /dev/null +++ b/pkg/relay/relay.go @@ -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 +} diff --git a/pkg/tts/aliyun.go b/pkg/tts/aliyun.go new file mode 100644 index 0000000..a2b60e2 --- /dev/null +++ b/pkg/tts/aliyun.go @@ -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, + } +} diff --git a/puml/common-flow.puml b/puml/common-flow.puml new file mode 100644 index 0000000..d080366 --- /dev/null +++ b/puml/common-flow.puml @@ -0,0 +1,26 @@ +@startuml 游戏通用逻辑 +start +:接收开始指令-MQTT; +:设备状态锁定-设备锁; +:播放开始语音-TTS; +:设备供电-继电器; +fork +:倒计时开始-计时器; +:播放bgm-音频; +partition 设备游戏 { + :游戏进行中-状态; +} +:播放穿插语音-TTS; +:停止bgm-音频; +:计时结束-计时器; +:播放结束语音-TTS; +fork again +:等待终止指令-MQTT; +:终止设备; +:播放终止语音-TTS; +end fork +:结束供电-继电器; +:设备状态解锁-设备锁; +:发送结束状态-MQTT; +end +@enduml diff --git a/puml/游戏.puml b/puml/游戏.puml new file mode 100644 index 0000000..d226a81 --- /dev/null +++ b/puml/游戏.puml @@ -0,0 +1,26 @@ +@startmindmap 游戏 ++ 游戏 +++ 入口 ++++ 播放欢迎语音 +++ 召唤神女 +++ 神女低语(无) ++++ 按下按钮 ++++ 播放语音 ++++ 发送结果数据 +++ 镇水神力 ++++ 播放法阵视频 +-- 镇收邪祟 +--- 控制设备启动 +--- 接收游戏结果 +--- 发送结果数据 +-- 流光寻踪(无) +-- 神女授书 +--- 控制设备吐卡 +--- 播放取卡提示语音 +-- 青云龙台 +--- 等待卡片插入 +--- 播放恭喜语音 +--- 屏幕恭喜文字 +--- 等待拔卡 +--- 结束屏幕提示 +@endmindmap diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..a2509c3 --- /dev/null +++ b/readme.md @@ -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` 发送一次当前状态