Merge branch 'main' into clean_beep
# Conflicts: # internal/routes/wait.go
This commit is contained in:
28
pkg/browser/browser.go
Normal file
28
pkg/browser/browser.go
Normal 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
65
pkg/oscx/osc.go
Normal 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
234
pkg/pjlink/pjlink.go
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package main
|
||||
|
||||
import "game-driver/pkg/relay"
|
||||
|
||||
func main() {
|
||||
relay.PrintPorts()
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
74
pkg/video/play.go
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
Reference in New Issue
Block a user