POSA_LEAKSMS/raspberry_pi4_pms7003/wire.py
cw2007 ec78be0bda feat(pms7003): 누수 노드에 미세먼지(PM2.5/PM10) 통합
센서1·3(누수 노드)에 PMS7003 를 온습도와 동일 sensor_id 로 통합하고 metric_type='pms7003'
로 구분해 수집한다. LED 는 스위칭 없이 상시 점등 방식으로 확정.

- raspberry_pi4_pms7003/: PMS7003 노드 풀세트(드라이버·센서·install·systemd·vendor·배선·연결테스트·프레임 단위테스트)
- 서버: evaluate_metric_thresholds() 에 pm25/pm10 선택 인자 추가(온습도 전용 호출 후방호환),
  sensor_data.php 수집 경로에 pms7003 metric 반영, config.php/예시에 PM 임계 추가
- SQL: migration_pms7003.sql · schema_sht30.sql 정합, tools/migration_leak_add_{pms7003,sht30} 마이그레이터
- tests/pm_threshold_test.php + run_tests.sh 러너 등록
- .gitignore: 노드별 config_local.py 커밋 차단
2026-07-19 10:46:33 +09:00

102 lines
4.1 KiB
Python

# =============================================================================
# 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()