diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..97991fb --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# MakerPlane CAN-FIX Arduino Library + +**Status:** Open Source — Experimental Amateur-Built Category +**License:** See COPYING +**Protocol:** CAN-FIX (CAN bus implementation of the Flight Information eXchange protocol) + +--- + +## What This Is + +This is an Arduino library that enables Arduino-based devices to participate in a **CAN-FIX avionics network**. CAN-FIX is an open-source protocol designed specifically for Experimental Amateur-Built (E-AB) aircraft to exchange flight data between avionics nodes in a vendor-neutral way. + +With this library, Arduino hardware can: +- Read and write named flight data parameters (airspeed, altitude, heading, engine data, etc.) on the CAN bus +- Act as a sensor node, sending data from connected hardware into the avionics network +- Act as an actuator or display node, receiving and responding to parameter updates +- Interoperate with other CAN-FIX devices including FIX-Gateway, pyEfis, and custom electronics + +## Protocol Background + +**FIX** (Flight Information eXchange) is a family of open, Creative Commons-licensed specifications for aircraft avionics communication. **CAN-FIX** is the CAN bus implementation of FIX. It is designed to: + +- Allow any builder to construct devices that communicate with other FIX-compatible hardware without licensing fees +- Provide a standard parameter namespace covering the full range of aircraft state (position, attitude, airspeed, engine data, control surfaces, systems status) +- Enable redundancy through multiple nodes publishing the same parameter type on separate identifiers + +See the [canfix-spec](../canfix-spec) repository for the full protocol specification. + +## Repository Contents + +| File | Description | +|---|---| +| `canfix.h` | Library header — parameter definitions, message structures, node API | +| `canfix.cpp` | Library implementation | +| `examples/` | Example Arduino sketches demonstrating common use patterns | +| `keywords.txt` | Arduino IDE syntax highlighting keywords | + +## Installation + +Follow standard Arduino library installation: +1. Download or clone this repository +2. Copy the folder into your Arduino `libraries/` directory +3. Restart the Arduino IDE +4. Access the library under **Sketch → Include Library → CAN-FIX** + +For detailed installation guidance see: http://arduino.cc/en/Guide/Libraries + +## Hardware Requirements + +- Any Arduino board with a CAN controller, or an Arduino paired with a CAN transceiver module (MCP2515-based shields are common in the MakerPlane ecosystem) +- CAN bus wiring per the CAN-FIX physical layer spec (120Ω termination at each end) + +## Important Disclaimer + +> **This library is experimental and is not suited for primary or backup flight/engine instrumentation or navigation. Use at your own risk.** +> For Experimental Amateur-Built aircraft use only. Not FAA-approved avionics software. diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..2799836 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,47 @@ +cmake_minimum_required(VERSION 3.12) +project(canfix_tests CXX) + +set(CMAKE_CXX_STANDARD 14) + +# ── GoogleTest via FetchContent ─────────────────────────────────────────────── +include(FetchContent) +FetchContent_Declare( + googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG release-1.12.1 + GIT_SHALLOW TRUE +) +# Prevent overriding the parent project's compiler/linker settings on Windows +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +FetchContent_GetProperties(googletest) +if(NOT googletest_POPULATED) + FetchContent_Populate(googletest) + add_subdirectory(${googletest_SOURCE_DIR} ${googletest_BINARY_DIR}) +endif() + +# ── canfix library (compiled with mock headers) ─────────────────────────────── +add_library(canfix_lib STATIC + ../canfix.cpp + mock_impl.cpp +) +target_include_directories(canfix_lib PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/mocks # provides can.h, EEPROM.h + ${CMAKE_CURRENT_SOURCE_DIR}/.. # provides canfix.h +) + +# ── Test executable ─────────────────────────────────────────────────────────── +add_executable(canfix_tests + test_canfix.cpp +) +target_include_directories(canfix_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/mocks + ${CMAKE_CURRENT_SOURCE_DIR}/.. +) +target_link_libraries(canfix_tests + canfix_lib + GTest::gtest_main +) + +# ── CTest integration ───────────────────────────────────────────────────────── +include(GoogleTest) +gtest_discover_tests(canfix_tests) diff --git a/tests/mock_impl.cpp b/tests/mock_impl.cpp new file mode 100644 index 0000000..660fcd4 --- /dev/null +++ b/tests/mock_impl.cpp @@ -0,0 +1,8 @@ +/* + * Global mock object instances — compiled once, linked with all test objects. + */ +#include "mocks/can.h" +#include "mocks/EEPROM.h" + +MockCanState g_can; +EEPROMClass EEPROM; diff --git a/tests/mocks/EEPROM.h b/tests/mocks/EEPROM.h new file mode 100644 index 0000000..e0be0b1 --- /dev/null +++ b/tests/mocks/EEPROM.h @@ -0,0 +1,28 @@ +/* + * Mock EEPROM.h for unit testing canfix.cpp outside of the Arduino environment. + * Simulates a 512-byte EEPROM with read/write and the bitRead/bitSet/bitClear macros. + */ +#pragma once +#include +#include + +#define EEPROM_SIZE 512 + +struct EEPROMClass { + uint8_t data[EEPROM_SIZE]; + + EEPROMClass() { memset(data, 0xFF, sizeof(data)); } + + uint8_t read(int addr) { return data[addr]; } + void write(int addr, uint8_t v) { data[addr] = v; } + + void reset() { memset(data, 0xFF, sizeof(data)); } +}; + +extern EEPROMClass EEPROM; + +// Arduino bit manipulation macros +#define bitRead(value, bit) (((value) >> (bit)) & 0x01) +#define bitSet(value, bit) ((value) |= (1UL << (bit))) +#define bitClear(value, bit) ((value) &= ~(1UL << (bit))) +#define bitWrite(value, bit, bv) ((bv) ? bitSet(value, bit) : bitClear(value, bit)) diff --git a/tests/mocks/can.h b/tests/mocks/can.h new file mode 100644 index 0000000..4c1dc64 --- /dev/null +++ b/tests/mocks/can.h @@ -0,0 +1,57 @@ +/* + * Mock can.h for unit testing canfix.cpp outside of the Arduino environment. + * Records last frame written so tests can inspect it. + */ +#pragma once +#include + +typedef uint8_t byte; +typedef uint16_t word; + +struct CanFrame { + uint16_t id; + uint8_t eid; + uint8_t data[8]; + uint8_t length; +}; + +// Commands and modes used by canfix.cpp +#define CMD_RESET 0 +#define MODE_CONFIG 1 +#define MODE_NORMAL 2 + +// Recorded state for test inspection +struct MockCanState { + CanFrame last_written; + int write_count; + uint8_t rx_status; // returned by getRxStatus() + CanFrame rx_frame[2]; // frame available for reading + + MockCanState() : write_count(0), rx_status(0) { + last_written = {}; + rx_frame[0] = {}; + rx_frame[1] = {}; + } +}; + +extern MockCanState g_can; + +class CAN { +public: + explicit CAN(uint8_t pin) { (void)pin; } + void sendCommand(uint8_t cmd) { (void)cmd; } + void setBitRate(int rate) { (void)rate; } + void setMode(uint8_t mode) { (void)mode; } + + uint8_t writeFrame(CanFrame frame) { + g_can.last_written = frame; + g_can.write_count++; + return 0; // success + } + + uint8_t getRxStatus() { return g_can.rx_status; } + + CanFrame readFrame(uint8_t buffer) { + return g_can.rx_frame[buffer]; + } +}; diff --git a/tests/test_canfix.cpp b/tests/test_canfix.cpp new file mode 100644 index 0000000..93bbbea --- /dev/null +++ b/tests/test_canfix.cpp @@ -0,0 +1,277 @@ +/* + * GoogleTest suite for canfix.cpp. + * + * Covers: + * 1. CFParameter FCB bit-field pack/unpack (setMetaData/getMetaData, + * setFlags/getFlags) + * 2. CanFix::checkParameterEnable — EEPROM bitmask logic + * 3. CanFix::sendParam — CAN frame encoding + * 4. CanFix::handleFrame — ID-range routing to callbacks + * 5. CanFix::getNodeNumber — EEPROM fallback to device ID + */ +#include +#include "mocks/can.h" +#include "mocks/EEPROM.h" +#include "../canfix.h" + +// ── Test fixture with fresh mock state for each test ────────────────────────── + +class CanFixTest : public ::testing::Test { +protected: + void SetUp() override { + g_can = MockCanState{}; + EEPROM.reset(); + } +}; + +// ── CFParameter FCB field tests ─────────────────────────────────────────────── + +TEST(CFParameterTest, SetAndGetMetadata_Zero) { + CFParameter p; + p.fcb = 0xFF; + p.setMetaData(0); + EXPECT_EQ(p.getMetaData(), 0); + // Lower nibble must be preserved + EXPECT_EQ(p.fcb & 0x0F, 0x0F); +} + +TEST(CFParameterTest, SetAndGetMetadata_NonZero) { + CFParameter p; + p.fcb = 0x00; + p.setMetaData(0x0A); + EXPECT_EQ(p.getMetaData(), 0x0A); +} + +TEST(CFParameterTest, MetaDataMaxValue) { + CFParameter p; + p.fcb = 0x00; + p.setMetaData(0x0F); + EXPECT_EQ(p.getMetaData(), 0x0F); +} + +TEST(CFParameterTest, SetAndGetFlags_Zero) { + CFParameter p; + p.fcb = 0xFF; + p.setFlags(0); + EXPECT_EQ(p.getFlags(), 0); + // Upper nibble must be preserved + EXPECT_EQ((p.fcb >> 4) & 0x0F, 0x0F); +} + +TEST(CFParameterTest, SetAndGetFlags_NonZero) { + CFParameter p; + p.fcb = 0x00; + p.setFlags(0x07); + EXPECT_EQ(p.getFlags(), 0x07); +} + +TEST(CFParameterTest, FlagsOnlyStoreLowNibble) { + CFParameter p; + p.fcb = 0x00; + p.setFlags(0xFF); // only low 4 bits should stick + EXPECT_EQ(p.getFlags(), 0x0F); +} + +TEST(CFParameterTest, MetaAndFlagsAreIndependent) { + CFParameter p; + p.fcb = 0x00; + p.setMetaData(0x05); + p.setFlags(0x03); + EXPECT_EQ(p.getMetaData(), 0x05); + EXPECT_EQ(p.getFlags(), 0x03); + EXPECT_EQ(p.fcb, (uint8_t)((0x05 << 4) | 0x03)); +} + +// ── CanFix::checkParameterEnable ────────────────────────────────────────────── + +class CheckParamEnableTest : public CanFixTest { +protected: + // device id=1, pin=0; constructor calls EEPROM which is freshly reset + CanFix cf{0, 1}; +}; + +TEST_F(CheckParamEnableTest, AllEnabledByDefault) { + // EEPROM.reset() fills with 0xFF; bit=1 means disabled, so + // a fresh EEPROM has all parameters *disabled*. + // After reset to 0x00 they are enabled. + EEPROM.reset(); + for (int addr = 0; addr < EEPROM_SIZE; addr++) { + EEPROM.write(addr, 0x00); + } + // Parameter 256 → byte 32, bit 0 + EXPECT_NE(cf.checkParameterEnable(256), 0); +} + +TEST_F(CheckParamEnableTest, DisabledParameterReturnZero) { + // Disable parameter 256: id=256 → index=32, offset=0 + EEPROM.write(32, 0x01); // set bit 0 + EXPECT_EQ(cf.checkParameterEnable(256), 0); +} + +TEST_F(CheckParamEnableTest, EnabledParameterReturnNonZero) { + EEPROM.write(32, 0x00); // clear bit 0 + EXPECT_NE(cf.checkParameterEnable(256), 0); +} + +TEST_F(CheckParamEnableTest, ParameterAtBitOffset7) { + // Parameter 263: index=32, offset=7 + EEPROM.write(32, 0x00); + EXPECT_NE(cf.checkParameterEnable(263), 0); + EEPROM.write(32, 0x80); // set bit 7 + EXPECT_EQ(cf.checkParameterEnable(263), 0); +} + +TEST_F(CheckParamEnableTest, ParameterAtNextByte) { + // Parameter 264: index=33, offset=0 + EEPROM.write(33, 0x00); + EXPECT_NE(cf.checkParameterEnable(264), 0); + EEPROM.write(33, 0x01); + EXPECT_EQ(cf.checkParameterEnable(264), 0); +} + +// ── CanFix::sendParam — CAN frame encoding ──────────────────────────────────── + +class SendParamTest : public CanFixTest { +protected: + CanFix cf{0, 42}; // device id = 42 +}; + +TEST_F(SendParamTest, FrameIdMatchesParamType) { + CFParameter p; + p.type = 0x0100; // ID 256 + p.index = 0; + p.fcb = 0; + p.length = 0; + cf.sendParam(p); + EXPECT_EQ(g_can.last_written.id, 0x0100u); +} + +TEST_F(SendParamTest, FrameData0IsNodeNumber) { + // getNodeNumber() falls back to deviceid only when EEPROM[EE_NODE]==0x00. + // Write 0x00 explicitly to trigger the fallback to deviceid=42. + EEPROM.write(EE_NODE, 0x00); + CFParameter p; + p.type = 0x0100; p.index = 0; p.fcb = 0; p.length = 0; + cf.sendParam(p); + EXPECT_EQ(g_can.last_written.data[0], 42u); +} + +TEST_F(SendParamTest, FrameData1IsIndex) { + CFParameter p; + p.type = 0x0100; p.index = 7; p.fcb = 0; p.length = 0; + cf.sendParam(p); + EXPECT_EQ(g_can.last_written.data[1], 7u); +} + +TEST_F(SendParamTest, FrameData2IsFcb) { + CFParameter p; + p.type = 0x0100; p.index = 0; p.fcb = 0xAB; p.length = 0; + cf.sendParam(p); + EXPECT_EQ(g_can.last_written.data[2], 0xABu); +} + +TEST_F(SendParamTest, FrameLengthIsPayloadPlusThree) { + CFParameter p; + p.type = 0x0100; p.index = 0; p.fcb = 0; + p.length = 3; + p.data[0] = 0xDE; p.data[1] = 0xAD; p.data[2] = 0xBE; + cf.sendParam(p); + EXPECT_EQ(g_can.last_written.length, 6u); + EXPECT_EQ(g_can.last_written.data[3], 0xDEu); + EXPECT_EQ(g_can.last_written.data[4], 0xADu); + EXPECT_EQ(g_can.last_written.data[5], 0xBEu); +} + +// ── CanFix::handleFrame — ID routing ───────────────────────────────────────── + +static bool s_param_called; +static CFParameter s_last_param; +static bool s_alarm_called; + +static void param_cb(CFParameter p) { + s_param_called = true; + s_last_param = p; +} +static void alarm_cb(byte id, word type, byte *data, byte length) { + (void)id; (void)type; (void)data; (void)length; + s_alarm_called = true; +} + +class HandleFrameTest : public CanFixTest { +protected: + CanFix cf{0, 1}; + + void SetUp() override { + CanFixTest::SetUp(); + s_param_called = false; + s_alarm_called = false; + cf.set_param_callback(param_cb); + cf.set_alarm_callback(alarm_cb); + } +}; + +TEST_F(HandleFrameTest, ParameterFrameFiresParamCallback) { + // ID in range 256–0x6DF → parameter callback + CanFrame frame; + frame.id = 0x0100; // 256 + frame.data[0] = 1; // node + frame.data[1] = 0; // index + frame.data[2] = 0; // fcb + frame.data[3] = 0xAB; + frame.length = 4; + + g_can.rx_status = 0x40; // buffer 0 has data + g_can.rx_frame[0] = frame; + cf.exec(); + + EXPECT_TRUE(s_param_called); + EXPECT_EQ(s_last_param.type, 0x0100u); + EXPECT_EQ(s_last_param.node, 1u); + EXPECT_EQ(s_last_param.data[0], 0xABu); +} + +TEST_F(HandleFrameTest, AlarmFrameFiresAlarmCallback) { + CanFrame frame; + frame.id = 0x0001; // ID < 256 → alarm + frame.data[0] = 0; frame.data[1] = 0; + frame.length = 2; + + g_can.rx_status = 0x40; + g_can.rx_frame[0] = frame; + cf.exec(); + + EXPECT_TRUE(s_alarm_called); + EXPECT_FALSE(s_param_called); +} + +TEST_F(HandleFrameTest, IdZeroIgnored) { + CanFrame frame; + frame.id = 0x0000; + frame.length = 0; + + g_can.rx_status = 0x40; + g_can.rx_frame[0] = frame; + cf.exec(); + + EXPECT_FALSE(s_param_called); + EXPECT_FALSE(s_alarm_called); +} + +// ── CanFix::getNodeNumber — EEPROM fallback ─────────────────────────────────── + +TEST_F(CanFixTest, GetNodeNumberFallsBackToDeviceIdWhenEepromZero) { + EEPROM.write(EE_NODE, 0x00); + CanFix cf{0, 77}; + // Verify via sendParam: data[0] should be 77 + CFParameter p; p.type = 256; p.index = 0; p.fcb = 0; p.length = 0; + cf.sendParam(p); + EXPECT_EQ(g_can.last_written.data[0], 77u); +} + +TEST_F(CanFixTest, GetNodeNumberReadsFromEepromWhenNonZero) { + EEPROM.write(EE_NODE, 42); + CanFix cf{0, 99}; // device id = 99 but EEPROM overrides + CFParameter p; p.type = 256; p.index = 0; p.fcb = 0; p.length = 0; + cf.sendParam(p); + EXPECT_EQ(g_can.last_written.data[0], 42u); +}