Merge branch 'main' into clean_beep

# Conflicts:
#	internal/routes/wait.go
This commit is contained in:
2025-07-09 11:58:14 +08:00
63 changed files with 2313 additions and 276 deletions

28
pkg/browser/browser.go Normal file
View File

@@ -0,0 +1,28 @@
package browser
import (
"context"
"github.com/go-rod/rod"
"github.com/go-rod/rod/lib/launcher"
"github.com/go-rod/rod/lib/launcher/flags"
)
// OpenApp 用APP模式打开网页
func OpenApp(c context.Context, url string) {
path, _ := launcher.LookPath()
l := launcher.NewAppMode(url).
Delete(flags.Env).
Set("kiosk").
Set("hide-scrollbars").
Set("disable-sync").
Set("disable-features", "GoogleSignin,IdentityConsistency,OmniboxUIExperimentation,GoogleSearch,Autofill,SafeSearch,SpeechRecognition").
Delete("disable-site-isolation-trials").
Bin(path)
p := l.MustLaunch()
defer l.Cleanup()
b := rod.New().ControlURL(p).MustConnect()
defer b.MustClose()
<-c.Done()
}

65
pkg/oscx/osc.go Normal file
View File

@@ -0,0 +1,65 @@
package oscx
import (
"github.com/hypebeast/go-osc/osc"
)
type Client struct {
o *osc.Client
}
func New(host string, port int) *Client {
return &Client{
o: osc.NewClient(host, port),
}
}
func (c *Client) StartCue(data string) error {
msg := osc.NewMessage("/beyond/general/StartCue", data)
return c.o.Send(msg)
}
func (c *Client) EnableLaserOutput() error {
msg := osc.NewMessage("/beyond/general/EnableLaserOutput")
return c.o.Send(msg)
}
func (c *Client) DisableLaserOutput() error {
msg := osc.NewMessage("/beyond/general/DisableLaserOutput")
return c.o.Send(msg)
}
func (c *Client) SetLaserOutput(data string) error {
msg := osc.NewMessage("/beyond/general/SetLaserOutput", data)
return c.o.Send(msg)
}
func (c *Client) SetLaserOutputColor(data string) error {
msg := osc.NewMessage("/beyond/general/SetLaserOutputColor", data)
return c.o.Send(msg)
}
func (c *Client) SetLaserOutputIntensity(data string) error {
msg := osc.NewMessage("/beyond/general/SetLaserOutputIntensity", data)
return c.o.Send(msg)
}
func (c *Client) SetLaserOutputPosition(data string) error {
msg := osc.NewMessage("/beyond/general/SetLaserOutputPosition", data)
return c.o.Send(msg)
}
func (c *Client) SetLaserOutputSize(data string) error {
msg := osc.NewMessage("/beyond/general/SetLaserOutputSize", data)
return c.o.Send(msg)
}
func (c *Client) SetLaserOutputSpeed(data string) error {
msg := osc.NewMessage("/beyond/general/SetLaserOutputSpeed", data)
return c.o.Send(msg)
}
func (c *Client) Status() error {
msg := osc.NewMessage("/beyond/general/Status")
return c.o.Send(msg)
}

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

