基本逻辑完成
This commit is contained in:
4
.cobra.yaml
Normal file
4
.cobra.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
author: 慕枫Go <mapleafgo@163.com>
|
||||||
|
year: 2024
|
||||||
|
license: none
|
||||||
|
useViper: true
|
||||||
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal 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
9
.idea/game-driver.iml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal 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
BIN
531241201.mp3
Normal file
Binary file not shown.
67
cmd/root.go
Normal file
67
cmd/root.go
Normal 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
11
config.yml
Normal 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
26
config/config.go
Normal 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
|
||||||
49
go.mod
Normal file
49
go.mod
Normal 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
208
go.sum
Normal 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
72
internal/common/device.go
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
58
internal/common/stopper.go
Normal file
58
internal/common/stopper.go
Normal 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{}
|
||||||
65
internal/middleware/bgm.go
Normal file
65
internal/middleware/bgm.go
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
16
internal/middleware/device.go
Normal file
16
internal/middleware/device.go
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
25
internal/middleware/json.go
Normal file
25
internal/middleware/json.go
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
19
internal/middleware/relay.go
Normal file
19
internal/middleware/relay.go
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
26
internal/middleware/sound_start.go
Normal file
26
internal/middleware/sound_start.go
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
32
internal/middleware/stop.go
Normal file
32
internal/middleware/stop.go
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
80
internal/middleware/timer.go
Normal file
80
internal/middleware/timer.go
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
21
internal/routes/command.go
Normal file
21
internal/routes/command.go
Normal 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
30
internal/schema/play.go
Normal 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
152
internal/server.go
Normal 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
134
leaf/context.go
Normal 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
144
leaf/leaf.go
Normal 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
69
leaf/mode.go
Normal 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
48
leaf/recovery.go
Normal 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
64
leaf/utils.go
Normal 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
11
main.go
Normal 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
80
pkg/audio/play.go
Normal 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
9
pkg/errorsx/error.go
Normal 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
7
pkg/ports.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "game-driver/pkg/relay"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
relay.PrintPorts()
|
||||||
|
}
|
||||||
28
pkg/relay/portlist.go
Normal file
28
pkg/relay/portlist.go
Normal 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
44
pkg/relay/relay.go
Normal 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
101
pkg/tts/aliyun.go
Normal 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
26
puml/common-flow.puml
Normal 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
26
puml/游戏.puml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
@startmindmap 游戏
|
||||||
|
+ 游戏
|
||||||
|
++ 入口
|
||||||
|
+++ 播放欢迎语音
|
||||||
|
++ 召唤神女
|
||||||
|
++ 神女低语(无)
|
||||||
|
+++ 按下按钮
|
||||||
|
+++ 播放语音
|
||||||
|
+++ 发送结果数据
|
||||||
|
++ 镇水神力
|
||||||
|
+++ 播放法阵视频
|
||||||
|
-- 镇收邪祟
|
||||||
|
--- 控制设备启动
|
||||||
|
--- 接收游戏结果
|
||||||
|
--- 发送结果数据
|
||||||
|
-- 流光寻踪(无)
|
||||||
|
-- 神女授书
|
||||||
|
--- 控制设备吐卡
|
||||||
|
--- 播放取卡提示语音
|
||||||
|
-- 青云龙台
|
||||||
|
--- 等待卡片插入
|
||||||
|
--- 播放恭喜语音
|
||||||
|
--- 屏幕恭喜文字
|
||||||
|
--- 等待拔卡
|
||||||
|
--- 结束屏幕提示
|
||||||
|
@endmindmap
|
||||||
86
readme.md
Normal file
86
readme.md
Normal 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` 发送一次当前状态
|
||||||
Reference in New Issue
Block a user