/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file    sts_aq_sgp30.c            								   			*
  * @author  Yunhorn (r) Technology Limited Application Team	               *
  * @brief   Yunhorn (r) SmarToilets (r) Product configuration file.		   *
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2025 Yunhorn Technology Limited.
  * Copyright (c) 2025 Shenzhen Yunhorn Technology Co., Ltd.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */


#include "sgp30.h"
#include "sensirion_arch_config.h"
#include "sensirion_common.h"
#include "sensirion_i2c.h"
#include "sgp_git_version.h"

#define SGP30_PRODUCT_TYPE 0
static const uint8_t SGP30_I2C_ADDRESS = 0x58;

/* command and constants for reading the serial ID */
#define SGP30_CMD_GET_SERIAL_ID 0x3682
#define SGP30_CMD_GET_SERIAL_ID_DURATION_US 500
#define SGP30_CMD_GET_SERIAL_ID_WORDS 3

/* command and constants for reading the featureset version */
#define SGP30_CMD_GET_FEATURESET 0x202f
#define SGP30_CMD_GET_FEATURESET_DURATION_US 10000
#define SGP30_CMD_GET_FEATURESET_WORDS 1

/* command and constants for on-chip self-test */
#define SGP30_CMD_MEASURE_TEST 0x2032
#define SGP30_CMD_MEASURE_TEST_DURATION_US 220000
#define SGP30_CMD_MEASURE_TEST_WORDS 1
#define SGP30_CMD_MEASURE_TEST_OK 0xd400

/* command and constants for IAQ init */
#define SGP30_CMD_IAQ_INIT 0x2003
#define SGP30_CMD_IAQ_INIT_DURATION_US 10000

/* command and constants for IAQ measure */
#define SGP30_CMD_IAQ_MEASURE 0x2008
#define SGP30_CMD_IAQ_MEASURE_DURATION_US 12000
#define SGP30_CMD_IAQ_MEASURE_WORDS 2

/* command and constants for getting IAQ baseline */
#define SGP30_CMD_GET_IAQ_BASELINE 0x2015
#define SGP30_CMD_GET_IAQ_BASELINE_DURATION_US 10000
#define SGP30_CMD_GET_IAQ_BASELINE_WORDS 2

/* command and constants for setting IAQ baseline */
#define SGP30_CMD_SET_IAQ_BASELINE 0x201e
#define SGP30_CMD_SET_IAQ_BASELINE_DURATION_US 10000

/* command and constants for raw measure */
#define SGP30_CMD_RAW_MEASURE 0x2050
#define SGP30_CMD_RAW_MEASURE_DURATION_US 25000
#define SGP30_CMD_RAW_MEASURE_WORDS 2

/* command and constants for setting absolute humidity */
#define SGP30_CMD_SET_ABSOLUTE_HUMIDITY 0x2061
#define SGP30_CMD_SET_ABSOLUTE_HUMIDITY_DURATION_US 10000

/* command and constants for getting TVOC inceptive baseline */
#define SGP30_CMD_GET_TVOC_INCEPTIVE_BASELINE 0x20b3
#define SGP30_CMD_GET_TVOC_INCEPTIVE_BASELINE_DURATION_US 10000
#define SGP30_CMD_GET_TVOC_INCEPTIVE_BASELINE_WORDS 1

/* command and constants for setting TVOC baseline */
#define SGP30_CMD_SET_TVOC_BASELINE 0x2077
#define SGP30_CMD_SET_TVOC_BASELINE_DURATION_US 10000

/**
 * sgp30_check_featureset() - Check if the connected sensor has a certain FS
 *
 * @needed_fs: The featureset that is required
 *
 * Return: STATUS_OK if the sensor has the required FS,
 *         SGP30_ERR_INVALID_PRODUCT_TYPE if the sensor is not an SGP30,
 *         SGP30_ERR_UNSUPPORTED_FEATURE_SET if the sensor does not
 *                                           have the required FS,
 *         an error code otherwise
 */
