This document describes the Bluetooth Low Energy (BLE) communication protocol used by the EM1003 air quality sensor device.
Communication Protocol:
- Service:
09de2880-1415-4e2c-a48a-3938e3288537 - Write Char:
0xFFF1- Send commands - Notify Char:
0xFFF4- Receive responses
Request Format (3 bytes):
[Sequence ID] [0x06] [Sensor ID]
Example: AC 06 08
Response Format (5 bytes):
[Sequence ID] [0x06] [Sensor ID] [Value High] [Value Low]
Example: AC 06 08 33 00
Value Parsing (Little Endian):
Value = (Low Byte) + (High Byte × 256)
Example: 33 00 → 0x33 + (0x00 × 256) = 51 decimal
8 Sensor IDs (ALL CONFIRMED):
0x01Temperature (°C)0x06Humidity (%)0x08Noise (dB)0x09PM2.5 (µg/m³)0x0AFormaldehyde (mg/m³)0x11PM10 (µg/m³)0x12TVOC (mg/m³)0x13eCO2 (ppm)
Buzzer Control (CONFIRMED):
- Command:
0x50 - Query state:
[seq_id][0x50][0x00]→ Response:[seq_id][0x50][0x00][state] - Turn on:
[seq_id][0x50][0x01][0x01]→ Response:[seq_id][0x05][0x01] - Turn off:
[seq_id][0x50][0x01][0x00]→ Response:[seq_id][0x05][0x01]
- Device Name: Can be read from standard BLE Device Name characteristic
- Example:
3GCG300ZY4
- Example:
- Device Name UUID:
00002A00-0000-1000-8000-00805F9B34FB(Standard BLE GATT)
- Service UUID:
09de2880-1415-4e2c-a48a-3938e3288537 - Position: Third service block when connected via BLE
- Purpose: Sensor data communication
-
Write Characteristic (Command)
- UUID:
0000FFF1-0000-1000-8000-00805F9B34FB(Short:0xFFF1) - Properties: WRITE
- Purpose: Send commands to request sensor data
- UUID:
-
Notify Characteristic (Response)
- UUID:
0000FFF4-0000-1000-8000-00805F9B34FB(Short:0xFFF4) - Properties: NOTIFY
- Purpose: Receive sensor data responses
- UUID:
The device uses a simple request-response protocol. To read sensor data, write a 3-byte command to the write characteristic (0xFFF1):
[Sequence ID] [Command Type] [Sensor ID]
Example Request: AC 06 08
-
Byte 0 - Sequence ID (
0xAC): Unique identifier for this request- Used to match responses to requests
- Can be any value (e.g.,
0xAC,0xAB,0x01, etc.) - The response will echo back this same sequence ID
-
Byte 1 - Command Type (
0x06): Command identifier0x06: Read sensor data (confirmed)- Other commands may exist but are not yet documented
-
Byte 2 - Sensor ID (
0x08): Which sensor to read- See sensor ID table below
The device responds via the notify characteristic (0xFFF4) with a 5-byte message:
[Sequence ID] [Command Type] [Sensor ID] [Value High Byte] [Value Low Byte]
Example Response: AC 06 08 33 00
- Byte 0 - Sequence ID (
0xAC): Matches the request sequence ID - Byte 1 - Command Type (
0x06): Echoes the command type - Byte 2 - Sensor ID (
0x08): Echoes the sensor ID - Bytes 3-4 - Value (
33 00): Sensor reading as 16-bit Big Endian unsigned integer- High byte first, then low byte
- Example:
33 00=0x3300= 13056 in hex notation, but interpreted as Big Endian =0x0033= 51 decimal
The device supports buzzer control through command 0x50.
Request Format (3 bytes):
[Sequence ID] [0x50] [0x00]
Response Format (4 bytes):
[Sequence ID] [0x50] [0x00] [State]
- State:
0x01= Buzzer ON,0x00= Buzzer OFF
Example:
Request: 00 50 00
Response: 00 50 00 01 # Buzzer is ON
Response: 00 50 00 00 # Buzzer is OFF
Request Format (4 bytes):
[Sequence ID] [0x50] [0x01] [State]
Response Format (3 bytes):
[Sequence ID] [0x05] [0x01]
Turn ON:
Request: 00 50 01 01
Response: 00 05 01 # Successfully turned ON
Turn OFF:
Request: 00 50 01 00
Response: 00 05 01 # Successfully turned OFF
The following sensor IDs have been observed:
| Sensor ID | Sensor Type | Unit | Formula | Example |
|---|---|---|---|---|
0x01 |
Temperature | °C | (raw - 4000) / 100 |
2C-1A (6700) = 27.00°C |
0x06 |
Humidity | % | raw / 100 |
DB-11 (4571) = 45.71% |
0x08 |
Noise Level | dB | raw (no conversion) |
31-00 (49) = 49 dB |
0x09 |
PM2.5 | µg/m³ | raw (no conversion) |
1C-00 (28) = 28 µg/m³ |
0x0A |
Formaldehyde | mg/m³ | (raw - 16384) / 1000 |
00-40 (16384) = 0.000 mg/m³ |
0x11 |
PM10 | µg/m³ | raw (no conversion) |
16-00 (22) = 22 µg/m³ |
0x12 |
TVOC | mg/m³ | raw × 0.001 |
01-00 (1) = 0.001 mg/m³ |
0x13 |
eCO2 | ppm | raw (no conversion) |
9B-01 (411) = 411 ppm |
All 8 sensor types have been confirmed through testing with real environmental data!
-
Temperature (0x01): Uses offset of 4000 to support negative temperatures
- Range: -40°C to 615.35°C (raw 0 to 65535)
- Negative example: -10°C = raw 3900
-
Humidity (0x06): Scaled by 100 for precision
- Range: 0% to 655.35% (raw 0 to 65535)
- Typical indoor range: 30-70%
-
Formaldehyde (0x0A): Uses offset of 16384 (0x4000)
- Baseline value: 16384 = 0.000 mg/m³
- Can measure positive and negative deviations from baseline
-
TVOC (0x12): Multiplies raw value by 0.001
- Different from other sensors which divide by scale factor
The value bytes in the response are 2 bytes in Little Endian format.
In Little Endian format, the least significant byte (LSB, low byte) comes first, followed by the most significant byte (MSB, high byte). This is the common format used in Intel x86 processors.
Why Little Endian? Many embedded systems and IoT devices use Little Endian format because it allows for easy byte-by-byte reading and incremental value building.
Comparison Table:
| Byte Order | Byte Sequence | Interpretation | Decimal Value |
|---|---|---|---|
| Little Endian (EM1003) | 33 00 |
0x33 + (0x00 × 256) |
51 |
| Little Endian (EM1003) | 9B 01 |
0x9B + (0x01 × 256) |
411 |
| Big Endian | 33 00 |
(0x33 × 256) + 0x00 |
13,056 |
IMPORTANT: The EM1003 device uses Little Endian, so 33 00 = 51, and 9B 01 = 411!
Example 1: Temperature - Response CD 06 01 2C 1A
- Sensor ID:
0x01= Temperature - Value bytes:
2C 1A(Little Endian) - Raw value:
0x2C + (0x1A × 256) = 44 + 6656= 6700 - Formula:
(6700 - 4000) / 100= 27.00°C - ✓ Confirmed with real measurement: 27.0°C
Example 2: Humidity - Response CB 06 06 DB 11
- Sensor ID:
0x06= Humidity - Value bytes:
DB 11(Little Endian) - Raw value:
0xDB + (0x11 × 256) = 219 + 4352= 4571 - Formula:
4571 / 100= 45.71% - ✓ Confirmed with real measurement: 45%
Example 3: Noise - Response AC 06 08 31 00
- Sensor ID:
0x08= Noise - Value bytes:
31 00(Little Endian) - Raw value:
0x31 + (0x00 × 256)= 49 - Formula:
raw(no conversion) = 49 dB - ✓ Confirmed with real measurement: 49 dB
Example 4: PM2.5 - Response AD 06 09 1C 00
- Sensor ID:
0x09= PM2.5 - Value bytes:
1C 00(Little Endian) - Raw value:
0x1C + (0x00 × 256)= 28 - Formula:
raw(no conversion) = 28 µg/m³ - ✓ Confirmed with real measurement: 28 µg/m³
Example 5: Formaldehyde - Response CD 06 0A 00 40
- Sensor ID:
0x0A= Formaldehyde - Value bytes:
00 40(Little Endian) - Raw value:
0x00 + (0x40 × 256) = 0 + 16384= 16384 - Formula:
(16384 - 16384) / 1000= 0.000 mg/m³ - ✓ Confirmed with real measurement: 0.00 mg/m³
Example 6: PM10 - Response AE 06 11 16 00
- Sensor ID:
0x11= PM10 - Value bytes:
16 00(Little Endian) - Raw value:
0x16 + (0x00 × 256)= 22 - Formula:
raw(no conversion) = 22 µg/m³ - ✓ Confirmed
Example 7: TVOC - Response AF 06 12 01 00
- Sensor ID:
0x12= TVOC - Value bytes:
01 00(Little Endian) - Raw value:
0x01 + (0x00 × 256)= 1 - Formula:
1 × 0.001= 0.001 mg/m³ - ✓ Confirmed with real measurement: 0.001 mg/m³
Example 8: eCO2 - Response B0 06 13 9B 01
- Sensor ID:
0x13= eCO2 - Value bytes:
9B 01(Little Endian) - Raw value:
0x9B + (0x01 × 256) = 155 + 256= 411 - Formula:
raw(no conversion) = 411 ppm - ✓ Confirmed with real measurement: 411 ppm
- Data Type: 16-bit unsigned integer
- Byte Order: Little Endian (LSB first)
- Range: 0 to 65535
- Parsing Formula:
value = low_byte + (high_byte × 256) - Python:
int.from_bytes(value_bytes[:2], byteorder='little')
- Determine which sensor ID corresponds to which physical sensor
- Collect multiple readings with known reference values
- Compare with other air quality sensors
- Test different environmental conditions
- Determine scaling factors and units:
- Temperature might be raw value / 10 (e.g., 235 = 23.5°C)
- Humidity might be percentage (e.g., 51 = 51%)
- PM values might be in µg/m³ (e.g., 25 = 25 µg/m³)
-
Temperature/Humidity (if available): Easiest to verify
- Compare with known thermometer/hygrometer
- Common formats:
- Temperature: Usually in 0.1°C units (e.g., 235 = 23.5°C)
- Humidity: Usually in 0.1% or 1% units
-
PM2.5/PM10:
- Usually in µg/m³
- Typical indoor range: 0-100 µg/m³
- Compare with reference PM sensor
-
TVOC/eCO2:
- TVOC: Usually in ppb (parts per billion)
- eCO2: Usually in ppm (parts per million)
-
Formaldehyde:
- Usually in µg/m³ or mg/m³
- Typical indoor range: 0-100 µg/m³
- ✅ Device name reading via standard BLE characteristic
- ✅ Service UUID:
09de2880-1415-4e2c-a48a-3938e3288537 - ✅ Write characteristic:
0xFFF1 - ✅ Notify characteristic:
0xFFF4 - ✅ Request format: 3 bytes
[seq][06][sensor_id] - ✅ Response format:
[seq][06][sensor_id][value_bytes] - ✅ Sequence ID matching works
- ✅ Value format: 2 bytes, Little Endian, unsigned 16-bit integer
- ✅ ALL 8 Sensor ID mappings confirmed:
0x01= Temperature (°C) - formula:(raw - 4000) / 1000x06= Humidity (%) - formula:raw / 1000x08= Noise Level (dB) - formula:raw0x09= PM2.5 (µg/m³) - formula:raw0x0A= Formaldehyde (mg/m³) - formula:(raw - 16384) / 10000x11= PM10 (µg/m³) - formula:raw0x12= TVOC (mg/m³) - formula:raw × 0.0010x13= eCO2 (ppm) - formula:raw
- ✅ Buzzer Control confirmed:
- Command
0x50for buzzer control - Query state: 3-byte request
[seq][0x50][0x00]→ 4-byte response[seq][0x50][0x00][state] - Set state: 4-byte request
[seq][0x50][0x01][state]→ 3-byte response[seq][0x05][0x01] - State values:
0x00= OFF,0x01= ON
- Command
- ❓ Other command types besides
0x06(sensor read) and0x50(buzzer control) - ❓ Error handling and timeout behavior
- ❓ Battery level reading (if supported)
1. Connect to device via BLE
2. Subscribe to notifications on 0xFFF4
# Reading Sensor Data
3. Write to 0xFFF1: [0xAC, 0x06, 0x08]
└─> Request sensor 0x08 (Noise) with sequence ID 0xAC
4. Receive from 0xFFF4: [0xAC, 0x06, 0x08, 0x31, 0x00]
└─> Response: seq=0xAC, cmd=0x06, sensor=0x08, value_bytes=[0x31, 0x00]
5. Parse Little Endian value: 0x31 + (0x00 × 256) = 49 dB
6. Repeat for other sensor IDs (0x01, 0x06, 0x09, 0x0A, 0x11, 0x12, 0x13)
# Buzzer Control
7. Query buzzer state:
Write to 0xFFF1: [0x00, 0x50, 0x00]
Receive from 0xFFF4: [0x00, 0x50, 0x00, 0x00]
└─> Buzzer is OFF
8. Turn on buzzer:
Write to 0xFFF1: [0x00, 0x50, 0x01, 0x01]
Receive from 0xFFF4: [0x00, 0x05, 0x01]
└─> Buzzer turned ON successfully
9. Turn off buzzer:
Write to 0xFFF1: [0x00, 0x50, 0x01, 0x00]
Receive from 0xFFF4: [0x00, 0x05, 0x01]
└─> Buzzer turned OFF successfully
# Reading Noise (confirmed sensor 0x08)
Send: AC 06 08
Receive: AC 06 08 31 00 # Little Endian: 0x31 + (0x00 × 256) = 49 dB
# Reading PM2.5 (confirmed sensor 0x09)
Send: AD 06 09
Receive: AD 06 09 1C 00 # Little Endian: 0x1C + (0x00 × 256) = 28 µg/m³
# Reading PM10 (confirmed sensor 0x11)
Send: AE 06 11
Receive: AE 06 11 16 00 # Little Endian: 0x16 + (0x00 × 256) = 22 µg/m³
# Reading TVOC (confirmed sensor 0x12 - special encoding)
Send: AF 06 12
Receive: AF 06 12 01 00 # Little Endian: 0x01 + (0x00 × 256) = 1 → 0.001 mg/m³
# Reading eCO2 (confirmed sensor 0x13)
Send: B0 06 13
Receive: B0 06 13 9B 01 # Little Endian: 0x9B + (0x01 × 256) = 155 + 256 = 411 ppmNote: Each request uses a different sequence ID (AC, AD, AE, AF) to track responses.
- Implement basic read functionality for all sensor IDs
- Log raw values for analysis
- Test with known environmental conditions
- Determine correct parsing for each sensor type
- Add proper units and scaling
- Implement periodic polling or continuous monitoring
- The device may require time between readings
- Some sensors may need warm-up time after power-on
- Check if multiple sensors can be read in sequence without delay
- Consider implementing retry logic for failed reads
- May need to handle notifications that arrive out of order
Document Version: 1.0 Last Updated: 2025-10-25 Status: Initial reverse engineering in progress