first commit

This commit is contained in:
zhaohengze
2026-05-14 17:53:52 +08:00
commit e1ffcf2e7d
156 changed files with 109026 additions and 0 deletions

View File

@ -0,0 +1,237 @@
#!/bin/bash
set -e
# ============================================
# 串口关机控制器 - 更换串口脚本
# ============================================
INSTALL_DIR="/opt/ttyshutdown"
CONFIG_FILE="${INSTALL_DIR}/config.json"
SIGNAL_SCRIPT="/usr/lib/systemd/system-shutdown/send_signal.sh"
SERVICE_NAME="ttyshutdown"
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; }
# 检查 root 权限
check_root() {
if [ "$(id -u)" -ne 0 ]; then
error "请使用 root 权限运行此脚本: sudo bash change_port.sh"
exit 1
fi
}
# 检查是否已安装
check_installed() {
if [ ! -f "$CONFIG_FILE" ]; then
error "未找到配置文件 $CONFIG_FILE,请先运行 install.sh 安装"
exit 1
fi
}
# 读取当前配置
read_current_config() {
CURRENT_PORT=$(grep -oP '"port"\s*:\s*"\K[^"]+' "$CONFIG_FILE")
CURRENT_BAUD=$(grep -oP '"baudRate"\s*:\s*\K[0-9]+' "$CONFIG_FILE")
CURRENT_WORD=$(grep -oP '"shutdownWord"\s*:\s*"\K[^"]+' "$CONFIG_FILE")
echo -e "${CYAN}=== 当前配置 ===${NC}"
echo " 串口设备: $CURRENT_PORT"
echo " 波特率: $CURRENT_BAUD"
echo " 关机指令: $CURRENT_WORD"
echo ""
}
# 扫描可用串口
scan_serial_ports() {
local ports=()
for pattern in "ttyUSB*" "ttyACM*" "ttyS*"; do
for dev in /dev/$pattern; do
if [ -e "$dev" ]; then
ports+=("$dev")
fi
done
done
echo "${ports[@]}"
}
# 交互选择串口
select_serial_port() {
local ports
ports=$(scan_serial_ports)
if [ -z "$ports" ]; then
warn "未检测到串口设备"
echo -e "请手动输入串口设备路径 (如 ${YELLOW}/dev/ttyUSB0${NC}): "
read -r SELECTED_PORT
if [ ! -e "$SELECTED_PORT" ]; then
warn "设备 $SELECTED_PORT 当前不存在,仍将配置(设备可能稍后接入)"
fi
return
fi
local port_array=($ports)
echo -e "检测到以下串口设备:${GREEN}"
local i=1
for p in "${port_array[@]}"; do
local desc=""
case "$p" in
/dev/ttyUSB*) desc="USB转串口" ;;
/dev/ttyACM*) desc="USB CDC虚拟串口" ;;
/dev/ttyS*) desc="原生串口(COM口)" ;;
esac
local marker=""
if [ "$p" = "$CURRENT_PORT" ]; then
marker=" ${YELLOW}<-- 当前${NC}"
fi
echo " [$i] $p ($desc)${marker}"
((i++))
done
echo -e "${NC}"
local last_index=${#port_array[@]}
echo " [$((last_index + 1))] 手动输入其他路径"
echo ""
local choice
while true; do
read -rp "请选择串口 [1-$((last_index + 1))]: " choice
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le "$((last_index + 1))" ]; then
break
fi
error "无效选择,请重新输入"
done
if [ "$choice" -le "$last_index" ]; then
SELECTED_PORT="${port_array[$((choice - 1))]}"
else
read -rp "请输入串口设备路径: " SELECTED_PORT
if [ -z "$SELECTED_PORT" ]; then
error "路径不能为空"
exit 1
fi
fi
}
# 更新 config.json
update_config() {
local port="$1"
local baud_rate="$2"
local shutdown_word="$3"
cat > "$CONFIG_FILE" <<EOF
{
"port": "${port}",
"baudRate": ${baud_rate},
"shutdownWord": "${shutdown_word}"
}
EOF
info "已更新配置文件: $CONFIG_FILE"
}
# 更新 send_signal.sh
update_send_signal() {
local port="$1"
local baud_rate="$2"
local shutdown_word="$3"
cat > "$SIGNAL_SCRIPT" <<EOF
#!/bin/bash
TTY_DEVICE="${port}"
ACTION=\$1
if [ "\$ACTION" = "poweroff" ] ; then
if [ -w "\$TTY_DEVICE" ]; then
stty -F "\$TTY_DEVICE" ${baud_rate} cs8 -cstopb -parenb
printf "ok\r\n" > "\$TTY_DEVICE"
fi
fi
exit 0
EOF
chmod +x "$SIGNAL_SCRIPT"
info "已更新关机钩子: $SIGNAL_SCRIPT"
}
# 重启服务
restart_service() {
systemctl restart "$SERVICE_NAME"
info "已重启服务: $SERVICE_NAME"
}
# 主流程
main() {
check_root
check_installed
echo -e "${CYAN}========================================${NC}"
echo -e "${CYAN} 串口关机控制器 - 更换串口${NC}"
echo -e "${CYAN}========================================${NC}"
echo ""
# 1. 显示当前配置
read_current_config
# 2. 选择新串口
select_serial_port
if [ "$SELECTED_PORT" = "$CURRENT_PORT" ]; then
warn "选择的新串口与当前串口相同,无需更改"
exit 0
fi
info "新串口: $SELECTED_PORT"
# 3. 可选修改波特率和关机指令
local new_baud="$CURRENT_BAUD"
local new_word="$CURRENT_WORD"
echo ""
read -rp "波特率 [当前 $CURRENT_BAUD,回车保持]: " input_baud
if [ -n "$input_baud" ]; then
new_baud="$input_baud"
fi
read -rp "关机指令 [当前 $CURRENT_WORD,回车保持]: " input_word
if [ -n "$input_word" ]; then
new_word="$input_word"
fi
# 4. 确认
echo ""
echo -e "${CYAN}=== 确认更改 ===${NC}"
echo " 串口设备: $CURRENT_PORT -> $SELECTED_PORT"
[ "$new_baud" != "$CURRENT_BAUD" ] && echo " 波特率: $CURRENT_BAUD -> $new_baud"
[ "$new_word" != "$CURRENT_WORD" ] && echo " 关机指令: $CURRENT_WORD -> $new_word"
echo ""
read -rp "确认更改? [Y/n]: " confirm
if [[ "$confirm" =~ ^[Nn] ]]; then
warn "已取消"
exit 0
fi
# 5. 执行更改
update_config "$SELECTED_PORT" "$new_baud" "$new_word"
update_send_signal "$SELECTED_PORT" "$new_baud" "$new_word"
restart_service
echo ""
echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN}串口更换完成!${NC}重启或重启ttyshutdown服务后生效"
echo -e "${GREEN}========================================${NC}"
echo -e " 新串口: ${CYAN}$SELECTED_PORT${NC}"
echo -e " 波特率: ${CYAN}$new_baud${NC}"
echo -e " 服务状态: ${CYAN}systemctl status ttyshutdown${NC}"
echo -e " 查看日志: ${CYAN}journalctl -u ttyshutdown -f${NC}"
}
main

