diff --git a/.github/workflows/sonar-scan.yml b/.github/workflows/sonar-scan.yml index 85270b8..d671ef0 100644 --- a/.github/workflows/sonar-scan.yml +++ b/.github/workflows/sonar-scan.yml @@ -24,16 +24,14 @@ jobs: - name: Checkout submodules run: git submodule update --init --recursive - - name: Set up Sonar Scanner 4.40 - run: | - export SONAR_SCANNER_VERSION=4.4.0.2170 - export SONAR_SCANNER_HOME=$HOME/.sonar/sonar-scanner-$SONAR_SCANNER_VERSION-linux - curl --create-dirs -sSLo $HOME/.sonar/sonar-scanner.zip https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-$SONAR_SCANNER_VERSION-linux.zip - unzip -o $HOME/.sonar/sonar-scanner.zip -d $HOME/.sonar/ - echo "$SONAR_SCANNER_HOME/bin" >> $GITHUB_PATH - curl --create-dirs -sSLo $HOME/.sonar/build-wrapper-linux-x86.zip https://sonarcloud.io/static/cpp/build-wrapper-linux-x86.zip - unzip -o $HOME/.sonar/build-wrapper-linux-x86.zip -d $HOME/.sonar/ - echo "$HOME/.sonar/build-wrapper-linux-x86" >> $GITHUB_PATH + # Setup java 17 to be default (sonar-scanner requirement as of 5.x) + - uses: actions/setup-java@v3 + with: + distribution: 'temurin' # See 'Supported distributions' for available options + java-version: '17' + + - name: Install sonar-scanner and build-wrapper + uses: sonarsource/sonarcloud-github-c-cpp@v2 - name: Download Nordic SDK run: | diff --git a/CMakeLists.txt b/CMakeLists.txt index 40c0f86..72868fe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,6 +8,8 @@ if(IDF_VERSION_MAJOR GREATER_EQUAL 4) SRCS "src/ruuvi_endpoint_5.h" SRCS "src/ruuvi_endpoint_6.c" SRCS "src/ruuvi_endpoint_6.h" + SRCS "src/ruuvi_endpoint_c5.c" + SRCS "src/ruuvi_endpoint_c5.h" SRCS "src/ruuvi_endpoint_ca_uart.c" SRCS "src/ruuvi_endpoint_ca_uart.h" SRCS "src/ruuvi_endpoint_ibeacon.c" @@ -20,6 +22,7 @@ elseif(CMAKE_PROJECT_NAME STREQUAL "ruuvi.node_nrf91.c") target_sources(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/ruuvi_endpoint_3.c) target_sources(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/ruuvi_endpoint_5.c) target_sources(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/ruuvi_endpoint_6.c) + target_sources(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/ruuvi_endpoint_c5.c) target_sources(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/ruuvi_endpoint_ca_uart.c) target_sources(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/ruuvi_endpoint_ibeacon.c) else() diff --git a/src/ruuvi_endpoint_c5.c b/src/ruuvi_endpoint_c5.c new file mode 100644 index 0000000..af7c2b4 --- /dev/null +++ b/src/ruuvi_endpoint_c5.c @@ -0,0 +1,203 @@ +#include "ruuvi_endpoint_c5.h" +#include "ruuvi_endpoints.h" +#include +#include +#include +#include + +#if RE_C5_ENABLED + +#define RE_C5_ACC_RATIO (1000.0f) +#define RE_C5_HUMI_RATIO (400.0f) +#define RE_C5_TEMP_RATIO (200.0f) +#define RE_C5_PRES_RATIO (1.0f) +#define RE_C5_PRES_OFFSET (-50000.0f) +#define RE_C5_BATT_RATIO (1000.0f) +#define RE_C5_BATT_OFFSET (1600) +#define RE_C5_BATT_MIN (1.6f) + +#define RE_C5_TXPWR_RATIO (2) +#define RE_C5_TXPWR_OFFSET (40) + +#define RE_C5_MAC_MAX (281474976710655) +#define RE_C5_MAC_MIN (0) + +#define RE_C5_BYTE_0_SHIFT (0U) +#define RE_C5_BYTE_1_SHIFT (8U) +#define RE_C5_BYTE_2_SHIFT (16U) +#define RE_C5_BYTE_3_SHIFT (24U) +#define RE_C5_BYTE_4_SHIFT (32U) +#define RE_C5_BYTE_5_SHIFT (40U) +#define RE_C5_BYTE_MASK (0xFFU) +#define RE_C5_BYTE_VOLTAGE_OFFSET (5U) +#define RE_C5_BYTE_VOLTAGE_MASK (0x7FFU) +#define RE_C5_BYTE_TX_POWER_OFFSET (0U) +#define RE_C5_BYTE_TX_POWER_MASK (0x1FU) + +// Avoid mocking simple function +#ifdef TEST +void re_clip (re_float * const value, const re_float min, const re_float max) +{ + if (*value > max) + { + *value = max; + } + + if (*value < min) + { + *value = min; + } +} +#endif + +static void re_c5_encode_set_address (uint8_t * const buffer, const re_c5_data_t * data) +{ + // Address is 64 bits, skip 2 first bytes + uint8_t addr_offset = RE_C5_OFFSET_ADDR_MSB; + uint64_t mac = data->address; + + if ( (RE_C5_MAC_MAX < data->address) || (RE_C5_MAC_MIN > data->address)) + { + mac = RE_C5_INVALID_MAC; + } + + buffer[addr_offset] = (mac >> RE_C5_BYTE_5_SHIFT) & RE_C5_BYTE_MASK; + addr_offset++; + buffer[addr_offset] = (mac >> RE_C5_BYTE_4_SHIFT) & RE_C5_BYTE_MASK; + addr_offset++; + buffer[addr_offset] = (mac >> RE_C5_BYTE_3_SHIFT) & RE_C5_BYTE_MASK; + addr_offset++; + buffer[addr_offset] = (mac >> RE_C5_BYTE_2_SHIFT) & RE_C5_BYTE_MASK; + addr_offset++; + buffer[addr_offset] = (mac >> RE_C5_BYTE_1_SHIFT) & RE_C5_BYTE_MASK; + addr_offset++; + buffer[addr_offset] = (mac >> 0) & RE_C5_BYTE_MASK; +} + +static void re_c5_encode_humidity (uint8_t * const buffer, const re_c5_data_t * data) +{ + uint16_t coded_humidity = RE_C5_INVALID_HUMIDITY; + re_float humidity = data->humidity_rh; + + if (!isnan (humidity)) + { + re_clip (&humidity, RE_C5_HUMI_MIN, RE_C5_HUMI_MAX); + coded_humidity = (uint16_t) roundf (humidity * RE_C5_HUMI_RATIO); + } + + buffer[RE_C5_OFFSET_HUMI_MSB] = coded_humidity >> RE_C5_BYTE_1_SHIFT; + buffer[RE_C5_OFFSET_HUMI_LSB] = coded_humidity & RE_C5_BYTE_MASK; +} + +static void re_c5_encode_temperature (uint8_t * const buffer, const re_c5_data_t * data) +{ + uint16_t coded_temperature = RE_C5_INVALID_TEMPERATURE; + re_float temperature = data->temperature_c; + + if (!isnan (temperature)) + { + re_clip (&temperature, RE_C5_TEMP_MIN, RE_C5_TEMP_MAX); + int16_t rounded_temperature = (int16_t) roundf (temperature * RE_C5_TEMP_RATIO); + // Type cast adds 2^16 to a negative signed value, not changing bits. + coded_temperature = (uint16_t) rounded_temperature; + } + + buffer[RE_C5_OFFSET_TEMP_MSB] = coded_temperature >> RE_C5_BYTE_1_SHIFT; + buffer[RE_C5_OFFSET_TEMP_LSB] = coded_temperature & RE_C5_BYTE_MASK; +} + +static void re_c5_encode_pressure (uint8_t * const buffer, const re_c5_data_t * data) +{ + uint16_t coded_pressure = RE_C5_INVALID_PRESSURE; + re_float pressure = data->pressure_pa; + + if (!isnan (pressure)) + { + re_clip (&pressure, RE_C5_PRES_MIN, RE_C5_PRES_MAX); + pressure += RE_C5_PRES_OFFSET; + coded_pressure = (uint16_t) roundf (pressure * RE_C5_PRES_RATIO); + } + + buffer[RE_C5_OFFSET_PRES_MSB] = coded_pressure >> RE_C5_BYTE_1_SHIFT; + buffer[RE_C5_OFFSET_PRES_LSB] = coded_pressure & RE_C5_BYTE_MASK; +} + + +static void re_c5_encode_pwr (uint8_t * const buffer, const re_c5_data_t * data) +{ + uint16_t coded_voltage = RE_C5_INVALID_VOLTAGE; + re_float voltage = data->battery_v; + uint16_t coded_tx_power = RE_C5_INVALID_POWER; + re_float tx_power = (re_float) data->tx_power; + + if (!isnan (voltage)) + { + re_clip (&voltage, RE_C5_VOLTAGE_MIN, RE_C5_VOLTAGE_MAX); + coded_voltage = (uint16_t) roundf ( (voltage * RE_C5_BATT_RATIO) + - RE_C5_BATT_OFFSET); + } + + // Check against original int value + if (RE_C5_INVALID_POWER != data->tx_power) + { + re_clip (&tx_power, RE_C5_TXPWR_MIN, RE_C5_TXPWR_MAX); + coded_tx_power = (uint16_t) roundf ( (tx_power + + RE_C5_TXPWR_OFFSET) + / RE_C5_TXPWR_RATIO); + } + + uint16_t power_info = ( (uint16_t) (coded_voltage << RE_C5_BYTE_VOLTAGE_OFFSET)) + + coded_tx_power; + buffer[RE_C5_OFFSET_POWER_MSB] = (power_info >> RE_C5_BYTE_1_SHIFT); + buffer[RE_C5_OFFSET_POWER_LSB] = (power_info & RE_C5_BYTE_MASK); +} + +static void re_c5_encode_movement (uint8_t * const buffer, const re_c5_data_t * data) +{ + uint8_t movement_count = RE_C5_INVALID_MOVEMENT; + + if (RE_C5_MVTCTR_MAX >= data->movement_count) + { + movement_count = data->movement_count; + } + + buffer[RE_C5_OFFSET_MVTCTR] = movement_count; +} + +static void re_c5_encode_sequence (uint8_t * const buffer, const re_c5_data_t * data) +{ + uint16_t measurement_seq = RE_C5_INVALID_SEQUENCE; + + if (RE_C5_SEQCTR_MAX >= data->measurement_count) + { + measurement_seq = data->measurement_count; + } + + buffer[RE_C5_OFFSET_SEQCTR_MSB] = (measurement_seq >> RE_C5_BYTE_1_SHIFT); + buffer[RE_C5_OFFSET_SEQCTR_LSB] = (measurement_seq & RE_C5_BYTE_MASK); +} + +re_status_t re_c5_encode (uint8_t * const buffer, const re_c5_data_t * data) +{ + re_status_t result = RE_SUCCESS; + + if ( (NULL == buffer) || (NULL == data)) + { + result |= RE_ERROR_NULL; + } + else + { + buffer[RE_C5_OFFSET_HEADER] = RE_C5_DESTINATION; + re_c5_encode_humidity (buffer, data); + re_c5_encode_temperature (buffer, data); + re_c5_encode_pressure (buffer, data); + re_c5_encode_movement (buffer, data); + re_c5_encode_sequence (buffer, data); + re_c5_encode_pwr (buffer, data); + re_c5_encode_set_address (buffer, data); + } + + return result; +} + +#endif diff --git a/src/ruuvi_endpoint_c5.h b/src/ruuvi_endpoint_c5.h new file mode 100644 index 0000000..3a95a1a --- /dev/null +++ b/src/ruuvi_endpoint_c5.h @@ -0,0 +1,91 @@ +/** + * Ruuvi Endpoint C5 helper. + * Defines necessary data for creating a Ruuvi data format C5 broadcast. + * Drops acceleration values to leave space for service UUID field + * + * License: BSD-3 + * Author: Otso Jousimaa + */ + +#ifndef RUUVI_ENDPOINT_C5_H +#define RUUVI_ENDPOINT_C5_H +#include "ruuvi_endpoints.h" +#include + +#define RE_C5_DESTINATION (0xC5U) +#define RE_C5_INVALID_TEMPERATURE (0x8000U) +#define RE_C5_INVALID_HUMIDITY (0xFFFFU) +#define RE_C5_INVALID_PRESSURE (0xFFFFU) +#define RE_C5_INVALID_ACCELERATION (0x8000U) +#define RE_C5_INVALID_SEQUENCE (0xFFFFU) +#define RE_C5_INVALID_MOVEMENT (0xFFU) +#define RE_C5_INVALID_VOLTAGE (0x07FFU) +#define RE_C5_INVALID_POWER (0x1FU) +#define RE_C5_INVALID_MAC (0xFFFFFFFFFFFFU) +#define RE_C5_DATA_LENGTH (18U) + +#define RE_C5_TEMP_MAX (163.835f) +#define RE_C5_TEMP_MIN (-163.835f) +#define RE_C5_HUMI_MAX (163.835f) +#define RE_C5_HUMI_MIN (0.0f) +#define RE_C5_PRES_MAX (115534.0f) +#define RE_C5_PRES_MIN (50000.0f) +#define RE_C5_VOLTAGE_MAX (3.646f) +#define RE_C5_VOLTAGE_MIN (1.6f) +#define RE_C5_TXPWR_MAX (20) +#define RE_C5_TXPWR_MIN (-40) +#define RE_C5_MVTCTR_MAX (254) +#define RE_C5_MVTCTR_MIN (0) +#define RE_C5_SEQCTR_MAX (65534) +#define RE_C5_SEQCTR_MIN (0) + +#define RE_C5_OFFSET_PAYLOAD (7U) + +#define RE_C5_OFFSET_HEADER (0U) +#define RE_C5_OFFSET_TEMP_MSB (1U) +#define RE_C5_OFFSET_TEMP_LSB (2U) +#define RE_C5_OFFSET_HUMI_MSB (3U) +#define RE_C5_OFFSET_HUMI_LSB (4U) +#define RE_C5_OFFSET_PRES_MSB (5U) +#define RE_C5_OFFSET_PRES_LSB (6U) +#define RE_C5_OFFSET_POWER_MSB (7U) +#define RE_C5_OFFSET_POWER_LSB (8U) +#define RE_C5_OFFSET_MVTCTR (9U) +#define RE_C5_OFFSET_SEQCTR_MSB (10U) +#define RE_C5_OFFSET_SEQCTR_LSB (11U) +#define RE_C5_OFFSET_ADDR_MSB (12U) + +/** @brief All data required for Ruuvi dataformat 5 package. */ +typedef struct +{ + re_float humidity_rh; + //!< Humidity in relative humidity percentage. + re_float pressure_pa; + //!< Pressure in pascals. + re_float temperature_c; + //!< Temperature in celcius. + re_float battery_v; + //!< Battery voltage, preferably under load such as radio TX. + uint16_t measurement_count; + //!< Running counter of measurement. + uint8_t movement_count; + //!< Number of detected movements. + uint64_t address; + //!< BLE address of device, most significant byte first. + int8_t tx_power; + //!< Transmission power of radio, in dBm. +} re_c5_data_t; + +/** + * @brief Encode given data to given buffer in Ruuvi DF5. + * + * NAN can be used as a placeholder for invalid / not available values. + * + * @param[in] buffer uint8_t array with length of 24 bytes. + * @param[in] data Struct containing all necessary information + * for encoding the data into buffer. + * @retval RE_SUCCESS if data was encoded successfully. + */ +re_status_t re_c5_encode (uint8_t * const buffer, const re_c5_data_t * data); + +#endif diff --git a/src/ruuvi_endpoints.h b/src/ruuvi_endpoints.h index a790fb8..b10102d 100644 --- a/src/ruuvi_endpoints.h +++ b/src/ruuvi_endpoints.h @@ -16,6 +16,9 @@ #if !defined(RE_8_ENABLED) # define RE_8_ENABLED (1U) #endif +#if !defined(RE_C5_ENABLED) +# define RE_C5_ENABLED (1U) +#endif #if !defined(RE_CA_ENABLED) # define RE_CA_ENABLED (1U) #endif @@ -29,7 +32,7 @@ #include -#define RUUVI_ENDPOINTS_SEMVER "4.0.0" //!< SEMVER of endpoints. +#define RUUVI_ENDPOINTS_SEMVER "4.1.0" //!< SEMVER of endpoints. #define RE_SUCCESS (0U) //!< Encoded successfully. #define RE_ERROR_DATA_SIZE (1U << 3U) //!< Data size too large/small. diff --git a/test/test_ruuvi_endpoint_c5.c b/test/test_ruuvi_endpoint_c5.c new file mode 100644 index 0000000..fd97122 --- /dev/null +++ b/test/test_ruuvi_endpoint_c5.c @@ -0,0 +1,209 @@ +#include "unity.h" + +#include "ruuvi_endpoint_c5.h" +#include + +static const re_c5_data_t m_re_c5_data_ok = +{ + .humidity_rh = 53.49, + .pressure_pa = 100044, + .temperature_c = 24.3, + .battery_v = 2.977, + .measurement_count = 205, + .movement_count = 66, + .address = 0xCBB8334C884F, + .tx_power = 4 +}; + +static const uint8_t valid_data[] = +{ + 0xC5, 0x12, 0xFC, 0x53, 0x94, 0xC3, 0x7C, + 0xAC, 0x36, 0x42, + 0x00, 0xCD, 0xCB, 0xB8, 0x33, 0x4C, 0x88, 0x4F +}; + +static const re_c5_data_t m_re_c5_data_ok_max = +{ + .humidity_rh = 163.8350, + .pressure_pa = 115534, + .temperature_c = 163.8350, + .battery_v = 3.646, + .measurement_count = 65534, + .movement_count = 254, + .address = 0xCBB8334C884F, + .tx_power = 20 +}; + +static const uint8_t max_data[] = +{ + 0xC5, 0x7F, 0xFF, 0xFF, 0xFE, 0xFF, 0xFE, + 0xFF, 0xDE, 0xFE, + 0xFF, 0xFE, 0xCB, 0xB8, 0x33, 0x4C, 0x88, 0x4F +}; + +static const re_c5_data_t m_re_c5_data_ok_min = +{ + .humidity_rh = 0.000, + .pressure_pa = 50000, + .temperature_c = -163.835, + .battery_v = 1.600, + .measurement_count = 0, + .movement_count = 0, + .address = 0xCBB8334C884F, + .tx_power = -40 +}; + +static const uint8_t min_data[] = +{ + 0xC5, 0x80, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0xCB, 0xB8, 0x33, 0x4C, 0x88, 0x4F +}; + +static const re_c5_data_t m_re_c5_data_underflow = +{ + .humidity_rh = -1, + .pressure_pa = 49999, + .temperature_c = -170, + .battery_v = 1.500, + .measurement_count = 0, + .movement_count = 0, + .address = 0xCBB8334C884F, + .tx_power = -41 +}; + + +static const re_c5_data_t m_re_c5_data_overflow = +{ + .humidity_rh = 200, + .pressure_pa = 200044, + .temperature_c = 170, + .battery_v = 3.700, + .measurement_count = 65534, + .movement_count = 254, + .address = 0xCBB8334C884F, + .tx_power = 25 +}; + +static const re_c5_data_t m_re_c5_data_invalid = +{ + .humidity_rh = NAN, + .pressure_pa = NAN, + .temperature_c = NAN, + .battery_v = NAN, + .measurement_count = 65535, + .movement_count = 255, + .address = 0xFFFFFFFFFFFF, + .tx_power = RE_C5_INVALID_POWER +}; + +static const uint8_t invalid_data[] = +{ + 0xC5, 0x80, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF +}; + +void setUp (void) +{ +} + +void tearDown (void) +{ +} + +/** + * @brief Typical encode operation + * + * @retval RD_SUCCESS on success test. + */ +void test_ruuvi_endpoint_c5_get_ok (void) +{ + re_status_t err_code = RE_SUCCESS; + uint8_t test_buffer[RE_C5_DATA_LENGTH] = {0}; + err_code = re_c5_encode (test_buffer, &m_re_c5_data_ok); + TEST_ASSERT (RE_SUCCESS == err_code); + TEST_ASSERT (! (memcmp (test_buffer, valid_data, sizeof (valid_data)))); +} + +void test_ruuvi_endpoint_c5_get_ok_max (void) +{ + re_status_t err_code = RE_SUCCESS; + uint8_t test_buffer[RE_C5_DATA_LENGTH] = {0}; + err_code = re_c5_encode ( (uint8_t * const) &test_buffer, + (const re_c5_data_t *) &m_re_c5_data_ok_max); + TEST_ASSERT (RE_SUCCESS == err_code); + TEST_ASSERT (! (memcmp (test_buffer, max_data, sizeof (max_data)))); +} + +void test_ruuvi_endpoint_c5_get_ok_min (void) +{ + re_status_t err_code = RE_SUCCESS; + uint8_t test_buffer[RE_C5_DATA_LENGTH] = {0}; + err_code = re_c5_encode ( (uint8_t * const) &test_buffer, + (const re_c5_data_t *) &m_re_c5_data_ok_min); + TEST_ASSERT (RE_SUCCESS == err_code); + TEST_ASSERT (! (memcmp (test_buffer, min_data, sizeof (min_data)))); +} + +/** + * @brief Null buffer + * + * @retval RE_ERROR_NULL on success test. + */ +void test_ruuvi_endpoint_c5_get_error_null_buffer (void) +{ + re_status_t err_code = RE_ERROR_NULL; + uint8_t * const p_test_buffer = NULL; + err_code = re_c5_encode (p_test_buffer, &m_re_c5_data_ok); + TEST_ASSERT (RE_ERROR_NULL == err_code); +} + +/** + * @brief Null data + * + * @retval RE_ERROR_NULL on success test. + */ +void test_ruuvi_endpoint_c5_get_error_null_data (void) +{ + re_status_t err_code = RE_ERROR_NULL; + uint8_t test_buffer[RE_C5_DATA_LENGTH] = {0}; + const re_c5_data_t * p_re_c5_data = NULL; + err_code = re_c5_encode ( (uint8_t * const) &test_buffer, + p_re_c5_data); + TEST_ASSERT (RE_ERROR_NULL == err_code); +} + +/** + * @brief True to test encode operation with invalid data + * + * @retval RD_SUCCESS on success test. + */ +void test_ruuvi_endpoint_c5_get_invalid_data (void) +{ + re_status_t err_code = RE_SUCCESS; + uint8_t test_buffer[RE_C5_DATA_LENGTH] = {0}; + err_code = re_c5_encode ( (uint8_t * const) &test_buffer, + (const re_c5_data_t *) &m_re_c5_data_invalid); + TEST_ASSERT (RE_SUCCESS == err_code); + TEST_ASSERT (! (memcmp (test_buffer, invalid_data, sizeof (invalid_data)))); +} + +void test_ruuvi_endpoint_c5_underflow (void) +{ + re_status_t err_code = RE_SUCCESS; + uint8_t test_buffer[RE_C5_DATA_LENGTH] = {0}; + err_code = re_c5_encode (test_buffer, &m_re_c5_data_underflow); + TEST_ASSERT (RE_SUCCESS == err_code); + TEST_ASSERT (! (memcmp (test_buffer, min_data, sizeof (min_data)))); +} + +void test_ruuvi_endpoint_c5_overflow (void) +{ + re_status_t err_code = RE_SUCCESS; + uint8_t test_buffer[RE_C5_DATA_LENGTH] = {0}; + err_code = re_c5_encode (test_buffer, &m_re_c5_data_overflow); + TEST_ASSERT (RE_SUCCESS == err_code); + TEST_ASSERT (! (memcmp (test_buffer, max_data, sizeof (max_data)))); +} +