Air Quality Monitor With 4G Network

After moving to new office and rummage my luggage, I found a 4G development board with a Air724UG model chip and a PM 2.5 detector module with friendly UART output. So, why not make it sending real-time air quality data like PM 2.5 and temperature to somewhere and visualize it?

Result

Hardware

EVB_Air724UG_A13 (From https://doc.openluat.com/wiki/29?wiki_page_id=3362)

I think it’s one of the best development board I’ve ever bought, as it supports LTE Cat.1, Bluetooth, Wi-Fi, camera and many other interface and features. But it only needs 69 CNY when on sale. But one of the back draws is that it lacks of well-organized document. And it only supports Lua and C development. They provide high-level APIs with Lua, and the usage of extent and standard is split into several documents. Their IDE integrated with VS Code also seems not working well.

M702 (From specifications)

The sensor I will use is 702 which you can easily find on many online shopping platforms. One of the interesting points is many resellers have this sensor and rename it with their name. That would be like they are actually the upstream factory. According to my test, its error is acceptable. Except, carbon dioxide and formaldehyde concentrations are calculated from TVOC rather than measurement value.

Software

Infrastructure

The infrastructure would be like the image showed. The SIM card specially used for IoT does not support connecting to common ports like 80 and 443. To simplify the debug and development process, I will use a relay server rather than connecting the IoT platform directly with MQTT protocol.

The sensor will start to send data once connected with power supply, but carbon dioxide and other data will only be accurate after warming up. The communication method is protocol and decode script is simple:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
    -- Check data length
    if string.len(data) ~= 17 then
        log.error("airmonitor::parse_uart - invalid data length")
        return
    end

    -- Byte 17 is checksum
    local checksum = string.byte(data, 17)
    log.debug("airmonitor::parse_uart - checksum: " .. checksum)
    -- Add all bytes together
    local sum = 0
    for i = 1, 16 do sum = sum + string.byte(data, i) end
    -- Checksum is the last byte
    if sum % 256 == checksum then
        log.debug("airmonitor::parse_uart - checksum ok")
    else
        log.error("airmonitor::parse_uart - checksum error")
        return
    end

    -- Byte 3 and 4 is eCO2
    local eco2 = string.byte(data, 3) * 256 + string.byte(data, 4)
    log.debug("airmonitor::parse_uart - eCO2: " .. eco2)
    -- Byte 5 and 6 is eCH2O
    local ech2o = string.byte(data, 5) * 256 + string.byte(data, 6)
    log.debug("airmonitor::parse_uart - eCH2O: " .. ech2o)
    -- Byte 7 and 8 is TVOC
    local tvoc = string.byte(data, 7) * 256 + string.byte(data, 8)
    log.debug("airmonitor::parse_uart - TVOC: " .. tvoc)
    -- Byte 9 and 10 is pm2.5
    local pm25 = string.byte(data, 9) * 256 + string.byte(data, 10)
    log.debug("airmonitor::parse_uart - pm2.5: " .. pm25)
    -- Byte 11 and 12 is pm10
    local pm10 = string.byte(data, 11) * 256 + string.byte(data, 12)
    log.debug("airmonitor::parse_uart - pm10: " .. pm10)
    -- Byte 13 and 14 is temperature
    local temperature = string.byte(data, 13) + string.byte(data, 14) / 100
    log.debug("airmonitor::parse_uart - temperature: " .. temperature)
    -- Byte 15 and 16 is humidity
    local humidity = string.byte(data, 15) + string.byte(data, 16) / 100
    log.debug("airmonitor::parse_uart - humidity: " .. humidity)

After decoded the data, we can encrypt it with RSA algorithm and encode it with base64:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    while true do
        while not DATA do sys.wait(1000) end

        if socket.isReady() then
            if udp_client == nil then
                udp_client = socket.udp()
                udp_client:connect("foo", "bar")
            else
                if last_timestamp ~= DATA.timestamp then
                    log.info("airmonitor::send_data", "Sending data to server")
                    data_crypted =
                        crypto.rsa_encrypt("PUBLIC_KEY", [[-----BEGIN PUBLIC KEY-----
                        foo
                        -----END PUBLIC KEY-----]], 2048, "PUBLIC_CRYPT", json.encode(DATA))
                    data_encoded =
                        crypto.base64_encode(data_crypted, #data_crypted)
                    udp_client:send(data_encoded)
                    last_timestamp = DATA.timestamp
                    sys.wait(2000)
                end
            end
            -- udp_client:close()
        end
    end

Once our server received the UDP diagram, we can parse the message and relay it to the OneNET IoT platform powered by China Mobile:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#[tokio::main]
async fn main() -> io::Result<()> {
    let sock = UdpSocket::bind("0.0.0.0:12345").await?;
    let mut buf = [0; 1024];
    loop {
        let (len, addr) = sock.recv_from(&mut buf).await?;
        println!("{:?} bytes received from {:?}", len, addr);
        let decoded = ignore!(decode(&buf[..len]));

        // RSA decryption
        let private_key = RsaPrivateKey::from_pkcs8_pem(PRIVATE_KEY).unwrap();
        let padding = PaddingScheme::new_pkcs1v15_encrypt();
        let decrypted = ignore!(private_key.decrypt(padding, &decoded));
        let decrypted = String::from_utf8(decrypted).unwrap();

        // Deserialize
        let message: Message = serde_json::from_str(&decrypted).unwrap();
        println!("{:?}", message);

        // Push data to OneNET
        ignore!(onenet::push_data(message).await);
    }
}

Here is my code used to authorize OneNET API:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pub fn get_token() -> String {
    let current_time = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_secs();

    let version = "2018-10-31";
    let et = current_time + 3600;
    let res = format!("products/{}/devices/{}", PRODUCT_ID, DEVICE_NAME);
    let method = "sha1";

    let string_for_sign = format!("{}\n{}\n{}\n{}", et, method, res, version);

    let key_decoded = base64::decode(PRODUCT_KEY).unwrap();
    let mut mac = HmacSha1::new_from_slice(&key_decoded).unwrap();
    mac.update(string_for_sign.as_bytes());
    let result = mac.finalize();
    let signature = base64::encode(result.into_bytes());

    format!(
        "version={}&res={}&et={}&method={}&sign={}",
        version, encode(&res), et, method, encode(&signature)
    )
}

Server Outputs

Finally, we can check all the data on OneNET platform.

OneNET Dashboard

Improvements

Current infrastructure is not elegant enough. I will try directly connect the OneNET platform with MQTT later. In previous tests, the socket is closed unexpected after first packages sent to the MQTT server. That’s still under investigation. Or we can send data to Prometheus and visualize it with Grafana.