加入 pjlink 控制

This commit is contained in:
2025-02-28 20:34:50 +08:00
parent 3a2fc431ac
commit 81f31f15a5
11 changed files with 253 additions and 16 deletions

View File

@@ -7,6 +7,7 @@ import (
"errors" "errors"
"game-driver/config" "game-driver/config"
"game-driver/config/game" "game-driver/config/game"
"game-driver/config/wait"
"game-driver/internal" "game-driver/internal"
"io/fs" "io/fs"
"log" "log"
@@ -70,11 +71,20 @@ func initConfig() {
} }
// 初始化游戏节点配置 // 初始化游戏节点配置
game.G = game.NewConfig(config.C.Point) game.C = game.NewConfig(config.C.Point)
if game.G != nil { // 如果需要游戏配置 if game.C != nil { // 如果需要游戏配置
err = viper.UnmarshalKey("game", &game.G) err = viper.UnmarshalKey("game", &game.C)
if err != nil { if err != nil {
log.Panicln("unmarshal game config failed: ", err) log.Panicln("unmarshal game config failed: ", err)
} }
} }
// 初始化游戏节点待机时配置
wait.C = wait.NewConfig(config.C.Point)
if wait.C != nil { // 如果需要游戏配置
err = viper.UnmarshalKey("wait", &wait.C)
if err != nil {
log.Panicln("unmarshal wait config failed: ", err)
}
}
} }

View File

@@ -4,6 +4,11 @@ relay:
maxTimeout: 60 # 单位 s必须大于 0 maxTimeout: 60 # 单位 s必须大于 0
log: log:
level: debug level: debug
tencentCLS:
endpoint:
secretID:
secretKey:
topicID:
file: file:
filename: logs/app.log filename: logs/app.log
maxSize: 10 maxSize: 10
@@ -20,6 +25,11 @@ aliyun:
volume: 100 # 音量取值范围0~100 volume: 100 # 音量取值范围0~100
voice: zhifeng_emo # 发音人 voice: zhifeng_emo # 发音人
speechRate: 50 # 语速,取值范围:-500~500 speechRate: 50 # 语速,取值范围:-500~500
wait:
ip:
port:
password:
id:
#game: #game:
# addr: /dev/ttyUSB0 # 点位 11 的串口地址 # addr: /dev/ttyUSB0 # 点位 11 的串口地址
# pushGroups: # 点位 10 的发卡器配置 # pushGroups: # 点位 10 的发卡器配置

View File

@@ -13,4 +13,4 @@ func NewConfig(point int) Config {
} }
} }
var G Config var C Config

8
config/wait/pjlink.go Normal file
View File

@@ -0,0 +1,8 @@
package wait
type PJLink struct {
Ip string
Port string
Password string
Id string
}

14
config/wait/wait.go Normal file
View File

@@ -0,0 +1,14 @@
package wait
type Config any
func NewConfig(point int) Config {
switch point {
case 2:
return PJLink{}
default:
return nil
}
}
var C Config

View File

