From f5dc2524d0a1a6ec796f0b4a7dd5966217c67ac3 Mon Sep 17 00:00:00 2001
From: Michel Rottleuthner <michel.rottleuthner@haw-hamburg.de>
Date: Thu, 22 Nov 2018 18:50:29 +0100
Subject: [PATCH] drivers: add driver for SDS011 active laser dust sensor

---
 drivers/Makefile.dep                     |   4 +
 drivers/Makefile.include                 |   4 +
 drivers/include/sds011.h                 | 317 +++++++++++++++++++
 drivers/sds011/Makefile                  |   1 +
 drivers/sds011/include/sds011_internal.h | 100 ++++++
 drivers/sds011/include/sds011_params.h   |  78 +++++
 drivers/sds011/sds011.c                  | 384 +++++++++++++++++++++++
 7 files changed, 888 insertions(+)
 create mode 100644 drivers/include/sds011.h
 create mode 100644 drivers/sds011/Makefile
 create mode 100644 drivers/sds011/include/sds011_internal.h
 create mode 100644 drivers/sds011/include/sds011_params.h
 create mode 100644 drivers/sds011/sds011.c

diff --git a/drivers/Makefile.dep b/drivers/Makefile.dep
index fc00e9f0b1..3f6df3479e 100644
--- a/drivers/Makefile.dep
+++ b/drivers/Makefile.dep
@@ -394,6 +394,10 @@ ifneq (,$(filter sdcard_spi,$(USEMODULE)))
   USEMODULE += xtimer
 endif
 
+ifneq (,$(filter sds011,$(USEMODULE)))
+  FEATURES_REQUIRED += periph_uart
+endif
+
 ifneq (,$(filter servo,$(USEMODULE)))
   FEATURES_REQUIRED += periph_pwm
 endif
diff --git a/drivers/Makefile.include b/drivers/Makefile.include
index d4ab792e10..936ca29a31 100644
--- a/drivers/Makefile.include
+++ b/drivers/Makefile.include
@@ -285,3 +285,7 @@ endif
 ifneq (,$(filter xbee,$(USEMODULE)))
   USEMODULE_INCLUDES += $(RIOTBASE)/drivers/xbee/include
 endif