static int16_t sgp30_check_featureset(uint16_t needed_fs)
{
    int16_t ret;
    uint16_t fs_version;
    uint8_t product_type;

    ret = sgp30_get_feature_set_version(&fs_version, &product_type);
    if (ret != STATUS_OK)
        return ret;

    if (product_type != SGP30_PRODUCT_TYPE)
        return SGP30_ERR_INVALID_PRODUCT_TYPE;

    if (fs_version < needed_fs)
        return SGP30_ERR_UNSUPPORTED_FEATURE_SET;

    return STATUS_OK;
}

int16_t sgp30_measure_test(uint16_t *test_result)
{
    uint16_t measure_test_word_buf[SGP30_CMD_MEASURE_TEST_WORDS];
    int16_t ret;

    *test_result = 0;

    ret = sensirion_i2c_delayed_read_cmd(
        SGP30_I2C_ADDRESS, SGP30_CMD_MEASURE_TEST,
        SGP30_CMD_MEASURE_TEST_DURATION_US, measure_test_word_buf,
        SENSIRION_NUM_WORDS(measure_test_word_buf));
    if (ret != STATUS_OK)
        return ret;

    *test_result = *measure_test_word_buf;
    if (*test_result == SGP30_CMD_MEASURE_TEST_OK)
        return STATUS_OK;

    return STATUS_FAIL;
}

int16_t sgp30_measure_iaq()
{
    return sensirion_i2c_write_cmd(SGP30_I2C_ADDRESS, SGP30_CMD_IAQ_MEASURE);
}

int16_t sgp30_read_iaq(uint16_t *tvoc_ppb, uint16_t *co2_eq_ppm)
{
    int16_t ret;
    uint16_t words[SGP30_CMD_IAQ_MEASURE_WORDS];

    ret = sensirion_i2c_read_words(SGP30_I2C_ADDRESS, words,
                                   SGP30_CMD_IAQ_MEASURE_WORDS);

    *tvoc_ppb = words[1];
    *co2_eq_ppm = words[0];

    return ret;
}

int16_t sgp30_measure_iaq_blocking_read(uint16_t *tvoc_ppb,
                                        uint16_t *co2_eq_ppm)
{
    int16_t ret;

    ret = sgp30_measure_iaq();
    if (ret != STATUS_OK)
        return ret;

    sensirion_sleep_usec(SGP30_CMD_IAQ_MEASURE_DURATION_US);

    return sgp30_read_iaq(tvoc_ppb, co2_eq_ppm);
}

int16_t sgp30_measure_tvoc()
{
    return sgp30_measure_iaq();
}

int16_t sgp30_read_tvoc(uint16_t *tvoc_ppb)
{
    uint16_t co2_eq_ppm;
    return sgp30_read_iaq(tvoc_ppb, &co2_eq_ppm);
}

int16_t sgp30_measure_tvoc_blocking_read(uint16_t *tvoc_ppb)
{
    uint16_t co2_eq_ppm;
    return sgp30_measure_iaq_blocking_read(tvoc_ppb, &co2_eq_ppm);
}

int16_t sgp30_measure_co2_eq()
{
    return sgp30_measure_iaq();
}

int16_t sgp30_read_co2_eq(uint16_t *co2_eq_ppm)
{
    uint16_t tvoc_ppb;
    return sgp30_read_iaq(&tvoc_ppb, co2_eq_ppm);
}

int16_t sgp30_measure_co2_eq_blocking_read(uint16_t *co2_eq_ppm)
{
    uint16_t tvoc_ppb;
    return sgp30_measure_iaq_blocking_read(&tvoc_ppb, co2_eq_ppm);
}

int16_t sgp30_measure_raw_blocking_read(uint16_t *ethanol_raw_signal,
                                        uint16_t *h2_raw_signal)
{
    int16_t ret;

    ret = sgp30_measure_raw();
    if (ret != STATUS_OK)
        return ret;

    sensirion_sleep_usec(SGP30_CMD_RAW_MEASURE_DURATION_US);

    return sgp30_read_raw(ethanol_raw_signal, h2_raw_signal);
}

