第12章 跨平台部署:从 Windows 打包到 Linux 工控机#
12.1 Windows 下的交付:PyInstaller 实战#
12.1.1 场景与痛点:客户的电脑上没有 Python#
你的串口调试上位机终于开发完成了。功能完善、测试通过、文档齐全。你信心满满地把整个项目文件夹发给了客户。
客户的回复是:
“双击 .py 文件,提示’无法打开此文件’?Python 是什么?”
或者更委婉一点:
“你发给我的东西我打开是乱码……”
这不是客户的错。客户的电脑上大概率没有安装 Python 环境,更不会有你项目依赖的 pyserial、pyqt5、loguru 等第三方库。你需要把你的 Python 项目打包成一个独立的可执行文件——双击就能运行,不需要安装任何东西。
PyInstaller 就是干这个活的。
12.1.2 PyInstaller 快速入门:一条命令搞定#
安装:
pip install pyinstallerbash最简单的打包——一条命令:
pyinstaller --onefile your_script.pybash执行完成后,你会在 dist/ 目录下找到一个 your_script.exe 文件。这个文件是”自包含的”——双击就能运行,不需要 Python 环境。
这条命令做了什么?
your_script.py
│
├── PyInstaller 分析所有 import 语句
├── 收集 Python 解释器(嵌入到 exe 中)
├── 收集所有依赖库(pyserial、requests、loguru 等)
├── 收集所有 DLL 文件
├── 压缩打包
│
└──→ dist/your_script.exe(一个独立的可执行文件)plaintext常用参数速查:
| 参数 | 说明 | 示例 |
|---|---|---|
--onefile | 打包成单个 exe 文件 | pyinstaller --onefile app.py |
--onedir | 打包成一个目录(默认模式,启动更快) | pyinstaller --onedir app.py |
--windowed | 不显示控制台窗口(GUI 程序用) | pyinstaller --windowed app.py |
--console | 显示控制台窗口(命令行工具用,默认) | pyinstaller --console app.py |
--name | 指定输出文件名 | pyinstaller --name "测试工装" app.py |
--icon | 指定图标文件 | pyinstaller --icon=app.ico app.py |
--add-data | 附加额外文件 | pyinstaller --add-data "config.ini;." app.py |
--hidden-import | 指定隐式导入 | pyinstaller --hidden-import=usb.backend libusb1 app.py |
--specpath | 指定 spec 文件生成路径 | pyinstaller --specpath=. app.py |
--onefile vs --onedir 的选型:
┌─────────────┬──────────────────────────┬─────────────────────────────┐
│ 维度 │ --onefile(单文件) │ --onedir(目录模式) │
├─────────────┼──────────────────────────┼─────────────────────────────┤
│ 交付形式 │ 一个 .exe 文件 │ 一个文件夹(含 exe + DLLs) │
│ 分发便利性 │ 极好(发一个文件就行) │ 一般(需要打包成 zip 发送) │
│ 启动速度 │ 较慢(每次需先解压到临时目录)│ 快(直接运行) │
│ 文件访问 │ 需要特殊处理(见下文) │ 正常访问 │
│ 适用场景 │ 简单命令行工具 │ 复杂项目(GUI + 配置 + DLL)│
└─────────────┴──────────────────────────┴─────────────────────────────┘plaintext实用建议:嵌入式上位机项目通常有配置文件、DLL、图标等外部资源,推荐用
--onedir模式。如果你的脚本非常简单(比如一个纯 Python 的命令行工具),可以用--onefile。
12.1.3 .spec 文件深度定制#
当命令行参数不够用时,你需要定制 .spec 文件。PyInstaller 第一次运行时会自动生成一个 .spec 文件(本质是 Python 语法的配置文件),你可以直接编辑它:
# 第一次运行,生成 spec 文件
pyinstaller --onefile --windowed main.py
# 之后可以直接编辑 spec 文件,然后用 spec 文件来打包
pyinstaller main.specbash一个完整的嵌入式上位机 .spec 文件示例:
# main.spec
# -*- mode: python ; coding: utf-8 -*-
import os
import sys
block_cipher = None
# ========== 路径配置 ==========
# 获取项目根目录(spec 文件所在目录)
BASE_DIR = os.path.dirname(os.path.abspath(SPEC))
# ========== 数据文件配置 ==========
# 格式:(源路径, 目标目录)
# 目标目录用 '.' 表示放在 exe 同级目录
datas = [
# 配置文件
(os.path.join(BASE_DIR, 'config', 'settings.ini'), 'config'),
(os.path.join(BASE_DIR, 'config', 'devices.json'), 'config'),
# 协议文档
(os.path.join(BASE_DIR, 'docs', 'protocol_v3.pdf'), 'docs'),
# 图标和界面资源
(os.path.join(BASE_DIR, 'assets', 'icon.ico'), 'assets'),
(os.path.join(BASE_DIR, 'assets', 'logo.png'), 'assets'),
# 厂家 DLL 文件(嵌入式场景中最关键的)
(os.path.join(BASE_DIR, 'drivers', 'CH347DLL.dll'), 'drivers'),
(os.path.join(BASE_DIR, 'drivers', 'CH347DLLA64.dll'), 'drivers'),
]
# ========== 隐式导入配置 ==========
# PyInstaller 自动分析可能遗漏的导入,需要手动指定
hiddenimports = [
'serial', # pyserial
'serial.tools.list_ports', # pyserial 的设备枚举工具
'PyQt5.sip', # PyQt5 的底层绑定
'usb.backend', # pyusb 的后端(如果用到)
'pkg_resources.py2_warn', # 部分旧库的兼容
]
# ========== 收集分析 ==========
a = Analysis(
['main.py'], # 入口脚本
pathex=[BASE_DIR], # 额外的 Python 搜索路径
binaries=[], # 额外的二进制文件(DLL/SO)
datas=datas, # 额外的数据文件
hiddenimports=hiddenimports, # 隐式导入
hookspath=[], # 自定义 hook 路径
hooksconfig={}, # hook 配置
runtime_hooks=[], # 运行时 hook
excludes=[ # 排除不需要的模块(减小体积)
'tkinter', # 不用 tkinter
'matplotlib', # 如果 GUI 用了 PyQt,排除 matplotlib
'numpy.testing', # 排除 numpy 测试模块
'unittest', # 排除测试框架
'test', # 排除测试目录
],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
# ========== 文件去重 ==========
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
# ========== 生成 EXE ==========
exe = EXE(
pyz,
a.scripts,
a.binaries, # --onefile 模式需要这行
a.zipfiles, # --onefile 模式需要这行
a.datas, # --onefile 模式需要这行
[],
name='TestStation', # 输出文件名(不要用中文,可能有编码问题)
debug=False, # 调试模式(排查打包问题时设为 True)
bootloader_ignore_signals=False,
strip=False, # 去除调试符号(减小体积)
upx=True, # 使用 UPX 压缩(需要安装 UPX)
upx_exclude=[ # 这些 DLL 不要用 UPX 压缩(可能损坏)
'vcruntime140.dll',
'python3.dll',
'python310.dll',
],
runtime_tmpdir=None, # 临时文件目录(--onefile 模式解压目录)
console=False, # False = 不显示控制台(GUI 程序)
icon='assets/icon.ico', # 程序图标
)
# ========== 如果用 --onedir 模式,注释掉上面的 exe 块,启用下面的 ==========
# coll = COLLECT(
# exe,
# a.binaries,
# a.zipfiles,
# a.datas,
# strip=False,
# upx=True,
# upx_exclude=[],
# name='TestStation',
# )python关于 UPX 压缩的说明:
UPX(Ultimate Packer for eXecutables)是一个可执行文件压缩工具。启用 UPX 可以显著减小 exe 文件的体积(通常能减小 30%~50%),但有两个注意事项:
- 需要先安装 UPX:从 https://github.com/upx/upx/releases ↗ 下载,放到系统 PATH 中
- 某些 DLL 经 UPX 压缩后可能无法正常加载,需要用
upx_exclude排除
如果不需要 UPX(推荐新手先不用),把 upx=True 改成 upx=False 即可。
12.1.4 经典问题:打包后找不到资源文件#
这是 PyInstaller 最经典、最让人抓狂的问题。你本地运行好好的,打包成 exe 后就报 FileNotFoundError。
根因分析:
当你用 --onefile 模式打包时,exe 运行时会把所有文件解压到一个临时目录(通常是 C:\Users\你的用户名\AppData\Local\Temp\_MEIxxxxxx\)。你的代码里写的相对路径 config/settings.ini 不会指向 exe 所在的目录,而是指向这个临时目录。
解决方案:路径兼容函数
在你的项目中加入这个工具函数,它能同时兼容”开发环境”和”打包后”两种情况:
# path_utils.py
import sys
import os
def get_base_path() -> str:
"""
获取资源文件的根路径
- 开发环境:返回脚本所在目录
- PyInstaller 打包后:返回 exe 所在目录(--onedir)或临时解压目录(--onefile)
"""
if getattr(sys, 'frozen', False):
# PyInstaller 打包后的环境
if hasattr(sys, '_MEIPASS'):
# --onefile 模式:资源在临时解压目录
return sys._MEIPASS
else:
# --onedir 模式:资源在 exe 所在目录
return os.path.dirname(sys.executable)
else:
# 开发环境:脚本所在目录
return os.path.dirname(os.path.abspath(__file__))
def get_resource_path(relative_path: str) -> str:
"""
获取资源文件的绝对路径
用法:
config_path = get_resource_path("config/settings.ini")
icon_path = get_resource_path("assets/icon.ico")
dll_path = get_resource_path("drivers/CH347DLL.dll")
"""
base_path = get_base_path()
full_path = os.path.join(base_path, relative_path)
return full_path
def get_app_dir() -> str:
"""
获取应用程序所在目录(exe 所在目录)
适用于需要在 exe 旁边创建/读写用户数据文件的场景,
如日志文件、用户配置文件、数据库文件等。
注意:这个目录不是资源目录,而是用户可写的目录。
"""
if getattr(sys, 'frozen', False):
return os.path.dirname(sys.executable)
else:
return os.path.dirname(os.path.abspath(__file__))python在项目中使用:
from path_utils import get_resource_path, get_app_dir
import configparser
# 读取打包进去的配置文件(只读)
config = configparser.ConfigParser()
config_path = get_resource_path("config/settings.ini")
config.read(config_path, encoding="utf-8")
# 读取打包进去的 DLL
import ctypes
dll_path = get_resource_path("drivers/CH347DLL.dll")
dll = ctypes.CDLL(dll_path)
# 日志文件写到 exe 旁边(可写)
from loguru import logger
log_dir = os.path.join(get_app_dir(), "logs")
os.makedirs(log_dir, exist_ok=True)
logger.add(
os.path.join(log_dir, "app_{time:YYYY-MM-DD}.log"),
rotation="50 MB",
retention="30 days",
encoding="utf-8"
)python路径逻辑总结:
开发环境:
├── main.py ← 脚本所在目录
├── config/settings.ini ← get_resource_path("config/settings.ini")
└── drivers/CH347DLL.dll ← get_resource_path("drivers/CH347DLL.dll")
--onedir 打包后:
├── dist/TestStation/
│ ├── TestStation.exe ← get_app_dir() 指向这里
│ ├── config/settings.ini ← get_resource_path("config/settings.ini")
│ ├── drivers/CH347DLL.dll ← get_resource_path("drivers/CH347DLL.dll")
│ ├── _internal/ ← PyInstaller 内部文件
│ └── ...
└── logs/app.log ← 日志写到 exe 旁边
--onefile 打包后:
├── dist/TestStation.exe ← get_app_dir() 指向这里
│
│ 运行时临时解压到:
│ C:\Users\xxx\AppData\Local\Temp\_MEIxxxxxx\
│ ├── config/settings.ini ← get_resource_path() 指向这里
│ ├── drivers/CH347DLL.dll ← get_resource_path() 指向这里
│ └── ...
│
└── logs/app.log ← 日志写到 exe 旁边plaintext12.1.5 DLL 找不到的幽灵错误#
嵌入式项目打包后最常见的另一个问题:程序能启动,但调用 DLL 时报错:
OSError: [WinError 126] 找不到指定的模块。plaintext或者更隐晦的:
OSError: [WinError 193] %1 不是有效的 Win32 应用程序。plaintext常见原因排查清单:
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
找不到指定的模块 | DLL 没有被打包进去 | 在 .spec 的 datas 中添加 DLL |
找不到指定的模块 | DLL 依赖的其他 DLL 缺失 | 用 Dependency Walker 或 dumpbin /dependents 查看依赖链 |
不是有效的 Win32 应用程序 | 32/64 位不匹配 | 64 位 Python 不能加载 32 位 DLL,反之亦然 |
DLL load failed | DLL 依赖的运行时缺失 | 安装对应的 Visual C++ Redistributable |
| 打包成功但运行报错 | DLL 在搜索路径中找不到 | 确认 DLL 被放到了正确的位置 |
用 dumpbin 检查 DLL 依赖链:
# 打开 Visual Studio 的 Developer Command Prompt
# 检查 DLL 的依赖关系
dumpbin /dependents CH347DLL.dllbash输出示例:
Dump of file CH347DLL.dll
File Type: DLL
Image has the following dependencies:
KERNEL32.dll
USER32.dll
MSVCR100.dll ← 这个是 Visual C++ 2010 运行时!
SETUPAPI.dllplaintext如果看到 MSVCRxxx.dll 或 VCRUNTIMExxx.dll,说明你的 DLL 依赖 Visual C++ 运行时。你需要确保目标机器上安装了对应的 VC++ Redistributable,或者把这些 DLL 也一起打包。
Python 版本与 DLL 位数匹配检查:
import struct
import ctypes
import platform
print(f"Python 位数: {struct.calcsize('P') * 8} 位") # 32 或 64
print(f"操作系统: {platform.architecture()[0]}") # 32bit 或 64bit
# 尝试加载 DLL 并捕获详细错误
try:
dll = ctypes.CDLL("CH347DLL.dll")
print("DLL 加载成功")
except OSError as e:
print(f"DLL 加载失败: {e}")
# 常见错误:
# WinError 126 - 找不到模块(DLL 文件不存在或依赖缺失)
# WinError 193 - 32/64 位不匹配python12.1.6 打包后体积优化#
PyInstaller 打包的 exe 文件通常会比较大(100MB~300MB),因为它把整个 Python 解释器和所有依赖都打包进去了。以下是一些优化策略:
策略1:排除不需要的模块
在 .spec 文件的 excludes 列表中添加你不需要的模块:
excludes=[
'tkinter', # 如果你用的是 PyQt,不需要 tkinter
'matplotlib', # 如果 GUI 程序不需要绘图
'numpy.testing', # numpy 的测试模块
'scipy', # 如果没有用到
'pandas.tests', # pandas 的测试模块
'IPython', # IPython 交互环境
'jupyter', # Jupyter 相关
'setuptools', # 安装工具
'pydoc', # 文档生成器
'doctest', # 文档测试
]python策略2:使用虚拟环境打包
不要在全局 Python 环境中打包——全局环境通常装了很多你项目不需要的库。创建一个干净的虚拟环境,只安装项目必需的依赖:
# 创建干净的虚拟环境
python -m venv build_env
build_env\Scripts\activate
# 只安装项目必需的依赖
pip install pyserial pyqt5 loguru requests
pip install pyinstaller
# 在这个干净环境中打包
pyinstaller main.specbash策略3:使用 UPX 压缩
如前所述,安装 UPX 并在 spec 文件中设置 upx=True。
优化效果参考:
| 优化措施 | 体积变化(参考值) |
|---|---|
| 全局环境打包 | ~250MB |
| 虚拟环境打包 | ~150MB |
| + 排除不需要的模块 | ~120MB |
| + UPX 压缩 | ~80MB |
| + strip 去除调试符号 | ~70MB |
12.1.7 打包流程 Checklist#
在实际项目中,建议按照以下清单逐项检查:
□ 1. 创建干净的虚拟环境,只安装必要依赖
□ 2. 在虚拟环境中测试程序运行正常
□ 3. 生成 .spec 文件(第一次运行 PyInstaller)
□ 4. 编辑 .spec 文件:
□ 添加所有外部资源文件(配置、DLL、图标)
□ 配置 hiddenimports
□ 配置 excludes
□ 设置正确的 console/windowed 模式
□ 5. 加入 path_utils.py 中的路径兼容函数
□ 6. 用 spec 文件打包:pyinstaller main.spec
□ 7. 在 dist/ 目录中测试 exe:
□ 功能是否正常
□ 配置文件是否能读取
□ DLL 是否能加载
□ 日志是否正常写入
□ 8. 把 dist/ 目录复制到一台"干净的"Windows 电脑上测试
□ 没有安装 Python 的电脑
□ 没有安装 VC++ 运行时的电脑(如报错则需要安装或打包 VC++ 运行时)
□ 9. 将 dist/ 目录压缩为 zip 发给客户plaintext12.2 后台静默运行:Windows 服务与开机自启#
12.2.1 场景与痛点#
你的测试工装脚本需要在工控机上 7×24 小时运行。但有个问题:
- 工控机偶尔会重启(停电恢复、系统更新)
- 重启后需要有人手动登录 Windows 并双击 exe 启动程序
- 如果是节假日没人值守,产线就停了
你需要让脚本在 Windows 启动时自动运行,而且不需要任何人登录。
有两种方案,根据你的需求选择:
| 方案 | 说明 | 适用场景 |
|---|---|---|
| Windows 服务(Service) | 系统级别的后台服务,开机自动启动,无需用户登录 | 需要无人值守运行的生产环境 |
| 任务计划程序 | Windows 内置的定时任务工具 | 简单的开机自启需求 |
12.2.2 方案一:注册为 Windows 服务(pywin32)#
Windows 服务是一种特殊的程序,它在系统启动时就自动运行,不需要任何人登录桌面。MySQL、Nginx、Redis 在 Windows 上都是以服务形式运行的。
安装 pywin32:
pip install pywin32bash编写服务包装脚本:
# win_service.py
"""
将 Python 脚本注册为 Windows 服务
用法:
安装服务:python win_service.py install
启动服务:python win_service.py start
停止服务:python win_service.py stop
卸载服务:python win_service.py remove
调试模式:python win_service.py debug
"""
import sys
import os
import time
import logging
import subprocess
import servicemanager
import win32event
import win32service
import win32serviceutil
class PythonService(win32serviceutil.ServiceFramework):
"""Windows 服务包装器"""
# 服务配置——修改这些字段以匹配你的项目
_svc_name_ = "TestStationService" # 服务名称(系统内部标识)
_svc_display_name_ = "测试工装监控服务" # 服务显示名称(在服务管理器中看到的名称)
_svc_description_ = "自动化测试工装的后台监控与数据采集服务" # 服务描述
_svc_deps_ = ["EventLog"] # 依赖的其他服务(可选)
def __init__(self, args):
win32serviceutil.ServiceFramework.__init__(self, args)
self.stop_event = win32event.CreateEvent(None, 0, 0, None)
self.is_running = True
self.process = None
# 配置日志
log_dir = os.path.join(os.path.dirname(sys.executable), "logs")
os.makedirs(log_dir, exist_ok=True)
logging.basicConfig(
filename=os.path.join(log_dir, "service.log"),
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
encoding="utf-8"
)
self.logger = logging.getLogger("Service")
def SvcStop(self):
"""服务停止时调用"""
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
self.logger.info("收到停止信号,正在关闭...")
self.is_running = False
win32event.SetEvent(self.stop_event)
# 终止子进程
if self.process and self.process.poll() is None:
self.process.terminate()
self.logger.info("子进程已终止")
def SvcDoRun(self):
"""服务启动时调用"""
servicemanager.LogMsg(
servicemanager.EVENTLOG_INFORMATION_TYPE,
servicemanager.PYS_SERVICE_STARTED,
(self._svc_name_, '')
)
self.logger.info(f"服务 {self._svc_name_} 已启动")
self.main()
def main(self):
"""服务主循环"""
# 获取主程序路径
base_dir = os.path.dirname(sys.executable)
main_script = os.path.join(base_dir, "main.exe")
if not os.path.exists(main_script):
# 如果是开发环境(非打包后),用 Python 直接运行
main_script = os.path.join(base_dir, "main.py")
cmd = [sys.executable, main_script]
else:
cmd = [main_script]
self.logger.info(f"启动主程序: {cmd}")
while self.is_running:
try:
# 启动主程序作为子进程
self.process = subprocess.Popen(
cmd,
cwd=base_dir,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
self.logger.info(f"主程序已启动,PID: {self.process.pid}")
# 等待子进程结束或收到停止信号
while self.is_running:
# 每秒检查一次子进程状态
result = win32event.WaitForSingleObject(
self.stop_event, 1000 # 超时 1000ms
)
if result == win32event.WAIT_OBJECT_0:
# 收到停止信号
break
if self.process.poll() is not None:
# 子进程意外退出
exit_code = self.process.returncode
self.logger.warning(
f"主程序意外退出,返回码: {exit_code},3秒后重启..."
)
time.sleep(3)
break # 跳出内层循环,外层循环会重启
except Exception as e:
self.logger.error(f"服务运行出错: {e}")
time.sleep(5)
if __name__ == "__main__":
if len(sys.argv) == 1:
# 由服务控制管理器启动
servicemanager.Initialize()
servicemanager.PrepareToHostSingle(PythonService)
servicemanager.StartServiceCtrlDispatcher()
else:
# 由命令行启动(install / start / stop / remove / debug)
win32serviceutil.HandleCommandLine(PythonService)python使用方法:
# 以管理员身份运行 CMD(必须用管理员权限!)
# 1. 安装服务
python win_service.py install
# 2. 启动服务
python win_service.py start
# 3. 在 Windows 服务管理器中查看
# 按 Win+R → 输入 services.msc → 找到"测试工装监控服务"
# 4. 停止服务
python win_service.py stop
# 5. 卸载服务
python win_service.py remove
# 6. 调试模式(前台运行,日志输出到控制台)
python win_service.py debugbash服务管理器中的显示效果:
┌──────────────────────────────────────────────────────────────────┐
│ 服务名称 │ 描述 │ 状态 │ 启动类型 │
├──────────────────────────────────────────────────────────────────┤
│ TestStationService│ 自动化测试工装的后台监控... │ 正在运行│ 自动 │
│ MySQL │ MySQL 数据库服务 │ 正在运行│ 自动 │
│ ... │ │ │ │
└──────────────────────────────────────────────────────────────────┘plaintext12.2.3 方案二:任务计划程序(简单场景)#
如果你不需要”无人值守”,只是想让 exe 在有人登录时自动启动,任务计划程序是最简单的方案:
手动配置步骤:
- 按
Win + R,输入taskschd.msc,打开任务计划程序 - 点击”创建基本任务”
- 名称填”测试工装自启动”
- 触发器选”当计算机启动时”或”当用户登录时”
- 操作选”启动程序”,浏览选择你的 exe 文件
- 完成
用 Python 脚本自动创建任务计划(方便部署):
# create_scheduled_task.py
"""
将测试工装注册为 Windows 开机自启任务
需要管理员权限运行
"""
import subprocess
import os
import sys
def create_startup_task(task_name: str, exe_path: str, description: str = ""):
"""
创建 Windows 计划任务(开机自启)
Args:
task_name: 任务名称
exe_path: exe 文件的完整路径
description: 任务描述
"""
if not os.path.exists(exe_path):
print(f"错误:文件不存在 - {exe_path}")
return False
# 使用 schtasks 命令创建计划任务
cmd = [
"schtasks", "/create",
"/tn", task_name, # 任务名称
"/tr", exe_path, # 程序路径
"/sc", "onstart", # 触发条件:系统启动时
"/rl", "highest", # 以最高权限运行
"/f", # 强制覆盖同名任务
]
if description:
cmd.extend(["/d", description])
try:
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
print(f"任务创建成功:{task_name}")
print(f"程序路径:{exe_path}")
return True
else:
print(f"任务创建失败:{result.stderr}")
return False
except Exception as e:
print(f"执行命令失败:{e}")
return False
def delete_task(task_name: str):
"""删除已有的计划任务"""
cmd = ["schtasks", "/delete", "/tn", task_name, "/f"]
try:
subprocess.run(cmd, capture_output=True, text=True)
print(f"任务已删除:{task_name}")
except Exception as e:
print(f"删除失败:{e}")
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Windows 计划任务管理")
parser.add_argument("action", choices=["install", "remove"], help="操作类型")
parser.add_argument("--name", default="TestStation", help="任务名称")
parser.add_argument("--exe", help="exe 文件路径")
args = parser.parse_args()
if args.action == "install":
if not args.exe:
# 默认使用 dist 目录下的 exe
args.exe = os.path.join(os.path.dirname(__file__), "dist", "TestStation.exe")
create_startup_task(args.name, args.exe)
elif args.action == "remove":
delete_task(args.name)python12.2.4 两种方案的选型决策#
你的脚本需要在什么情况下运行?
├── 只需要用户登录后自动启动
│ └── → 任务计划程序(简单、无需额外依赖)
│
├── 需要在无人登录时也能运行(工控机重启后自动恢复)
│ └── → Windows 服务(pywin32)
│ ├── 需要守护进程(子进程崩溃自动重启)
│ └── → 在服务的 main() 中实现子进程管理
│
└── 只是偶尔运行的脚本(定时执行)
└── → 任务计划程序(设置定时触发器)plaintext12.3 Linux 工控机交付#
越来越多的嵌入式设备运行的是 Linux 系统——树莓派、RK3568/RK3588、全志 H616 等。在这些设备上部署 Python 脚本与 Windows 有显著不同。
12.3.1 两种部署方案的选型决策树#
你的项目需要什么?
├── 需要跨环境一致性 / 多版本共存 / 快速迁移
│ └── → Docker 容器化部署
│ └── 适合:多台设备统一部署、需要隔离不同项目
│
├── 单项目部署 / 资源受限 / 快速上手
│ └── → venv + systemd
│ └── 适合:树莓派单项目、工控机资源紧张
│
└── 不确定?
└── → 先用 venv + systemd(简单直接)
└── 后续需要时再迁移到 Dockerplaintext详细对比:
| 维度 | venv + systemd | Docker 容器化 |
|---|---|---|
| 学习成本 | 低(Python 开发者基本都会) | 中(需要学 Docker 基础) |
| 资源开销 | 几乎无额外开销 | Docker 引擎本身占用约 100MB 内存 |
| 环境隔离 | 仅 Python 包隔离 | 完全隔离(系统库、网络、文件系统) |
| 跨设备一致性 | 需要手动保证版本一致 | 镜像即环境,完全一致 |
| 部署速度 | 快(pip install) | 快(docker pull + docker run) |
| 回滚能力 | 手动(需保留旧 venv) | 简单(切换镜像版本标签) |
| 适用场景 | 树莓派、RK3568 等单项目部署 | 多项目共存、多设备统一管理 |
| 最低硬件要求 | 几乎无限制 | 内存 ≥ 512MB,存储 ≥ 1GB |
12.3.2 方案一:venv + systemd 部署#
这是最简单直接的方案——在嵌入式 Linux 上创建虚拟环境,安装依赖,用 systemd 管理进程。
第一步:准备项目目录#
# SSH 登录到工控机
ssh pi@192.168.1.100
# 创建项目目录
sudo mkdir -p /opt/test_station
sudo chown pi:pi /opt/test_station
# 上传项目文件(从开发机执行)
scp -r ./src ./config ./requirements.txt pi@192.168.1.100:/opt/test_station/bash第二步:创建虚拟环境并安装依赖#
cd /opt/test_station
# 创建虚拟环境
python3 -m venv venv
# 激活虚拟环境
source venv/bin/activate
# 升级 pip
pip install --upgrade pip
# 安装依赖(使用国内镜像加速)
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
# 验证安装
python -c "import serial; print(f'pyserial {serial.__version__}')"
# 退出虚拟环境
deactivatebashrequirements.txt 示例:
pyserial==3.5
loguru==0.7.2
requests==2.31.0
pymodbus==3.6.4
paho-mqtt==1.6.1
pyqtgraph==0.13.3txt提示:建议在开发机上用
pip freeze > requirements.txt生成精确版本列表,确保工控机上安装的版本与开发环境完全一致。
第三步:编写 systemd 服务文件#
systemd 是 Linux 系统的服务管理器,类似于 Windows 的服务管理器。它能让你的 Python 脚本在系统启动时自动运行,崩溃后自动重启。
# /etc/systemd/system/test-station.service
[Unit]
Description=测试工装监控服务
Documentation=file:///opt/test_station/docs/README.md
After=network.target # 在网络就绪后启动
Wants=network.target # 希望网络可用
[Service]
Type=simple # 简单类型:主进程就是服务进程
User=pi # 运行用户(不要用 root!)
Group=pi # 运行用户组
WorkingDirectory=/opt/test_station # 工作目录
# 启动命令——使用虚拟环境中的 Python
ExecStart=/opt/test_station/venv/bin/python /opt/test_station/src/main.py
# 环境变量
Environment=PYTHONUNBUFFERED=1 # 禁用 Python 输出缓冲(确保日志实时写入)
Environment=APP_CONFIG=/opt/test_station/config/settings.ini
# 重启策略
Restart=on-failure # 进程异常退出时自动重启
RestartSec=5 # 重启前等待 5 秒
StartLimitIntervalSec=300 # 5 分钟内
StartLimitBurst=5 # 最多重启 5 次(防止无限重启)
# 日志配置
StandardOutput=journal # 标准输出写入 systemd 日志
StandardError=journal # 标准错误写入 systemd 日志
SyslogIdentifier=test-station # 日志标识符
# 资源限制
MemoryMax=512M # 最大内存 512MB(防止内存泄漏撑爆系统)
CPUQuota=80% # 最大 CPU 占用 80%
# 安全加固
ProtectSystem=strict # 禁止写入系统目录
ProtectHome=true # 禁止访问 /home 目录
ReadWritePaths=/opt/test_station/logs /opt/test_station/data # 允许写入的目录
NoNewPrivileges=true # 禁止提升权限
[Install]
WantedBy=multi-user.target # 在多用户模式下启动(等同于开机自启)ini第四步:启动服务#
# 重新加载 systemd 配置
sudo systemctl daemon-reload
# 启动服务
sudo systemctl start test-station
# 查看服务状态
sudo systemctl status test-station
# 设置开机自启
sudo systemctl enable test-station
# 查看实时日志
sudo journalctl -u test-station -f
# 查看最近 100 行日志
sudo journalctl -u test-station -n 100
# 重启服务
sudo systemctl restart test-station
# 停止服务
sudo systemctl stop test-stationbash服务状态输出示例:
● test-station.service - 测试工装监控服务
Loaded: loaded (/etc/systemd/system/test-station.service; enabled; vendor preset: enabled)
Active: active (running) since Mon 2025-07-28 10:30:00 CST; 2h 15min ago
Docs: file:///opt/test_station/docs/README.md
Main PID: 12345 (python)
Tasks: 3 (limit: 4096)
Memory: 85.3M (max: 512.0M)
CPU: 12.456s
CGroup: /system.slice/test-station.service
└─12345 /opt/test_station/venv/bin/python /opt/test_station/src/main.py
Jul 28 10:30:00 raspberrypi systemd[1]: Started 测试工装监控服务。
Jul 28 10:30:01 raspberrypi test-station[12345]: [INFO] 设备连接成功,端口 /dev/ttyUSB0
Jul 28 10:30:02 raspberrypi test-station[12345]: [INFO] MQTT 连接已建立plaintext完整部署脚本#
把上面的步骤封装成一个一键部署脚本:
#!/bin/bash
# deploy.sh - Linux 工控机一键部署脚本
# 用法:bash deploy.sh
set -e # 任何命令失败时立即退出
# ========== 配置区 ==========
APP_NAME="test-station"
APP_DIR="/opt/test_station"
APP_USER="pi"
APP_GROUP="pi"
PYTHON_VERSION="python3"
echo "=========================================="
echo " 测试工装部署脚本"
echo "=========================================="
# 1. 检查 Python 版本
echo "[1/6] 检查 Python 环境..."
$PYTHON_VERSION --version
if [ $? -ne 0 ]; then
echo "错误:未找到 Python3,请先安装"
exit 1
fi
# 2. 创建项目目录
echo "[2/6] 创建项目目录..."
sudo mkdir -p $APP_DIR/{src,config,logs,data,docs}
sudo chown -R $APP_USER:$APP_GROUP $APP_DIR
# 3. 复制项目文件(假设脚本在项目根目录执行)
echo "[3/6] 复制项目文件..."
cp -r src/* $APP_DIR/src/
cp -r config/* $APP_DIR/config/
cp requirements.txt $APP_DIR/
# 4. 创建虚拟环境并安装依赖
echo "[4/6] 创建虚拟环境并安装依赖..."
cd $APP_DIR
$PYTHON_VERSION -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
deactivate
echo "依赖安装完成"
# 5. 创建 systemd 服务文件
echo "[5/6] 配置 systemd 服务..."
sudo tee /etc/systemd/system/${APP_NAME}.service > /dev/null << EOF
[Unit]
Description=测试工装监控服务
After=network.target
Wants=network.target
[Service]
Type=simple
User=${APP_USER}
Group=${APP_GROUP}
WorkingDirectory=${APP_DIR}
ExecStart=${APP_DIR}/venv/bin/python ${APP_DIR}/src/main.py
Environment=PYTHONUNBUFFERED=1
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=300
StartLimitBurst=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=${APP_NAME}
MemoryMax=512M
[Install]
WantedBy=multi-user.target
EOF
# 6. 启动服务
echo "[6/6] 启动服务..."
sudo systemctl daemon-reload
sudo systemctl enable ${APP_NAME}
sudo systemctl start ${APP_NAME}
echo ""
echo "=========================================="
echo " 部署完成!"
echo "=========================================="
echo "查看状态:sudo systemctl status ${APP_NAME}"
echo "查看日志:sudo journalctl -u ${APP_NAME} -f"
echo "重启服务:sudo systemctl restart ${APP_NAME}"
echo "停止服务:sudo systemctl stop ${APP_NAME}"
echo "=========================================="bash12.3.3 方案二:Docker 容器化部署#
Docker 是一种”容器化”技术——它把你的程序、依赖、配置打包成一个”容器镜像”,在任何安装了 Docker 的机器上都能”一键运行”。
为什么嵌入式场景需要 Docker?
想象一下:你有 20 台工控机,每台都运行你的测试脚本。不用 Docker 的话,你需要在每台机器上重复执行部署脚本——安装 Python、创建 venv、pip install、配置 systemd。如果其中一台的系统版本不同,可能还会遇到各种依赖冲突。
用 Docker 的话:
# 在任何一台工控机上执行这一条命令,就能运行你的程序
docker run -d --name test-station \
--device=/dev/ttyUSB0 \
--restart=always \
your-registry/test-station:v1.0bash一条命令,环境完全一致,不需要关心目标机器上装了什么。
Docker 极简入门#
如果你从来没用过 Docker,先理解三个核心概念:
| 概念 | 类比 | 说明 |
|---|---|---|
| 镜像(Image) | 一个安装光盘 | 包含程序、依赖、配置的只读模板 |
| 容器(Container) | 一个运行中的虚拟机 | 从镜像创建的运行实例 |
| Dockerfile | 安装光盘的制作脚本 | 定义如何构建镜像的配置文件 |
安装 Docker(以 Debian/Ubuntu 为例):
# 更新包索引
sudo apt update
# 安装 Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
# 将当前用户添加到 docker 组(免 sudo)
sudo usermod -aG docker $USER
# 重新登录后生效,验证安装
docker --versionbashARM 设备(树莓派等)注意:
树莓派使用的是 ARM 架构,需要安装 ARM 版本的 Docker。上面的安装脚本会自动检测架构。在构建镜像时,也需要使用 ARM 兼容的基础镜像(下文会说明)。
编写 Dockerfile#
# Dockerfile
# ========== 第一阶段:构建阶段 ==========
FROM python:3.11-slim AS builder
WORKDIR /build
# 安装编译依赖(某些 Python 包需要编译 C 扩展)
RUN apt-get update && \
apt-get install -y --no-install-recommends \
gcc \
g++ \
&& rm -rf /var/lib/apt/lists/*
# 复制依赖文件
COPY requirements.txt .
# 安装 Python 依赖到独立目录
RUN pip install --no-cache-dir --prefix=/install \
-r requirements.txt \
-i https://pypi.tuna.tsinghua.edu.cn/simple
# ========== 第二阶段:运行阶段 ==========
FROM python:3.11-slim AS runtime
WORKDIR /app
# 安装运行时需要的系统库(不要安装编译工具)
RUN apt-get update && \
apt-get install -y --no-install-recommends \
libusb-1.0-0 \
udev \
&& rm -rf /var/lib/apt/lists/*
# 从构建阶段复制已安装的 Python 包
COPY --from=builder /install /usr/local
# 复制项目文件
COPY src/ ./src/
COPY config/ ./config/
# 创建日志和数据目录
RUN mkdir -p /app/logs /app/data
# 设置环境变量
ENV PYTHONUNBUFFERED=1
ENV APP_CONFIG=/app/config/settings.ini
# 入口命令
CMD ["python", "src/main.py"]dockerfile逐段解释:
第一阶段(builder):
├── 基于 python:3.11-slim 镜像(包含完整 Python 环境)
├── 安装 gcc/g++(编译 C 扩展用)
├── pip install --prefix=/install(把包装到独立目录)
└── 目的:编译和安装所有依赖
第二阶段(runtime):
├── 基于 python:3.11-slim(干净的 Python 环境)
├── 只安装运行时系统库(不装编译工具)
├── 从第一阶段复制已安装的 Python 包
├── 复制项目代码
└── 目的:构建最终的运行镜像(体积最小化)plaintext为什么要分两个阶段?
因为编译工具(gcc、g++、头文件)在运行时完全不需要。通过多阶段构建,最终镜像中不包含这些编译工具,体积可以减小 50% 以上。
构建和运行镜像#
# 构建镜像
docker build -t test-station:v1.0 .
# 查看镜像大小
docker images test-station
# REPOSITORY TAG SIZE
# test-station v1.0 180MB ← 对比不分阶段的 ~400MB
# 运行容器
docker run -d \
--name test-station \
--device=/dev/ttyUSB0 \ # 映射串口设备
--device=/dev/ttyUSB1 \ # 可以映射多个设备
-v /opt/test_station/logs:/app/logs \ # 日志持久化到宿主机
-v /opt/test_station/data:/app/data \ # 数据持久化到宿主机
-v /opt/test_station/config:/app/config \ # 配置文件可外部修改
--restart=always \ # 崩溃后自动重启
--memory=256m \ # 限制最大内存 256MB
--cpus=1.5 \ # 限制最大 CPU 核数
test-station:v1.0
# 查看运行状态
docker ps
docker logs test-station # 查看日志
docker logs -f test-station # 实时跟踪日志
docker stats test-station # 查看资源使用
# 停止和重启
docker stop test-station
docker start test-stervation
docker restart test-station
# 进入容器调试
docker exec -it test-station bash
# 更新版本
docker stop test-station
docker rm test-station
docker build -t test-station:v1.1 .
docker run -d --name test-station ... test-station:v1.1bash镜像体积优化#
工控机的存储通常很有限(16GB~64GB eMMC),镜像体积至关重要。
策略1:选用 slim 或 alpine 基础镜像
# 标准镜像:~900MB
FROM python:3.11
# slim 镜像:~150MB(推荐,兼容性最好)
FROM python:3.11-slim
# alpine 镜像:~50MB(最小,但部分 C 扩展可能编译失败)
FROM python:3.11-alpinedockerfile对于嵌入式项目,推荐 python:3.11-slim——它在体积和兼容性之间取得了最佳平衡。alpine 虽然更小,但使用 musl libc 而非 glibc,可能导致某些 Python 包(特别是包含 C 扩展的包如 numpy、pyserial)编译失败或运行异常。
策略2:多阶段构建(前面已经展示了)
策略3:利用 Docker 层缓存
# 错误示范:每次修改代码都要重新安装依赖
COPY . . # 代码变了
RUN pip install -r requirements.txt # 依赖没变,但得重装
# 正确示范:先复制依赖文件,再复制代码
COPY requirements.txt . # 依赖文件
RUN pip install -r requirements.txt # 依赖没变时使用缓存
COPY src/ ./src/ # 代码变了,但这层独立dockerfile策略4:清理不必要的文件
# 在同一层中安装和清理(不会增加镜像体积)
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc && \
pip install ... && \
apt-get purge -y gcc && \
apt-get autoremove -y && \
rm -rf /var/lib/apt/lists/*dockerfileARM 设备(树莓派等)的镜像构建#
如果你的工控机是 ARM 架构(如树莓派),有两种方式构建镜像:
方式1:直接在 ARM 设备上构建(简单但慢)
# 直接在树莓派上执行
docker build -t test-station:v1.0 .bash方式2:在 x86 开发机上交叉构建(快但需要配置)
# 在 x86 开发机上,使用 buildx 交叉构建 ARM 镜像
# 1. 安装 QEMU 模拟器
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
# 2. 创建 buildx builder
docker buildx create --name arm-builder --use
# 3. 构建 ARM64 镜像
docker buildx build --platform linux/arm64 \
-t test-station:v1.0-arm64 \
--load \
.
# 4. 推送到仓库或导出
docker save test-station:v1.0-arm64 -o test-station-arm64.tar
# 然后 scp 到树莓派上加载:docker load -i test-station-arm64.tarbash使用 Docker Compose 简化管理#
当你的项目涉及多个容器(比如测试脚本 + MQTT Broker + 数据库)时,用 Docker Compose 管理更加方便:
# docker-compose.yml
version: '3.8'
services:
# 测试工装主程序
test-station:
build: .
container_name: test-station
restart: always
devices:
- "/dev/ttyUSB0:/dev/ttyUSB0" # 映射串口
volumes:
- ./logs:/app/logs # 日志持久化
- ./data:/app/data # 数据持久化
- ./config:/app/config # 配置可外部修改
environment:
- PYTHONUNBUFFERED=1
- APP_CONFIG=/app/config/settings.ini
mem_limit: 256m # 内存限制
depends_on:
- mosquitto # 依赖 MQTT Broker
# MQTT Broker
mosquitto:
image: eclipse-mosquitto:2.0
container_name: mosquitto
restart: always
ports:
- "1883:1883"
volumes:
- ./mosquitto/config:/mosquitto/config
- ./mosquitto/data:/mosquitto/data
- ./mosquitto/log:/mosquitto/logyaml使用 Docker Compose:
# 启动所有服务
docker compose up -d
# 查看状态
docker compose ps
# 查看日志
docker compose logs -f test-station
# 停止所有服务
docker compose down
# 重新构建并启动
docker compose up -d --buildbash12.3.4 实战:串口设备在 Docker 中的权限处理#
嵌入式场景中,串口设备在 Docker 中的使用有一个常见的坑——权限问题。
容器内的进程默认以 root 运行,但串口设备(如 /dev/ttyUSB0)的权限可能不允许访问。
解决方案1:以 root 用户运行容器
docker run -d \
--user root \
--device=/dev/ttyUSB0 \
test-station:v1.0bash安全提示:这种方式简单但不够安全。如果条件允许,推荐方案2。
解决方案2:将用户加入 dialout 组
# 在宿主机上,将 docker 用户加入 dialout 组
sudo usermod -aG dialout $USER
# 在 Dockerfile 中创建相同组
RUN groupadd -g 20 dialout && usermod -aG dialout rootbash解决方案3:使用设备映射 + 权限设置
docker run -d \
--device=/dev/ttyUSB0:/dev/ttyUSB0:rwm \
--group-add $(stat -c '%g' /dev/ttyUSB0) \
test-station:v1.0bash这条命令做了两件事:
--device映射设备并设置读写权限(rwm = read + write + mknod)--group-add将容器内用户加入设备所属的用户组
12.4 AI 协作指南#
12.4.1 让 AI 帮你编写 Dockerfile#
Prompt 模板:
请帮我为以下 Python 项目编写 Dockerfile,要求:
项目信息:
- 项目名:测试工装监控系统
- Python 版本:3.11
- 依赖列表:[粘贴 requirements.txt 内容]
- 需要访问宿主机的串口设备 /dev/ttyUSB0
- 需要持久化日志目录和数据目录
技术要求:
1. 使用多阶段构建(builder + runtime),优化镜像体积
2. 基础镜像使用 python:3.11-slim
3. 配置国内 pip 镜像源加速
4. PYTHONUNBUFFERED=1 确保日志实时输出
5. 创建非 root 用户运行程序(但允许访问串口设备)
6. 包含 .dockerignore 文件内容
额外要求:
- 如果项目包含 C 扩展依赖,在 builder 阶段安装编译工具
- 在 runtime 阶段只保留运行时依赖
- 添加 HEALTHCHECK 指令plaintext12.4.2 让 AI 帮你编写 systemd 服务文件#
Prompt 模板:
请帮我编写 Linux systemd 服务文件,要求:
服务信息:
- 服务名:test-station
- 程序路径:/opt/test_station/venv/bin/python /opt/test_station/src/main.py
- 运行用户:pi
- 工作目录:/opt/test_station
功能要求:
1. 开机自启
2. 崩溃后自动重启(5秒延迟,5分钟内最多重启5次)
3. 限制最大内存 512MB
4. 限制 CPU 占用 80%
5. 标准输出和标准错误都写入 systemd 日志
6. 禁用 Python 输出缓冲
7. 只允许写入 logs 和 data 子目录
请同时生成:
1. .service 文件完整内容
2. 部署和管理命令清单(启动、停止、查看日志等)
3. 常见问题排查指南plaintext12.4.3 让 AI 帮你排查 PyInstaller 打包问题#
Prompt 模板:
我用 PyInstaller 打包了一个嵌入式上位机项目,打包后运行出现以下错误:
[粘贴错误信息]
项目信息:
- Python 版本:[3.10/3.11/3.12]
- PyInstaller 版本:[x.x.x]
- 操作系统:[Windows 10/11]
- 使用了以下第三方库:[列出所有 import]
- 打包命令:[粘贴你使用的命令]
- .spec 文件内容:[粘贴 spec 文件]
我的项目结构:
[粘贴目录树]
请帮我:
1. 分析错误的根因
2. 给出具体的修复方案(修改 spec 文件或其他操作)
3. 如果是 DLL 缺失问题,列出需要添加的所有文件
4. 预防同类问题的建议plaintext12.4.4 审查清单#
当 AI 生成部署代码后,你需要重点审查:
| 审查项 | 关注点 |
|---|---|
| Dockerfile 基础镜像 | 确认架构匹配(x86/ARM)、确认 Python 版本正确 |
| 串口设备权限 | 容器内是否能正常访问 /dev/ttyUSB0 |
| 环境变量 | 敏感信息(密码、密钥)不要硬编码在 Dockerfile 中 |
| 数据持久化 | 日志和数据目录是否正确挂载到宿主机 |
| systemd 用户权限 | 不要用 root 运行业务程序,但要确保有串口访问权限 |
| 重启策略 | 确认有合理的重启限制,防止无限重启耗尽资源 |
| 资源限制 | 内存和 CPU 限制是否合理,防止资源泄漏拖垮系统 |
| PyInstaller DLL | 所有外部 DLL 是否被正确打包,32/64位是否匹配 |
| 镜像体积 | 基础镜像选择、多阶段构建、是否清理了编译工具 |
| 网络配置 | 容器是否需要访问宿主机网络(如连接本地 MQTT) |
12.4.5 一键部署脚本的 AI 生成#
Prompt 模板:
请帮我编写一个 Linux 一键部署脚本 deploy.sh,要求:
部署内容:
- 项目名:test-station
- 部署目录:/opt/test_station
- 运行用户:pi
- Python 虚拟环境 + systemd 服务
脚本功能:
1. 检查 Python3 是否安装
2. 创建项目目录结构
3. 从当前目录复制项目文件
4. 创建 venv 并安装 requirements.txt 中的依赖
5. 生成并安装 systemd 服务文件
6. 启动服务并设置开机自启
7. 打印部署结果和常用管理命令
要求:
- 使用 set -e 确保任何步骤失败时停止
- 每个步骤有清晰的中文提示
- 包含回滚机制(部署失败时清理)
- 使用清华 pip 镜像源plaintext本章小结#
| 知识点 | 解决的工程痛点 |
|---|---|
| PyInstaller 基础打包 | 将 Python 脚本交付给没有 Python 环境的客户 |
| .spec 文件深度定制 | 处理外部 DLL、配置文件、图标等资源的打包 |
sys._MEIPASS 路径兼容 | 解决打包后”找不到资源文件”的经典问题 |
| DLL 依赖排查 | 解决 32/64 位不匹配、运行时缺失等幽灵错误 |
| 打包体积优化 | 虚拟环境打包 + 排除无用模块 + UPX 压缩 |
| Windows 服务(pywin32) | 工控机无人值守运行,崩溃自动重启 |
| 任务计划程序 | 简单的开机自启方案 |
| venv + systemd | Linux 工控机最简单直接的部署方式 |
| Docker 容器化 | 跨设备一致性部署,环境完全隔离 |
| 多阶段构建 | Docker 镜像体积优化(减小 50%+) |
| 串口设备权限 | Docker 容器中访问硬件设备 |
| Docker Compose | 多服务编排管理 |
一句话总结:Windows 用 PyInstaller 打包为 exe,让客户双击即用;Linux 工控机用 venv+systemd 或 Docker 部署,让脚本 7×24 稳定运行;所有部署问题都可以让 AI 帮你排查和生成配置文件——你只需要审查和微调。
动手练习:
- 用 PyInstaller 把你的项目打包为 exe,在一台没有安装 Python 的电脑上运行成功
- 编写
.spec文件,把项目的配置文件和 DLL 正确打包进去- 加入
path_utils.py中的路径兼容函数,确保打包后资源文件能正常读取- 在 Linux 虚拟机或树莓派上,用 venv + systemd 部署你的脚本
- 挑战题:编写 Dockerfile,把项目容器化部署,并验证串口设备能正常访问
- 挑战题:让 AI 为你生成一个完整的一键部署脚本,在工控机上运行成功
这是本书正文的最后一章。回顾整个学习旅程——从第1章的”够用”语法,到第2章的现代工具链,从第36章的硬件通讯实战,到第79章的数据处理与 GUI 开发,再到第10~12章的测试、运维与部署——你已经掌握了一套完整的”Python + 嵌入式”工程方法论。
这本书教你的不是语法,而是工程思维。 语法可以随时查、随时让 AI 生成,但”为什么用这个工具”、“怎么选型”、“怎么让 AI 帮我做得更好”——这些方法论,才是你作为嵌入式工程师在 AI 时代的核心竞争力。