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?
Hardware
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.
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
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)
)
}
|
Finally, we can check all the data on OneNET platform.
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.