View File

@ -0,0 +1,5 @@
{
"port": "/dev/ttyUSB0",
"baudRate": 115200,
"shutdownWord": "SHUTDOWN"
}

10
poweroff_linux/go.mod Normal file
View File

@ -0,0 +1,10 @@
module shutdown
go 1.25.6
require (
github.com/creack/goselect v0.1.2 // indirect
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 // indirect
go.bug.st/serial v1.6.4 // indirect
golang.org/x/sys v0.42.0 // indirect
)

8
poweroff_linux/go.sum Normal file
View File

@ -0,0 +1,8 @@
github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A=
go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=

234
poweroff_linux/install.sh Normal file
View File

@ -0,0 +1,234 @@
#!/bin/bash
set -e
# ============================================
# 串口关机控制器 - 安装脚本
# ============================================
INSTALL_DIR="/opt/ttyshutdown"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; }
# 检查 root 权限
check_root() {
if [ "$(id -u)" -ne 0 ]; then
error "请使用 root 权限运行此脚本: sudo bash install.sh"
exit 1
fi
}
# 扫描可用串口
scan_serial_ports() {
local ports=()
for pattern in "ttyUSB*" "ttyACM*" "ttyS*"; do
for dev in /dev/$pattern; do
if [ -e "$dev" ]; then
ports+=("$dev")
fi
done
done
echo "${ports[@]}"
}
# 交互选择串口
select_serial_port() {
echo -e "${CYAN}========================================${NC}"
echo -e "${CYAN} 串口关机控制器 - 安装向导${NC}"
echo -e "${CYAN}========================================${NC}"
echo ""
local ports
ports=$(scan_serial_ports)
if [ -z "$ports" ]; then
warn "未检测到串口设备"
echo -e "请手动输入串口设备路径 (如 ${YELLOW}/dev/ttyUSB0${NC}): "
read -r SELECTED_PORT
if [ ! -e "$SELECTED_PORT" ]; then
warn "设备 $SELECTED_PORT 当前不存在,仍将配置(设备可能稍后接入)"
fi
return
fi
# 转为数组
local port_array=($ports)
echo -e "检测到以下串口设备:${GREEN}"
local i=1
for p in "${port_array[@]}"; do
local desc=""
case "$p" in
/dev/ttyUSB*) desc="USB转串口" ;;
/dev/ttyACM*) desc="USB CDC虚拟串口" ;;
/dev/ttyS*) desc="原生串口(COM口)" ;;
esac
echo " [$i] $p ($desc)"
((i++))
done
echo -e "${NC}"
local last_index=${#port_array[@]}
echo " [$((last_index + 1))] 手动输入其他路径"
echo ""
local choice
while true; do
read -rp "请选择串口 [1-$((last_index + 1))]: " choice
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le "$((last_index + 1))" ]; then
break
fi
error "无效选择,请重新输入"
done
if [ "$choice" -le "$last_index" ]; then
SELECTED_PORT="${port_array[$((choice - 1))]}"
else
read -rp "请输入串口设备路径: " SELECTED_PORT
if [ -z "$SELECTED_PORT" ]; then
error "路径不能为空"
exit 1
fi
fi
}
# 生成 config.json
generate_config() {
local port="$1"
local baud_rate="$2"
local shutdown_word="$3"
cat > "${INSTALL_DIR}/config.json" <<EOF
{
"port": "${port}",
"baudRate": ${baud_rate},
"shutdownWord": "${shutdown_word}"
}
EOF
info "已生成配置文件: ${INSTALL_DIR}/config.json"
}
# 生成 send_signal.sh
generate_send_signal() {
local port="$1"
local baud_rate="$2"
local shutdown_word="$3"
cat > "/usr/lib/systemd/system-shutdown/send_signal.sh" <<EOF
#!/bin/bash
TTY_DEVICE="${port}"
ACTION=\$1
if [ "\$ACTION" = "poweroff" ] ; then
if [ -w "\$TTY_DEVICE" ]; then
stty -F "\$TTY_DEVICE" ${baud_rate} cs8 -cstopb -parenb
printf "ok\r\n" > "\$TTY_DEVICE"
fi
fi
exit 0
EOF
chmod +x "/usr/lib/systemd/system-shutdown/send_signal.sh"
info "已安装关机钩子: /usr/lib/systemd/system-shutdown/send_signal.sh"
}
# 生成 systemd service
generate_service() {
cat > /etc/systemd/system/ttyshutdown.service <<EOF
[Unit]
Description=ttyshutdown
[Service]
Type=simple
User=root
WorkingDirectory=${INSTALL_DIR}
ExecStart=${INSTALL_DIR}/shutdown
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
EOF
info "已生成 systemd 服务: /etc/systemd/system/ttyshutdown.service"
}
# 主安装流程
main() {
check_root
# 1. 选择串口
select_serial_port
info "已选择串口: $SELECTED_PORT"
# 2. 可选参数
local baud_rate=115200
local shutdown_word="SHUTDOWN"
echo ""
read -rp "波特率 [默认 115200]: " input_baud
if [ -n "$input_baud" ]; then
baud_rate="$input_baud"
fi
read -rp "关机指令 [默认 SHUTDOWN]: " input_word
if [ -n "$input_word" ]; then
shutdown_word="$input_word"
fi
echo ""
echo -e "${CYAN}=== 确认配置 ===${NC}"
echo " 串口设备: $SELECTED_PORT"
echo " 波特率: $baud_rate"
echo " 关机指令: $shutdown_word"
echo " 安装目录: $INSTALL_DIR"
echo ""
read -rp "确认安装? [Y/n]: " confirm
if [[ "$confirm" =~ ^[Nn] ]]; then
warn "已取消安装"
exit 0
fi
# 3. 检查二进制文件
if [ ! -f "${SCRIPT_DIR}/shutdown" ]; then
error "未找到预编译的 shutdown 二进制文件"
echo "请将 shutdown 二进制文件复制到 ${SCRIPT_DIR} 目录下"
exit 1
fi
# 4. 安装文件
mkdir -p "$INSTALL_DIR"
cp "${SCRIPT_DIR}/shutdown" "${INSTALL_DIR}/shutdown"
chmod +x "${INSTALL_DIR}/shutdown"
# 5. 生成配置
generate_config "$SELECTED_PORT" "$baud_rate" "$shutdown_word"
# 6. 安装关机钩子
mkdir -p /usr/lib/systemd/system-shutdown
generate_send_signal "$SELECTED_PORT" "$baud_rate" "$shutdown_word"
# 7. 生成并启用 systemd 服务
generate_service
systemctl daemon-reload
systemctl enable ttyshutdown.service
systemctl start ttyshutdown.service
echo ""
echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN} 安装完成!${NC}"
echo -e "${GREEN}========================================${NC}"
echo -e " 服务状态: ${CYAN}systemctl status ttyshutdown${NC}"
echo -e " 查看日志: ${CYAN}journalctl -u ttyshutdown -f${NC}"
echo -e " 停止服务: ${CYAN}systemctl stop ttyshutdown${NC}"
echo -e " 卸载: ${CYAN}sudo bash ${SCRIPT_DIR}/uninstall.sh${NC}"
}
main