int16_t sgp30_measure_raw()
{
    return sensirion_i2c_write_cmd(SGP30_I2C_ADDRESS, SGP30_CMD_RAW_MEASURE);
}

int16_t sgp30_read_raw(uint16_t *ethanol_raw_signal, uint16_t *h2_raw_signal)
{
    int16_t ret;
    uint16_t words[SGP30_CMD_RAW_MEASURE_WORDS];

    ret = sensirion_i2c_read_words(SGP30_I2C_ADDRESS, words,
                                   SGP30_CMD_RAW_MEASURE_WORDS);

    *ethanol_raw_signal = words[1];
    *h2_raw_signal = words[0];

    return ret;
}

int16_t sgp30_get_iaq_baseline(uint32_t *baseline)
{
    int16_t ret;
    uint16_t words[SGP30_CMD_GET_IAQ_BASELINE_WORDS];

    ret =
        sensirion_i2c_write_cmd(SGP30_I2C_ADDRESS, SGP30_CMD_GET_IAQ_BASELINE);

    if (ret != STATUS_OK)
        return ret;

    sensirion_sleep_usec(SGP30_CMD_GET_IAQ_BASELINE_DURATION_US);

    ret = sensirion_i2c_read_words(SGP30_I2C_ADDRESS, words,
                                   SGP30_CMD_GET_IAQ_BASELINE_WORDS);

    if (ret != STATUS_OK)
        return ret;

    *baseline = ((uint32_t)words[1] << 16) | ((uint32_t)words[0]);

    if (*baseline)
        return STATUS_OK;
    return STATUS_FAIL;
}

int16_t sgp30_set_iaq_baseline(uint32_t baseline)
{
    int16_t ret;
    uint16_t words[2] = {(uint16_t)((baseline & 0xffff0000) >> 16),
                         (uint16_t)(baseline & 0x0000ffff)};

    if (!baseline)
        return STATUS_FAIL;

    ret = sensirion_i2c_write_cmd_with_args(SGP30_I2C_ADDRESS,
                                            SGP30_CMD_SET_IAQ_BASELINE, words,
                                            SENSIRION_NUM_WORDS(words));

    sensirion_sleep_usec(SGP30_CMD_SET_IAQ_BASELINE_DURATION_US);

    return ret;
}

int16_t sgp30_get_tvoc_inceptive_baseline(uint16_t *tvoc_inceptive_baseline)
{
    int16_t ret;

    ret = sgp30_check_featureset(0x21);

    if (ret != STATUS_OK)
        return ret;

    ret = sensirion_i2c_write_cmd(SGP30_I2C_ADDRESS,
                                  SGP30_CMD_GET_TVOC_INCEPTIVE_BASELINE);

    if (ret != STATUS_OK)
        return ret;

    sensirion_sleep_usec(SGP30_CMD_GET_TVOC_INCEPTIVE_BASELINE_DURATION_US);

    return sensirion_i2c_read_words(
        SGP30_I2C_ADDRESS, tvoc_inceptive_baseline,
        SGP30_CMD_GET_TVOC_INCEPTIVE_BASELINE_WORDS);
}

int16_t sgp30_set_tvoc_baseline(uint16_t tvoc_baseline)
{
    int16_t ret;

    ret = sgp30_check_featureset(0x21);

    if (ret != STATUS_OK)
        return ret;

    if (!tvoc_baseline)
        return STATUS_FAIL;

    ret = sensirion_i2c_write_cmd_with_args(
        SGP30_I2C_ADDRESS, SGP30_CMD_SET_TVOC_BASELINE, &tvoc_baseline,
        SENSIRION_NUM_WORDS(tvoc_baseline));

    sensirion_sleep_usec(SGP30_CMD_SET_TVOC_BASELINE_DURATION_US);

    return ret;
}

