polluSensWeb is a free and open-source browser-based tool for connecting and visualizing real air quality sensor data directly from UART devices via Web Serial API. It is designed for education, teaching labs, and rapid IoT prototyping. Try it out yourself: here
"polluSensWeb" is an independent project. Any similarity to other software names is coincidental.
- Live serial data acquisition
- Frame parsing with startByte / endByte / checksum
- Dynamic charts with customizable signal style (color, thickness, tension)
- Multiple simultaneous charts
- Full CSV export (timestamp + all signals)
- Clearable log with raw serial packets and checksum result
- Configurable via external JSON file
- Supports webhooks
- Works offline after load (except webhooks)
- No external servers required
The following sensors are currently supported by polluSensWeb:
- Panasonic SN-GCJA5
- Honeywell HPMA115S0-XXX
- Air Master AM7 Plus
- Plantower PMSA003-S
- Plantower PS3003A
- Plantower PMS1003
- Plantower PMS5003
- Plantower PMS7003
- Plantower PMS6003
- Plantower PMS9103
- Plantower PMS3003
- Nova PM SDS011
- Sensirion SPS30
- SHUYI SY210
- TERA NextPM - thanks to Michael Lažan for testing! (senzorvzduchu.cz)
- SenseAir S8 004-0-0053
- SenseAir S88 Residential
- SenseAir S88 LP
- SenseAir S88 GH
- SenseAir K30
- SenseAir K33
- SenseAir eSENSE
- SenseAir S8 004-0-0017
- SenseAir K33 ICB
- Sensirion SCD30
- YYS DC01
- YYS D01
- YYS D01-P
- YYS D9
- YYS D3
- YYS D5
- YYS D7
- YYS D7B
- Winsen ZH03B ...more coming soon!
Chrome ≥ 89
Edge ≥ 89
Brave ≥ 1.24
Opera ≥ 75
Other Chromium-based browsers with Web Serial API
Web Serial API is not supported in Firefox / Safari.
Sensor configuration is loaded from:
https://raw.githubusercontent.com/WeSpeakEnglish/polluSensWeb/refs/heads/main/sensors.json
Each sensor config defines:
- Serial port settings
- Frame structure:
startByteendBytelengthchecksum.evalandchecksum.compare
- Command to send on connection
send_cmd_period: send once or periodically- Parsing expressions for each signal field
- User clicks Connect → serial port opened
- Initial command is sent (if configured)
- Incoming bytes are buffered
- Frames are detected (startByte + endByte + length)
- Checksum is validated
- If valid:
- Signals parsed
updateCharts(parsedData)called- Data appended to in-memory
collectedData - Raw packet logged
- If invalid → error logged
- Sensor Selector → Choose sensor config from
sensors.json - Connect / Disconnect → Open or close serial connection
- Clear Log → Clear log area
- Save CSV → Export full collected data since connection
- Chart Name → Title of chart
- Width / Height → Chart dimensions in px
- Chart datapoints → Chart length in datapoints
- Signals Section → List of available signals:
- Checkbox to include signal
- Color picker
- Tension (line smoothness)
- Thickness (line width)
- Create Chart → Create chart with selected signals
- Multiple charts can be created dynamically.
- Each chart:
- X-axis → Time (seconds, real-time)
- Y-axis → Signal values
- Multiple signals supported
- Chart header:
- Chart title
- ❌ Cross button → removes chart
- Displays raw packets in hex.
- Shows checksum result (✅ pass / ❌ fail).
- User can clear log manually.
When user clicks Save CSV:
- The
collectedDataarray (one object per frame) is exported to CSV:- First column:
timestamp(ISO 8601) - Remaining columns: all parsed signals
- First column:
- Filename:
polluSens_data_.csv
- CSV contains all data collected since connection started.
- Load polluSensWeb → sensors loaded from
sensors.json - User selects sensor → signals list updates
- User clicks Connect → serial connection opened, command sent
- Incoming data is parsed, validated, displayed on charts, and logged
- User can:
- Create / remove charts
- Export full CSV at any time
- Clear log as needed
- User can disconnect anytime
You can upload a custom JSON configuration using the "Custom JSON Sensor Configuration" input in the interface.
- Custom entries are marked with 🆕
Top-level structure:
{
"sensors": [
{ /* sensor object */ },
...
]
}Each sensor object describes how to read and interpret data from a UART-connected sensor.
| Field | Required | Type | Description |
|---|---|---|---|
name |
yes | string | Unique sensor name (shown in dropdown) |
inherits_from |
no | string | Name of another sensor to inherit from, ex. "Plantower PMSA003-S" |
command |
no | string | Hex string to send during connection (e.g. "7E 00 03 00 FC 7E") or "none" |
start_command |
no | string | Hex string to send after connect event (e.g. "7E 00 00 02 01 03 F9 7E") |
stop_command |
no | string | Hex string to send on disconnect (e.g. "7E 00 01 00 FE 7E") |
send_cmd_period |
no | number | If > 0, send command every N seconds, if = 0 - once |
port |
yes | object | fields: see below |
frame |
yes | object | fields: see below |
data |
yes | object | fields: see below |
| Field | Required | Type | Description |
|---|---|---|---|
baudRate |
yes | integer | connection speed, ex. 9600, 19200, 115200 |
dataBits |
yes | integer | bits in byte, typically 8 |
stopBits |
yes | integer | stop bits quantity, typically 1 |
parity |
yes | integer | parity, ex. "none", "even", "odd" |
| Field | Required | Type | Description |
|---|---|---|---|
startByte |
yes | string | start byte or bytes, ex. [66, 77], ["0x42", "0x4D"], 170, "0xAA", `"none" |
endByte |
yes | string | multi-byte terminator; similar to startByte |
length |
yes | string | frame length including start and stop bytes, in bytestuffing case / after unstuffing |
stuffing |
no | object | contain stuffing pairs: what to find and what to place instead, ex. ["7D 5E", "0x7E"], ["7D 5D", "0x7D"] |
| Field | Required | Type | Description |
|---|---|---|---|
eval |
yes | string | valid JS expression, assuming data[i] is i-th byte in received buffer, ex. "data.slice(1, 30).reduce((a, b) => a ^ b, 0)" |
compare |
yes | string | valid JS expression, assuming data[i] is i-th byte in received buffer, ex. data[30] |
Defines how to extract and interpret sensor readings from a binary data frame Example:
"Some parameter": {
"value": "((data[1] << 8) + data[2])>>>0",
"unit": "μg/m³"
},...| Field | Required | Type | Description |
|---|---|---|---|
value |
yes | string | valid JS expression, assuming data[i] is i-th byte in received buffer, ex. "((data[1] << 8) + data[2])>>>0" |
unit |
yes | string | units like, ex. "μg/m³" |
{
"sensors": [
{
"name": "Honeywell HPMA115S0-XXX",
"command": "none",
"port": {
"baudRate": 9600,
"dataBits": 8,
"stopBits": 1,
"parity": "none"
},
"frame": {
"length": 32,
"startByte": [66, 77],
"endByte": "none"
},
"data": {
"PM2.5": {
"value": "(data[6] << 8) + data[7]",
"unit": "μg/m³"
},
"PM10": {
"value": "(data[8] << 8) + data[9]",
"unit": "μg/m³"
}
},
"checksum": {
"eval": "data.slice(0, 30).reduce((a, b) => (a + b) & 0xFFFF, 0)",
"compare": "(data[30] << 8) + data[31]"
}
}
]
}You can reuse and override parts of existing sensors:
{
"name": "Your New Sensor PM2.5",
"inherits_from": "Plantower PMSA003",
"data": {
"Humidity": {
"value": "(data[14] << 8) + data[5]",
"unit": "%"
}
}
}This example keeps all settings from Plantower PMSA003 but adds a humidity value.
- All
value,eval, andcomparefields are evaluated using JavaScripteval(). - You can use decimal values (
66) or hex strings ("0x42") — no raw hex like0x42. - If your JSON fails to load, check the browser log or validate at https://jsonlint.com.
polluSensWeb can send parsed sensor data to any HTTP webhook.
Webhooks now support placeholders in both body and headers.
- Check Enable Webhook Sending in the Webhook card.
- Enter your Webhook URL.
- Select HTTP Method (
GET,POST,PUT). - Set interval in seconds (0 = send on every packet).
Headers can include placeholders, just like the body.
Supported placeholders:
{{ts}}– current timestamp (ISO format){{field:<name>}}– value of a specific sensor field
| Key | Value | Result Example |
|---|---|---|
| X-PM25 | {{field:PM2_5}} | X-PM25: 12.3 |
| X-TIME | {{ts}} | X-TIME: 2025-12-09T12:34:56Z |
| X-CUSTOM | {{field:PM10}} | X-CUSTOM: 20.1 |
Add headers using the + Add header button.
Body templates are JSON-based, supporting placeholders:
{{ts}}– timestamp{{field:<signal name>}}– sensor value{{#fields}} … {{/fields}}– loop through all sensor fields
{
"software_version": "polluSensWeb 1.0",
"timestamp": "{{ts}}",
"sensordatavalues": [
{{#fields}}
{ "value_type": "{{key}}", "value": "{{value}}" }
{{/fields}}
]
}For a packet:
PM1: 1.5, PM2.5: 12.3, PM10: 20.1
The processed body will be:
{
"software_version": "polluSensWeb 1.0",
"timestamp": "2025-12-09T12:34:56.789Z",
"sensordatavalues": [
{ "value_type": "PM1", "value": 1.5 },
{ "value_type": "PM2.5", "value": 12.3 },
{ "value_type": "PM10", "value": 20.1 }
]
}{
"software_version": "polluSensWeb 1.0",
"timestamp": "{{ts}}",
"sensordatavalues": [
{"PM2.5": "{{field:PM2.5 x0.4 calibrated}}"}
]
}Here PM2.5 x0.4 calibrated is the signal (signal names you see in the form where you select them for chart at the top of UI), before [units], for this example it was: PM2.5 x0.4 calibrated [μg/m³]
- Click Test Send Webhook Now to test.
- Webhooks respect your interval setting. If interval = 0, every parsed packet triggers a request.
Headers
X-PM25: {{field:PM2.5}}
X-Time: {{ts}}
Content-Type: application/json
Body
{
"pm25": {{field:PM2.5}},
"pm10": {{field:PM10}},
"time": "{{ts}}"
}Processed Request Example
POST https://webhook.site/xxxxxx
Headers:
X-PM25: 12.3
X-Time: 2025-12-09T12:34:56Z
Content-Type: application/json
Body:
{
"pm25": 12.3,
"pm10": 20.1,
"time": "2025-12-09T12:34:56Z"
}
- Headers are processed through the same template engine as the body.
- Placeholders not found in the data will be replaced with
"null"for fields. - Rate-limit protection is active by default.
Ready to use with any HTTP endpoint that accepts JSON or custom headers.
To run is selfhosted: replace CORS proxy and sensor list addresses in pollusensweb.js (at the very top) with your actual addresses:
const DEFAULT_SENSOR_LIST = "ADDRESS_TO_sensors.json";
const PROXY_URL = "ADDRESS_TO_proxy.php";Extact release archive or open git clone, open index.html in browser and load JSON file sensors.js via Custom JSON Sensor Configuration in UI.
- Default sensors:
sensors.json - Project homepage: pollutants.eu/sensor
- Hackaday project: Connect any UART sensor in your browser
