Continuous airborne particle monitoring system for the Wright Lab DUNE High Bay clean room (Yale University). Data are acquired from a Particles Plus Model 7301 optical particle counter via Modbus TCP, logged to CSV, and published as a live web dashboard on GitHub Pages after every sample cycle.
Live dashboard: https://rohit-raut.github.io/WLC-High-Bay-Monitoring/
Just cloned/forked this repo? Run the setup script:
chmod +x setup.sh
./setup.shThe script will:
- β Check Python version
- β Install dependencies (pymodbus, pyyaml)
- β Configure your particle counter IP
- β Test counter connectivity
- β Create config.local.yaml
- β Set up data directories
- β Verify installation
Then start monitoring:
python3 particle_plus.py --allThat's it! See Setup and Installation below for manual setup or troubleshooting.
Particle contamination control is a critical requirement for the construction and assembly of DUNE detector components. Airborne particulate matter can deposit onto sensitive detector surfaces during fabrication, potentially degrading performance. This system provides continuous, automated monitoring of the High Bay clean room environment to verify compliance with the specified ISO 14644-1 cleanliness class and to generate a persistent, time-stamped record of environmental conditions throughout detector assembly activities.
The Particles Plus Model 7301 measures airborne particles using laser light scattering (optical particle counting, OPC). Particles drawn through the instrument at a calibrated flow rate scatter light from a 785 nm diode laser; individual scattering pulses are sized and counted. The instrument reports differential counts (counts in that size bin per sample), volumetric concentrations (counts per mΒ³ and per ftΒ³), and estimated PM mass concentrations (Β΅g/mΒ³) computed from particle size and an assumed spherical particle geometry at standard density.
Airborne cleanliness class is evaluated automatically from the most recent sample using the ISO 14644-1:2015 particle concentration limits. The dashboard reports the worst-case ISO class across all monitored size channels. Color coding follows the convention below:
| ISO Class | Typical Use | Badge Color |
|---|---|---|
| ISO 3 or 4 | Semiconductor / pharmaceutical critical zones | Green |
| ISO 5 or 6 | General semiconductor assembly | Light green |
| ISO 7 | Electronic assembly, medical devices | Yellow |
| ISO 8 | General manufacturing | Orange |
| ISO 9 | Room air (near ambient) | Red |
| Item | Value |
|---|---|
| Instrument | Particles Plus Model 7301 |
| Measurement principle | Laser light scattering (OPC) |
| Laser | 785 nm diode |
| Flow rate | 0.1 CFM (2.83 L/min) |
| Sample duration | 60 seconds (configurable) |
| Sample interval | 30 minutes (configurable) |
| Communication | Modbus TCP (IEEE 802.3) |
| Counter IP | 10.66.66.68 |
| Modbus port | 502 |
| Host | noether cluster (rr979@noether) |
The instrument is configured with six simultaneous size channels covering the range relevant to ISO 14644-1 classification:
| Channel | Lower size threshold |
|---|---|
| ch1 | 0.3 Β΅m |
| ch2 | 0.5 Β΅m |
| ch3 | 1.0 Β΅m |
| ch4 | 2.5 Β΅m |
| ch5 | 5.0 Β΅m |
| ch6 | 10.0 Β΅m |
Each channel reports differential counts, volumetric concentration (counts/mΒ³), and cumulative PM mass (Β΅g/mΒ³). Note that PM mass for the smallest channels (0.3 Β΅m) will be at or below the instrument's floating-point precision floor and is expected to read zero; larger channels (5.0 Β΅m, 10.0 Β΅m) show meaningful mass concentrations.
Particles Plus 7301
|
Modbus TCP (port 502)
|
noether (Linux host)
|
particle_plus.py
βββ Triggers and monitors 1-min sample cycles
βββ Syncs all stored records to measurements.csv
βββ Syncs counter RTC to host system time (NTP-traceable)
βββ Generates self-contained index.html dashboard
βββ Streams live in-progress readings to live.csv (10 s interval)
βββ git commit + push to GitHub (main branch)
|
GitHub Pages
|
Live dashboard (public URL)
The system runs as a persistent background process (tmux session). After each completed sample cycle, it syncs all counter records, regenerates the dashboard, and pushes to GitHub. If the counter is unreachable, the dashboard is still pushed with an OFFLINE banner and the last recorded data, and the process retries automatically every 30 minutes.
The Particles Plus 7301 communicates over Modbus TCP. All multi-register values use word-swapped little-endian encoding (low word in the lower register address). String registers store one ASCII character per register in the low byte of each 16-bit word.
| Register | Type | Description |
|---|---|---|
| 5000 | U16 | Sampler state (0=Stopped, 1=Running, 2=Holding) |
| 5001 | U16 | Device status bits (bit 0=flow err, bit 1=laser err, bit 2=RTC not running) |
| 8004 | U16 | Erase command (write 0x9559 to erase all stored records) |
| 9001 | I32 | Total record count |
| 9002 | STR (11 regs) | Record date (YYYY-MM-DD), stored in latch buffer |
| 9013 | STR (9 regs) | Record time (HH:MM:SS), stored in latch buffer |
| 9074 | FLOAT | Sample duration (seconds) |
| 9076 | FLOAT | Flow rate (CFM) |
| 9078 | U16 | Sample status bits (bit 0=laser OK, bit 1=flow OK, bit 2=temp OK, bit 3=RH OK, bit 7=timestamp invalid) |
| 9079 | U16 | Temperature (raw x 0.1 = degrees C; 998+ = sensor absent) |
| 9080 | U16 | Relative humidity (%) |
| 1000 | STR (16 regs) | Admin password (write before any Protected Write register) |
| 1016 | STR (11 regs) | RTC date (Protected Write, requires password at reg 1000 first) |
| 1027 | STR (9 regs) | RTC time (Protected Write, requires password at reg 1000 first) |
| 10100 + 2i | FLOAT | Channel i+1 particle size threshold (Β΅m) |
| 10300 + 2i | FLOAT | Channel i+1 differential count (counts/sample) |
| 10500 + 2i | FLOAT | Channel i+1 differential concentration (counts/ftΒ³) |
| 10700 + 2i | FLOAT | Channel i+1 differential concentration (counts/mΒ³) |
| 10900 + 2i | FLOAT | Channel i+1 differential PM mass (Β΅g/mΒ³) |
| 11500 + 2i | FLOAT | Channel i+1 cumulative concentration (counts/mΒ³) |
| 11700 + 2i | FLOAT | Channel i+1 cumulative PM mass concentration (Β΅g/mΒ³) |
Records must be latched into the read buffer (register 9000) before reading. The system latches each record by index, reads all fields, then advances to the next record.
All measurements are appended to data/measurements.csv. Each row corresponds to one completed 1-minute sample.
| Column | Description |
|---|---|
record_number |
Sequential record index from instrument memory |
date |
Sample date from counter RTC (YYYY-MM-DD) |
time |
Sample time from counter RTC (HH:MM:SS) |
location |
Location label string stored on instrument |
sample_duration_s |
Actual sample duration in seconds |
flow_CFM |
Measured flow rate (CFM) |
laser_ok |
Laser status flag (True/False) |
flow_ok |
Flow sensor status flag |
temp_ok |
Temperature sensor status flag |
rh_ok |
Humidity sensor status flag |
timestamp_valid |
Counter timestamp validity flag |
temp_C |
Temperature (degrees C) from internal sensor |
RH_pct |
Relative humidity (%) |
ch{1-6}_size_um |
Particle size threshold for channel N (Β΅m) |
ch{1-6}_diff_counts |
Differential particle count (counts/sample) |
ch{1-6}_diff_ft3 |
Differential concentration (counts/ftΒ³) |
ch{1-6}_diff_m3 |
Differential concentration (counts/mΒ³), used for ISO classification |
ch{1-6}_diff_mass_ugm3 |
Differential PM mass concentration (Β΅g/mΒ³) |
ch{1-6}_sum_m3 |
Cumulative concentration (counts/mΒ³) |
ch{1-6}_pm_ugm3 |
Cumulative PM mass concentration (Β΅g/mΒ³) |
sync_time |
ISO 8601 timestamp of when the Pi read this record from the instrument |
The live dashboard is a self-contained static HTML page generated by particle_plus.py and served via GitHub Pages. It is rebuilt and pushed after every completed sample cycle (approximately every 30 minutes).
- Status strip: instrument connectivity, flow rate, record count, last sample time, and per-sensor status flags (laser, flow, temperature, RH)
- Summary cards: latest temperature (degF), relative humidity (%), and flow rate (CFM)
- ISO 14644-1 badge: real-time clean room classification computed from the most recent sample, color-coded by class
- Particle counts over time: time-series step plot of differential counts per sample across all 6 size channels (log scale). Gaps in sampling are represented as step-hold lines at the last recorded value, indicating periods of inactivity without fabricating data.
- PM mass concentration over time: time-series plot of cumulative PM mass (Β΅g/mΒ³) per channel
- Latest size distribution: bar chart of counts by particle size bin for the most recent sample
- Temperature and humidity over time: time-series from the live CSV (10-second resolution), distinct from the per-sample historical record
- Time range selector: adjustable from last 30 minutes to all 7 days; default is last 24 hours
- Offline banner: displayed when the counter is unreachable, showing the last known data with a clear warning
All timestamps on the dashboard use the counter's internal RTC, synchronized to the host system clock (NTP-traceable) at each sync cycle. The dashboard is rendered client-side using Plotly.js; no server is required.
The instrument's real-time clock is synchronized to the host system time at the start of every sync cycle. Registers 1016 (date) and 1027 (time) are Protected Write registers: the admin password must first be written to register 1000, after which the date and time can be written. The system logs the pre-write and post-write clock values and reports a warning if the readback does not match the written value, which typically indicates a password mismatch.
The default configuration assumes no admin password is set (COUNTER_PASSWORD = ''). If a password was configured on the instrument, it must be set in particle_plus.py.
- Python 3.8 or newer
pymodbus >= 3.5- Counter reachable at its configured IP address on port 502
- Optional:
pyyaml >= 6.0(for config file support) - Optional: Git with push access (only if you want GitHub Pages auto-push)
pip install -r requirements.txt
# Or minimal: pip install pymodbus>=3.5Important: By default, the system runs in monitoring-only mode (no GitHub push). This is safe for new installations. See GITHUB_PUSH_SETUP.md to enable GitHub Pages integration.
# 1. Clone the repository
cd /home/rr979/particle_plus
git clone git@github.com:Rohit-Raut/WLC-High-Bay-Monitoring.git .
# 2. Install dependency
pip install pymodbus>=3.5
# 3. Start in a persistent tmux session
tmux new -s particle
python3 particle_plus.py --all
# Ctrl+B, D to detach and leave runningRecommended: Use configuration files (easier, cleaner):
-
Copy the example config:
cp config.yaml config.local.yaml
-
Edit
config.local.yamlwith your settings:counter: ip: '10.66.66.68' # Your counter's IP port: 502 github: enabled: false # Set true only if you want GitHub Pages push
-
Run the system:
python3 particle_plus.py --all
Alternative: Edit particle_plus.py directly (lines 26-95), but config files are preferred for replicability.
python3 particle_plus.py --all Full system: sampling, syncing, live stream,
and dashboard push. Recommended for tmux.
python3 particle_plus.py --sample 24/7 sampling scheduler only. Triggers counter,
waits for completion, syncs, pushes dashboard.
python3 particle_plus.py --sync One-shot: pull all new records from counter to CSV
and sync the counter clock.
python3 particle_plus.py --live Stream live in-progress readings to live.csv
every 10 seconds (runs concurrently under --all).
python3 particle_plus.py --dashboard Regenerate index.html and push to GitHub Pages.
python3 particle_plus.py --stop Gracefully stop a running --all instance.
Sends SIGTERM; current sample cycle completes first.
WLC-High-Bay-Monitoring/
βββ particle_plus.py Main logger, Modbus interface, and dashboard generator
βββ local_serve.py Localhost server for the full-history local dashboard (SSH tunnel)
βββ network_serve.py Same as local_serve.py but binds to the network interface
βββ dev_serve.py Dev server: rebuilds index.html from JS + data, serves locally
βββ setup.sh Interactive installer for new hosts
βββ requirements.txt Python dependencies
βββ index.html Auto-generated public dashboard (served by GitHub Pages)
βββ index_local.html Auto-generated local dashboard (full history)
βββ features/ Feature modules (dashboard JS, alerts, data manager,
β temp_humidity_sensor/ β Shelly H&T MQTT logger + sensors.yaml)
βββ misc/ One-off utilities & archives: erase/trim/check tools, test.py
β (Modbus connectivity test), old dashboard snapshot, design mockups
βββ data/
βββ measurement_archive.csv All synced records ever (local only)
βββ live.csv 30-day rolling window of synced records
βββ env_live.csv 10-second temp/RH snapshots (30 days)
Runtime files created on the host (not tracked by git):
sync_log.txt: timestamped log of all operations and errorsparticle_plus.pid: PID file used by--stopto locate the running processdata/session_baseline.txt: record count at session start, used to exclude pre-existing records from the current session display
- The system only appends records with a
record_numberhigher than the highest already saved, preventing duplicates across sync cycles. - If the counter is powered off and restarted, the session baseline file ensures that records from previous sessions are not mixed into the current session's display.
- The counter's internal memory stores a fixed number of records. The
ERASE_AFTER_SYNCflag (defaultFalse) can be enabled after verifying that data has been committed to CSV to free instrument memory. - All Modbus float values use word-swapped IEEE 754 single precision. String registers use one ASCII character per 16-bit register in the low byte, which differs from the two-character-per-register encoding described in some Modbus documentation. This system implements the correct low-byte encoding, verified empirically against the instrument.
Rohit Raut, Wright Lab, Yale University DUNE Detector R&D Group