#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ variation_calc.py — vd960Loop variation ↔ ΔL / ΔL-L 换算工具 物理关系(推导见 docs/variation-analysis.md §9): CAPVD ∝ T = 2π·√(LC) (周期域, 归一化到 MEASUREMENT_BASE≈131072) variation = Origin − CAPVD (V1.05 起 3B 有符号补码, 正=金属进入方向) variation/Origin = 1 − √(1 + ΔL/L0) (精确) ΔL/L0 ≈ −2 × variation/Origin (一阶, |ΔL/L| ≪ 1 时, 残差 <0.2%) Δf/f ≈ variation/Origin (频域对应: CAPVD ∝ 1/f) L0 = 1/(4π²·f0²·C) (绝对电感, f0=上报频率, C=档位电容) 电容档位无关性: Origin 与 CAPVD 均 ∝ √C, variation/Origin 中 C 消掉, 四档电容(33/43/66/76nF)共用同一套 ΔL/L 换算表。 用法示例: # 正向: 频率+电容+variation+origin → L0, ΔL, ΔL/L python3 variation_calc.py --freq 100 --cap 43 --variation 216 --origin 131072 # 反推: 给定 ΔL/L 看需要多大 variation python3 variation_calc.py --freq 100 --cap 43 --dl-rel 0.33 # 解析协议 3B LE 有符号补码 (V1.05 variation 字段) python3 variation_calc.py --parse-var F6FFFF # 灵敏度档位对照表 python3 variation_calc.py --sens-table # 交互模式 python3 variation_calc.py --interactive """ import argparse import math import sys # ---------------------------------------------------------------- 常量 CAP_OPTIONS = (33, 43, 66, 76) # 线圈内部电容四档 (nF) ORIGIN_DEFAULT = 131072 # MEASUREMENT_BASE = 2^17 SENS_TABLE = {0: (216, 108), 1: (108, 72), 2: (36, 18), 3: (10, 9)} SENS_NAMES = {0: "0(低)", 1: "1(中)", 2: "2(高)", 3: "3(最高)"} # ---------------------------------------------------------------- 核心计算 def inductance(freq_hz, cap_nf): """绝对电感 L0 = 1/(4π²·f²·C), 返回 μH""" c = cap_nf * 1e-9 return 1.0 / (4.0 * math.pi * math.pi * freq_hz * freq_hz * c) * 1e6 def variation_ratio(variation, origin): """variation/Origin (有符号)""" return variation / origin def delta_ll(vr): """ΔL/L: (精确, 一阶), 返回相对值(负=电感减小)""" exact = vr * vr - 2.0 * vr approx = -2.0 * vr return exact, approx def variation_from_dl_rel(dl_rel_pct, origin): """反推: 给定 |ΔL/L|(%) → 需要的 variation (计数域)""" r = dl_rel_pct / 100.0 # ΔL/L = −r vr = 1.0 - math.sqrt(1.0 - r) # 精确 return vr * origin, vr def sens_hit(vr): """当前 variation/Origin 落在哪档进入阈值 (SensTable/65536)""" for sens in (0, 1, 2, 3): thr = SENS_TABLE[sens][0] / 65536.0 if abs(vr) >= thr: return sens, thr return None, None def parse_var_3byte(hex_str): """V1.05 协议 variation 字段: 3B LE 有符号补码 → int32 F6FFFF → −10; 000001 → 65536 """ h = hex_str.strip() if len(h) != 6: raise ValueError("需要 6 位 hex (3 字节 LE), 如 F6FFFF") b0 = int(h[0:2], 16) b1 = int(h[2:4], 16) b2 = int(h[4:6], 16) v = b0 | (b1 << 8) | (b2 << 16) if v & 0x800000: v |= 0xFF000000 if v >= 0x80000000: v -= 0x100000000 return v def fmt_sens_table(): lines = ["灵敏度档位 → ΔL/L 触发阈值 (vd960Loop SensTable, 一阶 ΔL/L≈−2×vr):"] lines.append(f"{'SENS':<8}{'进入表':<8}{'离开表':<8}{'Δf/f进入':<12}{'ΔL/L进入':<12}{'ΔL/L离开'}") for sens in (0, 1, 2, 3): sin, sout = SENS_TABLE[sens] vr_in = sin / 65536.0 vr_out = sout / 65536.0 lines.append(f"{SENS_NAMES[sens]:<8}{sin:<8}{sout:<8}" f"{vr_in * 100:>7.4f}%{'':<4}{-2 * vr_in * 100:>8.4f}%{'':<4}{-2 * vr_out * 100:>8.4f}%") return "\n".join(lines) def forward(freq, cap, variation, origin): """正向计算并打印""" f_hz = freq * 1000.0 if freq < 1e6 else freq l0 = inductance(f_hz, cap) vr = variation_ratio(variation, origin) exact, approx = delta_ll(vr) dl = l0 * exact sens, thr = sens_hit(vr) out = [] out.append("=" * 62) out.append("正向换算: 频率+电容+variation+Origin") out.append("=" * 62) out.append(f" 上报频率 f0 : {f_hz / 1000.0:>10.3f} kHz") out.append(f" 电容档 C : {cap:>10d} nF") out.append(f" 绝对电感 L0 : {l0:>10.3f} μH (L=1/(4π²f²C))") out.append(f" Origin : {origin:>10d} (CAPVD 基线)") out.append(f" variation : {variation:>10d} (3B 有符号)") out.append(f" variation/Origin : {vr * 100:>9.4f} % (≈ Δf/f)") out.append("-" * 62) out.append(f" ΔL/L 一阶 : {-2 * vr * 100:>9.4f} % (≈ −2×vr)") out.append(f" ΔL/L 精确 : {exact * 100:>9.4f} % (vr²−2vr)") out.append(f" ΔL (绝对) : {dl:>9.4f} μH") out.append("-" * 62) if sens is not None: out.append(f" ≥ SENS={SENS_NAMES[sens]} 进入阈值 (vr_thr={thr * 100:.4f}%)") else: out.append(f" 低于全部进入阈值 (最小档 SENS=3: {10 / 65536 * 100:.4f}%)") out.append("=" * 62) return "\n".join(out) def reverse(freq, cap, dl_rel_pct, origin): """反推: 给定 |ΔL/L|% → variation""" f_hz = freq * 1000.0 if freq < 1e6 else freq l0 = inductance(f_hz, cap) variation, vr = variation_from_dl_rel(dl_rel_pct, origin) out = [] out.append("=" * 62) out.append(f"反推: ΔL/L = −{dl_rel_pct}% 需要的 variation") out.append("=" * 62) out.append(f" L0 = {l0:.3f} μH (f0={f_hz / 1000:.3f}kHz, C={cap}nF)") out.append(f" ΔL = {l0 * (-dl_rel_pct / 100.0):.4f} μH") out.append(f" variation/Origin = {vr * 100:.4f} %") out.append(f" variation = {variation:.0f} (Origin={origin})") out.append(f" 一阶估算 = {origin * dl_rel_pct / 200.0:.0f} (vr≈r/2)") out.append("=" * 62) return "\n".join(out) def interactive(): print("vd960Loop variation 换算工具 — 交互模式 (Ctrl+C 退出)") cap = int(input("电容档 (33/43/66/76 nF): ")) if cap not in CAP_OPTIONS: print(f"警告: {cap}nF 不在标准档位 {CAP_OPTIONS} 中") freq = float(input("上报频率 (kHz, 如 100): ")) origin = int(input(f"Origin (默认 {ORIGIN_DEFAULT}): ") or ORIGIN_DEFAULT) while True: try: variation = int(input("variation (有符号, q 退出): ")) except ValueError: break print() print(forward(freq, cap, variation, origin)) def main(): p = argparse.ArgumentParser( description="vd960Loop variation ↔ ΔL/ΔL-L 换算工具", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__) p.add_argument("--freq", type=float, help="上报频率 (默认 kHz, <1e6 视为 kHz)") p.add_argument("--cap", type=int, choices=CAP_OPTIONS, help="电容档位 nF: 33/43/66/76") p.add_argument("--variation", type=int, help="variation (3B 有符号, 计数域)") p.add_argument("--origin", type=int, default=ORIGIN_DEFAULT, help=f"Origin 基线 (默认 {ORIGIN_DEFAULT})") p.add_argument("--dl-rel", type=float, help="反推模式: 给定 |ΔL/L|%% → 所需 variation") p.add_argument("--parse-var", metavar="HEX6", help="解析 V1.05 协议 3B LE 有符号补码 (如 F6FFFF = −10)") p.add_argument("--sens-table", action="store_true", help="打印灵敏度档位对照表") p.add_argument("--interactive", action="store_true", help="交互模式") args = p.parse_args() if args.sens_table: print(fmt_sens_table()) return if args.parse_var: v = parse_var_3byte(args.parse_var) print(f"3B LE 补码 {args.parse_var.upper()} → {v}") return if args.interactive: interactive() return if args.dl_rel is not None: if not args.freq or not args.cap: p.error("反推模式需要 --freq 和 --cap") print(reverse(args.freq, args.cap, args.dl_rel, args.origin)) return if None in (args.freq, args.cap, args.variation): p.error("需要 --freq --cap --variation (或 --dl-rel / --sens-table / --interactive)") print(forward(args.freq, args.cap, args.variation, args.origin)) if __name__ == "__main__": sys.exit(main())