@@ -28,7 +28,7 @@ type ResponseBody struct {
} }
func PushCard(ctx context.Context) leaf.HandlerFunc { func PushCard(ctx context.Context) leaf.HandlerFunc {
g := (game.G).(game.ConfigPush) g := (game.C).(game.ConfigPush)
readers := make([]card_reader.Reader, len(g.PushGroups)) readers := make([]card_reader.Reader, len(g.PushGroups))
devices := make([]*card_pusher.Device, len(g.PushGroups)) devices := make([]*card_pusher.Device, len(g.PushGroups))

View File

@@ -18,7 +18,7 @@ import (
) )
func WaitCard(ctx context.Context) leaf.HandlerFunc { func WaitCard(ctx context.Context) leaf.HandlerFunc {
g := (game.G).(game.ConfigWait) g := (game.C).(game.ConfigWait)
reader, err := card_reader.NewReader(g.Addr) reader, err := card_reader.NewReader(g.Addr)
if err != nil { if err != nil {

View File

@@ -3,11 +3,13 @@ package routes
import ( import (
"context" "context"
"fmt" "fmt"
"game-driver/config/wait"
"game-driver/internal/middleware" "game-driver/internal/middleware"
"game-driver/internal/schema" "game-driver/internal/schema"
"game-driver/leaf" "game-driver/leaf"
"game-driver/pkg/audio" "game-driver/pkg/audio"
"game-driver/pkg/browser" "game-driver/pkg/browser"
"game-driver/pkg/pjlink"
"game-driver/pkg/relay" "game-driver/pkg/relay"
"game-driver/pkg/tts" "game-driver/pkg/tts"
"game-driver/pkg/utils" "game-driver/pkg/utils"
@@ -151,6 +153,13 @@ func WaitAction(c *leaf.Context) {
defer wait.Done() defer wait.Done()
runAction(c, item, rules, webAction) runAction(c, item, rules, webAction)
}() }()
case schema.WaitPJLink:
// 执行投影仪打开
wait.Add(1)
go func() {
defer wait.Done()
runAction(c, item, rules, pjlinkAction)
}()
default: default:
zap.S().Infof("不支持的类型: %d\n", item.Type) zap.S().Infof("不支持的类型: %d\n", item.Type)
} }
@@ -255,3 +264,29 @@ func webAction(c context.Context, item schema.WaitItemModel) error {
browser.OpenApp(c, item.Data) browser.OpenApp(c, item.Data)
return nil return nil
} }
func pjlinkAction(c context.Context, _ schema.WaitItemModel) error {
cfg := (wait.C).(wait.PJLink)
pjc := pjlink.NewClient(cfg.Ip, cfg.Port, cfg.Password, cfg.Id)
err := pjc.Connect()
if err != nil {
return fmt.Errorf("连接 PJLink 设备异常: %w", err)
}
defer pjc.Close()
zap.S().Infoln("打开待机投影仪")
err = pjc.PowerOn()
if err != nil {
return fmt.Errorf("打开投影仪异常: %w", err)
}
<-c.Done()
zap.S().Infoln("关闭待机投影仪")
err = pjc.PowerOff()
if err != nil {
return fmt.Errorf("关闭投影仪异常: %w", err)
}
return nil
}

View File

@@ -8,6 +8,7 @@ const (
WaitTTS WaitTTS
WaitRelay WaitRelay
WaitWeb WaitWeb
WaitPJLink
) )
type WaitItemModel struct { type WaitItemModel struct {

36
json.md
View File

@@ -5,7 +5,7 @@ url: `server/wushan/2/wait`
```json ```json
{ {
"cron": "* * * *", "cron": "08:00-22:00 * * *",
"items": [ "items": [
{ {
"data": "file://./三峡龙脊BGM.mp3" "data": "file://./三峡龙脊BGM.mp3"
@@ -35,18 +35,36 @@ url: `server/wushan/2/play`
} }
``` ```
### STOP 待机 ## 点位5登龙云台
### 待机
url: `server/wushan/2/command` url: `server/wushan/5/wait`
```text ```json
stop-bg {
"cron": "08:00-22:00 * * *",
"items": [
{
"data": "file://./三峡龙脊BGM.mp3"
}
]
}
``` ```
### STOP GAME ### Game
url: `server/wushan/2/command` url: `server/wushan/5/play`
```text ```json
stop {
"bgm": "file://./青云龙台.mp3",
"power": true,
"volume":1,
"tts": {
"start": "刘佳勇者,恭喜你成功通关!"
},
"game": {
"wait": 10
}
}
``` ```

141
pkg/pjlink/pjlink.go Normal file
View File

@@ -0,0 +1,141 @@
package pjlink
import (
"bufio"
"crypto/md5"
"errors"
"fmt"
"net"
"strings"
"time"
)
var (
ErrAuthFailed = errors.New("授权验证失败")
ErrCommandError = errors.New("命令执行异常")
)
type Client struct {
Host string
Port string
Password string
ID string
conn net.Conn
}
func NewClient(host, port, password, id string) *Client {
return &Client{
Host: host,
Port: port,
Password: password,
ID: id,
}
}
func (c *Client) Connect() error {
address := net.JoinHostPort(c.Host, c.Port)
conn, err := net.DialTimeout("tcp", address, 5*time.Second)
if err != nil {
return err
}
c.conn = conn
// Read challenge
reader := bufio.NewReader(c.conn)
response, err := reader.ReadString('\r')
if err != nil {
c.Close()
return err
}
// Handle authentication
if strings.HasPrefix(response, "PJLINK 1") {
if c.Password == "" {
c.Close()
return ErrAuthFailed
}
challenge := strings.TrimSpace(strings.Split(response, " ")[2])
authString := fmt.Sprintf("%s%s", challenge, c.Password)
hashed := md5.Sum([]byte(authString))
authHash := fmt.Sprintf("%x", hashed)
_, err = fmt.Fprintf(c.conn, "%s\r", authHash)
if err != nil {
c.Close()
return err
}
authResponse, err := reader.ReadString('\r')
if err != nil || !strings.Contains(authResponse, "OK") {
c.Close()
return ErrAuthFailed
}
}
return nil
}
func (c *Client) sendCommand(command string) (string, error) {
if c.conn == nil {
return "", errors.New("not connected")
}
fullCommand := fmt.Sprintf("%%%s%s\r", c.ID, command)
_, err := c.conn.Write([]byte(fullCommand))
if err != nil {
return "", err
}
reader := bufio.NewReader(c.conn)
response, err := reader.ReadString('\r')
if err != nil {
return "", err
}
// Remove prefix and parse response
parts := strings.SplitN(response, "=", 2)
if len(parts) < 2 {
return "", ErrCommandError
}
result := strings.TrimSpace(parts[1])
if result == "ERR1" {
return "", ErrAuthFailed
} else if result == "ERR2" {
return "", ErrCommandError
}
return result, nil
}
func (c *Client) PowerOn() error {
response, err := c.sendCommand("POWR 1")
if err != nil {
return err
}
if response != "OK" {
return fmt.Errorf("unexpected response: %s", response)
}
return nil
}
func (c *Client) PowerOff() error {
response, err := c.sendCommand("POWR 0")
if err != nil {
return err
}
if response != "OK" {
return fmt.Errorf("unexpected response: %s", response)
}
return nil
}
func (c *Client) Close() {
if c.conn != nil {
c.conn.Close()
c.conn = nil
}
}