# ============================================================================= # wire.py - wire format for server transmission (body JSON + raw-body HMAC signature) # # [Why a separate module] # The server (php/api/sensor_data.php) verifies, against the received "body bytes as-is", # X-Signature = lowercase_hex(HMAC-SHA256(key=API_KEY, msg=raw_body)). # Therefore the "signed bytes" and the "actually transmitted bytes" must be 100% identical. # We build the body directly as a string, sign those bytes, then POST those same bytes as-is. # (Leaving serialization to requests' json= can change float/whitespace/order and break the signature.) # # [Byte-level compatibility targets] # PMS7003 is a Pi-only metric (no STM32 firmware), so pms_event_body() has no firmware # counterpart. But the three helpers below are copied VERBATIM from the SHT30 module's # wire.py so raw-body signing stays byte-identical to the shared reference: # - firmware/common/sig.c::sig_raw_body (STM32 HMAC signature) # - firmware/test/host/reference.py (verified Python reference) # - php/config.php::verify_signature_raw (server verification part) # # fixed2() <-> jb_fixed2() / reference.fixed2() (kept for parity; PM body is integer-only) # sign_raw() <-> sig_raw_body() / reference.sign_raw() # pms_event_body() <- Pi-only PMS7003 body (mirrors sht30_event_body field order/escaping) # ============================================================================= from __future__ import annotations import hashlib import hmac def fixed2(v: float) -> str: """Fixed 2-decimal format (round-half-up, locale-independent). Byte-identical to jb_fixed2() / reference.fixed2(). 24.0 -> "24.00" 48.5 -> "48.50" -3.245 -> "-3.25" """ neg = v < 0 if neg: v = -v scaled = int(v * 100.0 + 0.5) # round half up (C: (long long)(v*100+0.5)) ip = scaled // 100 fp = scaled % 100 sign = "-" if (neg and scaled != 0) else "" return f"{sign}{ip}.{fp:02d}" def _esc(s: str) -> str: r"""Escape JSON string value (identical to jb_estr / reference._esc). Escapes only '"', '\\', and control characters (<0x20). '/' is left as-is. Multibyte UTF-8 (e.g. Korean) is left as-is (server verifies raw bytes). """ out = [] for ch in s: o = ord(ch) if ch == '"': out.append('\\"') elif ch == '\\': out.append('\\\\') elif o < 0x20: out.append('\\u%04x' % o) else: out.append(ch) return "".join(out) def pms_event_body(device_id: str, device_location: str, sensor_id: int, sensor_name: str, event_type: str, timestamp: int, pm1_0: int, pm2_5: int, pm10: int, metric_status: str, app_version: str) -> str: """Generate PMS7003 event body JSON string. Field order is fixed (shared wire contract) and mirrors sht30_event_body() so the server can verify the raw body generically. metric_type is always "pms7003". PM values are serialized as BARE INTEGERS (µg/m³, atmospheric concentration). """ return ( '{' f'"device_id":"{_esc(device_id)}",' f'"device_location":"{_esc(device_location)}",' f'"sensor_id":{int(sensor_id)},' f'"sensor_name":"{_esc(sensor_name)}",' f'"event_type":"{_esc(event_type)}",' f'"timestamp":{int(timestamp)},' f'"metric_type":"pms7003",' f'"pm1_0":{int(pm1_0)},' f'"pm2_5":{int(pm2_5)},' f'"pm10":{int(pm10)},' f'"metric_status":"{_esc(metric_status)}",' f'"app_version":"{_esc(app_version)}"' '}' ) def sign_raw(api_key: str, body: str) -> str: """raw-body signature: lowercase_hex(HMAC-SHA256(key=api_key, msg=body)). Identical to sig_raw_body() / reference.sign_raw() / php verify_signature_raw. The body is encoded as UTF-8 then signed (same as transmitted bytes). """ return hmac.new(api_key.encode("utf-8"), body.encode("utf-8"), hashlib.sha256).hexdigest()