Files
MAG160C/docs/superpowers/plans/2026-07-12-mag160c-linux-sdk.md

21 KiB

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:

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_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:

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:

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:

- 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:

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:

struct TcmFrame {
    uint8_t main_cmd = 0;
    uint8_t sub_cmd = 0;
    uint16_t frame_id = 0;
    std::vector<uint8_t> payload;
};

uint8_t checksum(const uint8_t* data, size_t begin, size_t end);
std::vector<uint8_t> encode_tcm_frame(uint8_t main_cmd, uint8_t sub_cmd, uint16_t frame_id, const std::vector<uint8_t>& 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:

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:

cmake --build build
ctest --test-dir build --output-on-failure

Expected: test_c_api and test_tcm_frame pass.

  • Step 7: Record checkpoint.

Append:

- 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:

class Transport {
public:
    virtual ~Transport() = default;
    virtual mag160c_error_t write(const std::vector<uint8_t>& bytes) = 0;
    virtual mag160c_error_t read(std::vector<uint8_t>* 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:

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:

cmake --build build
ctest --test-dir build --output-on-failure

Expected: test_tcm_device and previous tests pass.

  • Step 6: Record checkpoint.

Append:

- 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:

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:

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:

cmake --build build
ctest --test-dir build --output-on-failure

Expected: device model tests pass without hardware.

  • Step 6: Record checkpoint.

Append:

- 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.

  • 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.

  • Step 2: Add public IR info struct and declarations.

Required struct fields:

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:

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);
  • 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.

  • Step 4: Verify.

Run:

cmake --build build
ctest --test-dir build --output-on-failure

Expected: IR skeleton tests and previous tests pass.

  • Step 5: Record checkpoint.

Append:

- 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.

  • 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.

  • Step 2: Add CLI target.

Add executable target mag160c-cli linked against mag160c_core when MAG160C_BUILD_CLI is ON.

  • Step 3: Verify.

Run:

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.

  • Step 4: Record checkpoint.

Append:

- 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:

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:

$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:

- 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:

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:

- 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.