@@ -0,0 +1,234 @@
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 fmt.Errorf("连接异常: %w", err)
}
c.conn = conn
// Read challenge
reader := bufio.NewReader(c.conn)
response, err := reader.ReadString('\r')
if err != nil {
c.close()
return fmt.Errorf("读取异常: %w", 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 fmt.Errorf("写入异常: %w", 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
} else if result == "ERR3" {
return "YES", nil
}
return result, nil
}
// PowerOn 打开投影机
func (c *Client) PowerOn() (string, error) {
err := c.connect()
if err != nil {
return "", fmt.Errorf("连接异常: %w", err)
}
defer c.close()
response, err := c.sendCommand("POWR 1")
if err != nil {
return "", err
}
if response == "YES" {
return response, nil
} else if response != "OK" {
return response, fmt.Errorf("unexpected response: %s", response)
}
return response, nil
}
// PowerOff 关闭投影机
func (c *Client) PowerOff() (string, error) {
err := c.connect()
if err != nil {
return "", fmt.Errorf("连接异常: %w", err)
}
defer c.close()
response, err := c.sendCommand("POWR 0")
if err != nil {
return "", err
}
if response == "YES" {
return response, nil
} else if response != "OK" {
return response, fmt.Errorf("unexpected response: %s", response)
}
return response, nil
}
// PowerOnSync 打开投影机
func (c *Client) PowerOnSync() (string, error) {
_, err := c.GetStatus()
if err != nil {
return "", err
}
_, _ = c.PowerOn()
// 轮询检查投影机状态,直到打开成功
for {
status, err := c.GetStatus()
if err != nil {
return "", err
}
if status == "1" {
return "投影机已打开", nil
} else {
// 如果投影机处于关闭状态,则尝试重新打开
_, _ = c.PowerOn()
}
time.Sleep(1 * time.Second)
}
}
// PowerOffSync 关闭投影机
func (c *Client) PowerOffSync() (string, error) {
_, err := c.GetStatus()
if err != nil {
return "", err
}
_, _ = c.PowerOff()
// 轮询检查投影机状态,直到关闭成功
for {
status, err := c.GetStatus()
if err != nil {
return "", err
}
if status == "0" {
return "投影机已关闭", nil
} else {
// 如果投影机处于打开状态,则尝试重新关闭
_, _ = c.PowerOff()
}
time.Sleep(1 * time.Second)
}
}
// GetStatus 获取投影机状态
func (c *Client) GetStatus() (string, error) {
err := c.connect()
if err != nil {
return "", fmt.Errorf("连接异常: %w", err)
}
defer c.close()
response, err := c.sendCommand("POWR ?")
if err != nil {
return "", err
}
if !strings.Contains("0123", response) {
return response, fmt.Errorf("unexpected response: %s", response)
}
return response, nil
}
func (c *Client) close() {
if c.conn != nil {
c.conn.Close()
c.conn = nil
}
}

View File

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

View File

@@ -1,9 +1,15 @@
package relay
import (
"context"
"errors"
"github.com/grid-x/modbus"
"go.uber.org/zap"
"io"
"io/fs"
"os"
"strings"
"time"
)
type Relay interface {
@@ -56,6 +62,29 @@ func (r *device) Off(num int) error {
func New(address string) (Relay, error) {
zap.S().Infoln("连接继电器: ", address)
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
a := make(chan struct{})
go func() {
defer close(a)
for {
select {
case <-ctx.Done():
zap.S().Infoln("等待继电器资源释放超时:", address)
return
case <-time.After(3 * time.Second):
_, err := os.OpenFile(address, os.O_RDWR, 0666)
var e *fs.PathError
if errors.As(err, &e) && strings.Contains(e.Error(), "busy") {
zap.S().Infoln("等待继电器资源释放:", address)
} else {
return
}
}
}
}()
<-a
h := modbus.NewRTUClientHandler(address)
h.SlaveID = 1
h.BaudRate = 9600

View File

@@ -61,6 +61,7 @@ func (tts *AliTTS) getToken() error {
if err != nil {
return err
} else if resultMessage.ErrMsg != "" {
zap.S().Errorf("获取Token失败: %s", resultMessage.ErrMsg)
return errorsx.ThirdPartyErr
}
tts.tokenResult = resultMessage.TokenResult
@@ -69,8 +70,9 @@ func (tts *AliTTS) getToken() error {
func (tts *AliTTS) Get(text string) (io.Reader, error) {
param := nls.DefaultSpeechSynthesisParam()
param.Volume = 100
param.Volume = tts.Volume
param.Voice = tts.Voice
param.SpeechRate = tts.SpeechRate
err := tts.getToken()
if err != nil {

View File

@@ -1,13 +1,24 @@
package utils
import "os"
import (
"os"
"os/exec"
)
// BlankOpen 打开屏幕
func BlankOpen() {
if found, err := exec.LookPath("xset"); err == nil {
exec.Command(found, "dpms", "force", "on").Run()
return
}
os.WriteFile("/sys/class/graphics/fb0/blank", []byte("0"), 0644)
}
// BlankClose 关闭屏幕
func BlankClose() {
if found, err := exec.LookPath("xset"); err == nil {
exec.Command(found, "dpms", "force", "off").Run()
return
}
os.WriteFile("/sys/class/graphics/fb0/blank", []byte("1"), 0644)
}

View File

@@ -64,7 +64,9 @@ func LinkAudio(link string) (bgm io.ReadCloser, err error) {
} else {
err = fmt.Errorf("不支持的链接协议: %v", u.String())
}
bgm = toSeeker(bgm)
if bgm != nil {
bgm = toSeeker(bgm)
}
}
return
}

View File

@@ -2,16 +2,12 @@ package utils
import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"path"
"strings"
)
// LinkVideo 链接视频,解析链接,网络文件会下载到临时目录并返回本地路径
func LinkVideo(link string) (local string, err error) {
func LinkVideo(link string) (path string, local bool, err error) {
if link == "" {
return
}
@@ -20,35 +16,14 @@ func LinkVideo(link string) (local string, err error) {
err = fmt.Errorf("URL 解析错误: %v", err)
} else {
if u.Scheme == "file" {
local, _ = strings.CutPrefix(link, "file://")
local = true
path, _ = strings.CutPrefix(link, "file://")
} else if u.Scheme == "http" || u.Scheme == "https" {
p, _ := url.PathUnescape(u.EscapedPath())
tmpLocal := path.Join(os.TempDir(), path.Base(p))
err = Download(link, tmpLocal)
if err != nil {
err = fmt.Errorf("链接文件获取失败: %v", err)
return
}
local = tmpLocal
local = false
path = link
} else {
err = fmt.Errorf("不支持的链接协议: %v", u.String())
}
}
return
}
// Download 下载文件
func Download(link string, local string) (err error) {
resp, err := http.Get(link)
if err != nil {
return
}
defer resp.Body.Close()
f, err := os.OpenFile(local, os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
return
}
defer f.Close()
_, err = io.Copy(f, resp.Body)
return
}

74
pkg/video/play.go Normal file
View File

@@ -0,0 +1,74 @@
package video
import (
"context"
"fmt"
libvlc "github.com/adrg/libvlc-go/v3"
)
func Play(ctx context.Context, path string, local bool) error {
// 1. 初始化 VLC
if err := libvlc.Init("--no-xlib"); err != nil {
return fmt.Errorf("VLC初始化失败: %w", err)
}
defer libvlc.Release()
// 2. 创建播放器
player, err := libvlc.NewPlayer()
if err != nil {
return fmt.Errorf("播放器创建失败: %w", err)
}
defer player.Stop()
defer player.Release()
// 3. 注册结束事件
eventManager, err := player.EventManager()
if err != nil {
return fmt.Errorf("事件管理器获取失败: %w", err)
}
done := make(chan struct{})
defer close(done)
_, err = eventManager.Attach(libvlc.MediaPlayerEndReached, func(libvlc.Event, interface{}) {
done <- struct{}{}
}, nil)
if err != nil {
return fmt.Errorf("事件绑定失败: %w", err)
}
// 4. 加载并播放文件
if local {
if _, err := player.LoadMediaFromPath(path); err != nil {
return fmt.Errorf("文件加载失败: %w", err)
}
} else {
if _, err := player.LoadMediaFromURL(path); err != nil {
return fmt.Errorf("文件加载失败: %w", err)
}
}
if err := player.Play(); err != nil {
return fmt.Errorf("播放启动失败: %w", err)
}
// 设置全屏模式
if err := player.SetFullScreen(true); err != nil {
return fmt.Errorf("设置全屏模式失败: %w", err)
}
// 设置音量为最大
if err := player.SetVolume(100); err != nil {
return fmt.Errorf("设置音量失败: %w", err)
}
// 5. 等待事件
fmt.Printf("正在播放: %s\n", path)
select {
case <-ctx.Done():
return fmt.Errorf("播放被用户中断")
case <-done:
fmt.Println("播放正常结束")
return nil
}
}

View File

@@ -1,10 +1,11 @@
package video
package video_old
import (
"bufio"
"context"
"go.uber.org/zap"
"io"
"os"
"os/exec"
"sync"
)
@@ -14,6 +15,11 @@ func Play(ctx context.Context, file string) error {
zap.S().Infoln("video file is empty")
return nil
}
// 判断文件是否存在
if _, err := os.Stat(file); err != nil {
zap.S().Errorf("视频文件不存在: %v", err)
return err
}
cmd := exec.CommandContext(ctx, "ffplay", "-autoexit", "-fs", file)
pipe, err := cmd.StderrPipe()
if err != nil {
@@ -23,16 +29,14 @@ func Play(ctx context.Context, file string) error {
var wait sync.WaitGroup
defer wait.Wait()
a := make(chan struct{})
defer close(a)
wait.Add(1)
go func() {
defer wait.Done()
reader := bufio.NewReader(pipe)
for {
select {
case <-a:
case <-ctx.Done():
return
default:
line, _, err := reader.ReadLine()
if err != nil {