View File

@ -0,0 +1,14 @@
#!/bin/bash
TTY_DEVICE="/dev/ttyACM0"
# 参数为 "poweroff", "halt", "reboot"等
ACTION=$1
# 当关机(poweroff)或重启(reboot)时才发送
if [ "$ACTION" = "poweroff" ] ; then
if [ -w "$TTY_DEVICE" ]; then
stty -F "$TTY_DEVICE" 115200 cs8 -cstopb -parenb
printf "ok\r\n" > "$TTY_DEVICE"
fi
fi
exit 0

BIN
poweroff_linux/shutdown Normal file

Binary file not shown.

164
poweroff_linux/shutdown.go Normal file
View File

@ -0,0 +1,164 @@
// 通过串口接收指令触发系统关机的程序
package main
import (
"bytes"
"encoding/json"
"log"
"os"
"os/exec"
"strings"
"time"
"go.bug.st/serial"
)
// Config 配置结构体,从 config.json 文件读取
type Config struct {
SerialPort string `json:"port"` // 串口设备路径,如 COM1 或 /dev/ttyUSB0
BaudRate int `json:"baudRate"` // 串口波特率
ShutdownWord string `json:"shutdownWord"` // 触发关机的指令文本
}
// loadConfig 从指定路径加载配置文件并解析为 Config 结构体
func loadConfig(configPath string) (*Config, error) {
data, err := os.ReadFile(configPath)
if err != nil {
return nil, err
}
var config Config
err = json.Unmarshal(data, &config)
if err != nil {
return nil, err
}
return &config, nil
}
func main() {
// 加载配置文件
config, err := loadConfig("config.json")
if err != nil {
log.Fatalf("加载配置文件失败: %v", err)
}
log.Printf("已监听串口 %s (波特率 %d),等待指令 '%s'...", config.SerialPort, config.BaudRate, config.ShutdownWord)
// 串口读取缓冲区和数据接收缓冲区
buf := make([]byte, 1024)
var buffer bytes.Buffer
// 外层循环:持续尝试连接串口
for {
mode := &serial.Mode{
BaudRate: config.BaudRate,
}
// 尝试打开串口
port, err := serial.Open(config.SerialPort, mode)
if err != nil {
log.Printf("打开串口 %s 失败: %v5秒后重试...", config.SerialPort, err)
time.Sleep(5 * time.Second)
continue
}
defer port.Close()
log.Printf("串口 %s 已连接", config.SerialPort)
// 设置读取超时为1秒避免阻塞
port.SetReadTimeout(time.Second * 1)
// 串口初始化后发送 start 指令
_, err = port.Write([]byte("start\r\n"))
if err != nil {
log.Printf("发送 start 失败: %v", err)
port.Close()
continue
}
log.Printf("已发送 start")
// 启动定时发送 blive的协程
sendDone := make(chan struct{})
go func() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
_, err := port.Write([]byte("blive\r\n"))
if err != nil {
log.Printf("发送 blive 失败: %v", err)
return
}
case <-sendDone:
return
}
}
}()
// 内层循环:读取串口数据
for {
n, err := port.Read(buf)
if err != nil {
if err.Error() == "timeout" {
continue
}
log.Printf("读取串口错误: %v重新连接...", err)
close(sendDone)
port.Close()
break
}
if n > 0 {
// 将接收到的数据写入缓冲区
buffer.Write(buf[:n])
data := buffer.String()
// 检查是否接收到完整的一行数据(以 \r\n 或 \n 结尾)
if strings.Contains(data, "\r\n") || strings.Contains(data, "\n") {
var lines []string
if strings.Contains(data, "\r\n") {
lines = strings.Split(dataAfter(data, "\r\n"), "\r\n")
} else {
lines = strings.Split(dataAfter(data, "\n"), "\n")
}
// 处理每一行数据
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
log.Printf("收到: %s", line)
// 如果匹配到关机指令,执行关机
if line == config.ShutdownWord {
executeShutdown()
}
}
buffer.Reset()
}
}
}
}
}
// dataAfter 获取字符串中最后一个分隔符之前的内容
// 用于处理跨数据包的行边界问题,保留最后一个不完整的行
func dataAfter(s, sep string) string {
idx := strings.LastIndex(s, sep)
if idx == -1 {
return s
}
return s[:idx]
}
// executeShutdown 执行系统关机命令
func executeShutdown() {
log.Println("触发关机指令!")
log.Println("执行关机命令: shutdown -h now")
cmd := exec.Command("shutdown", "-h", "now")
err := cmd.Run()
if err != nil {
log.Fatalf("关机命令执行失败: %v", err)
}
}

