This project builds a Smart Meeting Room Monitor to check:
CO₂ concentration (air freshness / ventilation quality)
Temperature & humidity (thermal comfort)
Pressure & gas resistance (IAQ indicator)
Light level (is the room in use / correctly lit?)
It combines sensors you already used separately in:
RAK1906 environmental indices:https://www.hackster.io/user2702447/rak1906-air-quality-monitoring-a-focused-guide-on-environme-d2c0d0
RAK1906 + RAK12004 advanced IAQ:https://www.hackster.io/user2702447/advanced-indoor-air-quality-monitoring-rak1906-and-rak12004-e05935
Light + IAQ:https://www.hackster.io/532551/smart-air-quality-and-light-monitoring-system-using-iot-3a3989
Data is sent over LoRaWAN using RAK4631 to The Things Network (TTN) and forwarded via webhook to Ubidots.
ObjectivesBuild a multi-sensor WisBlock node for meeting room comfort & safety.
Measure CO₂ + IAQ + light + comfort parameters.
Encode values into a compact binary payload.
Use TTN + Ubidots for dashboards and alerts.
Detect poor ventilation (high CO₂), uncomfortable temperature/humidity and inadequate lighting.
Target LevelIntermediate:
Makers, students, educators who already did WisBlock + TTN projects like:https://www.hackster.io/user2702447/getting-started-with-wisblock-and-the-things-network-ttn-0a7b84
Prerequisite KnowledgeArduino IDE basic usage
I²C sensor basics
LoRaWAN concepts (DevEUI, JoinEUI/AppEUI, AppKey)
Reading Serial Monitor and interpreting sensor data
Required Materials & SoftwareHardware1 × RAK4631 WisBlock Core
1 × RAK19007 WisBlock Base Board
1 × RAK1906 environmental sensor (BME680)
1 × RAK12004 CO₂ sensor (SCD30)
1 × RAK12019 ambient light sensor (BH1750)
1 × LoRaWAN gateway (e.g. WisGate Edge Lite 2)
1 × LoRa antenna
1 × USB-C cable
Optional: enclosure + battery
Software / Online ServicesArduino IDE
WisBlock board support as used in:https://www.hackster.io/user2702447/getting-started-with-wisblock-and-the-things-network-ttn-0a7b84
Libraries:
Adafruit_BME680
SparkFun_SCD30_Arduino_Library (for SCD30 / RAK12004)
BH1750
The Things Network / The Things Stack
Ubidots account
Estimated DurationAbout 2–3 hours including hardware, firmware, TTN + Ubidots and dashboard.
Learning OutcomesParticipants will learn to:
Assemble a WisBlock node with RAK1906 + RAK12004 + RAK12019.
Read CO₂, environmental, and light data together.
Encode and decode binary payloads for LoRaWAN.
Integrate TTN with Ubidots using webhook (as in:https://www.hackster.io/user2702447/set-up-ttn-webhooks-integrate-with-ubidots-a-guide-b715d9).
Build dashboards and alerts for healthy meeting rooms.
Steps for Configuration & ImplementationStep 1 – Hardware AssemblyMount RAK4631 onto RAK19007 base.
Plug RAK1906 into an I²C slot (same style as:https://www.hackster.io/user2702447/rak1906-air-quality-monitoring-a-focused-guide-on-environme-d2c0d0).
Plug RAK12004 (SCD30) into an I²C slot, as in:https://www.hackster.io/user2702447/advanced-indoor-air-quality-monitoring-rak1906-and-rak12004-e05935
Plug RAK12019 into an I²C slot, like:https://www.hackster.io/532551/smart-air-quality-and-light-monitoring-system-using-iot-3a3989
Attach the LoRa antenna.
Connect USB-C to the PC.
Step 2 – Arduino IDE & BoardsSame procedure as your “Getting Started with WisBlock + TTN”:https://www.hackster.io/user2702447/getting-started-with-wisblock-and-the-things-network-ttn-0a7b84
Open Arduino IDE.
Install / confirm WisBlock board support.
Select the RAK4631 board and the correct COM port.
Step 3 – Install LibrariesIn Arduino IDE → Sketch → Include Library → Manage Libraries… install:
Adafruit BME680 Library (Adafruit_BME680)
SparkFun SCD30 Arduino Library (SparkFun_SCD30_Arduino_Library)
BH1750 (ambient light sensor driver)
Step 4 – Full Arduino Sketch (Sensors + Payload)This sketch:
Initializes BME680 (RAK1906), SCD30 (RAK12004), BH1750 (RAK12019)
Reads all sensors
Builds a 16-byte payload
Prints data + payload hex for testing
Is ready to be integrated with your LoRaWAN send code from:https://www.hackster.io/user2702447/getting-started-with-wisblock-and-the-things-network-ttn-0a7b84
#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_BME680.h>
#include <SparkFun_SCD30_Arduino_Library.h>
#include <BH1750.h>
// ---------- Sensor Objects ----------
Adafruit_BME680 bme; // RAK1906 - BME680
SCD30 airSensor; // RAK12004 - SCD30
BH1750 lightMeter; // RAK12019 - BH1750
// Structure for all sensor data
struct SensorData {
float temp_bme; // °C
float hum_bme; // %RH
float pressure; // hPa
float gas_kOhm; // kΩ
float co2; // ppm
float temp_scd; // °C
float hum_scd; // %RH
float lux; // lux
};
SensorData data;
// -------- Helper functions for encoding ----------
void putInt16(uint8_t *buf, int16_t value, uint8_t index) {
buf[index] = (uint8_t)((value >> 8) & 0xFF);
buf[index + 1] = (uint8_t)(value & 0xFF);
}
void putUInt16(uint8_t *buf, uint16_t value, uint8_t index) {
buf[index] = (uint8_t)((value >> 8) & 0xFF);
buf[index + 1] = (uint8_t)(value & 0xFF);
}
// Payload format (16 bytes total):
// 0–1: temp_bme * 100 -> int16
// 2–3: hum_bme * 100 -> uint16
// 4–5: pressure * 10 -> uint16
// 6–7: gas_kOhm * 100 -> uint16
// 8–9: co2 ppm -> uint16
// 10–11: temp_scd * 100 -> int16
// 12–13: hum_scd * 100 -> uint16
// 14–15: lux -> uint16
void encodePayload(uint8_t *buffer, const SensorData &d) {
int16_t t_bme = (int16_t)(d.temp_bme * 100.0f);
uint16_t h_bme = (uint16_t)(d.hum_bme * 100.0f);
uint16_t p = (uint16_t)(d.pressure * 10.0f);
uint16_t gas = (uint16_t)(d.gas_kOhm * 100.0f);
uint16_t co2ppm = (uint16_t)(d.co2);
int16_t t_scd = (int16_t)(d.temp_scd * 100.0f);
uint16_t h_scd = (uint16_t)(d.hum_scd * 100.0f);
uint16_t lx = (uint16_t)(d.lux);
putInt16 (buffer, t_bme, 0);
putUInt16(buffer, h_bme, 2);
putUInt16(buffer, p, 4);
putUInt16(buffer, gas, 6);
putUInt16(buffer, co2ppm, 8);
putInt16 (buffer, t_scd, 10);
putUInt16(buffer, h_scd, 12);
putUInt16(buffer, lx, 14);
}
// ------------ Sensor initialization -------------
bool initSensors() {
bool ok = true;
// BME680 - RAK1906
if (!bme.begin(0x76)) {
Serial.println("BME680 (RAK1906) not found at 0x76");
ok = false;
} else {
bme.setTemperatureOversampling(BME680_OS_8X);
bme.setHumidityOversampling(BME680_OS_2X);
bme.setPressureOversampling(BME680_OS_4X);
bme.setIIRFilterSize(BME680_FILTER_SIZE_3);
bme.setGasHeater(320, 150); // 320°C for 150 ms
Serial.println("BME680 (RAK1906) initialized");
}
// SCD30 - RAK12004
if (!airSensor.begin()) {
Serial.println("SCD30 (RAK12004) not found");
ok = false;
} else {
// optional: airSensor.setMeasurementInterval(2); // seconds
Serial.println("SCD30 (RAK12004) initialized");
}
// BH1750 - RAK12019
if (!lightMeter.begin(BH1750::CONTINUOUS_HIGH_RES_MODE)) {
Serial.println("BH1750 (RAK12019) not found");
ok = false;
} else {
Serial.println("BH1750 (RAK12019) initialized");
}
return ok;
}
// ------------ Sensor reading -------------
bool readSensors(SensorData &d) {
// BME680
if (!bme.performReading()) {
Serial.println("Failed to perform BME680 reading");
return false;
}
d.temp_bme = bme.temperature;
d.hum_bme = bme.humidity;
d.pressure = bme.pressure / 100.0f; // Pa -> hPa
d.gas_kOhm = bme.gas_resistance / 1000.0f; // Ω -> kΩ
// SCD30 (CO2)
if (airSensor.dataAvailable()) {
d.co2 = airSensor.getCO2(); // ppm
d.temp_scd = airSensor.getTemperature(); // °C
d.hum_scd = airSensor.getHumidity(); // %RH
} else {
Serial.println("SCD30 data not ready yet");
// keep previous values if any; not fatal
}
// BH1750
d.lux = lightMeter.readLightLevel(); // lux
return true;
}
// ------------ Debug printing -------------
void printSensorData(const SensorData &d) {
Serial.print("BME680 -> T: ");
Serial.print(d.temp_bme); Serial.print(" °C, RH: ");
Serial.print(d.hum_bme); Serial.print(" %, P: ");
Serial.print(d.pressure); Serial.print(" hPa, Gas: ");
Serial.print(d.gas_kOhm); Serial.println(" kΩ");
Serial.print("SCD30 -> CO2: ");
Serial.print(d.co2); Serial.print(" ppm, T: ");
Serial.print(d.temp_scd);Serial.print(" °C, RH: ");
Serial.print(d.hum_scd); Serial.println(" %");
Serial.print("Light -> ");
Serial.print(d.lux); Serial.println(" lux");
}
void printPayload(const uint8_t *payload, size_t len) {
Serial.print("Payload [");
Serial.print(len);
Serial.print(" bytes]: ");
for (size_t i = 0; i < len; i++) {
if (payload[i] < 16) Serial.print("0");
Serial.print(payload[i], HEX);
Serial.print(" ");
}
Serial.println();
}
// ------------- Arduino setup/loop -------------
void setup() {
Serial.begin(115200);
while (!Serial) {
delay(10);
}
Serial.println("Smart Meeting Room CO2 & Comfort Monitor");
Serial.println("RAK4631 + RAK1906 + RAK12004 + RAK12019");
Wire.begin();
if (!initSensors()) {
Serial.println("One or more sensors failed to initialize.");
} else {
Serial.println("All sensors initialized successfully.");
}
// Initialize LoRaWAN here using your existing WisBlock TTN guide:
// https://www.hackster.io/user2702447/getting-started-with-wisblock-and-the-things-network-ttn-0a7b84
}
void loop() {
if (readSensors(data)) {
printSensorData(data);
uint8_t payload[16];
encodePayload(payload, data);
printPayload(payload, sizeof(payload));
// Integrate this payload with your LoRaWAN uplink logic from:
// https://www.hackster.io/user2702447/getting-started-with-wisblock-and-the-things-network-ttn-0a7b84
//
// Example concept:
// g_lora_app_data.buffer = payload;
// g_lora_app_data.buffsize = sizeof(payload);
// lmh_send(&g_lora_app_data, LORAWAN_PORT);
} else {
Serial.println("Sensor reading failed.");
}
// Send data every 60 seconds (adjust as needed)
delay(60000);
}
Step 5 – TTN Uplink Decoder (JavaScript)In TTN Console → Application → Payload formatters → Uplink, add:
function decodeUInt16(bytes, index) {
return (bytes[index] << 8) | bytes[index + 1];
}
function decodeInt16(bytes, index) {
var value = (bytes[index] << 8) | bytes[index + 1];
if (value & 0x8000) {
value = value - 0x10000;
}
return value;
}
function decodeUplink(input) {
var bytes = input.bytes;
if (!bytes || bytes.length < 16) {
return { errors: ["Invalid payload length"] };
}
var t_bme = decodeInt16(bytes, 0) / 100.0;
var rh_bme = decodeUInt16(bytes, 2) / 100.0;
var p = decodeUInt16(bytes, 4) / 10.0;
var gas = decodeUInt16(bytes, 6) / 100.0;
var co2 = decodeUInt16(bytes, 8) * 1.0;
var t_scd = decodeInt16(bytes, 10) / 100.0;
var rh_scd = decodeUInt16(bytes, 12) / 100.0;
var lux = decodeUInt16(bytes, 14) * 1.0;
return {
data: {
bme_temp_c: t_bme,
bme_hum_pct: rh_bme,
pressure_hpa: p,
gas_kohm: gas,
co2_ppm: co2,
scd_temp_c: t_scd,
scd_hum_pct: rh_scd,
lux: lux
}
};
}
These fields will be visible in TTN and then passed to Ubidots through your webhook integration, configured like in:https://www.hackster.io/user2702447/set-up-ttn-webhooks-integrate-with-ubidots-a-guide-b715d9
Step 6 – Ubidots Integration & DashboardIn TTN, create a Webhook → Ubidots for this application (same pattern as your webhook guide).
In Ubidots, create a Plugin for The Things Stack / TTN.
Ensure the plugin receives the JSON fields:
bme_temp_c, bme_hum_pct, pressure_hpa, gas_kohm,
co2_ppm, scd_temp_c, scd_hum_pct, lux
- Ensure the plugin receives the JSON fields:
bme_temp_c,bme_hum_pct,pressure_hpa,gas_kohm,co2_ppm,scd_temp_c,scd_hum_pct,lux
In Ubidots Devices, you’ll see a device with these variables.
Build a Meeting Room dashboard:
Time series for co2_ppm, bme_temp_c, bme_hum_pct
Gauge for co2_ppm with colored thresholds
Lux graph to see usage patterns (lights on/off)
- Build a Meeting Room dashboard:Time series for
co2_ppm,bme_temp_c,bme_hum_pctGauge forco2_ppmwith colored thresholdsLux graph to see usage patterns (lights on/off)
Configure Events / Alerts:
CO₂ alert when co2_ppm > 1000 (needs ventilation)
Comfort alert when bme_temp_c or bme_hum_pct out of range
“Lights on but CO₂ high” combined condition for smart hints
- Configure Events / Alerts:CO₂ alert when
co2_ppm > 1000(needs ventilation)Comfort alert whenbme_temp_corbme_hum_pctout of range“Lights on but CO₂ high” combined condition for smart hints
No readings from sensors
Check I²C slots exactly like in the original single-sensor projects:
RAK12019: https://www.hackster.io/532551/smart-air-quality-and-light-monitoring-system-using-iot-3a3989
- No readings from sensorsCheck I²C slots exactly like in the original single-sensor projects:RAK1906: https://www.hackster.io/user2702447/rak1906-air-quality-monitoring-a-focused-guide-on-environme-d2c0d0RAK12004: https://www.hackster.io/user2702447/advanced-indoor-air-quality-monitoring-rak1906-and-rak12004-e05935RAK12019: https://www.hackster.io/532551/smart-air-quality-and-light-monitoring-system-using-iot-3a3989
Weird values in TTN / Ubidots
Confirm payload format in encodePayload() matches the TTN decoder exactly (order + scaling).
- Confirm payload format in
encodePayload()matches the TTN decoder exactly (order + scaling). - Weird values in TTN / UbidotsConfirm payload format in
encodePayload()matches the TTN decoder exactly (order + scaling).
TTN receives but Ubidots doesn’t
Re-check TTN webhook + Ubidots plugin as in:https://www.hackster.io/user2702447/set-up-ttn-webhooks-integrate-with-ubidots-a-guide-b715d9
LoRaWAN join issues
Verify keys and region following:https://www.hackster.io/user2702447/getting-started-with-wisblock-and-the-things-network-ttn-0a7b84









Comments