int16_t sgp30_set_absolute_humidity(uint32_t absolute_humidity)
{
    int16_t ret;
    uint16_t ah_scaled;

    if (absolute_humidity > 256000)
        return STATUS_FAIL;

    /* ah_scaled = (absolute_humidity / 1000) * 256 */
    ah_scaled = (uint16_t)((absolute_humidity * 16777) >> 16);

    ret = sensirion_i2c_write_cmd_with_args(
        SGP30_I2C_ADDRESS, SGP30_CMD_SET_ABSOLUTE_HUMIDITY, &ah_scaled,
        SENSIRION_NUM_WORDS(ah_scaled));

    sensirion_sleep_usec(SGP30_CMD_SET_ABSOLUTE_HUMIDITY_DURATION_US);

    return ret;
}

const char *sgp30_get_driver_version()
{
    return SGP_DRV_VERSION_STR;
}

uint8_t sgp30_get_configured_address()
{
    return SGP30_I2C_ADDRESS;
}

int16_t sgp30_get_feature_set_version(uint16_t *feature_set_version,
                                      uint8_t *product_type)
{
    int16_t ret;
    uint16_t words[SGP30_CMD_GET_FEATURESET_WORDS];

    ret = sensirion_i2c_delayed_read_cmd(SGP30_I2C_ADDRESS,
                                         SGP30_CMD_GET_FEATURESET,
                                         SGP30_CMD_GET_FEATURESET_DURATION_US,
                                         words, SGP30_CMD_GET_FEATURESET_WORDS);

    if (ret != STATUS_OK)
        return ret;

    *feature_set_version = words[0] & 0x00FF;
    *product_type = (uint8_t)((words[0] & 0xF000) >> 12);

    return STATUS_OK;
}

int16_t sgp30_get_serial_id(uint64_t *serial_id)
{
    int16_t ret;
    uint16_t words[SGP30_CMD_GET_SERIAL_ID_WORDS];

    ret = sensirion_i2c_delayed_read_cmd(SGP30_I2C_ADDRESS,
                                         SGP30_CMD_GET_SERIAL_ID,
                                         SGP30_CMD_GET_SERIAL_ID_DURATION_US,
                                         words, SGP30_CMD_GET_SERIAL_ID_WORDS);

    if (ret != STATUS_OK)
        return ret;

    *serial_id = (((uint64_t)words[0]) << 32) | (((uint64_t)words[1]) << 16) |
                 (((uint64_t)words[2]) << 0);

    return STATUS_OK;
}

int16_t sgp30_iaq_init()
{
    int16_t ret =
        sensirion_i2c_write_cmd(SGP30_I2C_ADDRESS, SGP30_CMD_IAQ_INIT);
    sensirion_sleep_usec(SGP30_CMD_IAQ_INIT_DURATION_US);
    return ret;
}

int16_t sgp30_probe()
{
    int16_t ret = sgp30_check_featureset(0x20);

    if (ret != STATUS_OK)
        return ret;

    return sgp30_iaq_init();
}




/*
 * Copyright (c) 2018, Sensirion AG
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * * Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 * * Neither the name of Sensirion AG nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include "sgp30.h"

#include <inttypes.h>  // PRIu64
#include <stdio.h>     // printf
#include <unistd.h>    // sleep

/* TO USE CONSOLE OUTPUT (printf) AND WAIT (sleep) YOU MAY NEED TO ADAPT THE
 * INCLUDES ABOVE OR DEFINE THEM ACCORDING TO YOUR PLATFORM.
 * #define printf(...)
 * #define sleep(...)
 */

