Connect your UART pollution sensor directly in the browser. Easy.
JavaScript
13
151 commits
updated Sep 30, 2026
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.
"polluSensWeb" is an independent project. Any similarity to other software names is coincidental.

The following sensors are currently supported by polluSensWeb:
...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:
startByteendBytelengthchecksum.eval and checksum.comparesend_cmd_period: send once or periodicallyupdateCharts(parsedData) calledcollectedDatasensors.jsonWhen user clicks Save CSV:
collectedData array (one object per frame) is exported to CSV:
timestamp (ISO 8601)polluSens_data_.csv
sensors.jsonYou can upload a custom JSON configuration using the "Custom JSON Sensor Configuration" input in the interface.
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"). Classic mode only — ignored when commands array is present; use a repeat: 0 command instead |
stop_command | no | string | Hex string to send on disconnect (e.g. "7E 00 01 00 FE 7E"). Works in both classic and multi-command modes — always sent on disconnect regardless of whether command or commands is used |
send_cmd_period | no | number | If > 0, send command every N seconds, if = 0 - once |
commands | no | array | Array of command objects for multi-command sequences (see below) |
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. In multi-command mode this is the initial speed; individual commands may override it (see Per-command baud rate) |
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³" |
| Field | Required | Type | Description |
|---|---|---|---|
command | yes | string | Hex command string (e.g., "1B 41 6B 57 00 52 03 53") |
id | no | number | 0-based position index used by child sensors to override this command via inheritance (see Per-command override) |
repeat | no | number | 0 = init (run once), ≥1 = repeat N times per measurement cycle |
postDelay_ms | no | number | Delay in milliseconds after command before next operation |
baudRate | no | integer | Serial speed for this command. Port is reopened at this speed before sending if it differs from the current one (see Per-command baud rate) |
frame | no | object | Frame specification for response parsing (same as main frame) |
checksum | no | object | Checksum validation for response (same as main checksum) |
data | no | object | Data extraction rules for response (same as main data) |
{
"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]"
}
}
]
}
inherits_fromYou 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.
polluSensWeb now supports complex sensor initialization and measurement sequences using the commands array. This is essential for sensors that require multiple commands to configure, calibrate, or read data.
Each command in the commands array can have:
| Field | Type | Required | Description |
|---|---|---|---|
command | string | yes | Hex command to send (e.g., "1B 41 6B 57 00 52 03 53") |
frame | object | no | Frame specification for response parsing |
checksum | object | no | Checksum validation for response frame |
data | object | no | Data extraction rules for response frame |
repeat | number | no | 0 = init (run once), ≥1 = repeat N times per cycle |
postDelay_ms | number | no | Delay in milliseconds after command execution |
baudRate | integer | no | Serial speed for this command; port is reopened if the speed changes |
repeat: 0): Commands run once before measurement looprepeat: ≥1): Commands repeated continuouslyInit Commands (repeat: 0):
Cycle Commands (repeat: ≥1):
Frame Handling:
start_command / stop_command in multi-command mode:
start_command is not sent when commands array is present — use a repeat: 0 entry in commands for one-time init insteadstop_command is always sent on disconnect, regardless of whether the sensor uses classic or multi-command modeMulti-command example:
{
"commands": [
{
"command": "7E 00 03 00 FC 7E",
"repeat": 1,
"postDelay_ms": 50,
"frame": { ... },
"data": { ... },
"checksum": { ... }
},
{
"command": "67 00 05 00 FE 7E",
"repeat": 1,
"postDelay_ms": 50,
"frame": { ... },
"data": { ... },
"checksum": { ... }
}
]
}
Some devices need different serial speeds within a single transaction. A typical example is 1-Wire over UART (e.g. the usbtemp DS18B20 thermometer): the reset pulse is generated at 9600 baud, while data bits are sent at 115200 baud, where each UART byte represents one 1-Wire bit slot (00 = write 0, FF = write 1 / read slot).
Add an optional baudRate to any command in the commands array:
baudRate. If they differ, the port is closed and reopened at the new speed (Web Serial cannot change the speed of an open port). All other settings (dataBits, stopBits, parity) are taken from port.baudRate do not change the speed, so all existing configurations behave exactly as before.The switch takes a few milliseconds, so this is only suitable for protocols that tolerate idle gaps between bytes (1-Wire does; most request/response protocols do as well).
When the parent sensor uses a commands array, a child sensor can override individual commands without repeating the entire array. Use the id field on a child command to specify which position (0-based) in the parent's commands array to merge into.
Rules:
id is a 0-based integer index into the parent's commands arrayid, or with an id that is out of range, is appended as a new command after the inherited onesid field — only child overrides use itExample: parent defines two commands; child only changes postDelay_ms on the second one (id: 1):
{
"name": "MySensor Fast",
"inherits_from": "MySensor",
"commands": [
{ "id": 1, "postDelay_ms": 100 }
]
}
All other fields of command at position 1 (command, frame, data, checksum, repeat, etc.) are inherited unchanged from MySensor.
value, eval, and compare fields are evaluated using JavaScript eval().66) or hex strings ("0x42") — no raw hex like 0x42.postDelay_ms values to allow sensor processing time.repeat: 0 for one-time initialization commands.repeat: ≥1 for continuous measurement commands.baudRate only when a device needs different speeds within one sequence; leave it out otherwise.polluSensWeb can send parsed sensor data to any HTTP webhook.
Webhooks now support placeholders in both body and headers.
GET, POST, PUT).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³]
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"
}
"null" for fields.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.
sensors.json277 followers · starred Feb 2026
JavaScript
60.0%
HTML
22.4%
CSS
17.6%
Connect your UART pollution sensor directly in the browser. Easy.
JavaScript
13
151 commits
updated Sep 30, 2026
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.
"polluSensWeb" is an independent project. Any similarity to other software names is coincidental.

