Unverified Commit 87f3d0c1 authored by Daniel Friesel's avatar Daniel Friesel
Browse files

initial commit

parents
Loading
Loading
Loading
Loading

.gitignore

0 → 100644
+1 −0
Original line number Diff line number Diff line
config.lua

README.md

0 → 100644
+80 −0
Original line number Diff line number Diff line
# ESP8266 Lua/NodeMCU module for SDS011 particle monitor

This repository contains a Lua module (`sds011.lua`) as well as ESP8266/NodeMCU
MQTT gateway application example (`init.lua`) for the **SDS011** particulate
matter (PM2.5 and PM10) sensor.

## Dependencies

sds011.lua has been tested with Lua 5.1 on NodeMCU firmware 3.0.1
(Release 202112300746, integer build). It requires the following modules.

* struct

Most practical applications (such as the example in init.lua) also need the
following modules.

* gpio
* mqtt
* node
* softuart
* tmr
* uart
* wifi

## Setup

Connect the SDS011 sensor to your ESP8266/NodeMCU board as follows.

* SDS011 GND → ESP8266/NodeMCU GND
* SDS011 5V → 5V input (note that the "5V" pin of NodeMCU or D1 mini dev boards is connected to its USB input via a protective diode, so when powering the board via USB the "5V" output is more like 4.7V. I have not tested whether that is an issue)
* SDS011 TXD → NodeMCU D1 (ESP8266 GPIO5)
* SDS011 RXD → NodeMCU D2 (ESP8266 GPIO4)

If you use use different pins for TXD and RXD, you need to adjust the
softuart.setup call in the examples provided in this repository to reflect
those changes. Keep in mind that some ESP8266 pins must have well-defined logic
levels at boot time and may therefore be unsuitable for SDS011 connection.

## Usage

Copy **sds011.lua** to your NodeMCU board and set it up as follows.

```lua
sds011 = require("sds011")
port = softuart.setup(9600, 2, 1)
port:on("data", 10, uart_callback)

function uart_callback(data
	local pm25i, pm25d, pm10i, pm10d = sds011.parse_frame(data)
	if pm25i ~= nil then
		-- pm25i/pm10i contain the integer part (i.e., PM2.5 / PM10 value in µg/m³)
		-- pm25d/pm10d contain the decimal/fractional part (i.e., PM2.5 / PM10 fraction in .1 µg/m³, range 0 .. 9)
	else
		-- invalid checksum or non-data frame (i.e., acknowledgment of a write command)
	end
end
```

See **init.lua** for an example. To use it, you need to create a **config.lua** file with WiFI and MQTT settings:

```lua
station_cfg.ssid = "..."
station_cfg.pwd = "..."
mqtt_host = "..."
```

## SDS011 Configuration API

If desired, **sds011.lua** can be used to configure the SDS011 sensor.
Currently, the following commands are supported

* `sds011.set_report_mode(active)`
  * active == true: periodically report PM2.5 and PM10 values via UART
  * active == false: only report PM2.5 and PM10 values when queried
* `sds011.sleep(sleep)`
  * sleep == true: put sensor into sleep mode. The fan is turned off, no further measurements are performed
  * sleep == false: wake up sensor.
* `sds011.set_work_period(period)`
  * period == 0: continuous operation (about one measurement per second)
  * 0 < *period* ≤ 30: about one measurement every *period* minutes; fan turned off in-between

init.lua

0 → 100644
+73 −0
Original line number Diff line number Diff line
station_cfg = {}
dofile("config.lua")

delayed_restart = tmr.create()
chipid = node.chipid()
mqtt_prefix = "sensor/esp8266_" .. chipid
mqttclient = mqtt.Client("esp8266_" .. chipid, 120)

print("ESP8266 " .. chipid)

ledpin = 4
gpio.mode(ledpin, gpio.OUTPUT)
gpio.write(ledpin, 0)

sds011 = require("sds011")

function log_restart()
	print("Network error " .. wifi.sta.status() .. ". Restarting in 20 seconds.")
	delayed_restart:start()
end

function setup_client()
	gpio.write(ledpin, 1)
	publishing = true
	mqttclient:publish(mqtt_prefix .. "/state", "online", 0, 1, function(client)
		publishing = false
	end)
	port = softuart.setup(9600, 2, 1)
	port:on("data", 10, uart_callback)