int uut_sgp30_main(void) {
    uint16_t i = 0;
    int16_t err;
    uint16_t tvoc_ppb, co2_eq_ppm;
    uint32_t iaq_baseline;
    uint16_t ethanol_raw_signal, h2_raw_signal;

    const char* driver_version = sgp30_get_driver_version();
    if (driver_version) {
        printf("SGP30 driver version %s\n", driver_version);
    } else {
        printf("fatal: Getting driver version failed\n");
        return -1;
    }

    /* Initialize I2C bus */
    sensirion_i2c_init();

    /* Busy loop for initialization. The main loop does not work without
     * a sensor. */
    int16_t probe;
    while (1) {
        probe = sgp30_probe();

        if (probe == STATUS_OK)
            break;

        if (probe == SGP30_ERR_UNSUPPORTED_FEATURE_SET)
            printf(
                "Your sensor needs at least feature set version 1.0 (0x20)\n");

        printf("SGP sensor probing failed\n");
        sensirion_sleep_usec(1000000);
    }

    printf("SGP sensor probing successful\n");

    uint16_t feature_set_version;
    uint8_t product_type;
    err = sgp30_get_feature_set_version(&feature_set_version, &product_type);
    if (err == STATUS_OK) {
        printf("Feature set version: %u\n", feature_set_version);
        printf("Product type: %u\n", product_type);
    } else {
        printf("sgp30_get_feature_set_version failed!\n");
    }
    uint64_t serial_id;
    err = sgp30_get_serial_id(&serial_id);
    if (err == STATUS_OK) {
        printf("SerialID: %" PRIu64 "\n", serial_id);
    } else {
        printf("sgp30_get_serial_id failed!\n");
    }

    /* Read gas raw signals */
    err = sgp30_measure_raw_blocking_read(&ethanol_raw_signal, &h2_raw_signal);
    if (err == STATUS_OK) {
        /* Print ethanol raw signal and h2 raw signal */
        printf("Ethanol raw signal: %u\n", ethanol_raw_signal);
        printf("H2 raw signal: %u\n", h2_raw_signal);
    } else {
        printf("error reading raw signals\n");
    }

    /* Consider the two cases (A) and (B):
     * (A) If no baseline is available or the most recent baseline is more than
     *     one week old, it must discarded. A new baseline is found with
     *     sgp30_iaq_init() */
    err = sgp30_iaq_init();
    if (err == STATUS_OK) {
        printf("sgp30_iaq_init done\n");
    } else {
        printf("sgp30_iaq_init failed!\n");
    }
    /* (B) If a recent baseline is available, set it after sgp30_iaq_init() for
     * faster start-up */
    /* IMPLEMENT: retrieve iaq_baseline from presistent storage;
     * err = sgp30_set_iaq_baseline(iaq_baseline);
     */

    /* Run periodic IAQ measurements at defined intervals */
    while (1) {
        /*
         * IMPLEMENT: get absolute humidity to enable humidity compensation
         * uint32_t ah = get_absolute_humidity(); // absolute humidity in mg/m^3
         * sgp30_set_absolute_humidity(ah);
         */

        err = sgp30_measure_iaq_blocking_read(&tvoc_ppb, &co2_eq_ppm);
        if (err == STATUS_OK) {
            printf("tVOC  Concentration: %dppb\n", tvoc_ppb);
            printf("CO2eq Concentration: %dppm\n", co2_eq_ppm);
        } else {
            printf("error reading IAQ values\n");
        }

        /* Persist the current baseline every hour */
        if (++i % 3600 == 3599) {
            err = sgp30_get_iaq_baseline(&iaq_baseline);
            if (err == STATUS_OK) {
                /* IMPLEMENT: store baseline to presistent storage */
            }
        }

        /* The IAQ measurement must be triggered exactly once per second (SGP30)
         * to get accurate values.
         */
        sleep(1);  // SGP30
    }
    return 0;
}



/**********************************************************
 *@file    sgp30.h
 *@brief
 *         基于STM32 HAL 库的SGP30 甲醛传感器驱动
 *
 *@note
 *         1. 驱动默认使用硬件i2c 1
 *         2. 如果不需要打印报错信息,可以去掉printf相关
**********************************************************/

