POSA_LEAKSMS/raspberry_pi4_pms7003/README.md
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

7.5 KiB

Particulate-Matter (PMS7003) Sensor Module — Raspberry Pi 4 Model B only

A module that reads a Plantower PMS7003 laser particulate-matter sensor over the Raspberry Pi 4 Model B UART and periodically reports PM1.0 / PM2.5 / PM10 (µg/m³, atmospheric concentration) to the cafe24 server (php/api/sensor_data.php). The server stores the measurements and performs high-PM2.5 / high-PM10 threshold evaluation, SMS, and dashboard display.

This is a Pi-only metric — there is no STM32 firmware for the PMS7003. The module is a faithful mirror of the SHT30 module (../raspberry_pi4_sht30/): same measurement loop, startup→periodic events, raw-body HMAC signing, and fail-closed secret handling. PM is a new metric on the existing metric pipeline — distinguished only by metric_type="pms7003".

The PMS7003 co-locates on an existing leak node and reports under the same sensor_id as that node's leak sensor (Pi-C = 3, the other node = 1), exactly like the SHT30 co-location.


1. Files

File Role
pms7003_sensor.py Main — periodic measurement → body build/sign → POST (with retry), startup→periodic
pms7003_driver.py PMS7003 UART 32-byte frame read + checksum verification + PM decode (pyserial, simulation fallback)
wire.py Body JSON + raw-body HMAC signature (fixed2/_esc/sign_raw copied verbatim from the SHT30 module)
config.py Non-secret default settings (sensor id/interval/range/UART). Loads secrets from config_local
config_local.example.py Operational/secret-value template → copy to config_local.py for use (not committed)
requirements.txt Python dependencies (requests, pyserial, pinned)
install.sh Pi 4 install (air-gapped offline + online fallback, enables UART + disables the serial console)
vendor.sh Pre-download dependencies for air-gapped use (run on the build machine)
tests/test_pms_wire_and_frame.py Pure unit tests: body byte-string, known HMAC, frame decode + checksum rejection (no hardware)
systemd/pms7003-sensor.service Service unit for auto-start on boot

2. Hardware (Pi 4 Model B baseline)

  • Raspberry Pi 4 Model B (BCM2711) + Raspberry Pi OS Bookworm 64-bit
  • PMS7003 laser PM sensor + its breakout/adapter cable
  • Power: USB-C 5V/3A, network: Pi 4 Gigabit Ethernet (wired) or WiFi

2.1 Pinout (primary UART)

PMS7003 signal Pi 4 pin Note
VCC 5V (physical 2) PMS7003 needs 5V power (fan + laser)
GND GND (physical 6)
TXD (sensor → Pi) GPIO15 / RXD (physical 10) 3.3V logic — safe direct to the Pi
RXD (Pi → sensor) GPIO14 / TXD (physical 8) optional (only needed for passive-mode commands)
SET / RESET — optional; leave floating (internal pull-up = enabled/run)

The PMS7003 TXD idles at 3.3V logic, so it connects directly to the Pi RXD — no level shifter needed. We only read the sensor's TXD (active streaming mode), so wiring the Pi TXD→sensor RXD is optional.


3. Server compatibility (must verify)

This module reports to the current repo's php/ server. The server requires a raw-body HMAC signature.

X-Signature = lowercase_hex( HMAC-SHA256( key=API_KEY, msg=request body bytes ) )
  • API_KEY in config_local.py must be exactly identical to API_KEY in the server's config.local.php.
  • The server blocks replays via timestamp freshness (±600 seconds), so the Pi clock must be NTP-synchronized (enabled by default on Raspberry Pi OS).
  • It reports with the co-located leak node's sensor_id (3 by default, or 1 on the other node) and metric_type="pms7003".
  • Threshold alerts (high PM2.5 / PM10), SMS, and recovery evaluation are all performed by the server. This module only reports measurements.
  • The Pi-side metric_status is physical plausibility only (normal / out_of_range); the server never trusts it for alerting.

3.1 Wire body (fixed order)

{"device_id":"..","device_location":"..","sensor_id":3,"sensor_name":"..","event_type":"..","timestamp":0,"metric_type":"pms7003","pm1_0":8,"pm2_5":12,"pm10":18,"metric_status":"normal","app_version":".."}

pm1_0 / pm2_5 / pm10 are bare integers (µg/m³, atmospheric concentration, range 0..1000). The body is built directly and raw-POSTed so the signed bytes == sent bytes.


4. Installation

4.1 Prepare configuration values

cp config_local.example.py config_local.py
# Open config_local.py and fill in API_URL / API_KEY / DEVICE_ID / DEVICE_LOCATION
# On the id=1 node also set SENSOR_ID = 1 (Pi-C keeps the default 3).

If config_local.py is missing, execution stops immediately with a clear error (fail closed). Never commit config_local.py or API_KEY (handled by .gitignore).

4.2 Online install (internet-capable environment)

chmod +x install.sh
./install.sh
sudo reboot      # reboot once — the installer enables UART and disables the serial console

4.3 Air-gapped offline install

On an internet-connected machine of the same architecture (aarch64, ideally the same Pi 4 + Bookworm 64-bit), download the dependencies first to build a bundle, then move it to the target Pi 4 and install.

# (build machine, internet YES) generate wheelhouse/ + debs/
./vendor.sh

# after copying wheelhouse/ and debs/ into this folder on the target Pi 4:
# (target Pi 4, internet NO)
./install.sh        # if debs/ · wheelhouse/ exist, it installs offline automatically

pyserial is pure Python (arch-independent), so it can be downloaded on an x86 build machine. Other wheels/debs such as requests must be the aarch64 builds (running vendor.sh on the target arch is recommended).


5. Run / check

# UART present? (should symlink to ttyS0 or ttyAMA0)
ls -l /dev/serial0

# pure wire/frame self-check (passes without hardware or config_local.py)
python3 tests/test_pms_wire_and_frame.py

# actual run (simulation mode if pyserial missing / UART unavailable)
source venv/bin/activate
python3 pms7003_sensor.py

In environments without pyserial installed or with the UART unavailable (dev PC, etc.), it automatically runs in simulation mode.


6. Auto-start on boot (systemd)

# assumes the folder is placed at /home/pi/raspberry_pi4_pms7003 (otherwise edit the path in the .service)
sudo cp systemd/pms7003-sensor.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable pms7003-sensor
sudo systemctl start pms7003-sensor
sudo systemctl status pms7003-sensor
journalctl -u pms7003-sensor -f

SupplementaryGroups=dialout is required for /dev/serial0 access.


7. Security notes

  • Secrets (API_KEY), server URL, and device info live only in config_local.py (not committed). Committed files contain only placeholders.
  • The report body is protected against tampering by an HMAC-SHA256(API_KEY, raw_body) signature (X-Signature header) (immune to length-extension attacks). The legacy body-field sha256 fallback is disabled by default on the server.
  • The signed bytes and the transmitted bytes are always identical (wire.py builds the body directly, signs those bytes, then raw POSTs). fixed2 / _esc / sign_raw are copied verbatim from the SHT30 module so signing stays byte-for-byte identical to the shared reference / server verify_signature_raw.
  • If config_local.py is exposed, immediately rotate API_KEY (change it on the server at the same time).