+
+ifneq (,$(filter sds011,$(USEMODULE)))
+  USEMODULE_INCLUDES += $(RIOTBASE)/drivers/sds011/include
+endif
diff --git a/drivers/include/sds011.h b/drivers/include/sds011.h
new file mode 100644
index 0000000000..2aca1a57a8
--- /dev/null
+++ b/drivers/include/sds011.h
@@ -0,0 +1,317 @@
+/*
+ * Copyright (C) 2018 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_sds011 SDS011 Laser Dust Sensor
+ * @ingroup     drivers_sensors
+ * @brief       Driver SDS011 Laser Dust Sensor
+ * @{
+ *
+ * @file
+ * @brief       Interface for controlling SDS011 Laser Dust Sensor
+ *
+ * @author      Michel Rottleuthner <michel.rottleuthner@haw-hamburg.de>
+ */
+
+#ifndef SDS011_H
+#define SDS011_H
+
+#include <stdbool.h>
+
+#include "periph/gpio.h"
+#include "periph/uart.h"
+#include "mutex.h"
+#include "sds011_internal.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief SDS011 wildcard address to address all devices
+ */
+#define SDS011_DEVID_WILDCARD            (0xFFFF)
+
+/**
+ * @brief   Named return values
+ */
+enum {
+    SDS011_OK,               /**< all good */
+    SDS011_INVALID_RESPONSE, /**< invalid response */
+    SDS011_INVALID_CHKSUM,   /**< invalid checksum */
+    SDS011_ERROR,            /**< internal error */
+};
+
+/**
+ * @brief   Report mode of the SDS011 sensor
+ */
+typedef enum sds011_reporting_mode {
+    SDS011_RMODE_ACTIVE = 0, /**< continuously reporting values */
+    SDS011_RMODE_QUERY  = 1, /**< sensor needs to be queried */
+} sds011_reporting_mode_t;
+
+/**
+ * @brief   Work/sleep mode of the SDS011 sensor
+ */
+typedef enum sds011_working_mode {
+    SDS011_WMODE_SLEEP = 0, /**< laser & fan are disabled */
+    SDS011_WMODE_WORK  = 1, /**< laser & fan are enabled */
+} sds011_working_mode_t;
+
+/**
+ * @brief   Configuration parameters for SDS011 Laser Dust Sensor
+ */
+typedef struct {
+    uart_t   uart;    /**< UART device the sensor is connected to */
+    gpio_t   pwr_pin; /**< GPIO pin for disabling supply voltage for the sensor */
+    uint16_t dev_id;  /**< Unique sensor device ID */
+    bool     pwr_ah;  /**< Logic level of the power pin (true for active high) */
+} sds011_params_t;
+
+/**
+ * @brief   Data type for storing SDS011 sensor readings
+ */
+typedef struct {
+    uint16_t pm_2_5;  /**< Particulate Matter 2.5 concentration [0.1µg/m^3] */
+    uint16_t pm_10;   /**< Particulate Matter 10 concentration [0.1µg/m^3] */
+} sds011_data_t;
+
+/**
+ * @brief   callback for measurements actively reported by the SDS011 sensor
+ */
+typedef void (*sds011_callback_t)(sds011_data_t *data, void *ctx);
+
+/**
+ * @brief   Device descriptor definition for SDS011 Laser Dust Sensor
+ */
+typedef struct {
+    sds011_params_t   params;        /**< parameters for SDS011 device */
+    mutex_t           dev_lock;      /**< mutex to synchronize device access */
+    mutex_t           cb_lock;       /**< mutex to synchronize callbacks */
+    sds011_callback_t cb;            /**< callback deliver values async */
+    void              *cbctx;        /**< user context for the callback */
+    uint16_t          checksum;      /**< iteratively calculated checksum */
+    uint8_t           rx_mem[SDS011_FRAME_RECV_LEN]; /**< receive buffer */
+    uint8_t           pos;           /**< receive buffer position counter */
+} sds011_t;
+
+/**
+ * @brief   Initialize SDS011 Laser Dust Sensor
+ *
+ * @param[out] dev      device descriptor
+ * @param[in]  params   device configuration
+ *
+ * @pre     @p dev != NULL
+ * @pre     @p params != NULL
+ *
+ * @return              SDS011_OK on success
+ * @return              SDS011_ERROR on error
+ */
+int sds011_init(sds011_t *dev, const sds011_params_t *params);
+
+/**
+ * @brief   Enable power supply of SDS011 laser dust sensor
+ *
+ * @param[in] dev       device descriptor
+ *
+ * @pre     @p dev != NULL
+ */
+void sds011_power_on(const sds011_t *dev);
+
+/**
+ * @brief   Disable power supply of SDS011 laser dust sensor
+ *
+ * @param[in] dev       device descriptor
+ *
+ * @pre     @p dev != NULL
+ */
+void sds011_power_off(const sds011_t *dev);
+
+/**
+ * @brief   Read measurement values from SDS011 laser dust sensor
+ *
+ * @param[in]  dev       device descriptor
+ * @param[out] data      pointer for storing the values
+ *
+ * @return  SDS011_OK on success
+ * @return  SDS011_INVALID_RESPONSE when response doesn't match the request
+ * @return  SDS011_INVALID_CHKSUM when response checksum is invalid
+ * @return  SDS011_ERROR when other error occured
+ *
+ * @pre     @p dev != NULL
+ * @pre     @p data != NULL
+ */
+int sds011_read(sds011_t *dev, sds011_data_t *data);
+
+/**
+ * @brief   Register measurement callback
+ *
+ *          The registered callback is executed when new measurements were
+ *          received by the sensor. This function should be used together with
+ *          active reporting mode of the sensor that automatically sends new
+ *          measurements periodically (factory default setting of the sensor).
+ *
+ * @param[in]  dev       device descriptor
+ * @param[in]  cb        function to be called for new values (NULL for disable)
+ * @param[in]  ctx       context pointer that will be handed to the callback
+ *
+ * @return  SDS011_OK on success
+ * @return  SDS011_ERROR when error occured
+ *
+ * @pre     @p dev != NULL
+ */
+int sds011_register_callback(sds011_t *dev, sds011_callback_t cb, void *ctx);
+
+/**
+ * @brief   Get the current reporting mode of the sensor
+ *
+ * @param[in]   dev       device descriptor
+ * @param[out]  mode      SDS011_RMODE_ACTIVE: continuously report new values
+ *                        SDS011_RMODE_QUERY: new values need to be requested
+ *
+ * @return  SDS011_OK on success
+ * @return  SDS011_INVALID_RESPONSE when response doesn't match the request
+ * @return  SDS011_INVALID_CHKSUM when response checksum is invalid
+ * @return  SDS011_ERROR when other error occured
+ *
+ * @pre     @p dev != NULL
+ */
+int sds011_get_reporting_mode(sds011_t *dev, sds011_reporting_mode_t *mode);
+
+/**
+ * @brief   Set the reporting mode of the sensor
+ *
+ * @param[in]   dev       device descriptor
+ * @param[in]   mode      SDS011_RMODE_ACTIVE: continuously report new values
+ *                        SDS011_RMODE_QUERY: new values need to be requested
+ *
+ * @note    This setting is persistent even after a full power-cycle!
+ *          Factory default is SDS011_RMODE_ACTIVE
+ *
+ * @return  SDS011_OK on success
+ * @return  SDS011_INVALID_RESPONSE when response doesn't match the request
+ * @return  SDS011_INVALID_CHKSUM when response checksum is invalid
+ * @return  SDS011_ERROR when other error occured
+ *
+ * @pre     @p dev != NULL
+ */
+int sds011_set_reporting_mode(sds011_t *dev, sds011_reporting_mode_t mode);
+
+/**
+ * @brief   Get current working mode of the sensor
+ *
+ * @param[in]   dev       device descriptor
+ * @param[out]  mode      SDS011_WMODE_SLEEP: sensor is in sleep mode (~3 mA)
+ *                        SDS011_WMODE_WORK: sensor is in working mode (~65 mA)
+ *
+ * @return  SDS011_OK on success
+ * @return  SDS011_INVALID_RESPONSE when response doesn't match the request
+ * @return  SDS011_INVALID_CHKSUM when response checksum is invalid
+ * @return  SDS011_ERROR when other error occured
+ *
+ * @pre     @p dev != NULL
+ */
+int sds011_get_working_mode(sds011_t *dev, sds011_working_mode_t *mode);
+
+/**
+ * @brief   Set working mode of the sensor
+ *
+ * @param[in]   dev       device descriptor
+ * @param[in]   mode      SDS011_WMODE_SLEEP: put to sleep mode (~3 mA)
+ *                        SDS011_WMODE_WORK: put to working mode (~65 mA)
+ *
+ * @return  SDS011_OK on success
+ * @return  SDS011_INVALID_RESPONSE when response doesn't match the request
+ * @return  SDS011_INVALID_CHKSUM when response checksum is invalid
+ * @return  SDS011_ERROR when other error occured
+ *
+ * @pre     @p dev != NULL
+ */
+int sds011_set_working_mode(sds011_t *dev, sds011_working_mode_t mode);
+
+/**
+ * @brief   Get current working period of the sensor
+ *
+ * @param[in]   dev       device descriptor
+ * @param[out]  minutes   working period of the sensor in minutes
+ *
+ * @return  SDS011_OK on success
+ * @return  SDS011_INVALID_RESPONSE when response doesn't match the request
+ * @return  SDS011_INVALID_CHKSUM when response checksum is invalid
+ * @return  SDS011_ERROR when other error occured
+ *
+ * @pre     @p dev != NULL
+ */
+int sds011_get_working_period(sds011_t *dev, uint8_t *minutes);
+
+/**
+ * @brief   Set working period of the sensor
+ *
+ * @param[in]   dev       device descriptor
+ * @param[in]   minutes   0 - 30 new working period of the sensor in minutes
+ *                        0 for coninuous reporting mode
+ *                        1 - 30 for a period of @p minutes
+ *
+ * @note    For values greater than 0, the active duration (fan, laser enabled)
+ *          is always fixed to 30 seconds, while the sleep duration is adjusted
+ *          to give an overall period of @p minutes.
+ *
+ * @return  SDS011_OK on success
+ * @return  SDS011_INVALID_RESPONSE when response doesn't match the request
+ * @return  SDS011_INVALID_CHKSUM when response checksum is invalid
+ * @return  SDS011_ERROR when other error occured
+ *
+ * @pre     @p dev != NULL
+ */
+int sds011_set_working_period(sds011_t *dev, uint8_t minutes);
+
+/**
+ * @brief   Get firmware version of the sensor
+ *
+ * @param[in]   dev       device descriptor
+ * @param[out]  year      year of the firmware version
+ * @param[out]  mon       month of the firmware version
+ * @param[out]  day       day of the firmware version
+ *
+ * @return  SDS011_OK on success
+ * @return  SDS011_INVALID_RESPONSE when response doesn't match the request
+ * @return  SDS011_INVALID_CHKSUM when response checksum is invalid
+ * @return  SDS011_ERROR when other error occured
+ *
+ * @pre     @p dev != NULL
+ * @pre     @p year != NULL
+ * @pre     @p mon != NULL
+ * @pre     @p day != NULL
+ */
+int sds011_get_fw_version(sds011_t *dev, uint8_t *year, uint8_t *mon, uint8_t *day);
+
+/**
+ * @brief   Set device ID of the sensor
+ *
+ * @param[in]   dev            device descriptor
+ * @param[in]   sens_dev_id   ID as one number (ID byte 1 MSB, ID byte 2 LSB)
+ *
+ * @note    This setting is persistent even after a full power-cycle!
+ *          Factory default is an individual ID which is printed next to the
+ *          serial number barcode. For the number xxxx-abab the ID is 0xabab.
+ *
+ * @return  SDS011_OK on success
+ * @return  SDS011_INVALID_RESPONSE when response doesn't match the request
+ * @return  SDS011_INVALID_CHKSUM when response checksum is invalid
+ * @return  SDS011_ERROR when other error occured
+ *
+ * @pre     @p dev != NULL
+ */
+int sds011_set_dev_id(sds011_t *dev, uint16_t sens_dev_id);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* SDS011_H */
+/** @} */
diff --git a/drivers/sds011/Makefile b/drivers/sds011/Makefile
new file mode 100644
index 0000000000..48422e909a
--- /dev/null
+++ b/drivers/sds011/Makefile
@@ -0,0 +1 @@
+include $(RIOTBASE)/Makefile.base
diff --git a/drivers/sds011/include/sds011_internal.h b/drivers/sds011/include/sds011_internal.h
new file mode 100644
index 0000000000..e41fca0f1f
--- /dev/null
+++ b/drivers/sds011/include/sds011_internal.h
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2018 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_sds011
+  * @{
+  *
+  * @file
+  * @brief       Internal constants etc. for the SDS011 laser dust sensor
+  *
+  * @author      Michel Rottleuthner <michel.rottleuthner@haw-hamburg.de>
+  * @}
+  */
+#ifndef SDS011_INTERNAL_H
+#define SDS011_INTERNAL_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief SDS011 baud rate
+ */
+#define SDS011_UART_BAUDRATE             (9600U)
+
+ /**
+  * @name SDS011 frame lengths
+  * @{
+  */
+#define SDS011_FRAME_SEND_LEN            (19U)
+#define SDS011_FRAME_RECV_LEN            (10U)
+/** @} */
+
+/**
+ * @name SDS011 command values
+ * @{
+ */
+#define SDS011_CMD_DB1_SET_DR_MODE       (2U)
+#define SDS011_CMD_DB1_QUERY_DATA        (4U)
+#define SDS011_CMD_DB1_SET_DEV_ID        (5U)
+#define SDS011_CMD_DB1_SET_SLEEP_WORK    (6U)
+#define SDS011_CMD_DB1_CHECK_FIRMWARE    (7U)
+#define SDS011_CMD_DB1_SET_WORK_PERIOD   (8U)
+/** @} */
+
+/**
+ * @name SDS011 command option values
+ * @{
+ */
+#define SDS011_CMD_OPT_QUERY             (0U)
+#define SDS011_CMD_OPT_SET               (1U)
+#define SDS011_CMD_OPT_REPORT_ACTIVE     (0U)
+#define SDS011_CMD_OPT_REPORT_QUERY      (1U)
+#define SDS011_CMD_OPT_SLEEP             (0U)
+#define SDS011_CMD_OPT_WORK              (1U)
+/** @} */
+
+/**
+ * @name SDS011 frame constants
+ * @{
+ */
+#define SDS011_CMDID_QUERY               (0xB4)
+#define SDS011_RCMDID_REPLY              (0xC5)
+#define SDS011_RCMDID_DATA               (0xC0)
+#define SDS011_FRAME_TAIL                (0xAB)
+#define SDS011_FRAME_HEAD                (0xAA)
+#define SDS011_FRAME_CSUM_MSK            (0xFF)
+/** @} */
+
+/**
+ * @name SDS011 frame value indexes
+ * @{
+ */
+#define SDS011_FRAME_HEAD_IDX            (0U)
+#define SDS011_CMDID_IDX                 (1U)
+#define SDS011_DB1_IDX                   (2U)
+#define SDS011_DB2_IDX                   (3U)
+#define SDS011_DB3_IDX                   (4U)
+#define SDS011_DB4_IDX                   (5U)
+#define SDS011_DB5_IDX                   (6U)
+#define SDS011_DB6_IDX                   (7U)
+#define SDS011_DEVID1_IDX                (15U)
+#define SDS011_DEVID2_IDX                (16U)
+#define SDS011_FRAME_SEND_TAIL_IDX       (SDS011_FRAME_SEND_LEN - 1)
+#define SDS011_FRAME_RECV_TAIL_IDX       (SDS011_FRAME_RECV_LEN - 1)
+#define SDS011_FRAME_SEND_CSUM_IDX       (SDS011_FRAME_SEND_LEN - 2)
+#define SDS011_FRAME_RECV_CSUM_IDX       (SDS011_FRAME_RECV_LEN - 2)
+/** @} */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* SDS011_INTERNAL_H */
+/** @} */
diff --git a/drivers/sds011/include/sds011_params.h b/drivers/sds011/include/sds011_params.h
new file mode 100644
index 0000000000..c8ea97a5e7
--- /dev/null
+++ b/drivers/sds011/include/sds011_params.h
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2018 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_sds011
+ * @{
+ *
+ * @file
+ * @brief       SDS011 sensor specific configuration
+ *
+ * @author      Michel Rottleuthner <michel.rottleuthner@haw-hamburg.de>
+ */
+
+#ifndef SDS011_PARAMS_H
+#define SDS011_PARAMS_H
+
+#include "board.h"
+#include "periph/uart.h"
+#include "saul_reg.h"
+#include "sds011.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @name   Set default configuration parameters for the SDS011 driver
+ * @{
+ */
+#ifndef SDS011_PARAM_UART_DEV
+#define SDS011_PARAM_UART_DEV       (UART_DEV(1))
+#endif
+#ifndef SDS011_PARAM_PWR_PIN
+#define SDS011_PARAM_PWR_PIN        (GPIO_PIN(0, 0))
+#endif
+#ifndef SDS011_PARAM_PWR_PIN_AH
+#define SDS011_PARAM_PWR_PIN_AH     (true)
+#endif
+
+#ifndef SDS011_PARAMS
+#define SDS011_PARAMS               { .uart    = SDS011_PARAM_UART_DEV, \
+                                      .pwr_pin = SDS011_PARAM_PWR_PIN, \
+                                      .pwr_ah  = SDS011_PARAM_PWR_PIN_AH, \
+                                      .dev_id  = SDS011_DEVID_WILDCARD }
+#endif
+
+#ifndef SDS011_SAUL_INFO
+#define SDS011_SAUL_INFO            { .name = "SDS011" }
+#endif
+/** @} */
+
+/**
+ * @brief   SDS011 configuration
+ */
+static const  sds011_params_t sds011_params[] =
+{
+    SDS011_PARAMS
+};
+
+/**
+ * @brief   Allocate and configure entries to the SAUL registry
+ */
+saul_reg_info_t sds011_saul_info[] =
+{
+    SDS011_SAUL_INFO
+};
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* SDS011_PARAMS_H */
+/** @} */
diff --git a/drivers/sds011/sds011.c b/drivers/sds011/sds011.c
new file mode 100644
index 0000000000..06da9d6f6c
--- /dev/null
+++ b/drivers/sds011/sds011.c
@@ -0,0 +1,384 @@
+/*
+ * Copyright (C) 2018 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_sds011
+ * @{
+ *
+ * @file
+ * @brief       SDS011 Laser Dust Sensor driver implementation
+ *
+ * @author      Michel Rottleuthner <michel.rottleuthner@haw-hamburg.de>
+ *
+ * @}
+ */
+
+#include <string.h>
+
+#include "assert.h"
+#include "sds011.h"
+#include "periph/uart.h"
+
+/**
+ * @brief             UART receive callback
+ *
+ * @param[in]   arg   Context value previously handed to the uart_init call
+ * @param[in]   data  single byte received over UART
+ */
+static void _rx_cb(void *arg, uint8_t data)
+{
+    sds011_t *dev = (sds011_t*)arg;
+
+    /* frame MUST start with HEAD byte and the buffer must be cleared
+       before writing to it again */
+    if (((dev->pos == 0) && (data != SDS011_FRAME_HEAD)) ||
+         (dev->pos == SDS011_FRAME_RECV_LEN)) {
+        return;
+    }
+
+    dev->rx_mem[dev->pos] = data;
+
+    if ((dev->pos >= SDS011_DB1_IDX) &&
+        (dev->pos < SDS011_FRAME_RECV_CSUM_IDX)) {
+        dev->checksum += data;
+    }
+    else if ((dev->pos == SDS011_FRAME_RECV_LEN - 1) &&
+             (dev->rx_mem[SDS011_FRAME_HEAD_IDX] == SDS011_FRAME_HEAD) &&
+             (dev->rx_mem[SDS011_FRAME_RECV_TAIL_IDX] == SDS011_FRAME_TAIL)) {
+
+        dev->checksum &= SDS011_FRAME_CSUM_MSK;
+        if (dev->rx_mem[SDS011_FRAME_RECV_CSUM_IDX] == dev->checksum) {
+
+            if ((dev->cb != NULL) &&
+                (dev->rx_mem[SDS011_CMDID_IDX] == SDS011_RCMDID_DATA)) {
+                    sds011_data_t measure;
+                    measure.pm_2_5 = dev->rx_mem[SDS011_DB1_IDX] |
+                                    (dev->rx_mem[SDS011_DB2_IDX] << 8);
+                    measure.pm_10  = dev->rx_mem[SDS011_DB3_IDX] |
+                                    (dev->rx_mem[SDS011_DB4_IDX] << 8);
+                    dev->cb(&measure, dev->cbctx);
+                    dev->pos = -1;
+            }
+        }
+        else {
+            dev->pos = -1;
+        }
+
+        dev->checksum = 0;
+
+        /* unlock the mutex for the calling function */
+        mutex_unlock(&dev->cb_lock);
+    }
+
+    dev->pos++;
+}
+
+/**
+ * @brief             send command and wait for first replied message
+ *
+ * @param[in]   dev         SDS011 device the command is sent to
+ * @param[in]   data_bytes  data bytes to send within the command
+ * @param[in]   len         number of data bytes
+ * @param[out]  recv_frm    pointer where the received frame will be stored
+ *                          must at least provide SDS011_FRAME_RECV_LEN bytes
+ */
+int _send_recv_cmd(sds011_t *dev, uint8_t *data_bytes, size_t len, uint8_t *recv_frm)
+{
+    uint8_t cmd[SDS011_FRAME_SEND_LEN] = {0};
+    int checksum = 0;
+    int res = SDS011_ERROR;
+
+    cmd[SDS011_FRAME_HEAD_IDX]  = SDS011_FRAME_HEAD;
+    cmd[SDS011_CMDID_IDX] = SDS011_CMDID_QUERY;
+
+    for (unsigned i = 0; i < len; i++) {
+        cmd[SDS011_DB1_IDX + i] = data_bytes[i];
+        checksum += data_bytes[i];
+    }
+
+    cmd[SDS011_DEVID1_IDX] = (dev->params.dev_id >> 8) & 0xFF;
+    checksum += cmd[SDS011_DEVID1_IDX];
+    cmd[SDS011_DEVID2_IDX] = dev->params.dev_id & 0xFF;
+    checksum += cmd[SDS011_DEVID2_IDX];
+
+    cmd[SDS011_FRAME_SEND_LEN - 2] = checksum & SDS011_FRAME_CSUM_MSK;
+    cmd[SDS011_FRAME_SEND_TAIL_IDX] = SDS011_FRAME_TAIL;
+
+    mutex_lock(&dev->dev_lock);
+
+    dev->pos = 0;
+    dev->checksum = 0;
+
+    mutex_lock(&dev->cb_lock);
+
+    /* if no active reporting callback is registered, UART must be enabled first */
+    if((dev->cb == NULL) &&
+       (uart_init(dev->params.uart, SDS011_UART_BAUDRATE, _rx_cb, dev) != 0)) {
+        mutex_unlock(&dev->cb_lock);
+        mutex_unlock(&dev->dev_lock);
+        return SDS011_ERROR;
+    }
+
+    uart_write(dev->params.uart, cmd, SDS011_FRAME_SEND_LEN);
+
+    /* wait for the isr callback to unlock the mutex */
+    mutex_lock(&dev->cb_lock);
+
+    /* only copy data when checksum was valid */
+    if (dev->pos != 0) {
+        memcpy(recv_frm, dev->rx_mem, SDS011_FRAME_RECV_LEN);
+        /* mark the recv buffer as free */
+        dev->pos = 0;
+        dev->checksum = 0;
+
+        /* check if we received a valid response for the cmd sent*/
+        if(((recv_frm[SDS011_CMDID_IDX]  == SDS011_RCMDID_REPLY) &&
+            (cmd[SDS011_DB1_IDX] == recv_frm[SDS011_DB1_IDX]))
+           || ((recv_frm[SDS011_CMDID_IDX] == SDS011_RCMDID_DATA)
+            && (cmd[SDS011_DB1_IDX] == SDS011_CMD_DB1_QUERY_DATA))) {
+            res = SDS011_OK;
+        }
+        else {
+            res = SDS011_INVALID_RESPONSE;
+        }
+    }
+    else {
+        res = SDS011_INVALID_CHKSUM;
+    }
+
+    /* reset mutex state */
+    mutex_unlock(&dev->cb_lock);
+
+    /* if no active reporting callback is registered, UART can be disabled */
+    if((dev->cb == NULL) &&
+       (uart_init(dev->params.uart, SDS011_UART_BAUDRATE, NULL, NULL) != 0)) {
+        res = SDS011_ERROR;
+    }
+
+    /* release device */
+    mutex_unlock(&dev->dev_lock);
+
+    return res;
+}
+
+/**
+ * @brief       shorthand to get a single byte property with _send_recv_cmd
+ *
+ * @param[in]   dev         SDS011 device the command is sent to
+ * @param[in]   data_bytes  data bytes to send within the command
+ * @param[in]   len         number of data bytes
+ * @param[out]  p           pointer for storing single data byte of the reply
+ * @param[out]  p_idx       index of data byte we want to read
+ */
+static int _get_property(sds011_t *dev, uint8_t *data_bytes, size_t len,
+                         uint8_t *p, uint8_t p_idx)
+{
+    uint8_t recv[SDS011_FRAME_RECV_LEN];
+    int res = _send_recv_cmd(dev, data_bytes, len, recv);
+
+    if (res == SDS011_OK) {
+        *p = recv[p_idx];
+    }
+
+    return res;
+}
+
+int sds011_init(sds011_t *dev, const sds011_params_t *params)
+{
+    assert((dev != NULL) && (params != NULL) && (params->uart < UART_NUMOF));
+
+    if ((params->pwr_pin != GPIO_UNDEF) &&
+        (gpio_init(params->pwr_pin, GPIO_OUT) != 0)) {
+        return SDS011_ERROR;
+    }
+
+    memcpy(&dev->params, params, sizeof(sds011_params_t));
+
+    mutex_init(&dev->dev_lock);
+    mutex_init(&dev->cb_lock);
+
+    dev->cb = NULL;
+
+    sds011_power_on(dev);
+
+    return SDS011_OK;
+}
+
+int sds011_register_callback(sds011_t *dev, sds011_callback_t cb, void *ctx)
+{
+    assert(dev != NULL);
+    mutex_lock(&dev->dev_lock);
+    dev->cbctx = ctx;
+    dev->cb = cb;
+
+    /* either register un unregister the uart callback */
+    if (uart_init(dev->params.uart, SDS011_UART_BAUDRATE,
+                  cb == NULL ? NULL : _rx_cb,
+                  cb == NULL ? NULL : dev) != 0) {
+        mutex_unlock(&dev->dev_lock);
+        return SDS011_ERROR;
+    }
+    mutex_unlock(&dev->dev_lock);
+    return SDS011_OK;
+}
+
+void sds011_power_on(const sds011_t *dev)
+{
+    assert(dev != NULL);
+    if(dev->params.pwr_pin != GPIO_UNDEF) {
+        gpio_write(dev->params.pwr_pin, dev->params.pwr_ah);
+    }
+}
+
+void sds011_power_off(const sds011_t *dev)
+{
+    assert(dev != NULL);
+    if(dev->params.pwr_pin != GPIO_UNDEF) {
+        gpio_write(dev->params.pwr_pin, !dev->params.pwr_ah);
+    }
+}
+
+int sds011_get_reporting_mode(sds011_t *dev, sds011_reporting_mode_t *mode)
+{
+    assert(dev != NULL);
+    uint8_t cmd[] = {SDS011_CMD_DB1_SET_DR_MODE, SDS011_CMD_OPT_QUERY};
+    uint8_t prop = 0;
+    int res = _get_property(dev, cmd, sizeof(cmd), &prop, SDS011_DB3_IDX);
+    *mode = ((prop == 0) ? SDS011_RMODE_ACTIVE : SDS011_RMODE_QUERY);
+    return res;
+}
+
+int sds011_set_reporting_mode(sds011_t *dev, sds011_reporting_mode_t mode)
+{
+    assert(dev != NULL);
+    uint8_t cmd[] = {SDS011_CMD_DB1_SET_DR_MODE, SDS011_CMD_OPT_SET, mode};
+
+    uint8_t recv[SDS011_FRAME_RECV_LEN];
+    int res = _send_recv_cmd(dev, cmd, sizeof(cmd), recv);
+
+    if (res == SDS011_OK) {
+        if ((recv[SDS011_DB2_IDX] == SDS011_CMD_OPT_SET) &&
+            (recv[SDS011_DB3_IDX] == mode)) {
+            return SDS011_OK;
+        }
+
+        return SDS011_ERROR;
+    }
+
+    return res;
+}
+
+int sds011_read(sds011_t *dev, sds011_data_t *data)
+{
+    assert((dev != NULL) && (data != NULL));
+    uint8_t cmd[] = {SDS011_CMD_DB1_QUERY_DATA};
+
+    uint8_t recv[SDS011_FRAME_RECV_LEN];
+    int res = _send_recv_cmd(dev, cmd, sizeof(cmd), recv);
+
+    if (res == SDS011_OK) {
+        data->pm_2_5 = recv[SDS011_DB1_IDX] | (recv[SDS011_DB2_IDX] << 8);
+        data->pm_10  = recv[SDS011_DB3_IDX] | (recv[SDS011_DB4_IDX] << 8);
+    }
+
+    return res;
+}
+
+int sds011_set_dev_id(sds011_t *dev, uint16_t sens_dev_id)
+{
+    assert(dev != NULL);
+    uint8_t cmd[13] = {0};
+    cmd[0]  = SDS011_CMD_DB1_SET_DEV_ID;
+    cmd[11] = (sens_dev_id >> 8) & 0xFF;
+    cmd[12] = sens_dev_id & 0xFF;
+
+    uint8_t recv[SDS011_FRAME_RECV_LEN];
+    int res = _send_recv_cmd(dev, cmd, sizeof(cmd), recv);
+
+    if ((res == SDS011_OK) &&
+        (recv[SDS011_DB5_IDX] == cmd[11]) &&
+        (recv[SDS011_DB6_IDX] == cmd[12])) {
+        return SDS011_OK;
+    }
+
+    return SDS011_ERROR;
+}
+
+int sds011_get_working_mode(sds011_t *dev, sds011_working_mode_t *mode)
+{
+    assert(dev != NULL);
+    uint8_t cmd[] = {SDS011_CMD_DB1_SET_SLEEP_WORK, SDS011_CMD_OPT_QUERY};
+    uint8_t prop = 0;
+    int res = _get_property(dev, cmd, sizeof(cmd), &prop, SDS011_DB3_IDX);
+    *mode = ((prop == 0) ? SDS011_WMODE_SLEEP : SDS011_WMODE_WORK);
+    return res;
+}
+
+int sds011_set_working_mode(sds011_t *dev, sds011_working_mode_t mode)
+{
+    assert(dev != NULL);
+    uint8_t cmd[] = {SDS011_CMD_DB1_SET_SLEEP_WORK, SDS011_CMD_OPT_SET, mode};
+
+    uint8_t recv[SDS011_FRAME_RECV_LEN];
+    int res = _send_recv_cmd(dev, cmd, sizeof(cmd), recv);
+
+    if (res == SDS011_OK) {
+        if ((recv[SDS011_DB2_IDX] == SDS011_CMD_OPT_SET) &&
+            (recv[SDS011_DB3_IDX] == mode)) {
+            return SDS011_OK;
+        }
+
+        return SDS011_ERROR;
+    }
+
+    return res;
+}
+
+int sds011_get_working_period(sds011_t *dev, uint8_t *minutes)
+{
+    assert(dev != NULL);
+    uint8_t cmd[] = {SDS011_CMD_DB1_SET_WORK_PERIOD, SDS011_CMD_OPT_QUERY};
+    return _get_property(dev, cmd, sizeof(cmd), minutes, SDS011_DB3_IDX);
+}
+
+int sds011_set_working_period(sds011_t *dev, uint8_t minutes)
+{
+    assert(dev != NULL);
+    uint8_t cmd[] = {SDS011_CMD_DB1_SET_WORK_PERIOD, SDS011_CMD_OPT_SET, minutes};
+
+    uint8_t recv[SDS011_FRAME_RECV_LEN];
+    int res = _send_recv_cmd(dev, cmd, sizeof(cmd), recv);
+
+    if (res == SDS011_OK) {
+        if ((recv[SDS011_DB2_IDX] == SDS011_CMD_OPT_SET) &&
+            (recv[SDS011_DB3_IDX] == minutes)) {
+            return SDS011_OK;
+        }
+
+        return SDS011_ERROR;
+    }
+
+    return res;
+}
+
+int sds011_get_fw_version(sds011_t *dev, uint8_t *year, uint8_t *mon, uint8_t *day)
+{
+    assert((dev != NULL) && (year != NULL) && (mon != NULL) && (day != NULL));
+    uint8_t cmd[] = {SDS011_CMD_DB1_CHECK_FIRMWARE};
+
+    uint8_t recv[SDS011_FRAME_RECV_LEN];
+    int res = _send_recv_cmd(dev, cmd, sizeof(cmd), recv);
+
+    if (res == SDS011_OK) {
+        *year  = recv[SDS011_DB2_IDX];
+        *mon = recv[SDS011_DB3_IDX];
+        *day = recv[SDS011_DB4_IDX];
+    }
+
+    return res;
+}
-- 
GitLab