From c4093c29a4dc3769c69437085d7c4522be6ae5ce Mon Sep 17 00:00:00 2001 From: Michel Rottleuthner Date: Mon, 17 Feb 2020 11:02:37 +0100 Subject: [PATCH] drivers: add driver for Sensirion SPS30 particulate matter sensor --- drivers/Makefile.dep | 5 + drivers/Makefile.include | 4 + drivers/include/sps30.h | 286 +++++++++++++++++++++++++++ drivers/sps30/Makefile | 1 + drivers/sps30/include/sps30_params.h | 61 ++++++ drivers/sps30/sps30.c | 285 ++++++++++++++++++++++++++ 6 files changed, 642 insertions(+) create mode 100644 drivers/include/sps30.h create mode 100644 drivers/sps30/Makefile create mode 100644 drivers/sps30/include/sps30_params.h create mode 100644 drivers/sps30/sps30.c diff --git a/drivers/Makefile.dep b/drivers/Makefile.dep index a1a45531e7..f55b5ed4db 100644 --- a/drivers/Makefile.dep +++ b/drivers/Makefile.dep @@ -596,6 +596,11 @@ ifneq (,$(filter soft_spi,$(USEMODULE))) USEMODULE += xtimer endif +ifneq (,$(filter sps30,$(USEMODULE))) + FEATURES_REQUIRED += periph_i2c + USEMODULE += checksum +endif + ifneq (,$(filter srf02,$(USEMODULE))) FEATURES_REQUIRED += periph_i2c USEMODULE += xtimer diff --git a/drivers/Makefile.include b/drivers/Makefile.include index 43cb202e0c..2f51f0f13c 100644 --- a/drivers/Makefile.include +++ b/drivers/Makefile.include @@ -308,6 +308,10 @@ ifneq (,$(filter soft_spi,$(USEMODULE))) USEMODULE_INCLUDES += $(RIOTBASE)/drivers/soft_spi/include endif +ifneq (,$(filter sps30,$(USEMODULE))) + USEMODULE_INCLUDES += $(RIOTBASE)/drivers/sps30/include +endif + ifneq (,$(filter srf04,$(USEMODULE))) USEMODULE_INCLUDES += $(RIOTBASE)/drivers/srf04/include endif diff --git a/drivers/include/sps30.h b/drivers/include/sps30.h new file mode 100644 index 0000000000..ef6c12c56a --- /dev/null +++ b/drivers/include/sps30.h @@ -0,0 +1,286 @@ +/* + * Copyright (C) 2020 HAW Hamburg + * + * This file is subject to the terms and conditions of the GNU Lesser General + * Public License v2.1. See the file LICENSE in the top level directory for more + * details. + */ + +/** + * @defgroup drivers_sps30 Sensirion SPS30 Particulate Matter Sensor + * @ingroup drivers_sensors + * + * About + * ===== + * + * This driver provides an interface for the Sensirion SPS30 Sensor. + * The Datasheet can be found [here](https://1n.pm/oqluM). + * For now only I2C mode is supported. + * I2C speed must be set to standard mode (100 kbit/s) + * + * Wiring + * ====== + * + * In ASCII-land the connector side of the sensor would look like this: + * + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * +------------------------------------------------------------------------+ + * | ____________________ | + * | __| |__ | + * | |__ (1) (2) (3) (4) (5) __| | + * | | | | + * | |____________________| | + * | | + * +------------[#]------------[#]------------[#]------------[#]------------+ + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * The numbers refer to following pin names: + * + * | Pin Nr. | SPS30 Signal Name | Connect to | Notes | + * |---------|-------------------|------------|-------------------------------| + * | Pin 1 | VDD | 5 V | should be within +-10 % | + * | Pin 2 | I2C_SDA / UART_RX | SDA* | config by SPS30_PARAM_I2C_DEV | + * | Pin 3 | I2C_SCL / UART_TR | SCL* | config by SPS30_PARAM_I2C_DEV | + * | Pin 4 | SEL | GND | | + * | Pin 5 | GND | GND | | + * + * *The SCL and SDA pins of the SPS30 sensor are open drain so they must be + * pulled high. Consider that internal pull resistors might be too weak. + * So using external 10 kOhm resistors is recommended for that. + * + * @{ + * + * @file + * @brief Driver for the Sensirion SPS30 Particulate Matter Sensor + * + * @author Michel Rottleuthner + */ + +#ifndef SPS30_H +#define SPS30_H + +#include +#include "periph/gpio.h" +#include "periph/i2c.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief SPS30 device parameters + */ +typedef struct { + i2c_t i2c_dev; /**< I2C dev the sensor is connected to */ +} sps30_params_t; + +/** + * @brief SPS30 device instance + */ +typedef struct { + sps30_params_t p; /**< parameters of the sensor device */ +} sps30_t; + +/** + * @brief Set of measured particulate matter values + * + * @warning Do not change the member order, as it will break the code that + * populates the values in #sps30_read_measurement() + * + */ +typedef struct { + float mc_pm1; /**< Mass concentration of PM 1.0 [µg/m^3] */ + float mc_pm2_5; /**< Mass concentration of PM 2.5 [µg/m^3] */ + float mc_pm4; /**< Mass concentration of PM 4.0 [µg/m^3] */ + float mc_pm10; /**< Mass concentration of PM 10 [µg/m^3] */ + float nc_pm0_5; /**< Number concentration of PM 0.5 [µg/m^3] */ + float nc_pm1; /**< Number concentration of PM 1.0 [µg/m^3] */ + float nc_pm2_5; /**< Number concentration of PM 2.5 [µg/m^3] */ + float nc_pm4; /**< Number concentration of PM 4.0 [µg/m^3] */ + float nc_pm10; /**< Number concentration of PM 10 [µg/m^3] */ + float ps; /**< Typical particle size [µm] */ +} sps30_data_t; + +/** + * @brief SPS30 error codes (returned as negative values) + */ +typedef enum { + SPS30_OK = 0, /**< Everything went fine */ + SPS30_CRC_ERROR, /**< The CRC check of received data failed */ + SPS30_I2C_ERROR /**< Some I2C operation failed */ +} sps30_error_code_t; + +/** + * @brief Seconds the fan cleaning process takes in seconds + */ +#define SPS30_FAN_CLEAN_S (10U) + +/** + * @brief Length of serial and article code string + */ +#define SPS30_SER_ART_LEN (32U) + +/** + * @brief Default fan auto-clean interval in seconds (1 week) + */ +#define SPS30_DEFAULT_ACI_S (604800UL) + +/** + * @brief Maximum number of automatic retries on communication errors + * + * @details If no delays happen between individual requests to the sensor, it + * may happen that the sensor is not yet ready to serve data. + * Handling this within the driver simplifies application code by + * omitting sleep handling or retries there. + * This value may be overwritten to 0 if more fine-grained feedback + * is required or even increased if the device is connected over + * suboptimal wiring. + * + */ +#ifndef SPS30_ERROR_RETRY +#define SPS30_ERROR_RETRY (500U) +#endif + +/** + * @brief Initialize SPS30 sensor driver. + * On success the measurement mode will be active after calling. + * + * @param[out] dev Pointer to an SPS30 device handle + * @param[in] params Parameters for device initialization + * + * @return #SPS30_OK on success, negative #sps30_error_code_t on error + */ +int sps30_init(sps30_t *dev, const sps30_params_t *params); + +/** + * @brief Enable the measurement action. + * + * @details Starting the measurement activates the fan of the sensor and + * increases the power consumption from below 8 mA to an average + * of 60 mA. When the fan is starting up the consumption can reach + * up to 80 mA for around 200 ms. + * The measurement mode will stay active until either the power is + * turned off, a stop is requested (@ref sps30_stop_measurement), + * or the device is reset (#sps30_reset). + * + * @param[in] dev Pointer to an SPS30 device handle + * + * @return #SPS30_OK on success, negative #sps30_error_code_t on error + */ +int sps30_start_measurement(const sps30_t *dev); + +/** + * @brief Stops the measurement action. + * + * @details Stopping the measurement sets the device back to idle mode. + * + * @param[in] dev Pointer to an SPS30 device handle + * + * @return #SPS30_OK on success, negative #sps30_error_code_t on error + */ +int sps30_stop_measurement(const sps30_t *dev); + +/** + * @brief Ask the device if a measurement is ready for reading. + * + * @param[in] dev Pointer to an SPS30 device handle + * @param[out] error Pre-allocated memory to return #sps30_error_code_t + * or NULL if not interested + * + * @return true if a new measurement is available + * @return false if no new measurement is available + */ +bool sps30_data_ready(const sps30_t *dev, int *error); + +/** + * @brief Read a set of particulate matter measurements. + * + * @param[in] dev Pointer to an SPS30 device handle + * @param[out] data Pre-allocated memory to return measurements + * + * @return #SPS30_OK on success, negative #sps30_error_code_t on error + */ +int sps30_read_measurement(const sps30_t *dev, sps30_data_t *data); + +/** + * @brief Read the fan auto-clean interval. + * + * @details The default value is 604800 seconds (1 week). + * See also @ref sps30_start_fan_clean. + * + * @param[in] dev Pointer to an SPS30 device handle + * @param[out] seconds Pre-allocated memory for returning the interval, + * 0 stands for disabled auto-clean + * + * @return #SPS30_OK on success, negative #sps30_error_code_t on error + */ +int sps30_read_ac_interval(const sps30_t *dev, uint32_t *seconds); + +/** + * @brief Write the fan auto-clean interval. + * + * @details The new value will be effective immediately after writing but + * reading the updated value is only possible after resetting the + * sensor. + * This setting is persistent across resets and powerdowns. But if + * the sensor is powered off, the active time counter starts from + * zero again. If this is expected to happen, a fan cleaning cycle + * should be triggered manually at least once a week. + * See also @ref sps30_start_fan_clean. + * + * @param[in] dev Pointer to an SPS30 device handle + * @param[out] seconds The new interval in seconds, 0 for disable + * + * @return #SPS30_OK on success, negative #sps30_error_code_t on error + */ +int sps30_write_ac_interval(const sps30_t *dev, uint32_t seconds); + +/** + * @brief Run a fan cleaning cycle manually. + * + * @details This will spin up the fan to maximum speed to blow out dust for + * for 10 seconds. No new measurement values are available until + * the cleaning process is finished. + * + * @param[in] dev Pointer to an SPS30 device handle + * + * @return #SPS30_OK on success, negative #sps30_error_code_t on error + */ +int sps30_start_fan_clean(const sps30_t *dev); + +/** + * @brief Read the article code from the sensor as string. + * + * @param[in] dev Pointer to an SPS30 device handle + * @param[out] str Pre-allocated memory for returning the article code + * @param[in] len Length of the \p str buffer, must be 32 + * + * @return #SPS30_OK on success, negative #sps30_error_code_t on error + */ +int sps30_read_article_code(const sps30_t *dev, char *str, size_t len); + +/** + * @brief Read the serial number from the sensor as string. + * + * @param[in] dev Pointer to an SPS30 device handle + * @param[out] str Pre-allocated memory for returning the serial number + * @param[in] len Length of the \p str buffer, must be 32 + * + * @return #SPS30_OK on success, negative #sps30_error_code_t on error + */ +int sps30_read_serial_number(const sps30_t *dev, char *str, size_t len); + +/** + * @brief Reset the sensor. + * + * @param[in] dev Pointer to an SPS30 device handle + * + * @return #SPS30_OK on success, negative #sps30_error_code_t on error + */ +int sps30_reset(const sps30_t *dev); + +#ifdef __cplusplus +} +#endif +#endif /* SPS30_H */ +/** @} */ diff --git a/drivers/sps30/Makefile b/drivers/sps30/Makefile new file mode 100644 index 0000000000..48422e909a --- /dev/null +++ b/drivers/sps30/Makefile @@ -0,0 +1 @@ +include $(RIOTBASE)/Makefile.base diff --git a/drivers/sps30/include/sps30_params.h b/drivers/sps30/include/sps30_params.h new file mode 100644 index 0000000000..8870459554 --- /dev/null +++ b/drivers/sps30/include/sps30_params.h @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2020 HAW Hamburg + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup drivers_sps30 + * @brief Default configuration for Sensirion SPS30 sensors devices + * @author Michel Rottleuthner + * @file + * @{ + */ + +#ifndef SPS30_PARAMS_H +#define SPS30_PARAMS_H + +#include "board.h" +#include "sps30.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @name SPS30 default configuration parameters + * @{ + */ +#ifndef SPS30_PARAM_I2C_DEV +#define SPS30_PARAM_I2C_DEV (I2C_DEV(0)) +#endif + +#ifndef SPS30_PARAMS +#define SPS30_PARAMS { .i2c_dev = SPS30_PARAM_I2C_DEV } +#endif +#ifndef SPS30_SAUL_INFO +#define SPS30_SAUL_INFO { .name = "sps30" } +#endif +/**@}*/ + +/** + * @brief SPS30 configuration + */ +static const sps30_params_t sps30_params[] = +{ + SPS30_PARAMS +}; + +/** + * @brief Define the number of configured sensors + */ +#define SPS30_NUM ARRAY_SIZE(sps30_params) + +#ifdef __cplusplus +} +#endif + +#endif /* SPS30_PARAMS_H */ +/** @} */ diff --git a/drivers/sps30/sps30.c b/drivers/sps30/sps30.c new file mode 100644 index 0000000000..f5432ee85e --- /dev/null +++ b/drivers/sps30/sps30.c @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2020 Michel Rottleuthner + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup drivers_sps30 + * @brief Device driver for the Sensirion SPS30 particulate matter sensor + * @author Michel Rottleuthner + * @file + */ +#define LOG_LEVEL LOG_DEBUG +#include "log.h" + +#include +#include + +#include "checksum/crc8.h" +#include "sps30.h" +#include "xtimer.h" +#include "byteorder.h" +#include "kernel_defines.h" + +/** + * @name SPS30 literal definitions + * @{ + */ +#define SPS30_I2C_ADDR (0x69) /**< Fixed I2C address for the Sensor */ +#define SPS30_CRC_POLY (0x31) /**< Polynomial for the CRC calculation */ +#define SPS30_CRC_INIT (0xFF) /**< Init value for the CRC calculation */ +#define SPS30_MESAURE_MODE (0x03) /**< Fixed mode byte for start cmd */ +#define SPS30_DUMMY_BYTE (0x00) /**< Fixed dummy byte for unused bytes */ +#define SPS30_PTR_LEN (2) /**< Pointer address length in bytes */ +/** @} */ + +/** +* @name Address pointer values for all SPS30 I2C commands +* @{ +*/ +typedef enum { + SPS30_CMD_START_MEASURE = 0x0010, /**< Start measurement mode */ + SPS30_CMD_STOP_MEASURE = 0x0104, /**< Stop measurement mode */ + SPS30_CMD_RD_DATA_READY = 0x0202, /**< Read data-ready flag */ + SPS30_CMD_RD_MEASUREMENTS = 0x0300, /**< Read measured values */ + SPS30_CMD_RW_AUTOCLEAN = 0x8004, /**< Read/write autoclean interval */ + SPS30_CMD_START_FAN_CLEAN = 0x5607, /**< Start fan cleaning */ + SPS30_CMD_RD_ARTICLE = 0xD025, /**< Read article code */ + SPS30_CMD_RD_SERIAL = 0xD033, /**< Read serial number */ + SPS30_CMD_RESET = 0xD304, /**< Reset */ +} sps30_cmd_t; +/** @} */ + +/** + * @brief Combine payload and CRC into an SDS30 I2C data frame. + * + * @details The CRC for `data` is calculated and written to `crcd_data` + * together with the payload itself. + * The format of `data` must be just the pure payload: + * { data[0], data[1], data[2], ..., data[n]} + * The format of `crcd_data` same as it shall be sent to the SPS30. + * It consists of payload byte-pairs followed by a single CRC byte: + * { data[0], data[1], csum[0],.., data[n-1], data[n], csum[n-1] } + * + * @param[in] data Source buffer containing just the raw payload + * @param[in] len Size of the 'data' buffer + * @param[out] crcd_data Destination buffer for combined payload and CRCs + * + * @pre sizeof(crcd_data) must be equal to 1.5 * len + * + */ +static inline void _cpy_add_crc(uint8_t *data, size_t len, uint8_t *crcd_data) +{ + for (size_t elem = 0; elem < len / 2; elem++) { + int idx = (elem << 1); + crcd_data[idx + elem] = data[idx]; + crcd_data[idx + elem + 1] = data[idx + 1]; + crcd_data[idx + elem + 2] = crc8(&data[idx], 2, SPS30_CRC_POLY, + SPS30_CRC_INIT); + } +} + +/** + * @brief Check the CRC of an SDS30 I2C data frame. + * + * @details The CRC contained in the `crcd_data` is checked and the payload is + * copied to `data`. + * The format of `data` will be just the pure payload: + * { data[0], data[1], data[2], ..., data[n]} + * The format of `crcd_data` is same as it is read from the SPS30. + * It consists of payload byte-pairs followed by a single CRC byte: + * { data[0], data[1], csum[0],.., data[n-1], data[n], csum[n-1] } + * + * @pre sizeof(crcd_data) must be equal to 1.5 * len + * + * @param[out] data Destination buffer for just the raw payload + * @param[in] len Size of the 'data' buffer + * @param[in] crcd_data Source buffer containing combined payload and CRCs + * + * @return true if all CRCs are valid + * @return false if at least one CRC is invalid + */ +static inline bool _cpy_check_crc(uint8_t *data, size_t len, uint8_t *crcd_data) +{ + for (size_t elem = 0; elem < len / 2; elem++) { + int idx = (elem << 1); + data[idx] = crcd_data[idx + elem]; + data[idx + 1] = crcd_data[idx + elem + 1]; + + if (crc8(&data[idx], 2, SPS30_CRC_POLY, SPS30_CRC_INIT) != + (crcd_data[idx + elem + 2])) { + return false; + } + } + return true; +} + +/** + * @brief Communicates with an SPS30 device by reading or writing data. + * + * @details This performs all three data transfer types supported by SPS30. + * (1) `Set Pointer`: writes a 16 bit pointer address to the device + * (2) `Set Pointer & Read Data`: (1) followed by separate data-read + * (3) `Set Pointer & Write Data`: (1) combined with a data-write + * + * @param[in] dev Pointer to SPS30 device handle + * @param[in] ptr_addr 16 bit pointer address used as command + * @param[in/out] data Pre-allocated memory pointing to either the data + * that will be sent to the device or to memory that + * will hold the response. For type (1) transfers + * this parameter will be ignored. + * @param[in] len Length of `data` buffer, set to 0 for type (1) + * @param[in] read set to true for reading or false for writing + * + * @return SPS30_OK if everything went fine + * @return -SPS30_CRC_ERROR if the CRC check failed + * @return -SPS30_I2C_ERROR if the I2C communication failed + */ +static int _rx_tx_data(const sps30_t *dev, uint16_t ptr_addr, + uint8_t *data, size_t len, bool read) +{ + int res = 0; + unsigned retr = SPS30_ERROR_RETRY; + + if (i2c_acquire(dev->p.i2c_dev) != 0) { + LOG_ERROR("could not acquire I2C bus %d\n", dev->p.i2c_dev); + return -SPS30_I2C_ERROR; + } + + do { + size_t addr_data_crc_len = SPS30_PTR_LEN + len + len / 2; + uint8_t frame_data[addr_data_crc_len]; + frame_data[0] = ptr_addr >> 8; + frame_data[1] = ptr_addr & 0xFF; + + /* Both transfer types, `Set Pointer` and `Set Pointer & Read Data` + require writing a pointer address to the device in a separate write + transaction */ + if (len == 0 || read) { + res = i2c_write_bytes(dev->p.i2c_dev, SPS30_I2C_ADDR, + &frame_data[0], SPS30_PTR_LEN, 0); + } + + if (res == 0 && read) { + /* The `Set Pointer & Read Data` transfer type requires a separate + read transaction to actually read the data */ + res = i2c_read_bytes(dev->p.i2c_dev, SPS30_I2C_ADDR, + &frame_data[SPS30_PTR_LEN], + addr_data_crc_len - SPS30_PTR_LEN, 0); + + if (!_cpy_check_crc(data, len, &frame_data[SPS30_PTR_LEN])) { + res = -SPS30_CRC_ERROR; + } + } else { + /* For the `Set Pointer & Write Data` transfer type the full frame + is transmitted as one single chunk */ + _cpy_add_crc(data, len, &frame_data[SPS30_PTR_LEN]); + res = i2c_write_bytes(dev->p.i2c_dev, SPS30_I2C_ADDR, + &frame_data[0], addr_data_crc_len, 0); + } + } while (res != 0 && retr--); + + i2c_release(dev->p.i2c_dev); + + return res == 0 ? SPS30_OK : -SPS30_I2C_ERROR; +} + +int sps30_init(sps30_t *dev, const sps30_params_t *params) +{ + assert(dev && params); + dev->p = *params; + return sps30_start_measurement(dev); +} + +int sps30_start_measurement(const sps30_t *dev) +{ + assert(dev); + uint8_t data[] = {SPS30_MESAURE_MODE, SPS30_DUMMY_BYTE}; + return _rx_tx_data(dev, SPS30_CMD_START_MEASURE, (uint8_t*)data, + sizeof(data), false); +} + +int sps30_stop_measurement(const sps30_t *dev) +{ + assert(dev); + return _rx_tx_data(dev, SPS30_CMD_STOP_MEASURE, NULL, 0, false); +} + +bool sps30_data_ready(const sps30_t *dev, int *error) +{ + assert(dev); + uint8_t data[2]; + int res = _rx_tx_data(dev, SPS30_CMD_RD_DATA_READY, data, sizeof(data), + true); + if (*error) { + *error = res; + } + return (res == SPS30_OK) && data[1]; +} + +int sps30_read_measurement(const sps30_t *dev, sps30_data_t *data) +{ + /* This compile time check is needed to ensure the below method used for + endianness conversion will work as expected */ + BUILD_BUG_ON(sizeof(sps30_data_t) != (sizeof(float) * 10)); + assert(dev && data); + + /* The target buffer is also used for storing the raw data temporarily */ + int res = _rx_tx_data(dev, SPS30_CMD_RD_MEASUREMENTS, (uint8_t*)data, + sizeof(sps30_data_t), true); + + /* The sps30_data_t consists only of floats, so it is safe to treat it as + an array of 32 bit values for swapping to correct endianness */ + uint32_t *values = (uint32_t*)data; + + /* swap to the endianness of this platform */ + for (unsigned i = 0; i < (sizeof(sps30_data_t) / sizeof(uint32_t)); i++) { + values[i] = ntohl(values[i]); + } + + return res; +} + +int sps30_read_ac_interval(const sps30_t *dev, uint32_t *seconds) { + assert(dev); + int res = _rx_tx_data(dev, SPS30_CMD_RW_AUTOCLEAN, (uint8_t*)seconds, + sizeof(uint32_t), true); + *seconds = ntohl(*seconds); + return res; +} + +int sps30_write_ac_interval(const sps30_t *dev, uint32_t seconds) +{ + assert(dev); + seconds = htonl(seconds); + int res = _rx_tx_data(dev, SPS30_CMD_RW_AUTOCLEAN, (uint8_t*)&seconds, + sizeof(uint32_t), false); + return res; +} + +int sps30_start_fan_clean(const sps30_t *dev) +{ + assert(dev); + return _rx_tx_data(dev, SPS30_CMD_START_FAN_CLEAN, NULL, 0, false); +} + +int sps30_read_article_code(const sps30_t *dev, char *str, size_t len) +{ + assert(dev && str && (len == SPS30_SER_ART_LEN)); + return _rx_tx_data(dev, SPS30_CMD_RD_ARTICLE, (uint8_t*)str, len, true); +} + +int sps30_read_serial_number(const sps30_t *dev, char *str, size_t len) +{ + assert(dev && str && (len == SPS30_SER_ART_LEN)); + return _rx_tx_data(dev, SPS30_CMD_RD_SERIAL, (uint8_t*)str, len, true); +} + +int sps30_reset(const sps30_t *dev) +{ + assert(dev); + return _rx_tx_data(dev, SPS30_CMD_RESET, NULL, 0, false); +}