The following sensors are currently supported by polluSensWeb:
...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:
startByteendBytelengthchecksum.eval and checksum.comparesend_cmd_period: send once or periodicallyupdateCharts(parsedData) calledcollectedDatasensors.jsonWhen user clicks Save CSV:
collectedData array (one object per frame) is exported to CSV:
timestamp (ISO 8601)polluSens_data_.csv
sensors.jsonYou can upload a custom JSON configuration using the "Custom JSON Sensor Configuration" input in the interface.
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"). Classic mode only — ignored when commands array is present; use a repeat: 0 command instead |
stop_command | no | string | Hex string to send on disconnect (e.g. "7E 00 01 00 FE 7E"). Works in both classic and multi-command modes — always sent on disconnect regardless of whether command or commands is used |
send_cmd_period | no | number | If > 0, send command every N seconds, if = 0 - once |
commands | no | array | Array of command objects for multi-command sequences (see below) |
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. In multi-command mode this is the initial speed; individual commands may override it (see Per-command baud rate) |
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³" |
| Field | Required | Type | Description |
|---|---|---|---|
command | yes | string | Hex command string (e.g., "1B 41 6B 57 00 52 03 53") |
id | no | number | 0-based position index used by child sensors to override this command via inheritance (see Per-command override) |
repeat | no | number | 0 = init (run once), ≥1 = repeat N times per measurement cycle |
postDelay_ms | no | number | Delay in milliseconds after command before next operation |
baudRate | no | integer | Serial speed for this command. Port is reopened at this speed before sending if it differs from the current one (see Per-command baud rate) |
frame | no | object | Frame specification for response parsing (same as main frame) |
checksum | no | object | Checksum validation for response (same as main checksum) |
data | no | object | Data extraction rules for response (same as main data) |
{
"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]"
}
}
]
}
inherits_fromYou 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.
polluSensWeb now supports complex sensor initialization and measurement sequences using the commands array. This is essential for sensors that require multiple commands to configure, calibrate, or read data.
Each command in the commands array can have:
| Field | Type | Required | Description |
|---|---|---|---|
command | string | yes | Hex command to send (e.g., "1B 41 6B 57 00 52 03 53") |
frame | object | no | Frame specification for response parsing |
checksum | object | no | Checksum validation for response frame |
data | object | no | Data extraction rules for response frame |
repeat | number | no | 0 = init (run once), ≥1 = repeat N times per cycle |
postDelay_ms | number | no | Delay in milliseconds after command execution |
baudRate | integer | no | Serial speed for this command; port is reopened if the speed changes |
repeat: 0): Commands run once before measurement looprepeat: ≥1): Commands repeated continuouslyInit Commands (repeat: 0):
Cycle Commands (repeat: ≥1):
Frame Handling:
start_command / stop_command in multi-command mode:
start_command is not sent when commands array is present — use a repeat: 0 entry in commands for one-time init insteadstop_command is always sent on disconnect, regardless of whether the sensor uses classic or multi-command modeMulti-command example:
{
"commands": [
{
"command": "7E 00 03 00 FC 7E",
"repeat": 1,
"postDelay_ms": 50,
"frame": { ... },
"data": { ... },
"checksum": { ... }
},
{
"command": "67 00 05 00 FE 7E",
"repeat": 1,
"postDelay_ms": 50,
"frame": { ... },
"data": { ... },
"checksum": { ... }
}
]
}
Some devices need different serial speeds within a single transaction. A typical example is 1-Wire over UART (e.g. the usbtemp DS18B20 thermometer): the reset pulse is generated at 9600 baud, while data bits are sent at 115200 baud, where each UART byte represents one 1-Wire bit slot (00 = write 0, FF = write 1 / read slot).
Add an optional baudRate to any command in the commands array:
baudRate. If they differ, the port is closed and reopened at the new speed (Web Serial cannot change the speed of an open port). All other settings (dataBits, stopBits, parity) are taken from port.baudRate do not change the speed, so all existing configurations behave exactly as before.The switch takes a few milliseconds, so this is only suitable for protocols that tolerate idle gaps between bytes (1-Wire does; most request/response protocols do as well).
When the parent sensor uses a commands array, a child sensor can override individual commands without repeating the entire array. Use the id field on a child command to specify which position (0-based) in the parent's commands array to merge into.
Rules:
id is a 0-based integer index into the parent's commands arrayid, or with an id that is out of range, is appended as a new command after the inherited onesid field — only child overrides use itExample: parent defines two commands; child only changes postDelay_ms on the second one (id: 1):
{
"name": "MySensor Fast",
"inherits_from": "MySensor",
"commands": [
{ "id": 1, "postDelay_ms": 100 }
]
}
All other fields of command at position 1 (command, frame, data, checksum, repeat, etc.) are inherited unchanged from MySensor.
value, eval, and compare fields are evaluated using JavaScript eval().66) or hex strings ("0x42") — no raw hex like 0x42.postDelay_ms values to allow sensor processing time.repeat: 0 for one-time initialization commands.repeat: ≥1 for continuous measurement commands.baudRate only when a device needs different speeds within one sequence; leave it out otherwise.polluSensWeb can send parsed sensor data to any HTTP webhook.
Webhooks now support placeholders in both body and headers.
GET, POST, PUT).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³]
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"
}
"null" for fields.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.
sensors.json277 followers · starred Feb 2026
JavaScript
60.0%
HTML
22.4%
CSS
17.6%