Binary file not shown.

View File

@ -0,0 +1,13 @@
[Unit]
Description= ttyshutdown
[Service]
Type=simple
User=root
WorkingDirectory=/home/lyrobot010/shutdown
ExecStart=/home/lyrobot010/shutdown/shutdown
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,35 @@
#!/bin/bash
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
INSTALL_DIR="/opt/ttyshutdown"
if [ "$(id -u)" -ne 0 ]; then
echo -e "${RED}[ERROR]${NC} 请使用 root 权限运行: sudo bash uninstall.sh"
exit 1
fi
echo -e "${YELLOW}即将卸载串口关机控制器${NC}"
read -rp "确认卸载? [y/N]: " confirm
if [[ ! "$confirm" =~ ^[Yy] ]]; then
info "已取消"
exit 0
fi
systemctl stop ttyshutdown.service 2>/dev/null || true
systemctl disable ttyshutdown.service 2>/dev/null || true
rm -f /etc/systemd/system/ttyshutdown.service
systemctl daemon-reload
rm -f /usr/lib/systemd/system-shutdown/send_signal.sh
rm -rf "$INSTALL_DIR"
info "卸载完成"

View File

@ -0,0 +1,243 @@
# 串口关机控制器
通过串口接收指令触发 x86 Ubuntu 系统关机的程序。
## 功能概述
本程序持续监听指定的串口,当接收到特定的关机指令时,自动执行系统关机操作。适用于嵌入式 x86 Ubuntu 系统中需要通过外部设备(如 MCU/Arduino控制关机的场景。
程序启动后会主动发送 `start` 握手指令,并每秒定时发送 `blive` 心跳包,用于告知对端设备本机处于在线状态。
## 主要特性
- 持续监听串口数据
- 支持配置文件自定义串口参数
- 自动重连机制(串口断开后每 5 秒重新连接)
- 按行解析接收到的数据
- 串口连接后发送 `start` 握手指令
- 每秒定时发送 `blive` 心跳包
- 执行系统关机命令
## 通信协议
### 本机发送(→ 对端)
| 指令 | 触发时机 | 说明 |
|------|----------|------|
| `start\r\n` | 串口连接成功后立即发送 | 握手指令,通知对端本机已就绪 |
| `blive\r\n` | 每秒定时发送 | 心跳包,通知对端本机仍在运行 |
| `ok\r\n` | 系统关机poweroff时发送 | 关机钩子脚本发送,通知对端本机即将关机 |
### 本机接收(← 对端)
| 指令 | 说明 |
|------|------|
| `shutdownWord`(配置项) | 收到后立即触发系统关机,默认为 `SHUTDOWN` |
## 配置说明
配置文件为 `config.json`,与可执行文件放在同一目录下,包含以下参数:
| 参数 | 类型 | 说明 | 默认值 |
|------|------|------|--------|
| port | string | 串口设备路径 | `/dev/ttyUSB0` |
| baudRate | int | 串口波特率 | 115200 |
| shutdownWord | string | 触发关机的指令文本 | `SHUTDOWN` |
### 串口设备路径
x86 Ubuntu 下常见的串口设备路径格式:
| 设备类型 | 路径格式 | 说明 |
|----------|----------|------|
| USB 转串口 | `/dev/ttyUSBx` | USB-RS232/USB-TTL 转接器(如 CH340、CP2102、FT232 |
| USB CDC ACM | `/dev/ttyACMx` | USB 虚拟串口设备(使用USB时) |
| 原生串口 | `/dev/ttySx` | 主板自带 RS232 串口COM 口) |
> `x` 为设备编号,从 0 开始。可通过 `ls /dev/tty*` 或 `dmesg | grep tty` 查看系统中可用的串口设备。
### 配置示例
使用 USB 转串口:
```json
{
"port": "/dev/ttyUSB0",
"baudRate": 115200,
"shutdownWord": "SHUTDOWN"
}
```
使用 USB CDC
```json
{
"port": "/dev/ttyACM0",
"baudRate": 115200,
"shutdownWord": "SHUTDOWN"
}
```
使用主板原生串口:
```json
{
"port": "/dev/ttyS0",
"baudRate": 115200,
"shutdownWord": "SHUTDOWN"
}
```
## 交叉编译
本程序在 Windows 上开发,需要交叉编译为 x86 Linux 可执行文件:
```bash
set GOOS=linux
set GOARCH=amd64
set CGO_ENABLED=0
go build -o shutdown shutdown.go
```
PowerShell 环境:
```powershell
$env:GOOS = "linux"
$env:GOARCH = "amd64"
$env:CGO_ENABLED = "0"
go build -o shutdown shutdown.go
```
编译产物 `shutdown` 为 Linux x86_64 可执行文件,需上传至目标 Ubuntu 系统运行。
## 部署与运行
### 方式一:使用安装脚本(推荐)
项目提供 `install.sh` 安装脚本,可自动完成串口选择、配置生成、服务安装等全部流程。
#### 上传项目到目标系统
```bash
scp -r ./ user@<目标IP>:/home/user/shutdown/
```
#### 运行安装脚本
```bash
cd /home/user/shutdown
sudo bash install.sh
```
#### 安装流程
1. **选择串口** - 脚本自动扫描系统中的串口设备(`ttyUSB*``ttyACM*``ttyS*`),列出设备并标明类型,交互选择或手动输入路径
2. **配置参数** - 可自定义波特率(默认 115200和关机指令默认 SHUTDOWN
3. **确认安装** - 显示完整配置供确认
4. **自动编译** - 检测到 Go 环境则交叉编译,否则使用预编译二进制
5. **安装文件** - 二进制和配置文件安装到 `/opt/ttyshutdown/`
6. **关机钩子** - 生成 `/usr/lib/systemd/system-shutdown/send_signal.sh`,系统关机时通过串口发送 `ok\r\n` 通知对端
7. **systemd 服务** - 生成并启用 `ttyshutdown.service`
#### 安装后管理
```bash
# 查看服务状态
systemctl status ttyshutdown
# 查看实时日志
journalctl -u ttyshutdown -f
# 停止服务
systemctl stop ttyshutdown
# 更换串口
sudo bash change_port.sh
# 卸载
sudo bash uninstall.sh
```
### 更换串口
当需要更换串口设备时(如更换 USB 端口、更换转接器等),使用 `change_port.sh` 脚本可自动完成配置更新和服务重启。
```bash
sudo bash change_port.sh
```
#### 更换流程
1. **显示当前配置** - 展示当前串口设备、波特率、关机指令
2. **选择新串口** - 自动扫描系统中可用串口,当前使用的串口会标注 `<-- 当前`,交互选择或手动输入新路径
3. **可选修改参数** - 波特率和关机指令可直接回车保持不变,或输入新值修改
4. **确认更改** - 显示新旧配置对比供确认
5. **执行更改** - 同时更新 `config.json``send_signal.sh`,并自动重启 `ttyshutdown` 服务
> 脚本会同时更新 `config.json` 和 `send_signal.sh` 中的串口配置,确保两者一致。
### 方式二:手动部署
#### 1. 上传到目标系统
```bash
scp shutdown config.json user@<目标IP>:/home/user/shutdown/
```
#### 2. 添加执行权限
```bash
chmod +x shutdown
```
#### 3. 运行程序
```bash
sudo ./shutdown
```
## 注意事项
1. 程序需要 root 权限才能执行系统关机命令
2. 串口断开后会每 5 秒尝试重新连接
3. 接收的数据以换行符(`\n``\r\n`)为单位进行解析
4. 关机指令必须与配置文件中的 `shutdownWord` 完全匹配(区分大小写)
5. 串口重连后 `start` 握手和 `blive` 心跳会自动重新启动
6. 串口设备路径在设备重新插拔后可能变化ttyUSB0 → ttyUSB1建议使用 udev 规则绑定固定设备名
7. 安装脚本会将选定的串口设备同时写入 `config.json``send_signal.sh`,确保两者一致
## 日志输出
程序运行时会输出以下日志信息:
- 配置加载状态
- 串口连接状态
- `start` 握手指令发送状态
- 接收到的数据内容
- 关机指令触发和执行状态
- 串口错误和重连状态
## 关机钩子
系统关机时,`/usr/lib/systemd/system-shutdown/send_signal.sh` 会被 systemd 调用,在系统执行 poweroff 前通过串口向对端发送 `ok\r\n`,通知对端本机即将关机。
该脚本由安装脚本自动生成,其中 `TTY_DEVICE``config.json` 中的 `port` 保持一致。
## 文件说明
| 文件 | 说明 |
|------|------|
| `shutdown.go` | 主程序源码 |
| `config.json` | 串口配置文件(与二进制同目录) |
| `send_signal.sh` | systemd 关机钩子脚本,安装到 `/usr/lib/systemd/system-shutdown/` |
| `ttyshutdown.service` | systemd 服务单元文件 |
| `install.sh` | 安装脚本 |
| `uninstall.sh` | 卸载脚本 |
| `change_port.sh` | 更换串口脚本 |
## 技术实现
- 编程语言Go
- 目标平台x86 Ubuntu (linux/amd64)
- 依赖库:`go.bug.st/serial`(串口通信)
- 关机命令:`shutdown -h now`
- 交叉编译CGO_ENABLED=0 静态编译,无外部依赖