# ============================================================================= # 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] — output must be identical to the four below. # - firmware/common/jsonbody.c::jb_sht30_event (STM32 body generation) # - 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() # sht30_event_body() <-> jb_sht30_event() / reference.sht30_event_body() # sign_raw() <-> sig_raw_body() / reference.sign_raw() # ============================================================================= 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 sht30_event_body(device_id: str, device_location: str, sensor_id: int, sensor_name: str, event_type: str, timestamp: int, temperature_c: float, humidity_percent: float, metric_status: str, app_version: str) -> str: """Generate SHT30 event body JSON string. Field order is fixed to match firmware jb_sht30_event() (signature compatibility). metric_type is always fixed to "sht30". Temperature/humidity serialized to 2 decimals via fixed2. """ 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":"sht30",' f'"temperature_c":{fixed2(temperature_c)},' f'"humidity_percent":{fixed2(humidity_percent)},' 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()