feat: 新增 SerialMate 串口调试助手 v0.1.0
- 协议帧构成令牌序列可定制(包头/板ID/LEN口径/5种CRC x 3种覆盖), 默认兼容 DLPilot 光机协议
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
运行目录定位与设置持久化(BASE_DIR 语义与 DLPilot config.py 一致)
|
||||
==================================================================
|
||||
frozen(PyInstaller onefile)时 BASE_DIR=exe 所在目录(sys.executable),
|
||||
源码运行时=config.py 所在目录(即 src/)。设置写/读自 BASE_DIR 平铺的
|
||||
serial_mate.json, 不建子目录。
|
||||
|
||||
读容错: 文件缺失/损坏/缺键一律回落默认值, 并保证返回的 dict 键全齐,
|
||||
GUI 无需判空。
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
if getattr(sys, 'frozen', False):
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(sys.executable))
|
||||
else:
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
CONFIG_FILE = os.path.join(BASE_DIR, 'serial_mate.json')
|
||||
LEGACY_CONFIG = os.path.join(BASE_DIR, 'serial_assistant.json') # 更名前的配置
|
||||
PROFILES_FILE = os.path.join(BASE_DIR, 'board_profiles.json')
|
||||
|
||||
|
||||
def _migrate_legacy_config():
|
||||
"""SerialAssistant -> SerialMate 更名迁移: 旧配置搬为新名(一次性, 已存在则跳过)"""
|
||||
if os.path.exists(CONFIG_FILE) or not os.path.exists(LEGACY_CONFIG):
|
||||
return
|
||||
try:
|
||||
shutil.copy2(LEGACY_CONFIG, CONFIG_FILE)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
DEFAULTS = {
|
||||
'serial': {'port': '', 'baud': 115200, 'bytesize': 8, 'parity': 'N', 'stopbits': 1},
|
||||
'proto': {'tx_hdr': 'D5', 'rx_hdr': 'B5', 'board': '01',
|
||||
'crc_algo': 'SUM8', 'crc_scope': 'no_header', 'parse': True,
|
||||
'layout': 'HDR ID LEN PAYLOAD CRC', 'len_sem': 'payload'},
|
||||
'tx': {'mode': 'wrap', 'periodic': False, 'interval_ms': 1000, 'profile': ''},
|
||||
'rx': {'display': 'text', 'show_raw': True},
|
||||
'channels': [
|
||||
{'enabled': False, 'name': '通道1', 'mode': 'num', 'offset': 0,
|
||||
'fmt': 'u16be', 'index': 1, 'pattern': '(?<=:)\\d+', 'scale': 1.0},
|
||||
{'enabled': False, 'name': '通道2', 'mode': 'bin', 'offset': 0,
|
||||
'fmt': 's16be', 'index': 1, 'pattern': '(?<=:)\\d+', 'scale': 1.0},
|
||||
{'enabled': False, 'name': '通道3', 'mode': 'bin', 'offset': 0,
|
||||
'fmt': 'u16be', 'index': 1, 'pattern': '(?<=:)\\d+', 'scale': 1.0},
|
||||
{'enabled': False, 'name': '通道4', 'mode': 'bin', 'offset': 0,
|
||||
'fmt': 'u16be', 'index': 1, 'pattern': '(?<=:)\\d+', 'scale': 1.0},
|
||||
],
|
||||
}
|
||||
|
||||
# 板型档案的帧协议字段(不含 parse —— 解析开关是查看偏好, 不随板型走)
|
||||
_PROFILE_PROTO_KEYS = ('tx_hdr', 'rx_hdr', 'board', 'layout', 'len_sem',
|
||||
'crc_algo', 'crc_scope')
|
||||
|
||||
# 首个板型的命令预设(DLPilot 光机命令集, 全表见上游 protocols.md)
|
||||
DEFAULT_COMMANDS = (
|
||||
('握手', 'M1'), ('查版本', 'M999'),
|
||||
('开光机', 'M730S0'), ('关光机', 'M730S1'),
|
||||
('设光强', 'M731S100'), ('查电流', 'M732'),
|
||||
('查DMD温度', 'M201'), ('查LAMP温度', 'M202'),
|
||||
('TEC自动控温开', 'M7S0'), ('TEC自动控温关', 'M7S1'),
|
||||
('凝露点33.0°C', 'M8S330'), ('软件重启', 'M888'),
|
||||
)
|
||||
|
||||
|
||||
def default_profiles():
|
||||
"""首个板型档案: DLPilot 光机(协议字段取默认值)"""
|
||||
return [{'name': 'DLPilot光机',
|
||||
'proto': {k: DEFAULTS['proto'][k] for k in _PROFILE_PROTO_KEYS},
|
||||
'commands': [list(c) for c in DEFAULT_COMMANDS]}]
|
||||
|
||||
|
||||
def _merge(base, over):
|
||||
"""递归覆盖: over 缺键用 base 补齐, over 多余键保留(向前兼容旧配置)"""
|
||||
out = dict(base)
|
||||
for k, v in over.items():
|
||||
if k in base and isinstance(base[k], dict) and isinstance(v, dict):
|
||||
out[k] = _merge(base[k], v)
|
||||
else:
|
||||
out[k] = v
|
||||
return out
|
||||
|
||||
|
||||
def load():
|
||||
_migrate_legacy_config()
|
||||
"""读设置; 任何异常回落默认(键全齐), 不抛出"""
|
||||
try:
|
||||
with open(CONFIG_FILE, encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError('配置根不是对象')
|
||||
except Exception:
|
||||
return json.loads(json.dumps(DEFAULTS)) # 深拷贝默认值
|
||||
return _merge(DEFAULTS, data)
|
||||
|
||||
|
||||
def save(cfg):
|
||||
"""写设置; 失败静默(只读目录等场景不阻塞退出)"""
|
||||
try:
|
||||
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(cfg, f, ensure_ascii=False, indent=1)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def load_profiles():
|
||||
"""读板型档案(board_profiles.json); 缺失/损坏回落默认并落盘。
|
||||
档案结构: {'profiles': [{'name', 'proto'{7字段}, 'commands'[[名,命令]..]}..]}"""
|
||||
try:
|
||||
with open(PROFILES_FILE, encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
profiles = data.get('profiles') if isinstance(data, dict) else data
|
||||
if isinstance(profiles, list) and profiles:
|
||||
out = []
|
||||
for p in profiles:
|
||||
if not isinstance(p, dict) or not p.get('name'):
|
||||
continue
|
||||
out.append({'name': str(p['name']),
|
||||
'proto': {k: p.get('proto', {}).get(k,
|
||||
DEFAULTS['proto'][k])
|
||||
for k in _PROFILE_PROTO_KEYS},
|
||||
'commands': [[str(n), str(c)]
|
||||
for n, c in p.get('commands', [])
|
||||
if isinstance(n, str) and isinstance(c, str)]})
|
||||
if out:
|
||||
return out
|
||||
except Exception:
|
||||
pass
|
||||
profiles = default_profiles()
|
||||
save_profiles(profiles)
|
||||
return profiles
|
||||
|
||||
|
||||
def save_profiles(profiles):
|
||||
"""写板型档案; 失败静默并返回 False"""
|
||||
try:
|
||||
with open(PROFILES_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump({'profiles': profiles}, f, ensure_ascii=False, indent=1)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
Reference in New Issue
Block a user