Files
hulk 000781e2f5 GUI v0.2.13: chart Y-axis range control (right-edge dual-handle slider)
- TemperatureChart gains a right-edge slider on a FIXED -20..120C rail
  (user-specified): top handle = upper bound, bottom handle = lower bound,
  drag middle band = pan keeping span, click free rail = snap that bound.
  First drag engages manual mode (yManualEngaged -> sync button state +
  config); release quantizes to 0.1C via yRangeChanged, min span 0.5C.
  Bound values during drag must come from live display range, not the
  stale paint cache (two drags without repaint made pan jump the range).
- Auto/manual toggle moved into the card header (was a wrapped second
  row); pause/clear merged into header too; legend moved to top-left.
- Value labels next to handles only visible on hover/drag (constant
  labels covered the curve per user feedback).
- Out-of-range curves clipped back to the plot area (setClipRect);
  current-value marker suppressed outside the manual range.
- Draft textbox rows from v0.2.10 superseded; versions v0.2.10..v0.2.13.
- Fixed "no data yet" hint never disappearing: set_source cached a
  stale _has_data bool once at build time while data flows through the
  same list references; now evaluated live at paint time.
- config: y_axis key persists mode/bounds; invalid stored values fall
  back to auto. selftest settings isolated to a temp dir (user json with
  stored manual state broke the fresh-default assertions).
- selftest_gui: slider hit/drag/clamp/pan/clipping regression + hint
  gating; 83 checks all pass.
2026-10-15 21:30:21 +08:00

103 lines
3.6 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
工具运行配置: 运行目录定位 / filmforge_gui.json(串口记忆 + 监视设置)
================================================================
PyInstaller onefile 打包后 __file__ 指向临时解压目录,
配置必须落在 exe 旁边, 故 frozen 时改用 sys.executable 定位。
"""
import json
import os
import sys
import filmforge_rs485
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__))
SETTINGS_FILE = os.path.join(BASE_DIR, 'filmforge_gui.json')
LOG_DIR = os.path.join(BASE_DIR, 'logs')
# 曲线时间窗候选(秒): 下拉框文案 -> 秒
WINDOW_OPTIONS = (('2 分钟', 120), ('5 分钟', 300), ('15 分钟', 900),
('60 分钟', 3600), ('全部', 0))
DEFAULT_WINDOW_SEC = 300
MAX_SAMPLES = 200000 # 曲线内存上限(点), 超出丢最旧; 10Hz 下约 5.5 小时
# 串口记忆(协议固定 115200-8-N-1, 仍允许改以便调试)
SERIAL_CFG = {'port': '', 'baud': filmforge_rs485.DEFAULT_BAUD,
'bytesize': 8, 'parity': 'N', 'stopbits': 1}
REPORT_MS = 500 # 上报间隔输入框默认值
WINDOW_SEC = DEFAULT_WINDOW_SEC
THEME = 'System'
Y_MODE = 'auto' # 曲线 Y 轴(温度轴): 'auto'=自适应 / 'manual'=固定范围
Y_MIN = None # 手动 Y 轴上下限(℃), 仅 Y_MODE='manual' 且合法时生效
Y_MAX = None
def load_settings():
"""读 filmforge_gui.json(缺失/损坏按默认值), 更新模块级默认字典"""
try:
with open(SETTINGS_FILE, encoding='utf-8') as f:
data = json.load(f)
except Exception:
return
if not isinstance(data, dict):
return
ser = data.get('serial')
if isinstance(ser, dict):
SERIAL_CFG['port'] = str(ser.get('port') or '')
try:
SERIAL_CFG['baud'] = int(ser.get('baud') or filmforge_rs485.DEFAULT_BAUD)
except (TypeError, ValueError):
pass
try:
SERIAL_CFG['bytesize'] = int(ser.get('bytesize') or 8)
except (TypeError, ValueError):
pass
SERIAL_CFG['parity'] = str(ser.get('parity') or 'N').upper()[:1]
try:
SERIAL_CFG['stopbits'] = int(ser.get('stopbits') or 1)
except (TypeError, ValueError):
pass
for key, target in (('report_ms', 'REPORT_MS'), ('window_sec', 'WINDOW_SEC')):
try:
globals()[target] = int(data.get(key))
except (TypeError, ValueError):
pass
global THEME
theme = str(data.get('theme') or '')
if theme in ('Dark', 'Light', 'System'):
THEME = theme
ax = data.get('y_axis')
if isinstance(ax, dict):
mode = str(ax.get('mode') or '')
if mode in ('auto', 'manual'):
globals()['Y_MODE'] = mode
try:
globals()['Y_MIN'] = round(float(ax.get('min')), 2)
except (TypeError, ValueError):
pass
try:
globals()['Y_MAX'] = round(float(ax.get('max')), 2)
except (TypeError, ValueError):
pass
def save_settings():
"""写出全部设置(始终写全键)"""
data = {
'serial': dict(SERIAL_CFG),
'report_ms': REPORT_MS,
'window_sec': WINDOW_SEC,
'theme': THEME,
'y_axis': {'mode': Y_MODE, 'min': Y_MIN, 'max': Y_MAX},
}
try:
with open(SETTINGS_FILE, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
except OSError:
pass # 只读目录(exe 旁)下静默放弃, 不影响运行