# MAG160C Linux SDK Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox syntax for tracking. **Goal:** Build an original Linux SDK with a C++ core, stable C ABI, CLI diagnostics, and Python-callable wrapper for the MAG160C thermal camera kit. **Architecture:** Implement a C++17 core around libusb-1.0 and expose a C ABI from include/mag160c/mag160c.h. The CLI and Python wrapper both call the C ABI. TCM commands are implemented from recovered frame evidence; IR private streaming paths expose safe probe behavior and explicit protocol-unknown errors until USB traces provide the missing command packets. **Tech Stack:** CMake, C++17, C ABI, libusb-1.0, Python ctypes, pytest, CTest. ## Global Constraints - Use only original source code; do not link proprietary SDK binaries. - Current workspace is not a git repository; replace commit steps with verification plus updates to progress.md. - Keep public C ABI free of C++ types. - Use C++17. - libusb-1.0 is the only native runtime dependency for the core library. - Firmware update commands are not exposed as an easy accidental CLI action. - IR protocol gaps return MAG160C_ERR_PROTOCOL_UNKNOWN or MAG160C_ERR_UNSUPPORTED. --- ## File Structure Create: - CMakeLists.txt: root build, options, targets, CTest. - include/mag160c/mag160c.h: stable C ABI. - src/core/error.hpp and src/core/error.cpp: thread-local error messages and error names. - src/core/context.hpp and src/core/context.cpp: opaque context and libusb lifetime. - src/core/device.hpp and src/core/device.cpp: USB descriptor models and endpoint discovery. - src/core/tcm_frame.hpp and src/core/tcm_frame.cpp: TCM frame encode/decode/checksum. - src/core/tcm_device.hpp and src/core/tcm_device.cpp: TCM command payload builders. - src/core/ir_device.hpp and src/core/ir_device.cpp: IR camera probe/skeleton operations. - src/c_api.cpp: C ABI implementation over the C++ core. - tools/mag160c_cli.cpp: CLI entry point. - tests/cpp/test_c_api.cpp, tests/cpp/test_tcm_frame.cpp, tests/cpp/test_tcm_device.cpp, tests/cpp/test_device_model.cpp, tests/cpp/test_ir_skeleton.cpp. - python/mag160c/__init__.py, python/mag160c/core.py, python/mag160c/tcm.py, python/mag160c/ir.py. - python/tests/test_python_wrapper.py. - docs/linux-hardware-test.md and README.md. --- ### Task 1: Build scaffold and public C ABI lifecycle **Files:** - Create: CMakeLists.txt - Create: include/mag160c/mag160c.h - Create: src/core/error.hpp - Create: src/core/error.cpp - Create: src/core/context.hpp - Create: src/core/context.cpp - Create: src/c_api.cpp - Create: tests/cpp/test_c_api.cpp **Interfaces:** - Produces: mag160c_error_t, mag160c_context_t, mag160c_init, mag160c_shutdown, mag160c_last_error, mag160c_error_name. - [ ] Step 1: Create failing lifecycle test in tests/cpp/test_c_api.cpp. Use these assertions: ~~~cpp assert(std::strcmp(mag160c_error_name(MAG160C_OK), "MAG160C_OK") == 0); assert(std::strcmp(mag160c_error_name(MAG160C_ERR_PROTOCOL_UNKNOWN), "MAG160C_ERR_PROTOCOL_UNKNOWN") == 0); assert(mag160c_init(nullptr) == MAG160C_ERR_INVALID_ARGUMENT); mag160c_context_t* ctx = nullptr; assert(mag160c_init(&ctx) == MAG160C_OK); assert(ctx != nullptr); mag160c_shutdown(ctx); ~~~ - [ ] Step 2: Create CMakeLists.txt. Required target behavior: ~~~cmake cmake_minimum_required(VERSION 3.16) project(mag160c VERSION 0.1.0 LANGUAGES C CXX) option(MAG160C_BUILD_TESTS "Build MAG160C tests" ON) option(MAG160C_BUILD_CLI "Build MAG160C CLI" ON) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) find_package(PkgConfig QUIET) if(PkgConfig_FOUND) pkg_check_modules(LIBUSB QUIET libusb-1.0) endif() add_library(mag160c_core SHARED src/core/error.cpp src/core/context.cpp src/c_api.cpp) target_include_directories(mag160c_core PUBLIC include PRIVATE src) if(LIBUSB_FOUND) target_include_directories(mag160c_core PRIVATE ${LIBUSB_INCLUDE_DIRS}) target_link_libraries(mag160c_core PRIVATE ${LIBUSB_LIBRARIES}) target_compile_definitions(mag160c_core PRIVATE MAG160C_HAS_LIBUSB=1) else() target_compile_definitions(mag160c_core PRIVATE MAG160C_HAS_LIBUSB=0) endif() if(MAG160C_BUILD_TESTS) enable_testing() add_executable(test_c_api tests/cpp/test_c_api.cpp) target_link_libraries(test_c_api PRIVATE mag160c_core) add_test(NAME test_c_api COMMAND test_c_api) endif() ~~~ - [ ] Step 3: Create include/mag160c/mag160c.h with opaque handles and error enum. Required declarations: ~~~c typedef enum mag160c_error_t { MAG160C_OK = 0, MAG160C_ERR_INVALID_ARGUMENT = 1, MAG160C_ERR_NO_DEVICE = 2, MAG160C_ERR_PERMISSION = 3, MAG160C_ERR_USB = 4, MAG160C_ERR_TIMEOUT = 5, MAG160C_ERR_CHECKSUM = 6, MAG160C_ERR_PROTOCOL_UNKNOWN = 7, MAG160C_ERR_UNSUPPORTED = 8, MAG160C_ERR_INTERNAL = 9 } mag160c_error_t; typedef struct mag160c_context_t mag160c_context_t; typedef struct mag160c_ir_device_t mag160c_ir_device_t; typedef struct mag160c_tcm_device_t mag160c_tcm_device_t; mag160c_error_t mag160c_init(mag160c_context_t** out_ctx); void mag160c_shutdown(mag160c_context_t* ctx); const char* mag160c_last_error(void); const char* mag160c_error_name(mag160c_error_t code); ~~~ - [ ] Step 4: Implement src/core/error.* and src/core/context.*. Required behavior: - last error is thread-local; - mag160c_error_name returns stable string names; - Context construction succeeds without hardware; - when libusb is absent at build time, Context records has_libusb as false but still constructs. - [ ] Step 5: Implement src/c_api.cpp lifecycle. Required behavior: - mag160c_init rejects null output pointer; - mag160c_init allocates a context; - mag160c_shutdown accepts null through delete behavior; - mag160c_last_error returns an empty string after successful init. - [ ] Step 6: Verify. Run: ~~~powershell cmake -S . -B build -DMAG160C_BUILD_TESTS=ON cmake --build build ctest --test-dir build --output-on-failure ~~~ Expected: test_c_api passes. - [ ] Step 7: Record checkpoint in progress.md. Append: ~~~markdown - Implemented Task 1 scaffold: CMake, public C ABI lifecycle, error names, and lifecycle tests. ~~~ --- ### Task 2: TCM frame codec **Files:** - Create: src/core/tcm_frame.hpp - Create: src/core/tcm_frame.cpp - Create: tests/cpp/test_tcm_frame.cpp - Modify: CMakeLists.txt - Modify: include/mag160c/mag160c.h - Modify: src/c_api.cpp **Interfaces:** - Produces: mag160c::core::TcmFrame, encode_tcm_frame, decode_tcm_frame, mag160c_tcm_encode_frame, mag160c_tcm_decode_header. - [ ] Step 1: Create byte-exact codec tests. Required exact frame for rotate +5 frame id 1: ~~~text 7e 00 07 85 02 77 00 01 00 05 7f ~~~ Test cases: - encode_tcm_frame(0x02, 0x77, 0x0001, {0x00, 0x05}) equals the exact bytes above; - corrupting byte 3 returns MAG160C_ERR_CHECKSUM; - decoding a valid frame returns main 0x02, sub 0x77, frame id 1, payload {0x00, 0x05}. - [ ] Step 2: Implement src/core/tcm_frame.hpp. Required declarations: ~~~cpp struct TcmFrame { uint8_t main_cmd = 0; uint8_t sub_cmd = 0; uint16_t frame_id = 0; std::vector payload; }; uint8_t checksum(const uint8_t* data, size_t begin, size_t end); std::vector encode_tcm_frame(uint8_t main_cmd, uint8_t sub_cmd, uint16_t frame_id, const std::vector& payload); mag160c_error_t decode_tcm_frame(const uint8_t* data, size_t size, TcmFrame* out); ~~~ - [ ] Step 3: Implement src/core/tcm_frame.cpp. Rules: - header byte 0 is 0x7e; - bytes 1..2 are big-endian body length; - byte 3 is additive checksum over bytes 0..2; - body is main, sub, frame high, frame low, payload, body checksum; - body checksum is additive checksum over body bytes before the final checksum byte. - [ ] Step 4: Add C ABI frame utilities. Required declarations: ~~~c mag160c_error_t mag160c_tcm_encode_frame(uint8_t main_cmd, uint8_t sub_cmd, uint16_t frame_id, const uint8_t* payload, size_t payload_size, uint8_t* out_bytes, size_t out_capacity, size_t* out_size); mag160c_error_t mag160c_tcm_decode_header(const uint8_t* data, size_t size, uint8_t* out_main_cmd, uint8_t* out_sub_cmd, uint16_t* out_frame_id, size_t* out_payload_size); ~~~ - [ ] Step 5: Update build. Add src/core/tcm_frame.cpp to mag160c_core and add test_tcm_frame to CTest. - [ ] Step 6: Verify. Run: ~~~powershell cmake --build build ctest --test-dir build --output-on-failure ~~~ Expected: test_c_api and test_tcm_frame pass. - [ ] Step 7: Record checkpoint. Append: ~~~markdown - Implemented Task 2 TCM frame codec with byte-exact encode/decode tests and C ABI utility functions. ~~~ --- ### Task 3: TCM command builders **Files:** - Create: src/core/transport.hpp - Create: src/core/tcm_device.hpp - Create: src/core/tcm_device.cpp - Create: tests/cpp/test_tcm_device.cpp - Modify: CMakeLists.txt - Modify: include/mag160c/mag160c.h - Modify: src/c_api.cpp **Interfaces:** - Produces: Transport, TcmCommandBuilder, mag160c_tcm_build_rotate_frame, mag160c_tcm_build_light_frame. - [ ] Step 1: Create tests for known commands. Required expected outputs: - rotate +5: 7e 00 07 85 02 77 00 01 00 05 7f - after one command, rotate -3 with frame id 2: 7e 00 07 85 02 77 00 02 01 03 7f - green blink: 7e 00 09 87 02 32 00 01 01 00 ff 00 35 - [ ] Step 2: Create src/core/transport.hpp. Required interface: ~~~cpp class Transport { public: virtual ~Transport() = default; virtual mag160c_error_t write(const std::vector& bytes) = 0; virtual mag160c_error_t read(std::vector* out, int timeout_ms) = 0; }; ~~~ - [ ] Step 3: Create TcmCommandBuilder. Required behavior: - frame id starts at 1; - increments after each encoded frame; - wraps to 1 before reaching 0x8000; - rotate clamps input to [-128, 128]; - rotate payload is direction and magnitude, direction 0 for positive, 1 for negative; - light payload is [on, red, green, blue]; - red/green/blue/yellow channel value is 0xff; - off payload is [0,0,0,0]; - steady sub 0x31, blink sub 0x32, breath sub 0x33. - [ ] Step 4: Add C ABI dry-run builders. Required declarations: ~~~c typedef enum mag160c_tcm_light_color_t { MAG160C_TCM_LIGHT_OFF = 0, MAG160C_TCM_LIGHT_RED = 1, MAG160C_TCM_LIGHT_GREEN = 2, MAG160C_TCM_LIGHT_BLUE = 3, MAG160C_TCM_LIGHT_YELLOW = 4 } mag160c_tcm_light_color_t; typedef enum mag160c_tcm_light_mode_t { MAG160C_TCM_LIGHT_STEADY = 0, MAG160C_TCM_LIGHT_BLINK = 1, MAG160C_TCM_LIGHT_BREATH = 2 } mag160c_tcm_light_mode_t; mag160c_error_t mag160c_tcm_build_rotate_frame(int angle, uint8_t* out_bytes, size_t out_capacity, size_t* out_size); mag160c_error_t mag160c_tcm_build_light_frame(mag160c_tcm_light_color_t color, mag160c_tcm_light_mode_t mode, uint8_t* out_bytes, size_t out_capacity, size_t* out_size); ~~~ - [ ] Step 5: Verify. Run: ~~~powershell cmake --build build ctest --test-dir build --output-on-failure ~~~ Expected: test_tcm_device and previous tests pass. - [ ] Step 6: Record checkpoint. Append: ~~~markdown - Implemented Task 3 TCM command builder, dry-run C ABI helpers, and command payload tests. ~~~ --- ### Task 4: USB discovery and endpoint model **Files:** - Create: src/core/device.hpp - Create: src/core/device.cpp - Create: tests/cpp/test_device_model.cpp - Modify: CMakeLists.txt - Modify: include/mag160c/mag160c.h - Modify: src/c_api.cpp **Interfaces:** - Produces: EndpointPair, DeviceInfo, find_bulk_pair, mag160c_list_devices, mag160c_free_device_list. - [ ] Step 1: Create endpoint-selection tests. Cases: - endpoints {0x01 interrupt, 0x82 bulk, 0x03 bulk} returns bulk_in 0x82 and bulk_out 0x03; - endpoints {0x82 bulk} returns false because bulk OUT is absent. - [ ] Step 2: Implement device model. Required constants and types: ~~~cpp constexpr uint16_t MAG_IR_VENDOR_ID = 0x833c; constexpr uint16_t MAG_IR_PRODUCT_ID = 0x0001; enum class EndpointType { Other, Bulk, Interrupt, Isochronous }; struct EndpointDescriptor { uint8_t address = 0; EndpointType type = EndpointType::Other; }; struct EndpointPair { uint8_t bulk_in = 0; uint8_t bulk_out = 0; }; struct DeviceInfo { mag160c_device_info_t c_info{}; }; ~~~ - [ ] Step 3: Implement libusb enumeration. Required behavior: - if libusb is unavailable at build time, list_devices returns MAG160C_ERR_UNSUPPORTED; - if libusb is available and no matching device is present, list_devices returns MAG160C_OK with count 0; - only VID 0x833C and PID 0x0001 are matched initially; - first interface with both bulk IN and bulk OUT is reported. - [ ] Step 4: Add C ABI device list. Required declarations: ~~~c typedef struct mag160c_device_info_t { uint16_t vendor_id; uint16_t product_id; uint8_t bus; uint8_t address; uint8_t interface_number; uint8_t bulk_in_endpoint; uint8_t bulk_out_endpoint; char product[128]; char manufacturer[128]; char serial[128]; } mag160c_device_info_t; mag160c_error_t mag160c_list_devices(mag160c_context_t* ctx, mag160c_device_info_t** out_devices, size_t* out_count); void mag160c_free_device_list(mag160c_device_info_t* devices); ~~~ - [ ] Step 5: Verify. Run: ~~~powershell cmake --build build ctest --test-dir build --output-on-failure ~~~ Expected: device model tests pass without hardware. - [ ] Step 6: Record checkpoint. Append: ~~~markdown - Implemented Task 4 USB device model, endpoint-pair selection, and C ABI device listing/freeing. ~~~ --- ### Task 5: IR camera skeleton with explicit protocol gaps **Files:** - Create: src/core/ir_device.hpp - Create: src/core/ir_device.cpp - Create: tests/cpp/test_ir_skeleton.cpp - Modify: CMakeLists.txt - Modify: include/mag160c/mag160c.h - Modify: src/c_api.cpp **Interfaces:** - Produces: mag160c_ir_open_first, mag160c_ir_close, mag160c_ir_get_info, mag160c_ir_trigger_ffc, mag160c_ir_read_raw_once. - [x] Step 1: Create no-hardware-safe IR tests. Cases: - mag160c_ir_open_first(nullptr, nullptr) returns MAG160C_ERR_INVALID_ARGUMENT; - mag160c_ir_close(nullptr) is safe; - mag160c_ir_get_info(nullptr, nullptr) returns MAG160C_ERR_INVALID_ARGUMENT; - trigger_ffc on an opened skeleton device returns MAG160C_ERR_PROTOCOL_UNKNOWN. - [x] Step 2: Add public IR info struct and declarations. Required struct fields: ~~~c typedef struct mag160c_ir_info_t { uint16_t width; uint16_t height; uint16_t output_width; uint16_t output_height; uint8_t max_fps; uint8_t current_fps; char name[64]; char type[64]; } mag160c_ir_info_t; ~~~ Required functions: ~~~c mag160c_error_t mag160c_ir_open_first(mag160c_context_t* ctx, mag160c_ir_device_t** out_device); void mag160c_ir_close(mag160c_ir_device_t* device); mag160c_error_t mag160c_ir_get_info(mag160c_ir_device_t* device, mag160c_ir_info_t* out_info); mag160c_error_t mag160c_ir_trigger_ffc(mag160c_ir_device_t* device); mag160c_error_t mag160c_ir_read_raw_once(mag160c_ir_device_t* device, uint8_t* out_bytes, size_t out_capacity, size_t* out_size, int timeout_ms); ~~~ - [x] Step 3: Implement IR skeleton. Required behavior: - info returns width 160, height 120, output dimensions 160x120, name MAG160C, type vendor-bulk-ir; - trigger_ffc returns MAG160C_ERR_PROTOCOL_UNKNOWN with an explanatory last error; - read_raw_once sets out_size to 0 and returns MAG160C_ERR_PROTOCOL_UNKNOWN. - [x] Step 4: Verify. Run: ~~~powershell cmake --build build ctest --test-dir build --output-on-failure ~~~ Expected: IR skeleton tests and previous tests pass. - [x] Step 5: Record checkpoint. Append: ~~~markdown - Implemented Task 5 IR API skeleton with explicit protocol-unknown errors for unrecovered private IR commands. ~~~ --- ### Task 6: CLI diagnostics and dry-run commands **Files:** - Create: tools/mag160c_cli.cpp - Modify: CMakeLists.txt **Interfaces:** - Produces: mag160c-cli probe, mag160c-cli tcm-rotate --dry-run ANGLE, mag160c-cli tcm-light --dry-run COLOR MODE, mag160c-cli ir-info. - [x] Step 1: Implement CLI parser. Required command behavior: - no args prints usage and exits 1; - probe prints devices count and per-device VID/PID/bus/address/interface/endpoints; - tcm-rotate --dry-run 5 prints 7e 00 07 85 02 77 00 01 00 05 7f; - tcm-light --dry-run green blink prints 7e 00 09 87 02 32 00 01 01 00 ff 00 35; - ir-info prints MAG160C skeleton info. - [x] Step 2: Add CLI target. Add executable target mag160c-cli linked against mag160c_core when MAG160C_BUILD_CLI is ON. - [x] Step 3: Verify. Run: ~~~powershell cmake --build build .uildmag160c-cli.exe tcm-rotate --dry-run 5 .uildmag160c-cli.exe tcm-light --dry-run green blink .uildmag160c-cli.exe ir-info ~~~ If using a multi-config generator, use .\build\Debug\mag160c-cli.exe. Expected dry-run outputs match Step 1. - [x] Step 4: Record checkpoint. Append: ~~~markdown - Implemented Task 6 mag160c-cli with probe, TCM dry-run commands, and IR info diagnostics. ~~~ --- ### Task 7: Python ctypes wrapper **Files:** - Create: python/mag160c/__init__.py - Create: python/mag160c/core.py - Create: python/mag160c/tcm.py - Create: python/mag160c/ir.py - Create: python/tests/test_python_wrapper.py **Interfaces:** - Produces: mag160c.tcm.build_rotate_frame(angle), mag160c.tcm.build_light_frame(color, mode), mag160c.ir.info(). - [ ] Step 1: Implement core.py shared-library loading. Required behavior: - load MAG160C_LIBRARY env var first when set; - otherwise search build, build/Debug, build/Release, and current directory; - configure ctypes signatures for mag160c_error_name, mag160c_last_error, mag160c_tcm_build_rotate_frame, mag160c_tcm_build_light_frame; - raise Mag160CError with C error name and last error. - [ ] Step 2: Implement tcm.py. Required behavior: ~~~python assert build_rotate_frame(5) == bytes.fromhex("7e0007850277000100057f") assert build_light_frame("green", "blink") == bytes.fromhex("7e000987023200010100ff0035") ~~~ Invalid color or mode raises ValueError. - [ ] Step 3: Implement ir.py. Required behavior: - info() returns dict with name MAG160C, type vendor-bulk-ir, width 160, height 120, and status string describing that private streaming packets are not recovered. - [ ] Step 4: Add pytest coverage. Test: - rotate bytes exact; - green blink bytes exact; - invalid color raises ValueError. - [ ] Step 5: Verify. Run: ~~~powershell $env:PYTHONPATH = "C:\Project\MAG160C\python" python -m pytest python\tests -q ~~~ Expected: Python tests pass when shared library is in a searched build directory or MAG160C_LIBRARY points to it. - [ ] Step 6: Record checkpoint. Append: ~~~markdown - Implemented Task 7 Python ctypes wrapper for TCM dry-run helpers and initial IR status helper. ~~~ --- ### Task 8: Hardware-test docs and final no-hardware verification **Files:** - Create: docs/linux-hardware-test.md - Create or modify: README.md - Modify: progress.md - Modify: task_plan.md **Interfaces:** - Consumes: all previous tasks. - Produces: reproducible no-hardware verification and hardware checklist. - [ ] Step 1: Create docs/linux-hardware-test.md. Required sections: - no-hardware build and test commands; - udev rule for VID 833c PID 0001; - probe command and expected outputs; - safe TCM checks limited to dry-run commands in the first release; - IR checks that state full streaming requires recovered private command packets; - evidence to preserve from future hardware captures: lsusb -v, mag160c-cli probe, Windows USBPcap traces, raw Linux bulk bytes. - [ ] Step 2: Create README.md if absent. Required sections: - current scope; - build commands; - CLI examples; - Python examples; - link to docs/linux-hardware-test.md. - [ ] Step 3: Run full verification. Run: ~~~powershell cmake -S . -B build -DMAG160C_BUILD_TESTS=ON -DMAG160C_BUILD_CLI=ON cmake --build build ctest --test-dir build --output-on-failure $env:PYTHONPATH = "C:\Project\MAG160C\python" python -m pytest python\tests -q ~~~ Expected: - all CTest tests pass; - Python tests pass; - CLI dry-run examples print exact bytes from Task 6. - [ ] Step 4: Update planning files. In task_plan.md: - mark Phase 6 complete after implementation builds and no-hardware tests pass; - mark Phase 7 complete after docs/linux-hardware-test.md exists and verification commands are recorded. Append to progress.md: ~~~markdown - Implemented Task 8 documentation and ran full no-hardware verification for C++ tests, CLI dry-run examples, and Python wrapper tests. ~~~ --- ## Self-Review Checklist - Spec coverage: - C++ core library: Tasks 1 through 5. - Stable C ABI: Tasks 1 through 5. - CLI: Task 6. - Python-callable layer: Task 7. - No proprietary SDK linkage: Global Constraints and CMake design. - TCM known commands: Tasks 2 and 3. - IR protocol gaps: Task 5. - Tests without hardware: Tasks 1 through 8. - Hardware docs: Task 8. - Type consistency: - Public C handles use mag160c_context_t, mag160c_ir_device_t, and mag160c_tcm_device_t. - Error codes match mag160c_error_t. - TCM dry-run helpers use uint8_t* out_bytes, size_t out_capacity, and size_t* out_size. - Scope check: - Firmware update packet construction is not surfaced in the first easy CLI path. - IR streaming parity is not claimed without recovered packet evidence.