Files
ios-location-track/core/protocol.py
T
jbjf6666 85e9a53750 docs: 新增 Windows 移植交接文档
docs/WINDOWS_PORT.md 覆盖:
- 逐文件可移植性审计(核心逻辑层约 90% 可直接复用)
- 核心难点:iOS 17+ 的 RSD 隧道无法在 Windows 复用(macOS 实现依赖 XPC +
  ObjC runtime 私有框架),改用 pymobiledevice3 的 userspace 隧道
- 已实测确认的命令与 API(--userspace 挂在 lockdown start-tunnel / mounter
  auto-mount 上,remote start-tunnel 不支持;类名是 UserspaceRsdTunnel)
- 窗口启动、数据目录、打包与分发(DMG → Inno Setup)、图标(.icns → .ico)
- macOS 侧踩过的坑:镜像挂载用 Python API、必须打包包元数据、不要把 distutils
  放进 excludes、AlreadyMountedError 当成功处理
- 设计约束(DVT 长连接、只能改经纬度、last_point 与 last_reported 分离等)
- 验收基线(复用现有测试套件)与建议的工作顺序
2026-10-16 01:05:09 +08:00

161 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""公共常量与工具:前后端接口契约见 docs/PROTOCOL.md。
本模块不依赖任何第三方库。
"""
from __future__ import annotations
import json
import math
import time
import uuid
from typing import Any
APP_NAME = "iOS 虚拟定位轨迹工具"
APP_VERSION = "1.0.0"
PROTOCOL_VERSION = "v1"
DEFAULT_HOST = "127.0.0.1"
DEFAULT_PORT = 8787
# 地理常量
EARTH_RADIUS_M = 6371008.8
# 播放引擎默认参数
DEFAULT_SEND_HZ = 5.0
MIN_SEND_HZ = 1.0
MAX_SEND_HZ = 20.0
MODE_DEFAULTS = {
"driving": 13.9, # m/s ≈ 50 km/h
"cycling": 5.5, # m/s ≈ 20 km/h
"walking": 1.4, # m/s ≈ 5 km/h
"flight": 60.0, # m/s ≈ 216 km/h
}
MODE_LABELS = {
"driving": "驾车",
"cycling": "骑行",
"walking": "步行",
"flight": "飞行",
}
MIN_SPEED_SCALE = 0.1
MAX_SPEED_SCALE = 50.0
# 单点定位与轨迹点的合法范围
MIN_LAT, MAX_LAT = -85.05112878, 85.05112878
MIN_LNG, MAX_LNG = -180.0, 180.0
class AppError(Exception):
"""带机器码与中文提示的业务异常。"""
def __init__(self, message: str, code: str = "app_error", status: int = 400) -> None:
super().__init__(message)
self.message = message
self.code = code
self.status = status
def to_dict(self) -> dict[str, Any]:
return {"ok": False, "error": self.message, "code": self.code}
def now_ts() -> float:
return time.time()
def round6(value: float) -> float:
return round(float(value), 6)
def clamp(value: float, low: float, high: float) -> float:
"""夹取到 [low, high]。NaN 会直接抛错——NaN 参与比较恒为 False,会静默污染整条链路。"""
if not math.isfinite(value):
raise AppError("数值不是有限数(不允许 NaN / Infinity)", "bad_number")
if value < low:
return low
if value > high:
return high
return value
def new_id(prefix: str) -> str:
return f"{prefix}-{uuid.uuid4().hex[:10]}"
def dumps(obj: Any) -> str:
"""序列化为 JSON。
allow_nan=False:任何 NaN/Infinity 都会抛错而不是产出 `NaN` 字面量——
后者会让浏览器 JSON.parse 直接失败,把整帧数据丢掉。
"""
try:
return json.dumps(
obj, ensure_ascii=False, separators=(",", ":"), default=_json_default, allow_nan=False
)
except ValueError as exc:
raise AppError(f"内部数据包含非法数值(NaN/Infinity),已拒绝下发:{exc}", "bad_number") from exc
def finite(value: Any, field: str = "数值") -> float:
"""校验并返回有限浮点数。"""
try:
number = float(value)
except (TypeError, ValueError) as exc:
raise AppError(f"{field}必须是数字", "bad_number") from exc
if not math.isfinite(number):
raise AppError(f"{field}不是有限数(不允许 NaN / Infinity)", "bad_number")
return number
def _json_default(obj: Any) -> Any:
if isinstance(obj, (set, tuple)):
return list(obj)
if hasattr(obj, "isoformat"):
return obj.isoformat()
return str(obj)
def loads(raw: str | bytes) -> Any:
if isinstance(raw, bytes):
raw = raw.decode("utf-8", "replace")
return json.loads(raw)
def validate_latlng(lat: Any, lng: Any) -> tuple[float, float]:
"""校验并归一化经纬度,返回 (lat, lng)。"""
try:
flat = float(lat)
flng = float(lng)
except (TypeError, ValueError) as exc:
raise AppError("经纬度必须是数字", "bad_coords") from exc
if math.isnan(flat) or math.isnan(flng) or math.isinf(flat) or math.isinf(flng):
raise AppError("经纬度不是有效数字", "bad_coords")
if not (MIN_LAT <= flat <= MAX_LAT):
raise AppError(f"纬度超出有效范围({MIN_LAT:.4f} ~ {MAX_LAT:.4f})", "bad_coords")
if not (MIN_LNG <= flng <= MAX_LNG):
raise AppError("经度超出有效范围(-180 ~ 180)", "bad_coords")
return flat, flng
def validate_speed_scale(value: Any) -> float:
scale = finite(value, "倍速")
if scale <= 0:
raise AppError("倍速必须大于 0", "bad_speed")
if not (MIN_SPEED_SCALE <= scale <= MAX_SPEED_SCALE):
raise AppError(
f"倍速需在 {MIN_SPEED_SCALE} ~ {MAX_SPEED_SCALE} 之间", "bad_speed"
)
return scale
def validate_mode(mode: Any) -> str:
if mode is None:
return "driving"
text = str(mode).strip().lower()
if text not in MODE_DEFAULTS:
raise AppError(
"出行方式只支持 driving / cycling / walking / flight", "bad_mode"
)
return text