Cultivar Firmware
Arduino library and sketch for the Grove5 Cultivar appliance ENV-1 sensor board.
Grove5 — Open source software, honest hardware.
What it does
Runs on the STM32U585 MCU (via Arduino UNO Q) or any Arduino-compatible board. Reads the ENV-1-V0.1 sensor board — BME690 environmental sensor and optional SCD43 CO2 sensor — and outputs structured JSON payloads over serial every 5 minutes.
The daemon (cultivar-daemon) consumes these payloads on the Linux side.
Hardware
ENV-1-V0.1 — dual sensor board designed for the Cultivar appliance.
| Sensor | Measures | I2C Address |
|---|---|---|
| BME690 | Temp, humidity, pressure, gas resistance (10-point sweep) | 0x76 |
| SCD43 | Real CO2, temp, humidity | 0x62 (optional — DNP for colonization) |
I2C pins (ESP32S3 dev board — adjust for production):
- SDA → GPIO42
- SCL → GPIO41
Chamber identity is self-declared: if SCD43 is not detected at init, the board identifies as colonization. If SCD43 is present, it identifies as fruiting. No jumpers, no config — the populated BOM is the configuration.
Library: cultivar_env
Wraps the Bosch BME690 SensorAPI directly. No Bosch BSEC2 blob — raw sensor access only.
Installation
- Download Bosch BME690 SensorAPI
- Copy
bme69x.h,bme69x.c,bme69x_defs.hintocultivar_env/src/ - Install
Sensirion I2C SCD4xvia Arduino Library Manager - Install
cultivar_envvia Sketch → Include Library → Add .ZIP Library
Dependencies
- Sensirion I2C SCD4x by Sensirion
- Bosch BME690 SensorAPI v1.1.0 (bundled in
src/)
Quick start
#include <Wire.h>
#include <cultivar_env.h>
CultivarEnv env;
char jsonBuf[768];
const char* CHAMBER_ID;
void setup() {
Serial.begin(115200);
env.begin(Wire, 42, 41);
CHAMBER_ID = env.scd4x_ok ? "fruiting" : "colonization";
}
void loop() {
CultivarEnvPayload payload;
env.collect(payload);
env.toJSON(payload, jsonBuf, sizeof(jsonBuf));
Serial.println(jsonBuf); // → daemon via serial
delay(300000); // 5 min
}
Gas sweep — 10-point mycology profile
| # | Temp°C | Dur ms | Zone | Notes |
|---|---|---|---|---|
| 0 | 200 | 150 | 1 — Baseline | Humidity/CO2 proxy |
| 1 | 220 | 150 | 1 — Baseline | Light alcohols |
| 2 | 250 | 150 | 1 — Baseline | Ethanol |
| 3 | 280 | 180 | 2 — Biological | Acetic acid / bacterial marker |
| 4 | 300 | 180 | 2 — Biological | Terpene zone low |
| 5 | 320 | 180 | 2 — Biological | Geosmin / 1-octen-3-ol |
| 6 | 340 | 200 | 2 — Biological | Aldehyde zone |
| 7 | 360 | 200 | 3 — Heavy | Ammonia / nitrogen |
| 8 | 380 | 200 | 3 — Heavy | Sulfur compounds |
| 9 | 400 | 200 | 3 — Heavy | Heavy VOC fingerprint |
Gas resistance values are raw — humidity correction, baseline normalization, and compound inference are handled by cultivar-daemon on the Linux side.
JSON payload format
One payload per collection cycle, emitted as a single newline-terminated JSON line:
{
"chamber_id": "fruiting",
"ts": 300000,
"ambient": {"temp_c": 23.45, "humidity_pct": 82.10, "pressure_hpa": 929.88, "valid": true},
"co2": {"ppm": 1850, "temp_c": 24.10, "humidity_pct": 83.20, "valid": true},
"health": {"scd43_present": true, "temp_delta": 0.65, "humidity_delta": 1.10, "temp_ok": true, "humidity_ok": true},
"baseline": {"established": false, "samples_collected": 3},
"sweep": [
{"heater_temp_c": 200, "heater_dur_ms": 150, "zone": 1, "resistance": 85000000, "heater_stable": true, "valid": true},
{"heater_temp_c": 220, "heater_dur_ms": 150, "zone": 1, "resistance": 72000000, "heater_stable": true, "valid": true},
{"heater_temp_c": 250, "heater_dur_ms": 150, "zone": 1, "resistance": 61000000, "heater_stable": true, "valid": true},
{"heater_temp_c": 280, "heater_dur_ms": 180, "zone": 2, "resistance": 420000, "heater_stable": true, "valid": true},
{"heater_temp_c": 300, "heater_dur_ms": 180, "zone": 2, "resistance": 280000, "heater_stable": true, "valid": true},
{"heater_temp_c": 320, "heater_dur_ms": 180, "zone": 2, "resistance": 195000, "heater_stable": true, "valid": true},
{"heater_temp_c": 340, "heater_dur_ms": 200, "zone": 2, "resistance": 148000, "heater_stable": true, "valid": true},
{"heater_temp_c": 360, "heater_dur_ms": 200, "zone": 3, "resistance": 102400000,"heater_stable": true, "valid": true},
{"heater_temp_c": 380, "heater_dur_ms": 200, "zone": 3, "resistance": 102400000,"heater_stable": true, "valid": true},
{"heater_temp_c": 400, "heater_dur_ms": 200, "zone": 3, "resistance": 102400000,"heater_stable": true, "valid": true}
]
}
Collection timing
- Production: 5-minute interval — allows full thermal dissipation between heater cycles
- Development: 30-second interval — heater self-heating will elevate ambient temp readings
- Ambient reading (heater off, 3s settle) runs before each gas sweep
Board setup (Arduino IDE)
| Setting | Value |
|---|---|
| Board | ESP32S3 Dev Module |
| USB CDC On Boot | Enabled |
| Flash Size | 4MB |
| PSRAM | OPI PSRAM |
| Upload Speed | 921600 |
For Arduino UNO Q production deployment: board is STM32U585, Serial1 is used for daemon communication (reserved by Arduino Router — use Serial for debug only).
Notes on BME690 gas sensor
- First power-on: allow 15-30 min for gas resistance to stabilize
- Full sensor burn-in: ~7 days of cumulative operation per Bosch documentation
102400000 Ωis the clean-air ceiling / saturated sentinel value — valid and expected in clean environments- Resistance values are raw — do not compare absolute values across different units or sessions without normalization
Related repos
grove5/cultivar-daemon— Linux data pipelinegrove5/cultivar-types— shared typesgrove5/cultivar-ui— Svelte frontendgrove5/hardware— schematics, STLs
License
MIT — Grove5
Bosch BME690 SensorAPI: BSD-3-Clause — Bosch Sensortec GmbH