#include "sgp30.h"

sgp30_data_t sgp30_data;
I2C_HandleTypeDef SGP30_I2C_Handle_Name;

/**
 * @brief	向SGP30发送一条指令(16bit)
 * @param	cmd SGP30指令
 * @retval	成功返回HAL_OK
*/

static uint8_t sgp30_send_cmd(sgp30_cmd_t cmd)
{
    uint8_t cmd_buffer[2];
    cmd_buffer[0] = cmd >> 8;
    cmd_buffer[1] = cmd;
    return HAL_I2C_Master_Transmit(&SGP30_I2C_Handle_Name, SGP30_ADDR_WRITE, (uint8_t*) cmd_buffer, 2, 0xFFFF);
}

/**
 * @brief	软复位SGP30
 * @param	none
 * @retval	成功返回HAL_OK
*/
static int sgp30_soft_reset(void)
{
    uint8_t cmd = 0x06;
    return HAL_I2C_Master_Transmit(&SGP30_I2C_Handle_Name, 0x00, &cmd, 1, 0xFFFF);
}

/**
 * @brief	初始化SGP30空气质量测量模式
 * @param	none
 * @retval	成功返回0,失败返回-1
*/
int sgp30_init(void)
{
    int status;
    
    status = sgp30_soft_reset();
    if (status != HAL_OK) {
        return -1;
    }
    
    HAL_Delay(100);
    
    status = sgp30_send_cmd(INIT_AIR_QUALITY);
    
    HAL_Delay(100);
    
    return status == 0 ? 0 : -1;
}

/**
 * @brief	初始化SGP30空气质量测量模式
 * @param	none
 * @retval	成功返回HAL_OK
*/
static int sgp30_start(void)
{
    return sgp30_send_cmd(MEASURE_AIR_QUALITY);
}

#define CRC8_POLYNOMIAL 0x31

static uint8_t CheckCrc8(uint8_t* const message, uint8_t initial_value)
{
    uint8_t  remainder;	    //余数
    uint8_t  i = 0, j = 0;  ////循环变量

    /* 初始化 */
    remainder = initial_value;

    for(j = 0; j < 2;j++)
    {
        remainder ^= message[j];

         /* 从最高位开始依次计算  */
        for (i = 0; i < 8; i++)
        {
            if (remainder & 0x80)
            {
                remainder = (remainder << 1)^CRC8_POLYNOMIAL;
            }
            else
            {
                remainder = (remainder << 1);
            }
        }
    }

    /* 返回计算的CRC码 */
    return remainder;
}

/**
 * @brief	读取一次空气质量数据
 * @param	none
 * @retval	成功返回0,失败返回-1
*/
int sgp30_read(void)
{
    int status;
    uint8_t recv_buffer[6]={0};
    
    /* 启动测量 */
    status = sgp30_start();
    if (status != 0) {
        printf("sgp30 start fail\r\n");
        return -1;
    }
    
    HAL_Delay(100);
    
    /* 读取测量数据 */
    status = HAL_I2C_Master_Receive(&SGP30_I2C_Handle_Name, SGP30_ADDR_READ, (uint8_t*)recv_buffer, 6, 0xFFFF);
    if (status != HAL_OK) {
        printf("I2C Master Receive fail\r\n");
        return -1;
    }
    
    /* 校验接收的测量数据 */
    if (CheckCrc8(&recv_buffer[0], 0xFF) != recv_buffer[2]) {
        printf("co2 recv data crc check fail\r\n");
        return -1;
    }
    if (CheckCrc8(&recv_buffer[3], 0xFF) != recv_buffer[5]) {
        printf("tvoc recv data crc check fail\r\n");
        return -1;
    }
    
    
    /* 转换测量数据 */
    sgp30_data.co2  = recv_buffer[0] << 8 | recv_buffer[1];
    sgp30_data.tvoc = recv_buffer[3] << 8 | recv_buffer[4];
    
    return 0;
}