|
// =================================================================== |
|
// 🦐 ナル先生の屁ロガー (Fart Logger) - SGP30 + M5Stack ATOM Lite |
|
// =================================================================== |
|
// 主成分H2(9%) + 硫化水素/スカトール(TVOC) を SGP30 で検出 |
|
// eCO2 が閾値を超えたら ATOM Lite LED 赤フラッシュ+Serial通知 |
|
// |
|
// 配線: M5Stack ATOM Lite + M5 Unit Mini TVOC/eCO2 (U088) |
|
// ATOM Lite Grove → I2C: SDA=GPIO26, SCL=GPIO32, 5V, GND |
|
// LED: GPIO27 (内蔵 SK6812 1個) |
|
// |
|
// 依存: |
|
// - M5Atom (M5Stack 公式) |
|
// - Adafruit SGP30 Sensor Library |
|
// - FastLED (M5Atomの内蔵LED制御) |
|
// |
|
// 注意: |
|
// 人類の約1/3はメタン産生型腸内細菌でH2をほぼ出さない(false negativeあり) |
|
// 完璧な屁検出はできません 💦 |
|
// =================================================================== |
|
|
|
#include <M5Atom.h> |
|
#include <Wire.h> |
|
#include <Adafruit_SGP30.h> |
|
|
|
Adafruit_SGP30 sgp; |
|
|
|
// 閾値 |
|
const uint16_t ECO2_THRESHOLD_PPM = 1500; // 通常400ppm、屁で跳ね上がる |
|
const uint16_t TVOC_THRESHOLD_PPB = 2000; // 臭気成分閾値 |
|
const unsigned long BASELINE_MS = 30000; // 30秒のベースライン取得 |
|
|
|
unsigned long bootMs = 0; |
|
int detectCount = 0; |
|
|
|
void setup() { |
|
M5.begin(true, false, true); // Serial, I2C, LED |
|
delay(50); |
|
|
|
Serial.begin(115200); |
|
Serial.println("🦐 ナル先生の屁ロガー起動"); |
|
|
|
// SGP30 init |
|
if (!sgp.begin()) { |
|
Serial.println("❌ SGP30 not found! 配線確認"); |
|
while (1) { |
|
M5.dis.drawpix(0, 0xff0000); // 赤 |
|
delay(500); |
|
M5.dis.drawpix(0, 0x000000); |
|
delay(500); |
|
} |
|
} |
|
Serial.print("✅ SGP30 OK, serial: "); |
|
Serial.print(sgp.serialnumber[0], HEX); |
|
Serial.print(sgp.serialnumber[1], HEX); |
|
Serial.println(sgp.serialnumber[2], HEX); |
|
|
|
M5.dis.drawpix(0, 0x0000ff); // 青 = ベースライン取得中 |
|
bootMs = millis(); |
|
} |
|
|
|
void loop() { |
|
M5.update(); |
|
|
|
if (!sgp.IAQmeasure()) { |
|
Serial.println("❌ measurement failed"); |
|
delay(500); |
|
return; |
|
} |
|
|
|
uint16_t tvoc = sgp.TVOC; // ppb |
|
uint16_t eco2 = sgp.eCO2; // ppm (H2から算出) |
|
|
|
// raw H2 / Ethanol も取れる |
|
if (!sgp.IAQmeasureRaw()) { |
|
Serial.println("❌ raw measurement failed"); |
|
} |
|
uint16_t rawH2 = sgp.rawH2; |
|
uint16_t rawEth = sgp.rawEthanol; |
|
|
|
unsigned long elapsed = millis() - bootMs; |
|
bool inBaseline = (elapsed < BASELINE_MS); |
|
|
|
if (inBaseline) { |
|
// ベースライン中 = 青LED |
|
M5.dis.drawpix(0, 0x0000ff); |
|
} else if (eco2 >= ECO2_THRESHOLD_PPM || tvoc >= TVOC_THRESHOLD_PPB) { |
|
// 検出 = 赤LED フラッシュ |
|
detectCount++; |
|
Serial.printf("⚠️ [%d] 屁検出! TVOC=%u ppb / eCO2=%u ppm / H2raw=%u / Ethraw=%u\n", |
|
detectCount, tvoc, eco2, rawH2, rawEth); |
|
M5.dis.drawpix(0, 0xff0000); |
|
delay(200); |
|
M5.dis.drawpix(0, 0x000000); |
|
delay(100); |
|
M5.dis.drawpix(0, 0xff0000); |
|
delay(200); |
|
M5.dis.drawpix(0, 0x000000); |
|
|
|
// TODO: ここで Telegram Bot API or Adafruit IO へ POST |
|
// notifyTelegram(eco2, tvoc, rawH2); |
|
} else { |
|
// 通常 = 緑LED 微弱 |
|
M5.dis.drawpix(0, 0x002000); |
|
Serial.printf("[%lus] TVOC=%u eCO2=%u H2=%u Eth=%u\n", |
|
elapsed / 1000, tvoc, eco2, rawH2, rawEth); |
|
} |
|
|
|
delay(100); // 100ms間隔(SGP30推奨1秒・短縮はキャリブレ精度落ち) |
|
} |