diff --git a/PROJECT_PLAN.md b/PROJECT_PLAN.md index 6cefdb0..d001e6a 100644 --- a/PROJECT_PLAN.md +++ b/PROJECT_PLAN.md @@ -65,7 +65,7 @@ Explore modern C++ (C++23) and software engineering practices in embedded system - [x] Code coverage reporting - [x] Add static analysis through clang-tidy by default - [x] Add UART abstraction with RxHandler -- [ ] Add I2C abstraction +- [x] Add I2C abstraction - [ ] Add SPI abstraction - [ ] Add PWM abstraction - [ ] Add ADC abstraction diff --git a/README.md b/README.md index d420014..36b8ecf 100644 --- a/README.md +++ b/README.md @@ -187,21 +187,18 @@ The `uart_echo` application demonstrates: **Run on host emulator**: ```bash -# Terminal 1: Start Python emulator -cd py/host-emulator -python -m src.emulator - -# Terminal 2: Run uart_echo +# Terminal 1: Run uart_echo cd build/host/bin ./uart_echo -# Terminal 3: Send data via Python +# Terminal 2: Send data via Python python >>> from src.emulator import DeviceEmulator >>> emu = DeviceEmulator() >>> emu.start() ->>> emu.Uart1().send_data([72, 101, 108, 108, 111]) # "Hello" ->>> bytes(emu.Uart1().rx_buffer) # See echoed data +>>> emu.uart1().send_data([72, 101, 108, 108, 111]) # "Hello" +>>> bytes(emu.uart1().rx_buffer) # See echoed data +>>> emu.uart1().rx_buffer.clear() ``` ## Software Engineering Principles diff --git a/py/host-emulator/CMakeLists.txt b/py/host-emulator/CMakeLists.txt index 083a7a5..5337672 100644 --- a/py/host-emulator/CMakeLists.txt +++ b/py/host-emulator/CMakeLists.txt @@ -7,10 +7,11 @@ add_test( COMMAND ${host_emulator_venv_PYTHON} -m pytest ${CMAKE_CURRENT_SOURCE_DIR} --blinky=${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/$/blinky --uart-echo=${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/$/uart_echo + --i2c-demo=${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/$/i2c_demo WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) -set_tests_properties(host_emulator_test PROPERTIES DEPENDS "blinky;uart_echo") +set_tests_properties(host_emulator_test PROPERTIES DEPENDS "blinky;uart_echo;i2c_demo") set_tests_properties(host_emulator_test PROPERTIES DEPENDS host_emulator_venv) set_tests_properties(host_emulator_test PROPERTIES FIXTURES_SETUP host_emulator_venv) diff --git a/py/host-emulator/src/emulator.py b/py/host-emulator/src/emulator.py index afeb9d7..1f42540 100755 --- a/py/host-emulator/src/emulator.py +++ b/py/host-emulator/src/emulator.py @@ -211,6 +211,110 @@ def handle_message(self, message): return self.handle_response(message) +class I2C: + def __init__(self, name): + self.name = name + # Store data for each I2C address (address -> bytearray) + self.device_buffers = {} + self.on_response = None + self.on_request = None + + def handle_request(self, message): + response = { + "type": "Response", + "object": "I2C", + "name": self.name, + "address": message.get("address", 0), + "data": [], + "bytes_transferred": 0, + "status": Status.InvalidOperation.name, + } + + address = message.get("address", 0) + + if message["operation"] == "Send": + # Device is sending data to I2C peripheral + # Store the data in the buffer for this address + data = message.get("data", []) + if address not in self.device_buffers: + self.device_buffers[address] = bytearray() + self.device_buffers[address] = bytearray(data) + response.update( + { + "bytes_transferred": len(data), + "status": Status.Ok.name, + } + ) + print( + f"[I2C {self.name}] Wrote {len(data)} bytes to address " + f"0x{address:02X}: {bytes(data)}" + ) + + elif message["operation"] == "Receive": + # Device is receiving data from I2C peripheral + # Return data from the buffer for this address + size = message.get("size", 0) + if address in self.device_buffers: + bytes_to_send = min(size, len(self.device_buffers[address])) + data = list(self.device_buffers[address][:bytes_to_send]) + else: + # No data available, return empty + bytes_to_send = 0 + data = [] + response.update( + { + "data": data, + "bytes_transferred": bytes_to_send, + "status": Status.Ok.name, + } + ) + print( + f"[I2C {self.name}] Read {bytes_to_send} bytes from address " + f"0x{address:02X}: {bytes(data)}" + ) + + if self.on_request: + self.on_request(message) + return json.dumps(response) + + def handle_response(self, message): + print(f"[I2C {self.name}] Received response: {message}") + if self.on_response: + self.on_response(message) + return None + + def set_on_request(self, on_request): + self.on_request = on_request + + def set_on_response(self, on_response): + self.on_response = on_response + + def handle_message(self, message): + if message["object"] != "I2C": + return None + if message["name"] != self.name: + return None + if message["type"] == "Request": + return self.handle_request(message) + if message["type"] == "Response": + return self.handle_response(message) + + def write_to_device(self, address, data): + """Write data to a simulated I2C device (for testing)""" + if address not in self.device_buffers: + self.device_buffers[address] = bytearray() + self.device_buffers[address] = bytearray(data) + print( + f"[I2C {self.name}] Device buffer at 0x{address:02X} set to: {bytes(data)}" + ) + + def read_from_device(self, address): + """Read data from a simulated I2C device (for testing)""" + if address in self.device_buffers: + return bytes(self.device_buffers[address]) + return b"" + + class DeviceEmulator: def __init__(self): print("Creating DeviceEmulator") @@ -233,6 +337,9 @@ def __init__(self): self.uart_1 = Uart("UART 1", self.to_device_socket) self.uarts = [self.uart_1] + self.i2c_1 = I2C("I2C 1") + self.i2cs = [self.i2c_1] + def user_led1(self): return self.led_1 @@ -245,6 +352,9 @@ def user_button1(self): def uart1(self): return self.uart_1 + def i2c1(self): + return self.i2c_1 + def run(self): print("Starting emulator thread") try: @@ -255,28 +365,37 @@ def run(self): while self.running: print("Waiting for message...") message = from_device_socket.recv() - print(f"[Emulator] Received request: {message}") + # print(f"[Emulator] Received request: {message}") if message.startswith(b"{") and message.endswith(b"}"): # JSON message json_message = json.loads(message) if json_message["object"] == "Pin": for pin in self.pins: if response := pin.handle_message(json_message): - print(f"[Emulator] Sending response: {response}") + # print(f"[Emulator] Sending response: {response}") from_device_socket.send_string(response) - print("") + # print("") break else: raise UnhandledMessageError(message, " - Pin not found") elif json_message["object"] == "Uart": for uart in self.uarts: if response := uart.handle_message(json_message): - print(f"[Emulator] Sending response: {response}") + # print(f"[Emulator] Sending response: {response}") from_device_socket.send_string(response) - print("") + # print("") break else: raise UnhandledMessageError(message, " - Uart not found") + elif json_message["object"] == "I2C": + for i2c in self.i2cs: + if response := i2c.handle_message(json_message): + # print(f"[Emulator] Sending response: {response}") + from_device_socket.send_string(response) + # print("") + break + else: + raise UnhandledMessageError(message, " - I2C not found") else: raise UnhandledMessageError( message, f" - unknown object type: {json_message['object']}" diff --git a/py/host-emulator/tests/conftest.py b/py/host-emulator/tests/conftest.py index a133785..23f3eb1 100644 --- a/py/host-emulator/tests/conftest.py +++ b/py/host-emulator/tests/conftest.py @@ -18,6 +18,12 @@ def pytest_addoption(parser): default=None, help="Path to the uart_echo executable", ) + parser.addoption( + "--i2c-demo", + action="store", + default=None, + help="Path to the i2c_demo executable", + ) # Emulator must be stopped manually within each test @@ -73,3 +79,24 @@ def uart_echo(request): uart_echo_process.kill() uart_echo_process.wait(timeout=1) print(f"[Fixture] UartEcho return code: {uart_echo_process.returncode}") + + +# I2CDemo must be stopped manually within each test +@fixture() +def i2c_demo(request): + i2c_demo_arg = request.config.getoption("--i2c-demo") + i2c_demo_executable = pathlib.Path(i2c_demo_arg).resolve() + assert i2c_demo_executable.exists() + i2c_demo_process = subprocess.Popen( + [str(i2c_demo_executable)], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + + yield i2c_demo_process + + if i2c_demo_process.poll() is None: + print("[Fixture] Stopping i2c_demo") + i2c_demo_process.kill() + i2c_demo_process.wait(timeout=1) + print(f"[Fixture] I2CDemo return code: {i2c_demo_process.returncode}") diff --git a/py/host-emulator/tests/test_i2c_demo.py b/py/host-emulator/tests/test_i2c_demo.py new file mode 100644 index 0000000..d8850a1 --- /dev/null +++ b/py/host-emulator/tests/test_i2c_demo.py @@ -0,0 +1,134 @@ +"""Integration tests for I2C test application.""" + +import time + + +def test_i2c_demo_starts(emulator, i2c_demo): + """Test that i2c_demo starts successfully.""" + try: + # Give i2c_demo time to initialize + time.sleep(0.5) + + # Check that the process is still running + assert i2c_demo.poll() is None, "i2c_demo process terminated unexpectedly" + + finally: + emulator.stop() + i2c_demo.terminate() + i2c_demo.wait(timeout=1) + + +def test_i2c_demo_write_read_cycle(emulator, i2c_demo): + """Test that i2c_demo writes and reads from I2C device.""" + try: + device_address = 0x50 + test_pattern = [0xDE, 0xAD, 0xBE, 0xEF] + write_count = 0 + read_count = 0 + + def i2c_handler(message): + nonlocal write_count, read_count + if message.get("operation") == "Send": + # Device is writing to I2C peripheral + write_count += 1 + data = message.get("data", []) + address = message.get("address", 0) + + # Verify the data and address + assert address == device_address, f"Wrong address: 0x{address:02X}" + assert data == test_pattern, f"Wrong data: {data}" + + elif message.get("operation") == "Receive": + # Device is reading from I2C peripheral + read_count += 1 + address = message.get("address", 0) + assert address == device_address, f"Wrong address: 0x{address:02X}" + + emulator.i2c1().set_on_request(i2c_handler) + + # Pre-populate I2C device buffer with test pattern + emulator.i2c1().write_to_device(device_address, test_pattern) + + # Give i2c_demo time to run a few cycles + time.sleep(1.5) + + # Verify that writes and reads occurred + assert write_count > 0, "No I2C writes occurred" + assert read_count > 0, "No I2C reads occurred" + assert write_count == read_count, ( + f"Write/read mismatch: {write_count} writes, {read_count} reads" + ) + + finally: + emulator.stop() + i2c_demo.terminate() + i2c_demo.wait(timeout=1) + + +def test_i2c_demo_toggles_leds(emulator, i2c_demo): + """Test that i2c_demo toggles LEDs based on I2C operations.""" + try: + device_address = 0x50 + test_pattern = [0xDE, 0xAD, 0xBE, 0xEF] + + # Pre-populate I2C device buffer with correct test pattern + emulator.i2c1().write_to_device(device_address, test_pattern) + + # Give i2c_demo time to initialize + time.sleep(0.5) + + # Record initial LED states + initial_led1 = emulator.get_pin_state("LED 1") + initial_led2 = emulator.get_pin_state("LED 2") + + # Wait for exactly one more toggle cycle (~550ms per cycle) + time.sleep(0.3) + + # Check that LEDs have toggled + final_led1 = emulator.get_pin_state("LED 1") + final_led2 = emulator.get_pin_state("LED 2") + + # LED2 should have toggled (heartbeat) + assert final_led2 != initial_led2, ( + f"LED2 didn't toggle: {initial_led2} -> {final_led2}" + ) + + # LED1 should have toggled (data verification success) + assert final_led1 != initial_led1, ( + f"LED1 didn't toggle: {initial_led1} -> {final_led1}" + ) + + finally: + emulator.stop() + i2c_demo.terminate() + i2c_demo.wait(timeout=1) + + +def test_i2c_demo_data_mismatch(emulator, i2c_demo): + """Test that i2c_demo handles data mismatch correctly.""" + try: + device_address = 0x50 + wrong_pattern = [0x00, 0x11, 0x22, 0x33] # Different from test pattern + + # Pre-populate I2C device buffer with wrong data + emulator.i2c1().write_to_device(device_address, wrong_pattern) + + # Give i2c_demo time to run a few cycles + time.sleep(1.0) + + # LED1 should be off due to data mismatch + led1_state = emulator.get_pin_state("LED 1") + assert led1_state.name == "Low", f"LED1 should be off, but is {led1_state.name}" + + # LED2 should still be blinking (alive indicator) + initial_led2 = emulator.get_pin_state("LED 2") + time.sleep(0.6) + final_led2 = emulator.get_pin_state("LED 2") + assert final_led2 != initial_led2, ( + f"LED2 didn't toggle: {initial_led2} -> {final_led2}" + ) + + finally: + emulator.stop() + i2c_demo.terminate() + i2c_demo.wait(timeout=1) diff --git a/src/apps/CMakeLists.txt b/src/apps/CMakeLists.txt index 1bb5d35..af5273c 100644 --- a/src/apps/CMakeLists.txt +++ b/src/apps/CMakeLists.txt @@ -5,4 +5,5 @@ target_compile_options(app INTERFACE ${COMMON_COMPILE_OPTIONS}) target_link_libraries(app INTERFACE board error) add_subdirectory(blinky) -add_subdirectory(uart_echo) \ No newline at end of file +add_subdirectory(uart_echo) +add_subdirectory(i2c_demo) \ No newline at end of file diff --git a/src/apps/blinky/blinky.cpp b/src/apps/blinky/blinky.cpp index 6ed5891..78d1086 100644 --- a/src/apps/blinky/blinky.cpp +++ b/src/apps/blinky/blinky.cpp @@ -25,23 +25,17 @@ auto AppMain(board::Board& board) -> std::expected { } auto Blinky::Run() -> std::expected { - auto status = board_.UserLed1().SetHigh(); + auto status{board_.UserLed1().SetHigh()}; while (true) { status = status .and_then([this]() { mcu::Delay(500ms); - return board_.UserLed1().Get(); + return board_.UserLed1().Toggle(); }) - .and_then([this](mcu::PinState state) { - return (state == mcu::PinState::kHigh) - ? board_.UserLed1().SetLow() - : board_.UserLed1().SetHigh(); + .or_else([](auto error) -> std::expected { + return std::unexpected(error); }); - - if (!status) { - return std::unexpected(status.error()); - } } return {}; } diff --git a/src/apps/i2c_demo/CMakeLists.txt b/src/apps/i2c_demo/CMakeLists.txt new file mode 100644 index 0000000..18410d7 --- /dev/null +++ b/src/apps/i2c_demo/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.27) + +add_executable(i2c_demo i2c_demo.cpp) +target_compile_options(i2c_demo PRIVATE ${COMMON_COMPILE_OPTIONS}) +target_link_libraries(i2c_demo PRIVATE error sys mcu) diff --git a/src/apps/i2c_demo/i2c_demo.cpp b/src/apps/i2c_demo/i2c_demo.cpp new file mode 100644 index 0000000..b9c92d8 --- /dev/null +++ b/src/apps/i2c_demo/i2c_demo.cpp @@ -0,0 +1,86 @@ +#include "i2c_demo.hpp" + +#include +#include +#include +#include + +#include "apps/app.hpp" +#include "libs/board/board.hpp" +#include "libs/common/error.hpp" +#include "libs/mcu/delay.hpp" +#include "libs/mcu/i2c.hpp" +#include "libs/mcu/pin.hpp" + +namespace app { +using std::chrono::operator""ms; + +auto AppMain(board::Board& board) -> std::expected { + I2CDemo i2c_demo{board}; + if (!i2c_demo.Init()) { + return std::unexpected(common::Error::kUnknown); + } + if (!i2c_demo.Run()) { + return std::unexpected(common::Error::kUnknown); + } + return {}; +} + +auto I2CDemo::Init() -> std::expected { + return board_.Init(); +} + +auto I2CDemo::Run() -> std::expected { + // I2C device address to test + constexpr uint16_t kDeviceAddress{0x50}; + + // Test pattern to write/read + const std::array test_pattern{0xDE, 0xAD, 0xBE, 0xEF}; + + // Main loop - write pattern, read it back, verify + while (true) { + // Write test pattern to I2C device + auto write_result{board_.I2C1().SendData(kDeviceAddress, test_pattern)}; + if (!write_result) { + // Turn off LED1 on write error + std::ignore = board_.UserLed1().SetLow(); + mcu::Delay(100ms); + continue; + } + + // Small delay between write and read + mcu::Delay(50ms); + + // Read data back from I2C device + auto read_result{ + board_.I2C1().ReceiveData(kDeviceAddress, test_pattern.size())}; + if (!read_result) { + // Turn off LED1 on read error + std::ignore = board_.UserLed1().SetLow(); + mcu::Delay(100ms); + continue; + } + + // Verify received data matches test pattern + const auto received_span{read_result.value()}; + const bool data_matches{std::ranges::equal(received_span, test_pattern)}; + + // Toggle LED1 based on verification result + if (data_matches) { + std::ignore = board_.UserLed1().Toggle(); + } else { + // Data mismatch - turn off LED1 + std::ignore = board_.UserLed1().SetLow(); + } + + // Toggle LED2 to show we're alive + std::ignore = board_.UserLed2().Toggle(); + + // Delay before next iteration + mcu::Delay(500ms); + } + + return {}; +} + +} // namespace app diff --git a/src/apps/i2c_demo/i2c_demo.hpp b/src/apps/i2c_demo/i2c_demo.hpp new file mode 100644 index 0000000..0f1105b --- /dev/null +++ b/src/apps/i2c_demo/i2c_demo.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include "libs/board/board.hpp" + +namespace app { + +class I2CDemo { + public: + explicit I2CDemo(board::Board& board) : board_(board) {} + auto Init() -> std::expected; + auto Run() -> std::expected; + + private: + board::Board& board_; +}; + +} // namespace app diff --git a/src/apps/uart_echo/uart_echo.cpp b/src/apps/uart_echo/uart_echo.cpp index e139b4a..3585e57 100644 --- a/src/apps/uart_echo/uart_echo.cpp +++ b/src/apps/uart_echo/uart_echo.cpp @@ -39,12 +39,7 @@ auto UartEcho::Init() -> std::expected { std::ignore = board_.Uart1().Send(echo_data); // Toggle LED1 to indicate data received - const auto led_state = board_.UserLed1().Get(); - if (led_state && led_state.value() == mcu::PinState::kHigh) { - std::ignore = board_.UserLed1().SetLow(); - } else { - std::ignore = board_.UserLed1().SetHigh(); - } + std::ignore = board_.UserLed1().Toggle(); }); }); } @@ -52,8 +47,8 @@ auto UartEcho::Init() -> std::expected { auto UartEcho::Run() -> std::expected { // Send initial greeting message const std::string greeting{"UART Echo ready! Send data to echo it back.\n"}; - auto send_result = board_.Uart1().Send( - std::vector(greeting.begin(), greeting.end())); + auto send_result{board_.Uart1().Send( + std::vector(greeting.begin(), greeting.end()))}; if (!send_result) { return std::unexpected(send_result.error()); } @@ -62,21 +57,7 @@ auto UartEcho::Run() -> std::expected { // The actual echo happens via the RxHandler callback while (true) { mcu::Delay(1000ms); - const auto led_state = board_.UserLed2().Get(); - if (!led_state) { - return std::unexpected(led_state.error()); - } - if (led_state.value() == mcu::PinState::kHigh) { - auto status = board_.UserLed2().SetLow(); - if (!status) { - return std::unexpected(status.error()); - } - } else { - auto status = board_.UserLed2().SetHigh(); - if (!status) { - return std::unexpected(status.error()); - } - } + std::ignore = board_.UserLed2().Toggle(); } return {}; } diff --git a/src/libs/board/host/host_board.cpp b/src/libs/board/host/host_board.cpp index 983df68..40d4ded 100644 --- a/src/libs/board/host/host_board.cpp +++ b/src/libs/board/host/host_board.cpp @@ -20,6 +20,6 @@ auto HostBoard::Init() -> std::expected { auto HostBoard::UserLed1() -> mcu::OutputPin& { return user_led_1_; } auto HostBoard::UserLed2() -> mcu::OutputPin& { return user_led_2_; } auto HostBoard::UserButton1() -> mcu::InputPin& { return user_button_1_; } -auto HostBoard::I2C1() -> mcu::I2CController& { return i2c1_; } +auto HostBoard::I2C1() -> mcu::I2CController& { return i2c_1_; } auto HostBoard::Uart1() -> mcu::Uart& { return uart_1_; } } // namespace board diff --git a/src/libs/board/host/host_board.hpp b/src/libs/board/host/host_board.hpp index fbe4dd4..390864c 100644 --- a/src/libs/board/host/host_board.hpp +++ b/src/libs/board/host/host_board.hpp @@ -37,10 +37,8 @@ class HostBoard : public Board { } const mcu::ReceiverMap receiver_map_{ - {IsJson, user_led_1_}, - {IsJson, user_led_2_}, - {IsJson, user_button_1_}, - {IsJson, uart_1_}, + {IsJson, user_led_1_}, {IsJson, user_led_2_}, {IsJson, user_button_1_}, + {IsJson, uart_1_}, {IsJson, i2c_1_}, }; mcu::Dispatcher dispatcher_{receiver_map_}; mcu::ZmqTransport zmq_transport_{"ipc:///tmp/device_emulator.ipc", @@ -50,6 +48,6 @@ class HostBoard : public Board { mcu::HostPin user_led_2_{"LED 2", zmq_transport_}; mcu::HostPin user_button_1_{"Button 1", zmq_transport_}; mcu::HostUart uart_1_{"UART 1", zmq_transport_}; - mcu::HostI2CController i2c1_{}; + mcu::HostI2CController i2c_1_{"I2C 1", zmq_transport_}; }; } // namespace board diff --git a/src/libs/mcu/host/CMakeLists.txt b/src/libs/mcu/host/CMakeLists.txt index cf7dfa1..32f631b 100644 --- a/src/libs/mcu/host/CMakeLists.txt +++ b/src/libs/mcu/host/CMakeLists.txt @@ -52,12 +52,25 @@ target_link_libraries(test_host_uart cppzmq ) +add_executable(test_host_i2c test_host_i2c.cpp) +target_compile_options(test_host_i2c PRIVATE ${COMMON_COMPILE_OPTIONS}) + +target_link_libraries(test_host_i2c + PRIVATE + GTest::GTest + host_mcu + host_transport + nlohmann_json::nlohmann_json + cppzmq + ) + include(GoogleTest) gtest_discover_tests(test_host_transport) gtest_discover_tests(test_messages) gtest_discover_tests(test_dispatcher) gtest_discover_tests(test_host_uart) +gtest_discover_tests(test_host_i2c) # Code coverage configuration if(CODE_COVERAGE) @@ -71,4 +84,5 @@ if(CODE_COVERAGE) target_code_coverage(test_messages AUTO ALL) target_code_coverage(test_dispatcher AUTO ALL) target_code_coverage(test_host_uart AUTO ALL) + target_code_coverage(test_host_i2c AUTO ALL) endif() diff --git a/src/libs/mcu/host/emulator_message_json_encoder.hpp b/src/libs/mcu/host/emulator_message_json_encoder.hpp index ab58e0a..b8f4278 100644 --- a/src/libs/mcu/host/emulator_message_json_encoder.hpp +++ b/src/libs/mcu/host/emulator_message_json_encoder.hpp @@ -53,6 +53,7 @@ NLOHMANN_JSON_SERIALIZE_ENUM(OperationType, NLOHMANN_JSON_SERIALIZE_ENUM(ObjectType, { {ObjectType::kPin, "Pin"}, {ObjectType::kUart, "Uart"}, + {ObjectType::kI2C, "I2C"}, }) NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(PinEmulatorRequest, type, object, name, @@ -67,6 +68,12 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(UartEmulatorRequest, type, object, name, NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(UartEmulatorResponse, type, object, name, data, bytes_transferred, status) +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(I2CEmulatorRequest, type, object, name, + operation, address, data, size) + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(I2CEmulatorResponse, type, object, name, + address, data, bytes_transferred, status) + template inline auto Encode(const T& obj) -> std::string { return nlohmann::json(obj).dump(); diff --git a/src/libs/mcu/host/host_emulator_messages.hpp b/src/libs/mcu/host/host_emulator_messages.hpp index f62abd7..0288196 100644 --- a/src/libs/mcu/host/host_emulator_messages.hpp +++ b/src/libs/mcu/host/host_emulator_messages.hpp @@ -12,7 +12,7 @@ namespace mcu { enum class MessageType { kRequest = 1, kResponse }; enum class OperationType { kSet = 1, kGet, kSend, kReceive }; -enum class ObjectType { kPin = 1, kUart }; +enum class ObjectType { kPin = 1, kUart, kI2C }; struct PinEmulatorRequest { MessageType type{MessageType::kRequest}; @@ -67,4 +67,35 @@ struct UartEmulatorResponse { } }; +struct I2CEmulatorRequest { + MessageType type{MessageType::kRequest}; + ObjectType object{ObjectType::kI2C}; + std::string name; + OperationType operation; + uint16_t address{0}; + std::vector data; // For Send operation + size_t size{0}; // For Receive operation (buffer size) + auto operator==(const I2CEmulatorRequest& other) const -> bool { + return type == other.type && object == other.object && name == other.name && + operation == other.operation && address == other.address && + data == other.data && size == other.size; + } +}; + +struct I2CEmulatorResponse { + MessageType type{MessageType::kResponse}; + ObjectType object{ObjectType::kI2C}; + std::string name; + uint16_t address{0}; + std::vector data; // Received data + size_t bytes_transferred{0}; + common::Error status; + auto operator==(const I2CEmulatorResponse& other) const -> bool { + return type == other.type && object == other.object && name == other.name && + address == other.address && data == other.data && + bytes_transferred == other.bytes_transferred && + status == other.status; + } +}; + } // namespace mcu diff --git a/src/libs/mcu/host/host_i2c.cpp b/src/libs/mcu/host/host_i2c.cpp index 49c04c7..2fae354 100644 --- a/src/libs/mcu/host/host_i2c.cpp +++ b/src/libs/mcu/host/host_i2c.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "libs/common/error.hpp" #include "libs/mcu/host/emulator_message_json_encoder.hpp" @@ -12,45 +13,101 @@ #include "libs/mcu/i2c.hpp" namespace mcu { + auto HostI2CController::SendData(uint16_t address, std::span data) - -> std::expected { - std::ranges::copy(data, data_buffers_[address].begin()); + -> std::expected { + const I2CEmulatorRequest request{ + .type = MessageType::kRequest, + .object = ObjectType::kI2C, + .name = name_, + .operation = OperationType::kSend, + .address = address, + .data = std::vector(data.begin(), data.end()), + .size = 0, + }; + + auto send_result = transport_.Send(Encode(request)); + if (!send_result) { + return std::unexpected(send_result.error()); + } + + auto receive_result = transport_.Receive(); + if (!receive_result) { + return std::unexpected(receive_result.error()); + } + + const auto response = Decode(receive_result.value()); + if (response.status != common::Error::kOk) { + return std::unexpected(response.status); + } + return {}; } auto HostI2CController::ReceiveData(uint16_t address, size_t size) - -> std::expected, int> { - return std::span{data_buffers_[address].data(), - std::min(size, data_buffers_[address].size())}; + -> std::expected, common::Error> { + const I2CEmulatorRequest request{ + .type = MessageType::kRequest, + .object = ObjectType::kI2C, + .name = name_, + .operation = OperationType::kReceive, + .address = address, + .data = {}, + .size = size, + }; + + auto send_result = transport_.Send(Encode(request)); + if (!send_result) { + return std::unexpected(send_result.error()); + } + + auto receive_result = transport_.Receive(); + if (!receive_result) { + return std::unexpected(receive_result.error()); + } + + const auto response = Decode(receive_result.value()); + if (response.status != common::Error::kOk) { + return std::unexpected(response.status); + } + + // Store received data in buffer for this address + auto& buffer = data_buffers_[address]; + const size_t bytes_to_copy{std::min(response.data.size(), buffer.size())}; + std::copy_n(response.data.begin(), bytes_to_copy, buffer.begin()); + + return std::span{buffer.data(), bytes_to_copy}; } auto HostI2CController::SendDataInterrupt( uint16_t address, std::span data, - void (*callback)(std::expected)) -> std::expected { + std::function)> callback) + -> std::expected { callback(SendData(address, data)); return {}; } auto HostI2CController::ReceiveDataInterrupt( uint16_t address, size_t size, - void (*callback)(std::expected, int>)) - -> std::expected { + std::function, common::Error>)> + callback) -> std::expected { callback(ReceiveData(address, size)); return {}; } auto HostI2CController::SendDataDma( uint16_t address, std::span data, - void (*callback)(std::expected)) -> std::expected { + std::function)> callback) + -> std::expected { callback(SendData(address, data)); return {}; } auto HostI2CController::ReceiveDataDma( uint16_t address, size_t size, - void (*callback)(std::expected, int>)) - -> std::expected { + std::function, common::Error>)> + callback) -> std::expected { callback(ReceiveData(address, size)); return {}; } diff --git a/src/libs/mcu/host/host_i2c.hpp b/src/libs/mcu/host/host_i2c.hpp index 720da6f..c46c1c9 100644 --- a/src/libs/mcu/host/host_i2c.hpp +++ b/src/libs/mcu/host/host_i2c.hpp @@ -4,16 +4,19 @@ #include #include #include +#include #include #include "libs/mcu/host/receiver.hpp" +#include "libs/mcu/host/transport.hpp" #include "libs/mcu/i2c.hpp" namespace mcu { class HostI2CController final : public I2CController, public Receiver { public: - HostI2CController() = default; + explicit HostI2CController(std::string name, Transport& transport) + : name_{std::move(name)}, transport_{transport} {} HostI2CController(const HostI2CController&) = delete; HostI2CController(HostI2CController&&) = delete; auto operator=(const HostI2CController&) -> HostI2CController& = delete; @@ -21,29 +24,33 @@ class HostI2CController final : public I2CController, public Receiver { ~HostI2CController() override = default; auto SendData(uint16_t address, std::span data) - -> std::expected override; + -> std::expected override; auto ReceiveData(uint16_t address, size_t size) - -> std::expected, int> override; + -> std::expected, common::Error> override; - auto SendDataInterrupt(uint16_t address, std::span data, - void (*callback)(std::expected)) - -> std::expected override; + auto SendDataInterrupt( + uint16_t address, std::span data, + std::function)> callback) + -> std::expected override; auto ReceiveDataInterrupt( uint16_t address, size_t size, - void (*callback)(std::expected, int>)) - -> std::expected override; + std::function, common::Error>)> + callback) -> std::expected override; auto SendDataDma(uint16_t address, std::span data, - void (*callback)(std::expected)) - -> std::expected override; - auto ReceiveDataDma(uint16_t address, size_t size, - void (*callback)(std::expected, int>)) - -> std::expected override; + std::function)> + callback) -> std::expected override; + auto ReceiveDataDma( + uint16_t address, size_t size, + std::function, common::Error>)> + callback) -> std::expected override; auto Receive(const std::string_view& message) -> std::expected override; private: + const std::string name_; + Transport& transport_; std::unordered_map> data_buffers_; }; } // namespace mcu diff --git a/src/libs/mcu/host/host_pin.cpp b/src/libs/mcu/host/host_pin.cpp index aa9f321..8f596d4 100644 --- a/src/libs/mcu/host/host_pin.cpp +++ b/src/libs/mcu/host/host_pin.cpp @@ -28,6 +28,21 @@ auto HostPin::SetLow() -> std::expected { } return SendState(PinState::kLow); } + +auto HostPin::Toggle() -> std::expected { + if (direction_ == PinDirection::kInput) { + return std::unexpected(common::Error::kInvalidOperation); + } + auto current_state{GetState()}; + if (!current_state) { + return std::unexpected(current_state.error()); + } + if (current_state.value() == PinState::kHigh) { + return SendState(PinState::kLow); + } + return SendState(PinState::kHigh); +} + auto HostPin::Get() -> std::expected { return GetState(); } diff --git a/src/libs/mcu/host/host_pin.hpp b/src/libs/mcu/host/host_pin.hpp index dc365e1..b28a916 100644 --- a/src/libs/mcu/host/host_pin.hpp +++ b/src/libs/mcu/host/host_pin.hpp @@ -23,6 +23,7 @@ class HostPin final : public BidirectionalPin, public Receiver { -> std::expected override; auto SetHigh() -> std::expected override; auto SetLow() -> std::expected override; + auto Toggle() -> std::expected override; auto Get() -> std::expected override; auto SetInterruptHandler(std::function handler, diff --git a/src/libs/mcu/host/test_host_i2c.cpp b/src/libs/mcu/host/test_host_i2c.cpp new file mode 100644 index 0000000..ecbe777 --- /dev/null +++ b/src/libs/mcu/host/test_host_i2c.cpp @@ -0,0 +1,337 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "libs/mcu/host/dispatcher.hpp" +#include "libs/mcu/host/emulator_message_json_encoder.hpp" +#include "libs/mcu/host/host_emulator_messages.hpp" +#include "libs/mcu/host/host_i2c.hpp" +#include "libs/mcu/host/zmq_transport.hpp" +#include "libs/mcu/i2c.hpp" + +class HostI2CTest : public ::testing::Test { + protected: + static constexpr auto IsJson(const std::string_view& message) -> bool { + return message.starts_with("{") && message.ends_with("}"); + } + + void SetUp() override { + // Start emulator thread + emulator_running_ = true; + emulator_thread_ = std::thread{[this]() { EmulatorLoop(); }}; + + // Give emulator time to start + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Create dispatcher with empty receiver map (will update via reference + // later) + dispatcher_ = std::make_unique(receiver_map_storage_); + + // Create transport + device_transport_ = std::make_unique( + "ipc:///tmp/test_i2c_device_emulator.ipc", + "ipc:///tmp/test_i2c_emulator_device.ipc", *dispatcher_); + + // Now create I2C with transport + i2c_ = + std::make_unique("I2C 1", *device_transport_); + + // Add I2C to receiver map (dispatcher holds reference, so this updates it) + receiver_map_storage_.emplace_back(IsJson, std::ref(*i2c_)); + + // Give transport time to connect + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + void TearDown() override { + i2c_.reset(); + device_transport_.reset(); + dispatcher_.reset(); + + emulator_running_ = false; + if (emulator_thread_.joinable()) { + emulator_context_.shutdown(); + emulator_context_.close(); + emulator_thread_.join(); + } + } + + void EmulatorLoop() { + // Simulate I2C device buffers (address -> data) + std::map> i2c_device_buffers; + + try { + zmq::socket_t socket{emulator_context_, zmq::socket_type::pair}; + socket.bind("ipc:///tmp/test_i2c_device_emulator.ipc"); + + while (emulator_running_) { + std::array items = { + {{.socket = static_cast(socket), + .fd = 0, + .events = ZMQ_POLLIN, + .revents = 0}}}; + + const int ret{ + zmq::poll(items.data(), 1, std::chrono::milliseconds{50})}; + + if (ret == 0) { + continue; // Timeout + } + if (ret <= 0) { + continue; + } + + zmq::message_t message{}; + if (!socket.recv(message, zmq::recv_flags::none)) { + continue; + } + + const std::string_view message_str{ + static_cast(message.data()), message.size()}; + + const auto request = + mcu::Decode(std::string{message_str}); + mcu::I2CEmulatorResponse response{ + .type = mcu::MessageType::kResponse, + .object = mcu::ObjectType::kI2C, + .name = request.name, + .address = request.address, + .data = {}, + .bytes_transferred = 0, + .status = common::Error::kOk, + }; + + if (request.operation == mcu::OperationType::kSend) { + // Device sent data to I2C peripheral - store in device buffer + i2c_device_buffers[request.address] = request.data; + response.bytes_transferred = request.data.size(); + } else if (request.operation == mcu::OperationType::kReceive) { + // Device wants to receive data from I2C peripheral + if (i2c_device_buffers.contains(request.address)) { + const auto& buffer = i2c_device_buffers[request.address]; + const size_t bytes_to_send{std::min(request.size, buffer.size())}; + response.data = std::vector( + buffer.begin(), + buffer.begin() + static_cast(bytes_to_send)); + response.bytes_transferred = bytes_to_send; + } + } + + const auto response_str = mcu::Encode(response); + socket.send(zmq::buffer(response_str), zmq::send_flags::none); + } + } catch (const zmq::error_t& e) { + // Socket closed during shutdown, expected behavior + if (e.num() != ETERM) { + throw; + } + } + } + + std::vector< + std::pair, mcu::Receiver&>> + receiver_map_storage_; + std::unique_ptr dispatcher_; + std::unique_ptr device_transport_; + std::unique_ptr i2c_; + zmq::context_t emulator_context_{1}; + std::thread emulator_thread_; + std::atomic emulator_running_{false}; +}; + +TEST_F(HostI2CTest, SendData) { + const uint16_t device_address{0x42}; + const std::array send_data{0xDE, 0xAD, 0xBE, 0xEF}; + + auto result = i2c_->SendData(device_address, send_data); + EXPECT_TRUE(result); +} + +TEST_F(HostI2CTest, SendReceiveData) { + const uint16_t device_address{0x50}; + const std::array send_data{0x01, 0x02, 0x03, 0x04, 0x05}; + + // Send data to device + auto send_result = i2c_->SendData(device_address, send_data); + ASSERT_TRUE(send_result); + + // Receive data back from same device + auto recv_result = i2c_->ReceiveData(device_address, send_data.size()); + ASSERT_TRUE(recv_result); + + const auto received_span = recv_result.value(); + EXPECT_EQ(received_span.size(), send_data.size()); + + // Compare received data with sent data + EXPECT_TRUE(std::equal(received_span.begin(), received_span.end(), + send_data.begin(), send_data.end())); +} + +TEST_F(HostI2CTest, MultipleAddresses) { + const uint16_t address1{0x50}; + const uint16_t address2{0x51}; + const std::array data1{0xAA, 0xBB, 0xCC}; + const std::array data2{0x11, 0x22, 0x33, 0x44}; + + // Send to first address + auto send1_result = i2c_->SendData(address1, data1); + ASSERT_TRUE(send1_result); + + // Send to second address + auto send2_result = i2c_->SendData(address2, data2); + ASSERT_TRUE(send2_result); + + // Receive from first address + auto recv1_result = i2c_->ReceiveData(address1, data1.size()); + ASSERT_TRUE(recv1_result); + const auto received1_span = recv1_result.value(); + EXPECT_EQ(received1_span.size(), data1.size()); + EXPECT_TRUE(std::equal(received1_span.begin(), received1_span.end(), + data1.begin(), data1.end())); + + // Receive from second address + auto recv2_result = i2c_->ReceiveData(address2, data2.size()); + ASSERT_TRUE(recv2_result); + const auto received2_span = recv2_result.value(); + EXPECT_EQ(received2_span.size(), data2.size()); + EXPECT_TRUE(std::equal(received2_span.begin(), received2_span.end(), + data2.begin(), data2.end())); +} + +TEST_F(HostI2CTest, ReceiveWithoutSend) { + const uint16_t device_address{0x60}; + + // Try to receive from device that has no data + auto result = i2c_->ReceiveData(device_address, 10); + ASSERT_TRUE(result); + + // Should return empty span + const auto received_span = result.value(); + EXPECT_EQ(received_span.size(), 0); +} + +TEST_F(HostI2CTest, ReceivePartialData) { + const uint16_t device_address{0x70}; + const std::array send_data{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; + + // Send 10 bytes + auto send_result = i2c_->SendData(device_address, send_data); + ASSERT_TRUE(send_result); + + // Request only 5 bytes + auto recv_result = i2c_->ReceiveData(device_address, 5); + ASSERT_TRUE(recv_result); + + const auto received_span = recv_result.value(); + EXPECT_EQ(received_span.size(), 5); + + // Should receive first 5 bytes + EXPECT_TRUE(std::equal(received_span.begin(), received_span.end(), + send_data.begin(), send_data.begin() + 5)); +} + +TEST_F(HostI2CTest, SendDataInterrupt) { + const uint16_t device_address{0x42}; + const std::array send_data{0xAA, 0xBB, 0xCC}; + + bool callback_called{false}; + std::expected callback_result{}; + + auto result = + i2c_->SendDataInterrupt(device_address, send_data, + [&callback_called, &callback_result]( + std::expected result) { + callback_called = true; + callback_result = result; + }); + + EXPECT_TRUE(result); + EXPECT_TRUE(callback_called); + EXPECT_TRUE(callback_result); +} + +TEST_F(HostI2CTest, ReceiveDataInterrupt) { + const uint16_t device_address{0x50}; + const std::array send_data{0x01, 0x02, 0x03, 0x04}; + + // First send data + auto send_result = i2c_->SendData(device_address, send_data); + ASSERT_TRUE(send_result); + + bool callback_called{false}; + std::expected, common::Error> callback_result{ + std::unexpected(common::Error::kUnknown)}; + + auto result = i2c_->ReceiveDataInterrupt( + device_address, send_data.size(), + [&callback_called, &callback_result]( + std::expected, common::Error> result) { + callback_called = true; + callback_result = result; + }); + + EXPECT_TRUE(result); + EXPECT_TRUE(callback_called); + ASSERT_TRUE(callback_result); + + const auto received_span = callback_result.value(); + EXPECT_EQ(received_span.size(), send_data.size()); + EXPECT_TRUE(std::equal(received_span.begin(), received_span.end(), + send_data.begin(), send_data.end())); +} + +TEST_F(HostI2CTest, SendDataDma) { + const uint16_t device_address{0x42}; + const std::array send_data{0xDE, 0xAD, 0xBE}; + + bool callback_called{false}; + std::expected callback_result{}; + + auto result = + i2c_->SendDataDma(device_address, send_data, + [&callback_called, &callback_result]( + std::expected result) { + callback_called = true; + callback_result = result; + }); + + EXPECT_TRUE(result); + EXPECT_TRUE(callback_called); + EXPECT_TRUE(callback_result); +} + +TEST_F(HostI2CTest, ReceiveDataDma) { + const uint16_t device_address{0x55}; + const std::array send_data{0x10, 0x20, 0x30, 0x40, 0x50}; + + // First send data + auto send_result = i2c_->SendData(device_address, send_data); + ASSERT_TRUE(send_result); + + bool callback_called{false}; + std::expected, common::Error> callback_result{ + std::unexpected(common::Error::kUnknown)}; + + auto result = i2c_->ReceiveDataDma( + device_address, send_data.size(), + [&callback_called, &callback_result]( + std::expected, common::Error> result) { + callback_called = true; + callback_result = result; + }); + + EXPECT_TRUE(result); + EXPECT_TRUE(callback_called); + ASSERT_TRUE(callback_result); + + const auto received_span = callback_result.value(); + EXPECT_EQ(received_span.size(), send_data.size()); + EXPECT_TRUE(std::equal(received_span.begin(), received_span.end(), + send_data.begin(), send_data.end())); +} diff --git a/src/libs/mcu/i2c.hpp b/src/libs/mcu/i2c.hpp index 31d156a..d5e2973 100644 --- a/src/libs/mcu/i2c.hpp +++ b/src/libs/mcu/i2c.hpp @@ -2,8 +2,11 @@ #include #include +#include #include +#include "libs/common/error.hpp" + namespace mcu { class I2CController { @@ -11,27 +14,28 @@ class I2CController { virtual ~I2CController() = default; virtual auto SendData(uint16_t address, std::span data) - -> std::expected = 0; + -> std::expected = 0; virtual auto ReceiveData(uint16_t address, size_t size) - -> std::expected, int> = 0; + -> std::expected, common::Error> = 0; - virtual auto SendDataInterrupt(uint16_t address, - std::span data, - void (*callback)(std::expected)) - -> std::expected = 0; + virtual auto SendDataInterrupt( + uint16_t address, std::span data, + std::function)> callback) + -> std::expected = 0; virtual auto ReceiveDataInterrupt( uint16_t address, size_t size, - void (*callback)(std::expected, int>)) - -> std::expected = 0; + std::function, common::Error>)> + callback) -> std::expected = 0; - virtual auto SendDataDma(uint16_t address, std::span data, - void (*callback)(std::expected)) - -> std::expected = 0; + virtual auto SendDataDma( + uint16_t address, std::span data, + std::function)> callback) + -> std::expected = 0; virtual auto ReceiveDataDma( uint16_t address, size_t size, - void (*callback)(std::expected, int>)) - -> std::expected = 0; + std::function, common::Error>)> + callback) -> std::expected = 0; }; } // namespace mcu diff --git a/src/libs/mcu/pin.hpp b/src/libs/mcu/pin.hpp index c4935f3..9e2a70e 100644 --- a/src/libs/mcu/pin.hpp +++ b/src/libs/mcu/pin.hpp @@ -26,6 +26,7 @@ class OutputPin : public virtual InputPin { virtual auto SetHigh() -> std::expected = 0; virtual auto SetLow() -> std::expected = 0; + virtual auto Toggle() -> std::expected = 0; }; class BidirectionalPin : public virtual InputPin, public virtual OutputPin {