6 Commits

Author SHA1 Message Date
365c357ea2 Merge branch 'main' into clean_beep
# Conflicts:
#	internal/routes/wait.go
2025-07-09 11:58:14 +08:00
bad47c1f5f Merge branch 'refs/heads/main' into clean_beep 2024-12-23 14:18:49 +08:00
a7c241dc4e Merge branch 'refs/heads/main' into clean_beep 2024-12-19 17:43:51 +08:00
b78aa21e58 Merge branch 'refs/heads/main' into clean_beep 2024-12-19 16:22:20 +08:00
7986e1c0d5 Merge branch 'refs/heads/main' into clean_beep 2024-12-19 10:54:06 +08:00
9b9d479caf 注释掉beep代码 2024-11-22 14:50:24 +08:00
17 changed files with 315 additions and 754 deletions

3
.gitignore vendored
View File

@@ -1,7 +1,6 @@
/logs
/.idea
/.vscode
/.qwen
*.mp3
game-driver*
game-driver*

View File

@@ -1,43 +0,0 @@
when:
- event: tag
clone:
git:
image: docker.m.daocloud.io/woodpeckerci/plugin-git
settings:
depth: 1
steps:
# 构建多架构二进制文件
build:
image: docker.m.daocloud.io/golang:1.24-trixie
environment:
GOPROXY: https://goproxy.cn
commands:
- sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources
# 启用多架构支持并安装交叉编译工具
- dpkg --add-architecture arm64
- apt-get update
- apt-get install -y gcc-aarch64-linux-gnu pkg-config
- apt-get install -y libasound2-dev libvlc-dev
- apt-get install -y libasound2-dev:arm64 libvlc-dev:arm64
- mkdir -p release
# 构建 amd64 (native)
- PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig go build -ldflags="-w -s" -o release/game-driver-linux-amd64 .
# 构建 arm64 (cross-compile)
- PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc go build -ldflags="-w -s" -o release/game-driver-linux-arm64 .
- ls -lh release/
# 发布构建产物(可选)
release:
image: docker.m.daocloud.io/woodpeckerci/plugin-release
settings:
base-url: https://gitea.tides.top
title: ${CI_COMMIT_TAG}
api-key:
from_secret: gitea_token
files:
- release/game-driver-linux-amd64
- release/game-driver-linux-arm64
depends_on:
- build

View File

@@ -1,5 +1,5 @@
location: wushan # 项目名称
point: 5 # 点位
point: 3 # 点位
relay:
maxTimeout: 60 # 单位 s必须大于 0
standbyCache: # 待机缓存文件路径
@@ -17,9 +17,9 @@ log:
maxAge: 30
compress: true
mqtt:
url: mqtt://mqtt.wxsxlj.com:1883
clientID: wushan-5
password:
url: mqtt://wushan-mqtt.chaoshengshuzi.com:1883
clientID: wushan-3
password: wushan@1013
aliyun:
accessKeyID:
accessKeySecret:
@@ -31,8 +31,8 @@ aliyun:
# 激光秀点位 osc 参数
game:
host: 192.168.1.191
port: 8000
host: 192.168.0.167
port: 3033
# 待机投影仪控制参数
#wait:
@@ -65,4 +65,4 @@ game:
# empty: 24
# push: 10
# reset: 9
# pull: 11
# pull: 11

12
go.sum
View File

@@ -5,6 +5,8 @@ github.com/adrg/libvlc-go/v3 v3.1.6 h1:Cm22w6xNMDdzYCW8koHgAvjonYm4xbPP5TrlVTtMd
github.com/adrg/libvlc-go/v3 v3.1.6/go.mod h1:xJK0YD8cyMDejnrTFQinStE6RYCV1nlfS8KmqTpszSc=
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.92 h1:qespx4b6EexlXkvQUow9x0v1GnWUJYGU5FWYw3a4Wlg=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.92/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.93 h1:yHRWq/QmBJ3lC15zy1A1+TkvcAN+6dr1bgHsFghKvmk=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.93/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
github.com/aliyun/alibabacloud-nls-go-sdk v1.1.1 h1:LjItoNZuu5xHlsByFo+kr3nGa4LRIESCGWhfurayxBg=
@@ -149,6 +151,8 @@ github.com/warthog618/go-gpiocdev v0.9.1 h1:pwHPaqjJfhCipIQl78V+O3l9OKHivdRDdmgX
github.com/warthog618/go-gpiocdev v0.9.1/go.mod h1:dN3e3t/S2aSNC+hgigGE/dBW8jE1ONk9bDSEYfoPyl8=
github.com/warthog618/go-gpiosim v0.1.1 h1:MRAEv+T+itmw+3GeIGpQJBfanUVyg0l3JCTwHtwdre4=
github.com/warthog618/go-gpiosim v0.1.1/go.mod h1:YXsnB+I9jdCMY4YAlMSRrlts25ltjmuIsrnoUrBLdqU=
github.com/ysmood/fetchup v0.2.4 h1:2kfWr/UrdiHg4KYRrxL2Jcrqx4DZYD+OtWu7WPBZl5o=
github.com/ysmood/fetchup v0.2.4/go.mod h1:hbysoq65PXL0NQeNzUczNYIKpwpkwFL4LXMDEvIQq9A=
github.com/ysmood/fetchup v0.3.0 h1:UhYz9xnLEVn2ukSuK3KCgcznWpHMdrmbsPpllcylyu8=
github.com/ysmood/fetchup v0.3.0/go.mod h1:hbysoq65PXL0NQeNzUczNYIKpwpkwFL4LXMDEvIQq9A=
github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ=
@@ -179,6 +183,8 @@ golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL
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-20250228200357-dead58393ab7 h1:aWwlzYV971S4BXRS9AmqwDLAD85ouC6X+pocatKY58c=
golang.org/x/exp v0.0.0-20250228200357-dead58393ab7/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
@@ -189,6 +195,8 @@ 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.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -196,9 +204,13 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
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.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View File

