diff --git a/CMakeLists.txt b/CMakeLists.txt index 2158012..ede7e8f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,3 +11,4 @@ if(NOT CONFIG_RUST) endif() add_subdirectory(subsys) +add_subdirectory(drivers) diff --git a/Kconfig b/Kconfig index 71f891c..0ee8054 100755 --- a/Kconfig +++ b/Kconfig @@ -1,5 +1,6 @@ menu "Zephyr Playground" rsource "subsys/Kconfig" +rsource "drivers/Kconfig" -endmenu \ No newline at end of file +endmenu diff --git a/cmake/Util.cmake b/cmake/Util.cmake index f7106d0..87655f4 100755 --- a/cmake/Util.cmake +++ b/cmake/Util.cmake @@ -14,6 +14,9 @@ function(ProjectSetup snippets) list(APPEND SNIPPET_ROOT $ENV{NOTUS_ROOT}) set(SNIPPET_ROOT ${SNIPPET_ROOT} PARENT_SCOPE) + + list(APPEND DTS_ROOT $ENV{NOTUS_ROOT}) + set(DTS_ROOT ${DTS_ROOT} PARENT_SCOPE) set(SNIPPET ${snippets} PARENT_SCOPE) @@ -35,6 +38,7 @@ endfunction() function(SetupTarget target_name) target_include_directories(${target_name} PUBLIC $ENV{NOTUS_ROOT}/includes) + target_include_directories(${target_name} PUBLIC $ENV{NOTUS_ROOT}) target_compile_options( ${target_name} @@ -74,4 +78,4 @@ function(RunClangdTidy target sources) COMMENT "Running clangd-tidy" ) endif() -endfunction() \ No newline at end of file +endfunction() diff --git a/drivers/CMakeLists.txt b/drivers/CMakeLists.txt new file mode 100644 index 0000000..a5b547c --- /dev/null +++ b/drivers/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory_ifdef(CONFIG_SENSOR sensor) +message("Hello1") diff --git a/drivers/Kconfig b/drivers/Kconfig new file mode 100644 index 0000000..62e573c --- /dev/null +++ b/drivers/Kconfig @@ -0,0 +1,5 @@ +menu "Drivers" + +rsource "sensor/Kconfig" + +endmenu diff --git a/drivers/sensor/CMakeLists.txt b/drivers/sensor/CMakeLists.txt new file mode 100644 index 0000000..a8c9765 --- /dev/null +++ b/drivers/sensor/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory_ifdef(CONFIG_EZO_EC ezo_ec) +message("Hello2") diff --git a/drivers/sensor/Kconfig b/drivers/sensor/Kconfig new file mode 100644 index 0000000..3ccd22f --- /dev/null +++ b/drivers/sensor/Kconfig @@ -0,0 +1 @@ +rsource "ezo_ec/Kconfig" diff --git a/drivers/sensor/ezo_ec/CMakeLists.txt b/drivers/sensor/ezo_ec/CMakeLists.txt new file mode 100644 index 0000000..6afaaa4 --- /dev/null +++ b/drivers/sensor/ezo_ec/CMakeLists.txt @@ -0,0 +1,4 @@ +zephyr_library() +zephyr_library_sources(ezo_ec.c) +zephyr_library_sources_ifdef(CONFIG_EZO_EC_EMUL emul_ezo_ec.c) +message("Hello3") diff --git a/drivers/sensor/ezo_ec/Kconfig b/drivers/sensor/ezo_ec/Kconfig new file mode 100644 index 0000000..eb4134e --- /dev/null +++ b/drivers/sensor/ezo_ec/Kconfig @@ -0,0 +1,14 @@ +config EZO_EC + bool "EZO-EC Sensor" + default n + depends on I2C + help + Enable EZO-EC + + +config EZO_EC_EMUL + bool "EZO-EC Emul" + default n + depends on I2C_EMUL + help + Enable EZO-EC Emulator diff --git a/drivers/sensor/ezo_ec/emul_ezo_ec.c b/drivers/sensor/ezo_ec/emul_ezo_ec.c new file mode 100644 index 0000000..b4c0266 --- /dev/null +++ b/drivers/sensor/ezo_ec/emul_ezo_ec.c @@ -0,0 +1,150 @@ +#define DT_DRV_COMPAT atlas_ezo_ec +#define LOG_LEVEL CONFIG_I2C_LOG_LEVEL + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "emul_ezo_ec.h" +#include "ezo_ec_util.h" + +LOG_MODULE_REGISTER(EMUL_EZO_EC); //NOLINT + +// Future Updates: +// - Move to a circular buffer for input and output data +// - Add ability to set response code +// - Add support for the internal state? + +// Runtime Data +struct emul_ezo_ec_data +{ + struct i2c_emul emul; + char input[EZO_EC_BUFFER_SIZE]; + char output[EZO_EC_BUFFER_SIZE]; +}; + +void emul_ezo_ec_get_input(const struct emul *emul, char *buf, int len) +{ + struct emul_ezo_ec_data *data = emul->data; + memcpy(buf, data->input, len); +} + +void emul_ezo_ec_set_output(const struct emul *emul, const char *buf, int len) +{ + struct emul_ezo_ec_data *data = emul->data; + memset(data->output, 0, EZO_EC_BUFFER_SIZE); + data->output[0] = 1; // Hardcode response code for now; + memcpy(data->output+1, buf, len); +} + +void emul_ezo_ec_reset_buffers(const struct emul *emul) +{ + struct emul_ezo_ec_data *data = emul->data; + memset(data->input,0, EZO_EC_BUFFER_SIZE); + memset(data->output, 0, EZO_EC_BUFFER_SIZE); +} + +struct ezo_ec_config emul_ezo_ec_get_config(const struct emul *emul) +{ + // Here we are casting away const which is BAD but is required for unit testing + // to allow us to inject custom configs that are normally created at compile time + const struct ezo_ec_config *config = (struct ezo_ec_config *)emul->dev->config; + return *config; +} + +void emul_ezo_ec_set_config(const struct emul *emul, struct ezo_ec_config config) +{ + struct ezo_ec_config *current_config = (struct ezo_ec_config *)emul->dev->config; + *current_config = config; +} + +static int emul_ezo_ec_write(struct emul_ezo_ec_data *data, uint8_t len, const uint8_t *buf) +{ + if (len > EZO_EC_BUFFER_SIZE) + { + LOG_ERR("Write too large"); + return -ENXIO; + } + + memcpy(data->input, buf, len); + return 0; +} + +static int emul_ezo_ec_read(struct emul_ezo_ec_data *data, uint8_t len, uint8_t *buf) +{ + if (len > EZO_EC_BUFFER_SIZE) + { + LOG_ERR("Read to large"); + return -ENXIO; + } + + memcpy(buf, data->output, len); + + return 0; +} + +static int emul_ezo_ec_transfer(const struct emul *target, struct i2c_msg *msgs, int num_msgs, int addr) +{ + struct emul_ezo_ec_data *data = target->data; + + i2c_dump_msgs(target->dev, msgs, num_msgs, addr); + + if ((msgs[0].flags & I2C_MSG_RW_MASK) == I2C_MSG_READ) + { + emul_ezo_ec_read(data, msgs[0].len, msgs[0].buf); + } + else if ((msgs[0].flags & I2C_MSG_RW_MASK) == I2C_MSG_WRITE) + { + emul_ezo_ec_write(data, msgs[0].len, &msgs[0].buf[0]); + } + else + { + LOG_ERR("Unknown transfer"); + return -EIO; + } + + return 0; +} + +const static struct i2c_emul_api emul_ezo_ec_api = { + .transfer = emul_ezo_ec_transfer, +}; + +static int emul_ezo_ec_init(const struct emul *target, const struct device *parent) +{ + LOG_INF("emul_ezo_ec_init"); + const struct emul_ezo_ec_config *config = target->cfg; + struct emul_ezo_ec_data *data = target->data; + + ARG_UNUSED(config); + ARG_UNUSED(data); + ARG_UNUSED(parent); + + emul_ezo_ec_reset_buffers(target); + + char info[] = "?i,EC,2.16"; + emul_ezo_ec_set_output(target, info, (int)strlen(info)); + + return 0; +} + +// clang-format off +#define ezo_ec_EMUL(inst) \ + static struct emul_ezo_ec_data emul_ezo_ec_data_##inst; \ + EMUL_DT_INST_DEFINE(inst, \ + &emul_ezo_ec_init, \ + &emul_ezo_ec_data_##inst, \ + NULL, \ + &emul_ezo_ec_api, \ + NULL) + +DT_INST_FOREACH_STATUS_OKAY(ezo_ec_EMUL) // NOLINT +// clang-format on diff --git a/drivers/sensor/ezo_ec/emul_ezo_ec.h b/drivers/sensor/ezo_ec/emul_ezo_ec.h new file mode 100644 index 0000000..7253560 --- /dev/null +++ b/drivers/sensor/ezo_ec/emul_ezo_ec.h @@ -0,0 +1,23 @@ +#ifndef APP_INCLUDE_EMUL_ezo_ec_H_ +#define APP_INCLUDE_EMUL_ezo_ec_H_ + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include "ezo_ec_util.h" +#include + +void emul_ezo_ec_get_input(const struct emul *emul, char *buf, int len); +void emul_ezo_ec_set_output(const struct emul *emul, const char *buf, int len); +void emul_ezo_ec_reset_buffers(const struct emul *emul); + +struct ezo_ec_config emul_ezo_ec_get_config(const struct emul *emul); +void emul_ezo_ec_set_config(const struct emul *emul, struct ezo_ec_config config); + +#ifdef __cplusplus +} +#endif + +#endif // APP_INCLUDE_EMUL_ezo_ec_H_ diff --git a/drivers/sensor/ezo_ec/ezo_ec.c b/drivers/sensor/ezo_ec/ezo_ec.c new file mode 100644 index 0000000..d345dde --- /dev/null +++ b/drivers/sensor/ezo_ec/ezo_ec.c @@ -0,0 +1,183 @@ +#define DT_DRV_COMPAT atlas_ezo_ec + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ezo_ec_util.h" +#include "drivers/sensors/ezo_ec.h" + +LOG_MODULE_REGISTER(ezo_ec, CONFIG_SENSOR_LOG_LEVEL); // NOLINT + +// TODO: +// Add while loop (with max timeout) for checking reponse codes +// Add all channels +// Add all attr +// Add support for DTS based configuration + +static int ezo_ec_sample_fetch(const struct device *dev, enum sensor_channel chan) +{ + struct ezo_ec_data *data = dev->data; + const struct ezo_ec_config *config = dev->config; + + __ASSERT_NO_MSG(chan == SENSOR_CHAN_ALL); + + if (i2c_write_dt(&config->i2c, READ_COMMAND, 1) < 0) + { + LOG_ERR("Failed to request sample"); + return -EIO; + } + + k_msleep(READ_RESPONSE_TIME_MS); + + if (i2c_read_dt(&config->i2c, data->output, EZO_EC_BUFFER_SIZE) < 0) + { + LOG_ERR("Failed to read sample"); + return -EIO; + } + + if(data->output[0] != OK_RESPONSE_CODE) + { + LOG_ERR("Reponse Code not 1"); + return -EIO; + } + + return 0; +} + +static int str_double_to_sensor_val(const struct ezo_ec_data *data, struct sensor_value *val) +{ + char *endptr = NULL; + + // 1 byte offset for response code + double value = strtod(data->output + 1, &endptr); + + if(data->output + 1 == endptr) + { + return -EIO; + } + + sensor_value_from_double(val, value); + + return 0; +} + +static int ezo_ec_channel_get(const struct device *dev, enum sensor_channel chan, struct sensor_value *val) +{ + const struct ezo_ec_data *data = dev->data; + + enum sensor_channel_ezo_ec our_chan = (enum sensor_channel_ezo_ec)chan; + + if (our_chan == SENSOR_CHAN_CONDUCTIVITY) + { + return str_double_to_sensor_val(data, val); + } + + return -ENOTSUP; + +} + +static int calibrate(const struct device *dev) +{ + const struct ezo_ec_config *config = dev->config; + + if (i2c_write_dt(&config->i2c, CALIBRATE_DRY_COMMAND, sizeof(CALIBRATE_DRY_COMMAND)) < 0) + { + LOG_ERR("Failed to request sample"); + return -EIO; + } + + k_msleep(CALIBRATE_RESPONSE_TIME_MS); + + uint8_t response = 0; + + if (i2c_read_dt(&config->i2c, &response, 1) < 0) + { + LOG_ERR("Failed to read calibration response"); + return -EIO; + } + + if(response != OK_RESPONSE_CODE) + { + LOG_ERR("Reponse Code not 1"); + return -EIO; + } + + return 0; + +} + +static int ezo_ec_attr_set(const struct device *dev, enum sensor_channel chan, + enum sensor_attribute attr, const struct sensor_value *val) +{ + ARG_UNUSED(chan); + ARG_UNUSED(val); + + if(attr == SENSOR_ATTR_CALIBRATION) + { + return calibrate(dev); + } + + LOG_ERR("Attribute not recognised. Err: %i", attr); + return -ENOTSUP; +} + + +int ezo_ec_init(const struct device *dev) +{ + const struct ezo_ec_config *const config = dev->config; + + if (!device_is_ready(config->i2c.bus)) + { + LOG_ERR("I2C bus device not ready"); + return -ENODEV; + } + + if (i2c_write_dt(&config->i2c, INFO_COMMAND, 1) < 0) + { + LOG_ERR("Failed sending info command"); + return -EIO; + } + + k_msleep(INFO_RESPONSE_TIME_MS); + + char buf[EZO_EC_BUFFER_SIZE]; + if(i2c_read_dt(&config->i2c, (uint8_t*)&buf, EZO_EC_BUFFER_SIZE)) + { + LOG_ERR("Failed to get info command"); + return -EIO; + } + + char expected[] = "?i,EC,2.16"; + if (buf[0] != OK_RESPONSE_CODE && strcmp(expected, buf+1) != 0) + { + LOG_ERR("Incorrect Device Info %s", buf); + return -EIO; + } + + return 0; +} + +static const struct sensor_driver_api ezo_ec_api_funcs = { + .sample_fetch = ezo_ec_sample_fetch, + .channel_get = ezo_ec_channel_get, + .attr_set = ezo_ec_attr_set, +}; + +#define ezo_ec_DEFINE(inst) \ + static struct ezo_ec_data ezo_ec_data_##inst; \ + \ + static struct ezo_ec_config ezo_ec_config_##inst = { \ + .i2c = I2C_DT_SPEC_INST_GET(inst), \ + }; \ + \ + DEVICE_DT_INST_DEFINE(inst, ezo_ec_init, NULL, &ezo_ec_data_##inst, &ezo_ec_config_##inst, POST_KERNEL, CONFIG_SENSOR_INIT_PRIORITY, &ezo_ec_api_funcs); + +DT_INST_FOREACH_STATUS_OKAY(ezo_ec_DEFINE) // NOLINT diff --git a/drivers/sensor/ezo_ec/ezo_ec_util.h b/drivers/sensor/ezo_ec/ezo_ec_util.h new file mode 100644 index 0000000..13e6558 --- /dev/null +++ b/drivers/sensor/ezo_ec/ezo_ec_util.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +#define EZO_EC_BUFFER_SIZE 30 +#define OK_RESPONSE_CODE 1 + +#define INFO_COMMAND "i" +#define INFO_RESPONSE_TIME_MS 300 + +#define READ_COMMAND "R" +#define READ_RESPONSE_TIME_MS 600 + +#define CALIBRATE_DRY_COMMAND "Cal,dry" +#define CALIBRATE_RESPONSE_TIME_MS 600 + +#define PROBE_COMMAND "K," +#define PROBE_RESPONSE_TIME_MS 300 + + +int ezo_ec_init(const struct device *dev); + +struct ezo_ec_config +{ + struct i2c_dt_spec i2c; +}; + +struct ezo_ec_data +{ + double conductiviy; + char output[EZO_EC_BUFFER_SIZE]; +}; + +#ifdef __cplusplus +} +#endif diff --git a/dts/bindings/sensor/atlas,ezo-ec.yaml b/dts/bindings/sensor/atlas,ezo-ec.yaml new file mode 100644 index 0000000..5afbf7b --- /dev/null +++ b/dts/bindings/sensor/atlas,ezo-ec.yaml @@ -0,0 +1,8 @@ +description: | + Sensor + +compatible: "atlas,ezo-ec" + +include: ["i2c-device.yaml", "sensor-device.yaml"] + +properties: diff --git a/include/drivers/sensors/ezo_ec.h b/include/drivers/sensors/ezo_ec.h new file mode 100644 index 0000000..59e3024 --- /dev/null +++ b/include/drivers/sensors/ezo_ec.h @@ -0,0 +1,22 @@ +#ifndef INCLUDE_DRIVERS_SENSOR_EZO_EC_H_ +#define INCLUDE_DRIVERS_SENSOR_EZO_EC_H_ + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include +#include + +enum sensor_channel_ezo_ec +{ + SENSOR_CHAN_CONDUCTIVITY = SENSOR_CHAN_PRIV_START, +}; + + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/tests/ezo_ec/CMakeLists.txt b/tests/ezo_ec/CMakeLists.txt new file mode 100644 index 0000000..bbf7bb6 --- /dev/null +++ b/tests/ezo_ec/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.20.0) +list(APPEND CMAKE_MODULE_PATH "$ENV{NOTUS_ROOT}/cmake") +include(Util) + +ProjectSetup("standard;testing") + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(load_cell) + +set(SOURCES + main.cpp +) + +target_sources(app PRIVATE ${SOURCES}) +SetupTarget(app) + +RunClangdTidy(app "${SOURCES}") diff --git a/tests/ezo_ec/boards/native_sim.overlay b/tests/ezo_ec/boards/native_sim.overlay new file mode 100644 index 0000000..63a2ad6 --- /dev/null +++ b/tests/ezo_ec/boards/native_sim.overlay @@ -0,0 +1,6 @@ +&i2c0 { + ezo_ec: ezoec@64 { + compatible = "atlas,ezo-ec"; + reg = <0x64>; + }; +}; diff --git a/tests/ezo_ec/main.cpp b/tests/ezo_ec/main.cpp new file mode 100644 index 0000000..c0df7ed --- /dev/null +++ b/tests/ezo_ec/main.cpp @@ -0,0 +1,115 @@ +#include +#include +#include +#include +#include +#include + +#include "drivers/sensor/ezo_ec/emul_ezo_ec.h" +#include "drivers/sensor/ezo_ec/ezo_ec_util.h" +#include "drivers/sensors/ezo_ec.h" + +#include "testing.hpp" + +namespace { + +const struct device *ezo = DEVICE_DT_GET(DT_NODELABEL(ezo_ec)); // NOLINT +const static struct emul *emul = nullptr; // NOLINT + +void set_output(const char *buf, int len) +{ + emul_ezo_ec_set_output(emul,buf, len); +} + +void get_input(char *buf, int len) +{ + emul_ezo_ec_get_input(emul,buf,len); +} + +void reset_emul() +{ + emul_ezo_ec_reset_buffers(emul); +} + +ezo_ec_config get_emul_config() +{ + return emul_ezo_ec_get_config(emul); +} + +void re_init(struct ezo_ec_config config) +{ + emul_ezo_ec_set_config(emul, config); + + std::string_view info = "?i,EC,2.16\0"; + set_output(info.data(), info.length()); // NOLINT + + // This is a bit hacky... but it works! + // Calls the init functions and updates the state used + // by `device_is_ready()`. Code taken from Zephyr's init.c + int ret = ezo_ec_init(emul->dev); + if (ret != 0) + { + if (ret < 0) + { + ret = -ret; + } + if (ret > UINT8_MAX) + { + ret = UINT8_MAX; + } + emul->dev->state->init_res = ret; + } + else + { + emul->dev->state->init_res = 0; + } +} + +void* setup() +{ + emul = emul_get_binding("ezoec@64"); + + struct ezo_ec_config config = get_emul_config(); + re_init(config); + + zassert_true(device_is_ready(ezo), "Device is not ready"); + return nullptr; +} + +void teardown(void * /* f */) +{ + reset_emul(); +} + +ZTEST(t_ezo_ec, test_attr_set) +{ + zassert_ok(sensor_attr_set(ezo, SENSOR_CHAN_ALL, SENSOR_ATTR_CALIBRATION, nullptr)); + + std::array input{}; // NOLINT + get_input(input.data(), input.size()); + std::string input_str(input.begin(), input.end()); + zassert_equal(strcmp(input_str.c_str(), "Cal,dry"), 0); +} + + +ZTEST(t_ezo_ec, test_fetch_and_get) +{ + std::string_view output = "4000.0"; + set_output(output.data(), output.length()); // NOLINT + + zassert_ok(sensor_sample_fetch(ezo)); + + sensor_value val {}; + zassert_ok(sensor_channel_get(ezo, (enum sensor_channel)SENSOR_CHAN_CONDUCTIVITY, &val)); + + std::array input{}; + get_input(input.data(), input.size()); + zassert_equal(input[0], 'R'); + + zassert_equal(sensor_value_to_double(&val), 4000.0); + +} + +ZTEST_SUITE(t_ezo_ec, NULL, setup, NULL, NULL, teardown); // NOLINT + +} diff --git a/tests/ezo_ec/prj.conf b/tests/ezo_ec/prj.conf new file mode 100644 index 0000000..549e5f6 --- /dev/null +++ b/tests/ezo_ec/prj.conf @@ -0,0 +1,6 @@ +CONFIG_I2C=y +CONFIG_EMUL=y +CONFIG_I2C_EMUL=y +CONFIG_EZO_EC=y +CONFIG_EZO_EC_EMUL=y +CONFIG_SENSOR=y diff --git a/tests/ezo_ec/testcase.yaml b/tests/ezo_ec/testcase.yaml new file mode 100644 index 0000000..9add19e --- /dev/null +++ b/tests/ezo_ec/testcase.yaml @@ -0,0 +1,5 @@ +tests: + drivers.sensor.ezo_ec: + platform_allow: native_sim + integration_platforms: + - native_sim