Skip to content

Instantly share code, notes, and snippets.

@hfiennes
Created February 1, 2019 03:47
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hfiennes/6876140e6a574ead6865f5c1399b7876 to your computer and use it in GitHub Desktop.
Save hfiennes/6876140e6a574ead6865f5c1399b7876 to your computer and use it in GitHub Desktop.
Sensirion SPS30 air quality sensor
// Ensure we have a default reading
lastreading <- { "mass_pm1.0":0, "mass_pm2.5":0, "mass_pm4.0":0, "mass_pm10":0 };
// Code to convert particlate density to AQI index
// based on https://gist.github.com/kfury/822bbba2cb0f946abb73baa156722ab1
function Linear(AQIhigh, AQIlow, Conchigh, Conclow, Conc) {
local a=((Conc-Conclow)/(Conchigh-Conclow))*(AQIhigh-AQIlow)+AQIlow;
return math.floor(a+0.5);
}
function PM25toAQI(Conc) {
local c=(math.floor(10*Conc))/10;
local AQI = 9999;
if (c>=0 && c<12.1) {
AQI=Linear(50,0,12,0,c);
} else if (c>=12.1 && c<35.5) {
AQI=Linear(100,51,35.4,12.1,c);
} else if (c>=35.5 && c<55.5) {
AQI=Linear(150,101,55.4,35.5,c);
} else if (c>=55.5 && c<150.5) {
AQI=Linear(200,151,150.4,55.5,c);
} else if (c>=150.5 && c<250.5) {
AQI=Linear(300,201,250.4,150.5,c);
} else if (c>=250.5 && c<350.5) {
AQI=Linear(400,301,350.4,250.5,c);
} else if (c>=350.5 && c<500.5) {
AQI=Linear(500,401,500.4,350.5,c);
}
return AQI;
}
// When we get data from the device, log it and keep a copy
device.on("aq", function(v) {
lastreading = v;
//server.log(http.jsonencode(lastreading));
});
// Handle slack calls
http.onrequest(function(req, res) {
local status = format("Current air quality (Sensirion SPS30): PM1.0: %.1fug/m3, PM2.5: %.1fug/m3, PM4: %.1fug/m3, PM10: %.f1ug/m3, ",
lastreading["mass_pm1.0"], lastreading["mass_pm2.5"],
lastreading["mass_pm4.0"], lastreading["mass_pm10"]);
local aqi = PM25toAQI(lastreading["mass_pm2.5"]);
status += format("PM2.5 AQI index %d ", aqi);
if (aqi < 51) status += "(good)";
else if (aqi < 101) status += "(moderate)";
else if (aqi < 151) status += "(unhealthy for sensitive groups)";
else if (aqi < 201) status += "(unhealthy)";
else if (aqi < 301) status += "(very unhealthy)";
else if (aqi < 501) status += "(hazardous)";
else status += "(get out of there)";
server.log("http request from: "+req.headers["x-forwarded-for"]+" - "+status);
// Check for slack
if (req.path == "/slack") {
res.send(200, http.jsonencode({"text": status}));
} else {
// Otherwise, if someone hits the agent, just send status as plain text
res.send(200, status+"\n");
}
});
// Sensirion SPS30 air quality sensor
// hugo@electricimp.com 20190131
// Connected to an imp001 april board:
// pin 1 on SPS30 to VCC on april
// pin 2 on SPS30 to pin 8 on april
// pin 3 on SPS30 to pin 9 on april
// pin 4 on SPS30 not connected
// pin 5 on SPS30 to GND on april
uart <- hardware.uart1289;
// Current serial state and collected packet
state <- -1;
packet <- "";
// Send a packet to the sensor; this does the checksum calculation, stuffing, and HDLC framing
function sendpacket(addr, cmd, data) {
// Form core of packet
local packet = format("%c%c%c%s", addr, cmd, data.len(), data);
// Calculate checksum
local chk = 0;
foreach(c in packet) chk += c;
packet += format("%c", chk ^ 0xff);
// Byte-stuff as needed
local stuffed = "";
foreach(c in packet) {
if (c == 0x7d || c == 0x7e || c == 0x11 || c == 0x13) {
// Stuff it
stuffed += "\x7d"+(c ^ 0x20).tochar();
} else {
// No need to stuff
stuffed += c.tochar();
}
}
// Send it with start and stop
uart.write("\x7e"+stuffed+"\x7e");
}
// Deal with receiving a single byte
function rx() {
local b = uart.read();
// Waiting for start of a packet?
if (state == -1) {
// Packets always start with 0x7e
if (b == 0x7e) {
packet = "";
state++;
return;
}
}
// Collect bytes until another 0x7e
if (b != 0x7e) packet += b.tochar();
else {
// Did we just see another 0x7e, with nothing collected?
// We're getting back in sync
if (packet == "") return;
// Process the received packet
rxpacket(packet);
// Reset state for next one
state = -1;
}
}
// We've got a whole packet, check that it's valid
function rxpacket(pkt) {
// Unstuff packet
local unstuffed = "";
local xor = 0;
foreach(c in pkt) {
if (c == 0x7d) xor = 0x20;
else {
// Unstuff if needed, reset stuff indicator
unstuffed += (c ^ xor).tochar();
xor = 0;
}
}
// Remove checksum from received packet
local rxchk = unstuffed[unstuffed.len()-1];
unstuffed = unstuffed.slice(0, unstuffed.len()-1);
// Calculate checksum
local chk = 0;
foreach(c in unstuffed) chk += c;
chk = (chk & 0xff) ^ 0xff;
// Do they match?
if (chk == rxchk) {
// Process received packet
switch(unstuffed[1]) {
case 0x03: // Reading packet
// Check length is as expected
if (unstuffed.len() != 44) break;
// Move it into a blob to extract the floats
local b = blob();
b.writestring(unstuffed.slice(4));
b.seek(0, 'b');
// Swap endianness
b.swap4();
// Extract data
local reading = {
"mass_pm1.0": b.readn('f'),
"mass_pm2.5": b.readn('f'),
"mass_pm4.0": b.readn('f'),
"mass_pm10": b.readn('f'),
"number_pm0.5": b.readn('f'),
"number_pm1.0": b.readn('f'),
"number_pm2.5": b.readn('f'),
"number_pm4.0": b.readn('f'),
"number_pm10": b.readn('f'),
"typ_size": b.readn('f')
};
agent.send("aq", reading);
break;
}
} else {
server.log(format("chk error rx %02x calc %02x", rxchk, chk))
}
}
// Configure UART and attach receive handler
uart.configure(115200, 8, PARITY_NONE, 1, NO_CTSRTS, rx);
const START_MEASUREMENT = 0x00;
const READ_MEASURED_VALUES = 0x03;
// Ensure fan is started
sendpacket(0, START_MEASUREMENT, "\x01\x03");
// Every second, kick off a reading; the receive handler will collect the
// data, parse it, and send it to the cloud
function takereading() {
// Ask sensor for data
sendpacket(0, READ_MEASURED_VALUES, "");
// Do it again in a second
imp.wakeup(1, takereading);
}
takereading();
server.log("AQ sensor online");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment