Skip to content

sgp30.tc

SGP30 VOC/eCO2 Sensor Driver

Source on GitHub

// SGP30 VOC/eCO2 Sensor Driver
// I2C address: 0x58
// Measures: eCO2 (ppm), TVOC (ppb)
// CRC8: poly 0x31, init 0xFF (Sensirion)
// Must measure every second for IAQ baseline algorithm

int sgp_addr = 0;
int sgp_bus = 0;
int sgp_ok = 0;
int sgp_eco2 = 0;
int sgp_tvoc = 0;
int sgp_tick = 0;
char sgp_buf[10];
char sgp_lbl[32];

// CRC8 Sensirion: poly 0x31, init 0xFF, over 2 data bytes
int sgp_crc(int b1, int b2) {
    int crc = 0xFF ^ b1;
    int j = 0;
    while (j < 8) {
        if (crc & 0x80) crc = ((crc << 1) ^ 0x31) & 0xFF;
        else crc = (crc << 1) & 0xFF;
        j++;
    }
    crc = crc ^ b2;
    j = 0;
    while (j < 8) {
        if (crc & 0x80) crc = ((crc << 1) ^ 0x31) & 0xFF;
        else crc = (crc << 1) & 0xFF;
        j++;
    }
    return crc;
}

// Send 16-bit I2C command
void sgp_cmd(int cmd) {
    sgp_buf[0] = cmd & 0xFF;
    i2cWrite(sgp_addr, cmd >> 8, sgp_buf, 1, sgp_bus);
}

int sgp_scan() {
    int bus = 0;
    while (bus < 2) {
        if (i2cSetDevice(0x58, bus)) {
            sgp_addr = 0x58;
            sgp_bus = bus;
            // Get serial number to verify sensor
            sgp_buf[0] = 0x82;
            i2cWrite(sgp_addr, 0x36, sgp_buf, 1, sgp_bus);
            delay(10);
            if (i2cRead0(sgp_addr, sgp_buf, 9, sgp_bus)) {
                // Verify first word CRC
                if (sgp_crc(sgp_buf[0], sgp_buf[1]) == sgp_buf[2]) {
                    // Init IAQ algorithm
                    sgp_cmd(0x2003);
                    delay(10);
                    i2cSetActiveFound(sgp_addr, "SGP30", sgp_bus);
                    return 1;
                }
            }
            sgp_addr = 0;
        }
        bus++;
    }
    return 0;
}

void EverySecond() {
    if (!sgp_addr) return;

    if (sgp_tick == 0) {
        // First call: send IAQ Measure command
        sgp_cmd(0x2008);
        sgp_tick = 1;
    } else {
        // Read response from previous command (>12ms ago — well exceeded)
        if (i2cRead0(sgp_addr, sgp_buf, 6, sgp_bus)) {
            if (sgp_crc(sgp_buf[0], sgp_buf[1]) == sgp_buf[2] &&
                sgp_crc(sgp_buf[3], sgp_buf[4]) == sgp_buf[5]) {
                sgp_eco2 = (sgp_buf[0] << 8) | sgp_buf[1];
                sgp_tvoc = (sgp_buf[3] << 8) | sgp_buf[4];
                sgp_ok = 1;
            }
        }
        // Send next command immediately
        sgp_cmd(0x2008);
    }
}

void WebCall() {
    char buf[48];
    if (sgp_ok) {
        LGetString(5, sgp_lbl);  // eCO2
        strcpy(buf, "{s}SGP30 ");
        strcat(buf, sgp_lbl);
        strcat(buf, "{m}");
        webSend(buf);
        sprintf(buf, "%d ppm{e}", sgp_eco2);
        webSend(buf);
        LGetString(6, sgp_lbl);  // TVOC
        strcpy(buf, "{s}SGP30 ");
        strcat(buf, sgp_lbl);
        strcat(buf, "{m}");
        webSend(buf);
        sprintf(buf, "%d ppb{e}", sgp_tvoc);
        webSend(buf);
    } else {
        webSend("{s}SGP30{m}not ready{e}");
    }
}

void JsonCall() {
    if (!sgp_ok) return;
    char buf[64];
    sprintf(buf, ",\"SGP30\":{\"eCO2\":%d", sgp_eco2);
    responseAppend(buf);
    sprintf(buf, ",\"TVOC\":%d}", sgp_tvoc);
    responseAppend(buf);
}

void OnExit() {
    if (sgp_addr) {
        I2cResetActive(sgp_addr, sgp_bus);
    }
}

int main() {
    sgp_ok = 0;
    sgp_addr = 0;
    sgp_tick = 0;

    if (sgp_scan()) {
        char buf[48];
        sprintf(buf, "SGP30 found at 0x%x on bus %d", sgp_addr, sgp_bus);
        addLog(buf);
    } else {
        addLog("SGP30 not found");
    }
    return 0;
}