end

function connect_mqtt()
	print("IP address: " .. wifi.sta.getip())
	print("Connecting to MQTT " .. mqtt_host)
	delayed_restart:stop()
	mqttclient:on("connect", setup_client)
	mqttclient:on("offline", log_restart)
	mqttclient:lwt(mqtt_prefix .. "/state", "offline", 0, 1)
	mqttclient:connect(mqtt_host)
end

function connect_wifi()
	print("WiFi MAC: " .. wifi.sta.getmac())
	print("Connecting to ESSID " .. station_cfg.ssid)
	wifi.eventmon.register(wifi.eventmon.STA_GOT_IP, connect_mqtt)
	wifi.eventmon.register(wifi.eventmon.STA_DHCP_TIMEOUT, log_restart)
	wifi.eventmon.register(wifi.eventmon.STA_DISCONNECTED, log_restart)
	wifi.setmode(wifi.STATION)
	wifi.sta.config(station_cfg)
	wifi.sta.connect()
end

function uart_callback(data)
	local pm25i, pm25f, pm10i, pm10f = sds011.parse_frame(data)
	if pm25i == nil then
		print("Invalid or data-less SDS011 frame")
		return
	end
	local json_str = string.format('{"pm25_ugm3": %d.%d, "pm10_ugm3": %d.%d, "rssi_dbm": %d}', pm25i, pm25f, pm10i, pm10f, wifi.sta.getrssi())
	if not publishing then
		publishing = true
		gpio.write(ledpin, 0)
		mqttclient:publish(mqtt_prefix .. "/data", json_str, 0, 0, function(client)
			publishing = false
			gpio.write(ledpin, 1)
			collectgarbage()
		end)
	end
end

delayed_restart:register(20 * 1000, tmr.ALARM_SINGLE, node.restart)

connect_wifi()

sds011.lua

0 → 100644
+74 −0
Original line number Diff line number Diff line
local sds011 = {}

local c_head = 0xaa
local c_tail = 0xab
local c_id = 0xb4

local c_read = 0x00
local c_write = 0x01

local c_report_mode = 0x02
local c_active = 0x00
local c_passive = 0x01

local c_query = 0x04

local c_sleepcmd = 0x06
local c_sleep = 0x00
local c_work = 0x01
local c_workperiod = 0x08

function sds011.finish_cmd(cmd)
	cmd = cmd .. string.char(0xff, 0xff)
	local checksum = 0
	for i = 3, string.len(cmd) do
		checksum = (checksum + string.byte(cmd, i)) % 256
	end
	cmd = cmd .. string.char(checksum, c_tail)
	return cmd
end

function sds011.set_report_mode(active)
	local cmd = string.char(c_head, c_id, c_report_mode, c_write)
	if active then
		cmd = cmd .. string.char(c_active)
	else
		cmd = cmd .. string.char(c_passive)
	end
	cmd = cmd .. string.char(0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
	return sds011.finish_cmd(cmd)
end

function sds011.sleep(sleep)
	local cmd = string.char(c_head, c_id, c_sleepcmd, c_write)
	if sleep then
		cmd = cmd .. string.char(c_sleep)
	else
		cmd = cmd .. string.char(c_work)
	end
	cmd = cmd .. string.char(0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
	return sds011.finish_cmd(cmd)
end

function sds011.set_work_period(period)
	-- period == 0 : continuous operation, about one measurement per second
	-- period > 0  : about one measurement every <period> minutes, fan is turned off in-between
	if period < 0 or period > 30 then
		return
	end
	local cmd = string.char(c_head, c_id, c_workperiod, c_write, period)
	cmd = cmd .. string.char(0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
	return sds011.finish_cmd(cmd)
end

function sds011.parse_frame(data)
	local header, command, pm25l, pm25h, pm10l, pm10h, id1, id2, sum, tail = struct.unpack("BBBBBBBBBB", data)
	if header ~= c_head or command ~= 0xc0 or (pm25l + pm25h + pm10l + pm10h + id1 + id2) % 256 ~= sum or tail ~= c_tail then
		return nil
	end
	pm25 = pm25h * 256 + pm25l
	pm10 = pm10h * 256 + pm10l
	return pm25 / 10, pm25 % 10, pm10 / 10, pm10 % 10
end

return sds011