- Overview
- Features
- Supported Sensors
- Hardware Components
- Projects
- Project Structure
- Getting Started
- Debug Mode vs Production Mode
- Usage
- Button Controls & Power Management
- BLE Communication
- OLED Display
- Adding New Sensors
- Troubleshooting
- Glossary
- Contributing
- License
- Authors
- Acknowledgments
SlaveBox (branded as RoomSense) is an ESP32-based sensor hub designed for environmental monitoring and data acquisition. It provides a clean, modular architecture for managing multiple I2C sensors simultaneously with automatic scanning, reading, and data aggregation. Data can be transmitted wirelessly via BLE and displayed on an integrated OLED screen.
The repository contains three projects:
| Project | Target | Purpose |
|---|---|---|
SlaveBox_XiaoS3 |
Seeed XIAO ESP32S3 | Primary production firmware |
SlaveBox |
ESP32 WROOM | Alternative with button & screen-sleep support |
SlaveBox_XiaoC3_Mock |
Seeed XIAO ESP32-C3 | Mock firmware β no physical sensors needed |
- β Modular Design - Easy to add/remove sensors
- β Clean Code - Helper functions keep main code simple
- β Auto-Discovery - Automatically scans and identifies I2C sensors
- β Structured Data - Returns organized sensor data in nested maps
- β BLE Connectivity - Wireless data transmission with secure pairing
- β OLED Display - Real-time status and pairing PIN display
- β Debug / Production Modes - Switch between fast-loop dev mode and power-saving production builds
- β Debug Support - Built-in debug output for troubleshooting
- Multi-Sensor Support: Simultaneously read from multiple I2C sensors
- Automatic I2C Scanning: Detects connected sensors on startup
- Modular Helper System: Each sensor has dedicated helper files
- Centralized Management: SensorManager handles all sensor operations
- Structured Data Output: Returns data as
std::map<String, std::map<String, float>> - BLE Data Transmission: Stream sensor data wirelessly to connected devices
- Secure BLE Pairing: PIN-based pairing with on-screen display
- OLED Display: 128x32 SSD1306 screen for status and pairing info
- Visual Progress Indicator: Circular countdown timer during pairing
- Button Controls: Short press to wake display, long press for always-on mode (WROOM only)
- Screen Power Management: Auto-sleep after 10 seconds of inactivity (WROOM only)
- Debug / Production Build Modes: Compile-time
DEBUG_MODEflag controls verbosity and power usage - Mock Firmware: Full ESP32-C3 mock for development without hardware sensors
| Sensor | Type | I2C Address | Measurements |
|---|---|---|---|
| BME280 | Environmental | 0x77 |
Temperature, Humidity, Pressure |
| SGP30 | Air Quality | 0x58 |
eCO2, TVOC |
| BH1750 | Light Sensor | 0x23 |
Ambient Light (lux) |
| Component | Description | I2C Address |
|---|---|---|
| Seeed XIAO ESP32S3 | Primary small-form-factor controller | - |
| Seeed XIAO ESP32-C3 | Mock firmware target | - |
| ESP32 WROOM | Alternative microcontroller (button + sleep support) | - |
| SSD1306 OLED | 128x32 pixel display | 0x3C |
| BME280 | Environmental sensor | 0x77 |
| SGP30 | Air quality sensor | 0x58 |
| BH1750 | Light sensor | 0x23 |
| Push Button | User input (Optional, GPIO 4 on WROOM) | - |
The main production firmware for the Seeed Studio XIAO ESP32S3. Identical feature set to the WROOM version minus the physical button and screen power manager (not yet wired on S3).
- I2C pins:
D4 (GPIO5)= SDA,D5 (GPIO6)= SCL - Supports:
debugandproductionPlatformIO environments
The original firmware targeting the ESP32 WROOM dev board. Adds a physical button on GPIO 4 for:
-
Short press: Wake OLED and reset auto-sleep timer
-
Long press (2 s+): Toggle always-on mode
-
I2C pins:
GPIO 21= SDA,GPIO 22= SCL -
Button: GPIO 4 β GND (INPUT_PULLUP)
-
Supports:
debugandproductionPlatformIO environments
A complete mock firmware for the Seeed Studio XIAO ESP32-C3 that emulates the full behaviour of the real firmware without needing physical I2C sensors attached.
How it works:
MockSensorManagergenerates realistic, organically-drifting sensor values using sinusoidal math + noise β no I2C hardware requiredBLEHelperis a stub:sendMap()logs identical JSON payloads to Serial instead of BLE-notifying (easy to verify with Python bridge)ScreenHelper/DisplayHelperare the real SSD1306 implementations β plug in an OLED and it worksmain.cppis byte-for-byte identical to the real S3 firmware β swap in real helpers to convert to production
Mock sensor behaviour:
| Sensor | Base Value | Variation |
|---|---|---|
| BME280 temperature | 22 Β°C | Β±1.5 Β°C slow drift + Β±0.2 Β°C noise |
| BME280 humidity | 55 % | Β±5 % slow drift |
| BME280 pressure | 1013.25 hPa | Β±2 hPa slow drift |
| SGP30 eCO2 | 400 ppm (warm-up 15 s) | 400β900 ppm after warm-up |
| SGP30 TVOC | 0 ppb (warm-up) | 10β70 ppb after warm-up |
| BH1750 light | 350 lux | Β±120 lux cloud drift + fluorescent ripple |
SlaveBoxCode/
βββ SlaveBox_XiaoS3/ # Primary β Seeed XIAO ESP32S3
β βββ include/ # Header files
β βββ src/ # main.cpp, sensor helpers, BLE, display
β βββ platformio.ini # debug + production environments
β
βββ SlaveBox/ # Alternative β ESP32 WROOM
β βββ include/ # Header files (incl. ButtonHandler, ScreenPowerManager)
β βββ src/ # main.cpp, sensor helpers, BLE, display, button
β βββ platformio.ini # debug + production environments
β
βββ SlaveBox_XiaoC3_Mock/ # Mock firmware β Seeed XIAO ESP32-C3
β βββ include/ # Header files (mock + real APIs)
β βββ src/ # MockSensorManager, real ScreenHelper, BLE stub
β βββ platformio.ini # Single environment (seeed_xiao_esp32c3)
β
βββ requirements.txt # Python dependencies for BLE bridge
βββ README.md
Hardware:
- ESP32 Development Board (XIAO ESP32S3 recommended, or WROOM)
- SSD1306 OLED Display (128x32, I2C)
- Supported I2C sensors (BME280, SGP30, BH1750)
- Push button (optional, WROOM only)
- jumper wires
Software:
- PlatformIO IDE or PlatformIO CLI
- USB cable for programming
| Library | Version | Purpose |
|---|---|---|
| Adafruit BME280 | ^2.3.0 | Temperature/Humidity/Pressure sensor |
| Adafruit SGP30 | ^2.0.3 | Air quality sensor |
| BH1750 (claws) | ^1.3.0 | Light sensor |
| ArduinoJson | ^7.2.1 | JSON serialization for BLE data |
| Adafruit GFX | ^1.12.4 | Graphics library for OLED |
| Adafruit SSD1306 | ^2.5.16 | OLED display driver |
| XIAO S3 Pin | Function | Connection |
|---|---|---|
| D4 (GPIO5) | SDA | SDA (all I2C devices) |
| D5 (GPIO6) | SCL | SCL (all I2C devices) |
| 3V3 | VCC | VCC (all sensors & display) |
| GND | GND | GND (all devices) |
| ESP32 Pin | Function | Connection |
|---|---|---|
| GPIO 21 | SDA | SDA (all I2C devices) |
| GPIO 22 | SCL | SCL (all I2C devices) |
| GPIO 4 | GPIO | Button (other side to GND) |
| 3.3V | VCC | VCC (all sensors & display) |
| GND | GND | GND (all devices) |
Note: Multiple I2C devices share the same SDA/SCL bus. All sensors and the OLED connect in parallel.
-
Clone the repository:
git clone https://github.com/JetsGPT/SlaveBoxCode.git cd SlaveBoxCode -
Open the desired project folder (
SlaveBox_XiaoS3,SlaveBox, orSlaveBox_XiaoC3_Mock) in PlatformIO. -
Build and upload:
# Debug mode (default β fast loop, verbose output) pio run -e debug -t upload # Production mode (power-saving β 5-minute sleep interval) pio run -e production -t upload
-
Monitor serial output:
pio device monitor
Both real projects (SlaveBox and SlaveBox_XiaoS3) support two compile-time build modes controlled by the DEBUG_MODE preprocessor flag.
# Debug mode (development)
pio run -e debug -t upload
# Production mode (deployment / battery-powered use)
pio run -e production -t upload| Feature | Debug Mode (DEBUG_MODE=1) |
Production Mode (DEBUG_MODE=0) |
|---|---|---|
| Sensor read interval | Every loop iteration (~2β3 s) | Every 5 minutes |
| Serial output | Full verbose scan log | Silent (no output) |
| OLED display | Active, cycles through metrics | Off (WROOM: button-wake only) |
| BLE transmission | Every read | Every read (if connected) |
| Sleep | None | Light sleep between reads |
| Estimated power | ~80 mA active | ~0.8 mA (light sleep avg.) |
In production mode, after each sensor read and BLE transmission the device calls:
esp_sleep_enable_timer_wakeup(5ULL * 60ULL * 1000000ULL); // 5 minutes
esp_light_sleep_start();Why light sleep instead of deep sleep? Light sleep preserves the BLE connection context and bonding keys in RAM. Deep sleep would reset the BLE stack and force re-pairing on every wake. Light sleep draws ~0.8 mA vs ~80 mA active β a 99% reduction in average current over a 5-minute cycle.
The DEBUG_MODE flag is set exclusively in platformio.ini β no source file changes needed:
[env:production]
build_flags =
-DDEBUG_MODE=0The main loop automatically scans and reads all sensors and transmits data over BLE:
#include <Arduino.h>
#include "runSetup.h"
#include "SensorManager.h"
#include "BLEHelper.h"
#include "DisplayHelper.h"
void setup() {
runSetup();
initializeSensors();
delay(1000);
}
void loop() {
std::map<String, std::map<String, float>> sensorData = scanAndReadAllSensors(true);
if (!bleHelper.isPairing()) {
if (sensorData.empty()) {
displayNoSensors();
} else {
displaySensorData(sensorData);
}
}
if (bleHelper.isConnected()) {
bleHelper.sendMap(sensorData);
}
}The scanAndReadAllSensors() function returns data in this structure:
{
"BME280": {
"temperature": 25.5, // Β°C
"humidity": 60.0, // %
"pressure": 1013.25 // hPa
},
"SGP30": {
"eCO2": 400, // ppm
"TVOC": 0 // ppb
},
"BH1750": {
"light": 250.5 // lux
}
}=== Scanning I2C bus ===
β Found BME280 at 0x77
Readings:
temperature: 25.50
humidity: 60.00
pressure: 1013.25
β Found SGP30 at 0x58
Readings:
eCO2: 400
TVOC: 0
β Found BH1750 at 0x23
Readings:
light: 250.50
=== Scan complete ===
Initializes the I2C bus and prepares sensors for reading.
Scans all known sensors and returns their readings.
Parameters:
debugβ Enable/disable serial output (default:true)
Returns: Nested map {SensorName: {ValueType: Value}}
Retrieves sensor information by I2C address.
SensorInfo* sensor = getSensorByAddress(0x77); // Get BME280 infostd::map<String, float> getValues_BME280(uint8_t addr, bool debug);
// Returns: {"temperature", "humidity", "pressure"}std::map<String, float> getValues_SGP30(bool debug);
// Returns: {"eCO2", "TVOC"}std::map<String, float> getValues_BH1750(uint8_t addr, bool debug);
// Returns: {"light"}(WROOM variant only β
SlaveBox/)
Connect a momentary push button between GPIO 4 and GND. The internal pull-up resistor is enabled automatically.
| Action | Function |
|---|---|
| Short Press | Wake the display and reset the auto-sleep timer |
| Long Press (2+ seconds) | Toggle "Always On" mode (disables auto-sleep) |
| State | Behaviour |
|---|---|
| Default | Screen starts OFF |
| Auto-Sleep | Screen off after 10 s of inactivity |
| Wake on Pairing | Screen auto-wakes to show BLE pairing PIN |
| Always-On Mode | Disables auto-sleep (toggled via long press) |
ButtonHandler button(4); // GPIO pin 4, 50ms debounce, 2000ms long press
button.begin(); // Initialize GPIO
button.update(); // Call every loop iteration
if (button.wasPressed()) { ... } // Short press detected
if (button.wasLongPressed()) { ... } // Long press detected
if (button.isHeld()) { ... } // Button currently heldscreenPowerManager.begin(); // Initialize (screen starts OFF)
screenPowerManager.update(); // Call every loop (checks timeout)
screenPowerManager.wake(); // Turn on and reset timer
screenPowerManager.sleep(); // Turn off immediately
screenPowerManager.toggleAlwaysOn(); // Toggle always-on mode
if (screenPowerManager.isScreenOn()) { ... }
if (screenPowerManager.isAlwaysOn()) { ... }SlaveBox includes a BLE server for wireless data transmission.
| UUID | Name | Description |
|---|---|---|
cfa59c64-aeaf-42ac-bf8d-bc4a41ef5b0c |
Service | Main sensor service |
49c92b70-42f5-49c3-bc38-5fe05b3df8e0 |
Sensor Data | JSON-formatted sensor readings |
3bee5811-4c6c-449a-b368-0b1391c6c1dc |
Sensor Type | Primary sensor type identifier |
9d62dc0c-b4ef-40c4-9383-15bdc16870de |
Box ID | Device identifier |
// Initialize BLE with device name and box ID
bleHelper.begin("RoomSense-Sensor-01", "box_room_001");
// Check if a client is connected
if (bleHelper.isConnected()) {
bleHelper.sendMap(sensorData);
}
// Check if pairing is in progress
if (bleHelper.isPairing()) {
// PIN is being displayed on screen
}- A 6-digit PIN is generated and displayed on the OLED screen
- A 30-second countdown timer shows remaining time
- User enters the PIN on their phone/device
- Screen shows "SUCCESS!" or "FAILED!" based on result
The 128x32 SSD1306 OLED display provides visual feedback.
bool initScreen(); // Initialize display
updateScreen("Temperature", "24.5 C", true); // Header + value
updateScreenWithProgress("ENTER PIN (25s)", "123-456", 83); // With countdown arc
clearScreen(); // Clear display
setDisplayPower(true); // Power on
setDisplayPower(false); // Power offdisplaySensorData(sensorData, 2000); // Cycle metrics, 2 s each
displayNoSensors(); // Show "No Sensors" message| Mode | Description |
|---|---|
| Header + Value | Title with divider line and large value text |
| Value Only | Large centered text (showHeader = false) |
| Progress Mode | Header, value, and circular countdown arc |
A Python-based BLE to MQTT bridge is available for integrating with home automation systems.
pip install -r requirements.txtrequirements.txt:
bleak>=0.21.0
aiomqtt>=1.2.0
| Package | Purpose |
|---|---|
| bleak | BLE client library for Python |
| aiomqtt | Async MQTT client for publishing sensor data |
Follow these steps to add a new I2C sensor:
include/NewSensorHelper.h:
#ifndef NEWSENSORHELPER_H
#define NEWSENSORHELPER_H
#include <Arduino.h>
#include <map>
std::map<String, float> getValues_NewSensor(uint8_t addr, bool debug);
#endifsrc/NewSensorHelper.cpp:
#include "NewSensorHelper.h"
#include <Wire.h>
static bool initialized = false;
std::map<String, float> getValues_NewSensor(uint8_t addr, bool debug) {
if (!initialized) {
// Initialize sensor
initialized = true;
}
std::map<String, float> values;
values["measurement"] = sensorValue;
return values;
}Add include:
#include "NewSensorHelper.h"Add wrapper:
static std::map<String, float> readNewSensor(uint8_t addr, bool debug) {
return getValues_NewSensor(addr, debug);
}Add to sensor array:
static SensorInfo sensors[] = {
// ... existing sensors ...
{0xYY, "NewSensor", readNewSensor}
};Your sensor will now be automatically scanned and read every loop.
| Problem | Solution |
|---|---|
| No sensors detected | Check I2C wiring (SDA/SCL). Verify 3.3V power. |
| OLED not working | Confirm I2C address is 0x3C. Check for loose connections. |
| BLE not advertising | Ensure no other device is connected. Restart the ESP32. |
| BLE pairing fails | Enter PIN within 30 seconds. Ensure phone Bluetooth is on. |
| Button not responding | Verify button is between GPIO 4 and GND. (WROOM only) |
| Display stays off | Press button to wake. Long press to disable auto-sleep. (WROOM only) |
| Sensor readings are wrong | Allow sensors to warm up (SGP30 needs 15 seconds). |
| Production mode not sleeping | Ensure you uploaded with pio run -e production -t upload. |
#include <Wire.h>
void setup() {
Wire.begin(21, 22);
Serial.begin(115200);
for (byte addr = 1; addr < 127; addr++) {
Wire.beginTransmission(addr);
if (Wire.endTransmission() == 0) {
Serial.printf("Found device at 0x%02X\n", addr);
}
}
}
void loop() {}Enable verbose output to see detailed sensor readings:
auto data = scanAndReadAllSensors(true); // Verbose output
auto data = scanAndReadAllSensors(false); // Silent modeOr build with -DDEBUG_MODE=1 for full development experience.
| Term | Definition |
|---|---|
| I2C | Inter-Integrated Circuit β two-wire serial communication protocol |
| BLE | Bluetooth Low Energy β wireless protocol optimized for low power |
| SDA | Serial Data Line β I2C data wire |
| SCL | Serial Clock Line β I2C clock wire |
| eCO2 | Equivalent Carbon Dioxide β CO2 estimation based on VOC (ppm) |
| TVOC | Total Volatile Organic Compounds β air quality measure (ppb) |
| lux | Unit of illuminance (light intensity) |
| hPa | Hectopascal β atmospheric pressure unit (1 hPa = 1 mbar) |
| UUID | Universally Unique Identifier β identifies BLE services/characteristics |
| OLED | Organic Light-Emitting Diode β display technology |
| Light Sleep | ESP32 low-power mode that preserves RAM, BLE state, and bonding keys |
| DEBUG_MODE | Compile-time flag: 1 = fast dev loop, 0 = power-saving production |
Contributions are welcome!
- Fork the repository
- Create a feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
- Follow the existing code style
- Add comments for complex logic
- Update documentation for new features
- Test on actual hardware when possible
- Support for additional sensors
- Web-based configuration interface
- MQTT direct publishing from ESP32
- Data logging to SD card
- Multi-room mesh networking
This project is licensed under the MIT License β see the LICENSE file for details.
- Julian β Initial work
- Adafruit for sensor and display libraries
- bblanchon for ArduinoJson library
- claws for BH1750 library
- PlatformIO for the excellent development platform
- ESP32 community for support and documentation
Made with β€οΈ for the IoT community