@@ -1,602 +0,0 @@
#!/usr/bin/env python3
"""
设备初始化自动化脚本
根据 init_device.md 文档自动执行设备初始化步骤
"""
import subprocess
import sys
import os
import argparse
import tempfile
import re
from pathlib import Path
from typing import Optional, List
from dataclasses import dataclass
from contextlib import contextmanager
@dataclass
class InitConfig:
"""初始化配置"""
install_chromium: bool = False
username: Optional[str] = None
dry_run: bool = False
class Logger:
"""优雅的日志输出"""
@staticmethod
def section(title: str):
"""输出章节标题"""
print(f"\n{'='*60}")
print(f" {title}")
print(f"{'='*60}")
@staticmethod
def step(description: str):
"""输出步骤说明"""
print(f"\n🔧 {description}")
@staticmethod
def success(message: str):
"""输出成功信息"""
print(f"{message}")
@staticmethod
def warning(message: str):
"""输出警告信息"""
print(f"⚠️ {message}")
@staticmethod
def error(message: str):
"""输出错误信息"""
print(f"{message}")
@staticmethod
def info(message: str):
"""输出信息"""
print(f" {message}")
class CommandRunner:
"""命令执行器"""
def __init__(self, dry_run: bool = False):
self.dry_run = dry_run
def _execute_command(self, command: str) -> subprocess.CompletedProcess:
"""执行命令的核心逻辑"""
with subprocess.Popen(
command,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1
) as process:
output_lines = []
# 实时读取并显示输出
for line in iter(process.stdout.readline, ''):
print(line.rstrip())
output_lines.append(line)
# 等待进程完成
process.wait()
return subprocess.CompletedProcess(
command,
process.returncode,
''.join(output_lines),
""
)
def run(self, command: str, description: str = "", check: bool = True) -> subprocess.CompletedProcess:
"""执行命令"""
Logger.step(description or command)
if self.dry_run:
Logger.info(f"[预览模式] 命令: {command}")
return subprocess.CompletedProcess(command, 0, "", "")
try:
result = self._execute_command(command)
if check and result.returncode != 0:
raise subprocess.CalledProcessError(result.returncode, command, result.stdout)
Logger.success("执行成功")
return result
except subprocess.CalledProcessError as e:
Logger.error(f"命令执行失败 (退出码: {e.returncode})")
if e.stderr:
print(f"错误详情: {e.stderr.strip()}")
if check:
sys.exit(1)
except Exception as e:
Logger.error(f"执行异常: {e}")
if check:
sys.exit(1)
@contextmanager
def temp_file_with_content(content: str, suffix: str = ""):
"""创建临时文件上下文管理器"""
with tempfile.NamedTemporaryFile(mode='w', suffix=suffix, delete=False) as f:
f.write(content)
temp_path = f.name
try:
yield temp_path
finally:
Path(temp_path).unlink(missing_ok=True)
class DeviceInitializer:
"""设备初始化器"""
# 常量定义
SECTION_OPTIMIZE_BOOT = "优化系统启动时间"
SECTION_UPDATE_SYSTEM = "更新系统包列表"
SECTION_INSTALL_BASIC = "安装基础依赖"
SECTION_ADD_REPOS = "添加第三方软件源"
SECTION_INSTALL_PACKAGES = "安装主要软件包"
SECTION_CONFIGURE_SYSTEM = "配置系统设置"
SECTION_AUTO_STARTX = "配置图形界面自动启动"
SECTION_AUTO_LOGIN = "配置自动登录"
SECTION_CONFIGURE_I3 = "配置 i3 窗口管理器"
SECTION_MANUAL_STEPS = "手动配置步骤"
SECTION_COMPLETE = "初始化完成"
def __init__(self, config: InitConfig):
self.config = config
# 获取实际用户名sudo 下运行时从 SUDO_USER 获取)
self.username = config.username or os.getenv('SUDO_USER') or os.getenv('USER')
self.user_home = Path(f"/home/{self.username}")
self.runner = CommandRunner(config.dry_run)
# 软件包配置
self.base_packages = [
"fontconfig",
"fonts-noto-cjk",
"fonts-noto-color-emoji",
"unclutter",
"xorg",
"i3-wm",
"libvlc-dev",
"vlc-plugin-base",
"vlc-plugin-video-output",
"libasound2-dev",
"alsa-utils",
"trzsz",
"wireguard",
"wireguard-tools"
]
# 基础软件源(始终需要)
self.repositories = [
"ppa:trzsz/ppa"
]
# Chromium 仅在需要时添加的源
self.chromium_repo = "ppa:xtradeb/apps"
def _edit_systemd_service(self, service_name: str, override_content: str, description: str):
"""兼容地编辑 systemd 服务配置"""
# 直接创建 override 目录和文件,兼容所有 systemctl 版本
override_dir = f"/etc/systemd/system/{service_name}.d"
override_file = f"{override_dir}/override.conf"
# 创建目录
self.runner.run(f"sudo mkdir -p {override_dir}", f"创建服务 override 目录: {override_dir}")
# 写入配置文件
with temp_file_with_content(override_content) as temp_file:
self.runner.run(f"sudo cp {temp_file} {override_file}", f"创建服务配置文件: {override_file}")
# 重新加载 systemd 配置
self.runner.run("sudo systemctl daemon-reload", "重新加载 systemd 配置")
Logger.success(f"{description} - 已完成")
def optimize_boot_time(self):
"""优化 Ubuntu 24 开机时间"""
Logger.section(self.SECTION_OPTIMIZE_BOOT)
override_content = "[Service]\nTimeoutStartSec=2sec\n"
self._edit_systemd_service(
"systemd-networkd-wait-online.service",
override_content,
"配置网络等待服务超时时间为2秒"
)
def update_system(self):
"""更新系统包"""
Logger.section(self.SECTION_UPDATE_SYSTEM)
self.runner.run("sudo apt-get update", "刷新软件包索引")
def install_basic_packages(self):
"""安装基础包"""
Logger.section(self.SECTION_INSTALL_BASIC)
basic_packages = ["curl", "gpg"]
packages_str = " ".join(basic_packages)
self.runner.run(f"sudo apt-get install -y {packages_str}", f"安装基础包: {', '.join(basic_packages)}")
def add_repositories(self):
"""添加软件源"""
Logger.section(self.SECTION_ADD_REPOS)
# 基础源始终添加Chromium 源仅在需要安装 Chromium 时添加
repos = list(self.repositories)
if self.config.install_chromium:
repos.append(self.chromium_repo)
Logger.info("已选择安装 Chromium将添加其 PPA 源")
else:
Logger.info("未选择安装 Chromium跳过添加 xtradeb/apps PPA")
for repo in repos:
self.runner.run(f"sudo add-apt-repository -y {repo}", f"添加软件源: {repo}")
self.runner.run("sudo apt-get update", "更新软件包索引")
def install_packages(self):
"""安装主要软件包"""
Logger.section(self.SECTION_INSTALL_PACKAGES)
packages = self.base_packages.copy()
if self.config.install_chromium:
packages.insert(0, "ungoogled-chromium")
Logger.info("已包含 ungoogled-chromium")
packages_str = " ".join(packages)
package_list = ", ".join(packages)
# 安装软件包,实时显示进度
self.runner.run(f"sudo apt-get install -y {packages_str}", f"安装软件包: {package_list}")
def configure_system(self):
"""配置系统设置"""
Logger.section(self.SECTION_CONFIGURE_SYSTEM)
# 设置时区
self.runner.run("sudo timedatectl set-timezone Asia/Shanghai", "设置时区为上海")
# 添加用户到相关组
groups = "audio,video,dialout"
self.runner.run(
f"sudo usermod -aG {groups} {self.username}",
f"将用户 {self.username} 添加到 {groups}"
)
def setup_auto_startx(self):
"""设置自动启动 Xorg"""
Logger.section(self.SECTION_AUTO_STARTX)
bashrc_path = self.user_home / ".bashrc"
startx_config = """
# 自动启动 Xorg 和窗口管理器
if [ -z "$DISPLAY" ] && [ "$(tty)" = "/dev/tty1" ]; then
startx
fi
"""
if self.config.dry_run:
Logger.step("配置自动启动 Xorg")
Logger.info(f"[预览模式] 将添加配置到 {bashrc_path}")
return
# 检查是否已存在配置
if bashrc_path.exists():
try:
content = bashrc_path.read_text(encoding='utf-8')
if 'startx' in content and 'tty1' in content:
Logger.warning("自动启动 Xorg 配置已存在,跳过")
return
except Exception as e:
Logger.warning(f"读取 .bashrc 文件失败: {e}")
# 添加配置
try:
with open(bashrc_path, 'a', encoding='utf-8') as f:
f.write(startx_config)
# 设置文件所有权为实际用户
self.runner.run(f"sudo chown {self.username}:{self.username} {bashrc_path}",
f"设置 .bashrc 所有权为 {self.username}")
Logger.success(f"已配置自动启动 Xorg: {bashrc_path}")
except Exception as e:
Logger.error(f"配置自动启动 Xorg 失败: {e}")
def setup_auto_login(self):
"""设置自动登录"""
Logger.section(self.SECTION_AUTO_LOGIN)
override_content = f"""[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin {self.username} --noclear %I $TERM
"""
self._edit_systemd_service(
"getty@tty1.service",
override_content,
f"配置用户 {self.username} 自动登录"
)
def configure_i3(self):
"""配置 i3 窗口管理器"""
Logger.section(self.SECTION_CONFIGURE_I3)
i3_config_dir = self.user_home / ".config" / "i3"
i3_config_path = i3_config_dir / "config"
if self.config.dry_run:
Logger.step("配置 i3 窗口管理器")
Logger.info(f"[预览模式] 将配置 i3 配置文件: {i3_config_path}")
return
# 创建 i3 配置目录
i3_config_dir.mkdir(parents=True, exist_ok=True)
# 检查是否已有 i3 配置文件
if not i3_config_path.exists():
Logger.info("i3 配置文件不存在,从系统默认配置复制")
self._copy_system_i3_config(i3_config_path)
else:
Logger.info("i3 配置文件已存在,直接更新配置")
# 无论是新生成还是已存在,都更新配置
self._update_i3_config(i3_config_path)
# 设置配置文件所有权为实际用户
self.runner.run(f"sudo chown -R {self.username}:{self.username} {i3_config_dir}", f"设置 i3 配置目录所有权为 {self.username}")
Logger.success(f"已配置 i3 窗口管理器: {i3_config_path}")
def _copy_system_i3_config(self, config_path: Path):
"""从系统默认配置复制 i3 配置文件"""
Logger.step("从系统默认配置复制 i3 配置文件")
system_config = "/etc/i3/config"
self.runner.run(f"cp {system_config} {config_path}", f"复制系统默认 i3 配置到 {config_path}")
# 移除 i3-config-wizard 指令
try:
content = config_path.read_text(encoding='utf-8')
if 'i3-config-wizard' in content:
# 移除包含 i3-config-wizard 的行
lines = content.splitlines()
filtered_lines = [line for line in lines if 'i3-config-wizard' not in line]
with open(config_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(filtered_lines) + '\n')
Logger.info("已移除系统配置中的 i3-config-wizard 指令")
except Exception as e:
Logger.warning(f"移除 i3-config-wizard 指令失败: {e}")
Logger.success("已从系统默认配置复制 i3 配置文件")
def _update_i3_config(self, config_path: Path):
"""更新现有 i3 配置文件"""
Logger.step("更新现有 i3 配置文件")
try:
content = config_path.read_text(encoding='utf-8')
except Exception as e:
Logger.error(f"读取 i3 配置文件失败: {e}")
return
# 添加启动配置项
startup_configs = [
"exec --no-startup-id unclutter -root # 隐藏鼠标",
"exec --no-startup-id xset dpms 0 0 0 # 关闭屏幕自动关闭",
"exec --no-startup-id xset s off # 关闭屏幕保护"
]
config_modified = False
for config_line in startup_configs:
if config_line not in content:
content += f"\n{config_line}\n"
Logger.info(f"添加配置: {config_line}")
config_modified = True
# 移除 i3bar 配置块
if "bar {" in content:
# 使用正则表达式直接移除整个 bar 配置块
bar_pattern = r'bar\s*\{[^}]*\}'
new_content = re.sub(bar_pattern, '', content, flags=re.DOTALL)
if new_content != content:
content = new_content
Logger.info("已移除 i3bar 状态栏配置")
config_modified = True
# 只在有修改时才写回文件
if config_modified:
try:
with open(config_path, 'w', encoding='utf-8') as f:
f.write(content)
Logger.success("i3 配置文件更新完成")
except Exception as e:
Logger.error(f"写入 i3 配置文件失败: {e}")
else:
Logger.info("i3 配置无需修改")
def show_manual_steps(self):
"""显示需要手动完成的步骤"""
Logger.section(self.SECTION_MANUAL_STEPS)
Logger.info("请手动完成以下配置步骤:")
print("\n📋 WireGuard 配置:")
print(" 1. 从服务器获取 WireGuard 配置文件")
print(" 2. 保存到 /etc/wireguard/wg0.conf")
print(" 3. 启用并启动 WireGuard:")
print(" sudo systemctl enable wg-quick@wg0")
print(" sudo systemctl start wg-quick@wg0")
print("\n📋 系统音量配置:")
print(" 1. 运行 alsamixer 进入音量控制界面")
print(" 2. 调节 master 音量到合适级别")
print(" 3. 按 ESC 退出")
print(" 4. 运行 sudo alsactl store 保存音量设置")
print("\n📋 i3 窗口管理器:")
print(" 1. 重启系统后会自动进入 i3")
print(" 2. 脚本已自动配置必要的设置(隐藏鼠标、禁用屏保等)")
print(" 3. 可以根据需要进一步自定义 ~/.config/i3/config")
def run_initialization(self):
"""运行完整的初始化流程"""
Logger.section("设备初始化开始")
Logger.info(f"目标用户: {self.username}")
Logger.info(f"用户主目录: {self.user_home}")
Logger.info(f"安装 Chromium: {'' if self.config.install_chromium else ''}")
Logger.info(f"预览模式: {'' if self.config.dry_run else ''}")
# 初始化步骤配置
initialization_steps = [
("优化系统启动", self.optimize_boot_time),
("更新系统包", self.update_system),
("安装基础依赖", self.install_basic_packages),
("添加软件源", self.add_repositories),
("安装主要软件", self.install_packages),
("配置系统设置", self.configure_system),
("配置自动启动X", self.setup_auto_startx),
("配置自动登录", self.setup_auto_login),
("配置i3窗口管理器", self.configure_i3),
]
try:
for step_name, step_func in initialization_steps:
Logger.info(f"正在执行: {step_name}")
step_func()
self.show_manual_steps()
Logger.section(self.SECTION_COMPLETE)
Logger.success("🎉 所有自动化步骤已完成")
Logger.warning("⚠️ 请重启系统以使所有更改生效")
Logger.info("💡 重启后请完成 WireGuard 和音量的手动配置")
except KeyboardInterrupt:
Logger.error("❌ 用户中断了初始化过程")
sys.exit(1)
except Exception as e:
Logger.error(f"❌ 初始化过程中发生错误: {e}")
sys.exit(1)
class SystemValidator:
"""系统环境验证器"""
@staticmethod
def validate_environment():
"""验证运行环境"""
Logger.section("验证系统环境")
# 检查是否通过 sudo 运行
if os.geteuid() != 0:
Logger.error("此脚本需要使用 sudo 运行")
Logger.info("请使用: sudo python3 init-device.py")
sys.exit(1)
# 检查是否有原始用户信息
if not os.getenv('SUDO_USER'):
Logger.error("请不要直接以 root 用户运行此脚本")
Logger.info("请使用普通用户通过 sudo 运行: sudo python3 init-device.py")
sys.exit(1)
Logger.info(f"检测到通过 sudo 运行脚本,原用户: {os.getenv('SUDO_USER')}")
Logger.success("系统环境验证通过")
def create_argument_parser() -> argparse.ArgumentParser:
"""创建命令行参数解析器"""
parser = argparse.ArgumentParser(
description="设备初始化自动化脚本",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
使用示例:
%(prog)s # 基础安装
%(prog)s --chromium # 包含 Chromium 的完整安装
%(prog)s --dry-run # 预览模式,不实际执行
%(prog)s --username user1 # 指定用户名
"""
)
parser.add_argument(
"--chromium",
action="store_true",
help="安装 ungoogled-chromium 浏览器"
)
parser.add_argument(
"--username",
type=str,
help="指定用户名(默认使用当前用户)"
)
parser.add_argument(
"--dry-run",
action="store_true",
help="预览模式:仅显示将要执行的操作,不实际执行"
)
parser.add_argument(
"--version",
action="version",
version="%(prog)s 1.0.0"
)
return parser
def show_preview(config: InitConfig):
"""显示预览信息"""
Logger.section("预览模式")
steps = [
DeviceInitializer.SECTION_OPTIMIZE_BOOT,
DeviceInitializer.SECTION_UPDATE_SYSTEM,
f"{DeviceInitializer.SECTION_INSTALL_BASIC} (curl, gpg)",
DeviceInitializer.SECTION_ADD_REPOS,
f"{DeviceInitializer.SECTION_INSTALL_PACKAGES}" + (" (包含 ungoogled-chromium)" if config.install_chromium else ""),
f"{DeviceInitializer.SECTION_CONFIGURE_SYSTEM} (时区、用户组)",
DeviceInitializer.SECTION_AUTO_STARTX,
DeviceInitializer.SECTION_AUTO_LOGIN,
DeviceInitializer.SECTION_CONFIGURE_I3,
"显示手动配置步骤"
]
Logger.info("将要执行的操作步骤:")
for i, step in enumerate(steps, 1):
print(f" {i:2d}. {step}")
Logger.warning("这是预览模式,不会实际执行任何操作")
def main():
"""主函数"""
parser = create_argument_parser()
args = parser.parse_args()
# 创建配置
config = InitConfig(
install_chromium=args.chromium,
username=args.username,
dry_run=args.dry_run
)
# 预览模式
if config.dry_run:
show_preview(config)
return
# 验证环境
SystemValidator.validate_environment()
# 运行初始化
initializer = DeviceInitializer(config)
initializer.run_initialization()
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
Logger.error("用户中断操作")
sys.exit(1)
except Exception as e:
Logger.error(f"程序异常退出: {e}")
sys.exit(1)

View File

@@ -2,10 +2,7 @@
```bash
# 在 systemd-networkd-wait-online.service Service 加入 TimeoutStartSec=2sec
sudo EDITOR=vim systemctl edit systemd-networkd-wait-online.service
# 在打开的编辑器中添加:
# [Service]
# TimeoutStartSec=2sec
sudo vim /etc/systemd/system/network-online.target.wants/systemd-networkd-wait-online.service
```
### 初始化设备
@@ -13,9 +10,9 @@ sudo EDITOR=vim systemctl edit systemd-networkd-wait-online.service
```bash
sudo apt update
sudo apt install curl gpg
sudo add-apt-repository ppa:xtradeb/apps # 不安装 ungoogled-chromium 时,不要添加。可能与系统源的库冲突
sudo add-apt-repository ppa:xtradeb/apps
sudo add-apt-repository ppa:trzsz/ppa
sudo apt install -y ungoogled-chromium fontconfig fonts-noto-cjk fonts-noto-color-emoji unclutter xorg i3-wm libvlc-dev vlc-plugin-base vlc-plugin-video-output libasound2-dev alsa-utils trzsz wireguard wireguard-tools
sudo apt install -y ungoogled-chromium fonts-noto-cjk fonts-noto-color-emoji unclutter xorg i3-wm libvlc-dev libasound2-dev alsa-utils trzsz wireguard wireguard-tools
sudo timedatectl set-timezone Asia/Shanghai
sudo usermod -aG audio,video,dialout $USER
```
@@ -51,14 +48,10 @@ fi
### 自动登录
使用 systemctl edit 修改 getty@tty1 服务
编辑 `/etc/systemd/system/getty.target.wants/getty@tty1.service` 文件,将 `ExecStart` 行修改为
```bash
sudo EDITOR=vim systemctl edit getty@tty1.service
# 在打开的编辑器中添加:
# [Service]
# ExecStart=
# ExecStart=-/sbin/agetty --autologin <your_username> --noclear %I $TERM
ExecStart=-/sbin/agetty --autologin <your_username> --noclear %I $TERM
```
其中:

View File

@@ -3,9 +3,7 @@ package middleware
import (
"game-driver/internal/schema"
"game-driver/leaf"
"game-driver/pkg/audio"
"game-driver/pkg/utils"
"github.com/gopxl/beep/v2/speaker"
"go.uber.org/zap"
"sync"
)
@@ -30,30 +28,30 @@ func PlayBgm() leaf.HandlerFunc {
// 发送结束信号
defer close(a)
wait.Add(1)
go func() {
defer wait.Done()
zap.S().Infoln("开始播放背景音乐")
defer zap.S().Infoln("结束背景音乐播放")
ctrl, closer, e := audio.PlayBgmMP3(bgm)
defer closer()
if e != nil {
zap.S().Errorln("播放背景音乐异常:", e)
return
}
select {
case <-a:
{
speaker.Lock()
ctrl.Streamer = nil
speaker.Unlock()
return
}
}
}()
//wait.Add(1)
//go func() {
// defer wait.Done()
//
// zap.S().Infoln("开始播放背景音乐")
// defer zap.S().Infoln("结束背景音乐播放")
//
// ctrl, closer, e := audio.PlayBgmMP3(bgm)
// defer closer()
// if e != nil {
// zap.S().Errorln("播放背景音乐异常:", e)
// return
// }
//
// select {
// case <-a:
// {
// speaker.Lock()
// ctrl.Streamer = nil
// speaker.Unlock()
// return
// }
// }
//}()
} else {
zap.S().Infoln("未解析到背景音乐")
}

View File

@@ -6,15 +6,14 @@ import (
"game-driver/leaf"
"game-driver/pkg/utils"
"game-driver/pkg/video"
"go.uber.org/zap"
)
func OnlyVideo(c *leaf.Context) {
payload := leaf.Value[*schema.PlayModal](c, middleware.PayloadJSONKey)
// utils.BlankOpen()
// defer utils.BlankClose()
utils.BlankOpen()
defer utils.BlankClose()
if url, ok := payload.Game["video"]; ok {
path, local, err := utils.LinkVideo(url.(string))

View File

@@ -6,7 +6,6 @@ import (
"game-driver/internal/schema"
"game-driver/pkg/utils"
"game-driver/pkg/video"
"go.uber.org/zap"
)
@@ -20,8 +19,8 @@ func Video(item schema.WaitItemModel) func(c context.Context) error {
zap.S().Infoln("播放待机视频")
defer zap.S().Infoln("结束待机视频")
// utils.BlankOpen()
// defer utils.BlankClose()
utils.BlankOpen()
defer utils.BlankClose()
err = video.Play(c, path, local)
if err != nil {

View File

@@ -4,7 +4,7 @@ import (
"context"
"game-driver/internal/schema"
"game-driver/pkg/browser"
"game-driver/pkg/utils"
"go.uber.org/zap"
)
@@ -13,8 +13,8 @@ func Web(item schema.WaitItemModel) func(c context.Context) error {
zap.S().Infoln("打开待机网页")
// 控制背光
// utils.BlankOpen()
// defer utils.BlankClose()
utils.BlankOpen()
defer utils.BlankClose()
browser.OpenApp(c, item.Data)
return nil

240
internal/routes/wait.go Normal file
View File

@@ -0,0 +1,240 @@
package routes
import (
"game-driver/internal/middleware"
"game-driver/internal/schema"
"game-driver/leaf"
"game-driver/pkg/relay"
"game-driver/pkg/tts"
"game-driver/pkg/utils"
"game-driver/pkg/video"
"go.uber.org/zap"
"sync"
"time"
)
func timerAction(timestamp int64) <-chan struct{} {
a := make(chan struct{})
go func() {
if timestamp == 0 {
close(a)
} else {
<-time.After(time.Until(time.Unix(timestamp, 0)))
close(a)
}
}()
return a
}
func WaitAction(c *leaf.Context) {
payload := leaf.Value[*schema.WaitModel](c, middleware.PayloadJSONKey)
if payload.Start != 0 && payload.End != 0 && time.Unix(payload.Start, 0).After(time.Unix(payload.End, 0)) {
zap.S().Infoln("开始时间大于结束时间")
return
}
if payload.End != 0 {
cancel := leaf.WithDeadline(c, time.Unix(payload.End, 0))
defer cancel()
}
select {
case <-c.Done():
case <-timerAction(payload.Start):
// 等待组
var wait sync.WaitGroup
defer wait.Wait()
for _, item := range payload.Items {
switch item.Type {
case schema.WaitAudio:
// 执行音乐播放
wait.Add(1)
go func() {
defer wait.Done()
audioAction(c, item, payload.TimeModel)
}()
case schema.WaitTTS:
// 执行TTS播放
wait.Add(1)
go func() {
defer wait.Done()
ttsAction(c, item, payload.TimeModel)
}()
case schema.WaitRelay:
// 执行继电器供电
wait.Add(1)
go func() {
defer wait.Done()
relayAction(c, item, payload.TimeModel)
}()
case schema.WaitVideo:
// 执行视频播放
wait.Add(1)
go func() {
defer wait.Done()
videoAction(c, item, payload.TimeModel)
}()
case schema.WaitWeb:
default:
zap.S().Infof("不支持的类型: %d\n", item.Type)
}
}
}
}
func audioAction(c *leaf.Context, item schema.WaitItemModel, root schema.TimeModel) {
if item.Start != 0 && time.Unix(item.Start, 0).Before(time.Unix(root.Start, 0)) {
zap.S().Infoln("开始时间小于根任务开始时间")
return
}
if item.End != 0 {
cancel := leaf.WithDeadline(c, time.Unix(item.End, 0))
defer cancel()
}
_, err := utils.LinkAudio(item.Data)
if err != nil {
zap.S().Errorln("音频数据获取异常: ", err)
return
}
select {
case <-c.Done():
case <-timerAction(item.Start):
{
zap.S().Infoln("播放待机音乐")
defer zap.S().Infoln("结束待机音乐")
//ctrl, closer, e := audio.PlayBgmMP3(data)
//defer closer()
//if e != nil {
// zap.S().Errorln("播放待机音乐异常", e)
// return
//}
//
//select {
//case <-c.Done():
// {
// speaker.Lock()
// ctrl.Streamer = nil
// speaker.Unlock()
// }
//}
}
}
}
func ttsAction(c *leaf.Context, item schema.WaitItemModel, root schema.TimeModel) {
if item.Start != 0 && time.Unix(item.Start, 0).Before(time.Unix(root.Start, 0)) {
zap.S().Infoln("开始时间小于根任务开始时间")
return
}
if item.End != 0 {
cancel := leaf.WithDeadline(c, time.Unix(item.End, 0))
defer cancel()
}
_, err := tts.DefaultTTS.Get(item.Data)
if err != nil {
zap.S().Errorln("语音合成异常: ", err)
return
}
select {
case <-c.Done():
case <-timerAction(item.Start):
{
zap.S().Infoln("循环播放待机 TTS 语音")
defer zap.S().Infoln("结束待机 TTS 语音")
for {
//audio.PlayWav(c, reader)
select {
case <-c.Done():
return
case <-time.After(time.Duration(item.Interval) * time.Second):
}
}
}
}
}
func relayAction(c *leaf.Context, item schema.WaitItemModel, root schema.TimeModel) {
if item.Start != 0 && time.Unix(item.Start, 0).Before(time.Unix(root.Start, 0)) {
zap.S().Infoln("开始时间小于根任务开始时间")
return
}
if item.End != 0 {
cancel := leaf.WithDeadline(c, time.Unix(item.End, 0))
defer cancel()
}
r, err := relay.New(item.Data)
if err != nil {
zap.S().Errorln("继电器初始化异常: ", err)
return
}
defer r.Close()
select {
case <-c.Done():
case <-timerAction(item.Start):
{
zap.S().Infoln("待机继电器供电")
defer zap.S().Infoln("待机继电器断电")
r.On(0)
<-c.Done()
r.Off(0)
}
}
}
func videoAction(c *leaf.Context, item schema.WaitItemModel, root schema.TimeModel) {
if item.Start != 0 && time.Unix(item.Start, 0).Before(time.Unix(root.Start, 0)) {
zap.S().Infoln("开始时间小于根任务开始时间")
return
}
if item.End != 0 {
cancel := leaf.WithDeadline(c, time.Unix(item.End, 0))
defer cancel()
}
local, err := utils.LinkVideo(item.Data)
if err != nil {
zap.S().Errorln("视频文件获取异常: ", err)
return
}
select {
case <-c.Done():
case <-timerAction(item.Start):
{
zap.S().Infoln("循环播放待机视频")
defer zap.S().Infoln("结束待机视频")
utils.BlankOpen()
defer utils.BlankClose()
for {
err := video.Play(c, local)
if err != nil {
zap.S().Infof("视频播放异常: %s", err)
return
}
select {
case <-c.Done():
return
case <-time.After(time.Duration(item.Interval) * time.Second):
}
}
}
}
}

View File

@@ -83,13 +83,13 @@ func Run() {
cls, err := logger.NewTenCls(fmt.Sprintf("game-driver-%s-%v", config.C.Location, config.C.Point))
if err != nil {
log.Println("初始化腾讯云日志服务异常: ", err)
logger.InitDevLogger()
} else {
cls.Start()
defer cls.Close()
logger.InitProLogger(cls)
}
cls.Start()
defer cls.Close()
logger.InitProLogger(cls)
//logger.InitDevLogger()
// 应用退出时刷新所有缓冲日志
defer logger.Sync()
@@ -101,7 +101,7 @@ func Run() {
zap.S().Infoln("当前IP: ", addrs)
// 启动时关闭屏幕
// utils.BlankClose()
utils.BlankClose()
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)

View File

@@ -8,16 +8,15 @@ import (
"github.com/gopxl/beep/v2/wav"
"go.uber.org/zap"
"io"
"time"
)
var DefaultSampleRate = beep.SampleRate(44100)
func init() {
err := speaker.Init(DefaultSampleRate, DefaultSampleRate.N(time.Second/10))
if err != nil {
panic("扬声器初始化异常: " + err.Error())
}
//err := speaker.Init(DefaultSampleRate, DefaultSampleRate.N(time.Second/10))
//if err != nil {
// panic("扬声器初始化异常: " + err.Error())
//}
zap.S().Infoln("扬声器初始化完成")
}

View File

@@ -14,61 +14,51 @@ func New(host string, port int) *Client {
}
}
// StartCue 播放节目
func (c *Client) StartCue(data string) error {
msg := osc.NewMessage("/beyond/general/StartCue", data)
return c.o.Send(msg)
}
// EnableLaserOutput 打开激光
func (c *Client) EnableLaserOutput() error {
msg := osc.NewMessage("/beyond/general/EnableLaserOutput")
return c.o.Send(msg)
}
// DisableLaserOutput 关闭激光
func (c *Client) DisableLaserOutput() error {
msg := osc.NewMessage("/beyond/general/DisableLaserOutput")
return c.o.Send(msg)
}
// SetLaserOutput 设置激光输出
func (c *Client) SetLaserOutput(data string) error {
msg := osc.NewMessage("/beyond/general/SetLaserOutput", data)
return c.o.Send(msg)
}
// SetLaserOutputColor 设置激光颜色
func (c *Client) SetLaserOutputColor(data string) error {
msg := osc.NewMessage("/beyond/general/SetLaserOutputColor", data)
return c.o.Send(msg)
}
// SetLaserOutputIntensity 设置激光强度
func (c *Client) SetLaserOutputIntensity(data string) error {
msg := osc.NewMessage("/beyond/general/SetLaserOutputIntensity", data)
return c.o.Send(msg)
}
// SetLaserOutputPosition 设置激光位置
func (c *Client) SetLaserOutputPosition(data string) error {
msg := osc.NewMessage("/beyond/general/SetLaserOutputPosition", data)
return c.o.Send(msg)
}
// SetLaserOutputSize 设置激光尺寸
func (c *Client) SetLaserOutputSize(data string) error {
msg := osc.NewMessage("/beyond/general/SetLaserOutputSize", data)
return c.o.Send(msg)
}
// SetLaserOutputSpeed 设置激光速度
func (c *Client) SetLaserOutputSpeed(data string) error {
msg := osc.NewMessage("/beyond/general/SetLaserOutputSpeed", data)
return c.o.Send(msg)
}
// Status 获取状态
func (c *Client) Status() error {
msg := osc.NewMessage("/beyond/general/Status")
return c.o.Send(msg)

View File

@@ -6,7 +6,6 @@ import (
"fmt"
"game-driver/config"
"game-driver/leaf"
"game-driver/pkg/audio"
"game-driver/pkg/errorsx"
"go.uber.org/zap"
"io"
@@ -47,7 +46,7 @@ func (tts *AliTTS) Sound(text string) {
}
buf, err := tts.Get(text)
if err == nil && buf != nil {
audio.PlayWav(tts.ctx, buf)
//audio.PlayWav(tts.ctx, buf)
} else {
zap.S().Errorln("AliTTS 请求异常: ", err)
}

View File

@@ -3,26 +3,12 @@ package utils
import (
"os"
"os/exec"
"sync"
)
var (
xsetBin string
once sync.Once
)
func init() {
once.Do(func() {
if found, err := exec.LookPath("xset"); err == nil {
xsetBin = found
}
})
}
// BlankOpen 打开屏幕
func BlankOpen() {
if xsetBin != "" {
exec.Command(xsetBin, "dpms", "force", "on").Run()
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)
@@ -30,8 +16,8 @@ func BlankOpen() {
// BlankClose 关闭屏幕
func BlankClose() {
if xsetBin != "" {
exec.Command(xsetBin, "dpms", "force", "off").Run()
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)

16
todo.md
View File

@@ -4,21 +4,17 @@
```bash
# 在 systemd-networkd-wait-online.service Service 加入 TimeoutStartSec=2sec
sudo EDITOR=vim systemctl edit systemd-networkd-wait-online.service
# 在打开的编辑器中添加:
# [Service]
# TimeoutStartSec=2sec
sudo vim /etc/systemd/system/network-online.target.wants/systemd-networkd-wait-online.service
```
### 配置时区
```bash
sudo timedatectl set-timezone Asia/Shanghai
```
## linux 下播放音频
```bash
```bash
sudo apt install libasound2-dev alsa-utils
```
@@ -111,14 +107,10 @@ fi
### 自动登录
使用 systemctl edit 修改 getty@tty1 服务
编辑 `/etc/systemd/system/getty.target.wants/getty@tty1.service` 文件,将 `ExecStart` 行修改为
```bash
sudo EDITOR=vim systemctl edit getty@tty1.service
# 在打开的编辑器中添加:
# [Service]
# ExecStart=
# ExecStart=-/sbin/agetty --autologin <your_username> --noclear %I $TERM
ExecStart=-/sbin/agetty --autologin <your_username> --noclear %I $TERM
```
其中: