建立 MAG160C 逆向工程交接仓库

This commit is contained in:
ZXCLI
2026-08-11 19:08:44 +08:00
commit 8409b27ba3
3135 changed files with 534408 additions and 0 deletions
@@ -0,0 +1,729 @@
# 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<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:
~~~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<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:
~~~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.
@@ -0,0 +1,265 @@
# MAG160C Linux SDK Design
Date: 2026-07-12
## Status
Draft for user review. Approved approach: C++ core library + stable C ABI + CLI + Python-callable layer.
## Goal
Build an original Linux-usable SDK for the MAG160C USB thermal camera kit, based on interoperability evidence from the bundled Windows and Android SDKs.
The deliverable should support:
- a reusable C/C++ library;
- a command-line sample/tool;
- Python-callable access to the same library;
- clear boundaries between confirmed behavior and unknown IR camera private protocol details.
## Key evidence driving the design
- IR camera USB identity: VID 0x833C, PID 0x0001.
- Android MagDevice passes an Android USB file descriptor into native LinkCamera(fd), then calls MAG-style native APIs.
- Android libcoresdk.so exports MAG_* APIs and uses libusb_bulk_transfer.
- Windows ThermalSDK.dll wraps CoreSDKLib.dll MAG_* APIs.
- Windows CoreSDKLib.dll imports libusb0 bulk read/write APIs.
- TCM control framing is recoverable from Java bytecode:
- packet starts with 0x7e;
- big-endian body length;
- additive byte checksums for header and body;
- main/sub/frame fields;
- payload starts after frame id.
- TCM commands for motor, proximity, light, audio, power, schedule task, and firmware update are known enough to implement.
## Architecture
The project should be split into four layers.
### 1. Core C++ library
Internal name: libmag160c_core.
Responsibilities:
- Own libusb context/device handles.
- Enumerate USB devices.
- Match known MAG IR camera VID/PID.
- Discover bulk endpoints for IR/TCM-like devices.
- Encode and decode TCM frames.
- Provide IR camera lifecycle abstractions matching the SDK shape:
- open/close;
- start/stop;
- trigger FFC;
- camera info;
- raw capture/probe hooks;
- callback plumbing.
- Return explicit unsupported/protocol-unknown errors where low-level IR packet details are not yet known.
The C++ layer may use RAII and internal classes, but those classes are not the public ABI.
### 2. Stable C ABI
Public header: include/mag160c/mag160c.h.
Responsibilities:
- Expose opaque handles.
- Provide simple structs and error codes.
- Avoid C++ types in public signatures.
- Support CLI and Python bindings through the same ABI.
Proposed API families:
- Context:
- mag160c_init
- mag160c_shutdown
- mag160c_last_error
- Device discovery:
- mag160c_list_devices
- mag160c_free_device_list
- IR camera:
- mag160c_ir_open
- mag160c_ir_close
- mag160c_ir_get_info
- mag160c_ir_start
- mag160c_ir_stop
- mag160c_ir_trigger_ffc
- mag160c_ir_read_raw_once
- TCM:
- mag160c_tcm_open
- mag160c_tcm_close
- mag160c_tcm_rotate
- mag160c_tcm_read_distance
- mag160c_tcm_set_light
- mag160c_tcm_set_audio
- mag160c_tcm_get_version
- Utilities:
- mag160c_tcm_encode_frame
- mag160c_tcm_decode_frame
### 3. CLI tool
Binary name: mag160c-cli.
Initial commands:
- probe
- list matching USB devices and endpoints;
- print VID/PID, bus/address, interfaces, endpoints.
- tcm-rotate <angle>
- send known motor rotate command.
- tcm-light <off|red|green|blue|yellow> [steady|blink|breath]
- send known light command.
- tcm-distance
- read/parse proximity distance if the TCM device is available.
- ir-info
- open IR device if possible and show descriptors/endpoints.
- ir-capture-raw --out <file>
- attempt one raw read using discovered endpoints.
- if protocol command is unknown, fail with a clear MAG160C_ERR_PROTOCOL_UNKNOWN.
The CLI is both a sample and a hardware diagnostic tool.
### 4. Python-callable layer
Package name: mag160c.
Recommended first implementation: ctypes wrapper over the C ABI.
Why ctypes first:
- no compiled Python extension needed;
- easier to use in experiments;
- shares the exact tested C ABI used by the CLI.
Python modules:
- mag160c.core
- shared-library loading, error handling, ctypes structs.
- mag160c.tcm
- rotate, light, distance helpers.
- mag160c.ir
- probe, open, info, raw capture helper.
- mag160c.cli
- optional Python CLI wrapper for quick scripts.
## Data flow
### TCM command flow
Caller -> C ABI -> C++ TCM codec -> libusb bulk OUT -> device -> libusb bulk IN -> C++ TCM parser -> caller.
TCM frames are framed, checksummed, and correlated by frame id. The implementation should keep a 15-bit-ish monotonically increasing frame id compatible with the Android behavior: start at 1, increment, wrap before 0x8000.
### IR camera flow
Caller -> C ABI -> C++ IR device -> libusb endpoint discovery -> protocol-specific start/read/stop path.
Known today:
- device is vendor-specific bulk USB;
- proprietary SDK converts raw/response words into temperature data;
- exact start/read packet protocol remains unknown.
Therefore initial IR behavior should focus on descriptor probing, safe endpoint discovery, raw read experiments, and clearly marked unsupported operations until protocol evidence is recovered.
## Error handling
Use explicit error codes:
- MAG160C_OK
- MAG160C_ERR_INVALID_ARGUMENT
- MAG160C_ERR_NO_DEVICE
- MAG160C_ERR_PERMISSION
- MAG160C_ERR_USB
- MAG160C_ERR_TIMEOUT
- MAG160C_ERR_CHECKSUM
- MAG160C_ERR_PROTOCOL_UNKNOWN
- MAG160C_ERR_UNSUPPORTED
- MAG160C_ERR_INTERNAL
Every public function returns an error code or a documented sentinel. Detailed messages are available through mag160c_last_error.
The CLI must print actionable messages, especially for Linux USB permission problems and unknown IR protocol paths.
## Build system
Use CMake.
Targets:
- mag160c_core shared library
- mag160c_cli executable
- mag160c_tests test executable
- optional install target
Dependencies:
- libusb-1.0
- standard C/C++ runtime
- Python only for wrapper/tests, not for core build
No vendored proprietary SDK binaries should be linked into the implementation.
## Testing strategy without hardware
Unit tests:
- TCM frame encode/decode round trips.
- checksum validation.
- command payload construction.
- frame id wrapping.
- error-code behavior for null/invalid arguments.
Mock tests:
- fake USB transport interface for TCM read/write.
- fake device descriptor/endpoints for probe logic.
CLI dry-run tests:
- commands that support --dry-run print the bytes they would send.
- probe gracefully reports no device when hardware is absent.
Hardware tests to document:
- verify udev permissions;
- run mag160c-cli probe;
- run tcm-light/tcm-rotate with small safe values;
- run ir-info;
- run ir-capture-raw and preserve captured bytes for protocol analysis.
## Scope boundaries
In scope for first implementation:
- project scaffolding;
- C ABI and Python wrapper;
- TCM frame codec and known commands;
- USB enumeration and endpoint discovery;
- CLI diagnostics;
- IR API skeleton with honest protocol-unknown errors.
Not in scope until more evidence exists:
- exact proprietary IR start/stream command sequence;
- full raw-to-temperature parity;
- face/RGB camera logic beyond noting that RGB is ordinary UVC/V4L2 territory;
- firmware update execution by default. Firmware update commands may be encoded but should be guarded and not exposed as an easy accidental CLI action.
## Open decisions
- Whether to expose firmware update commands at all in the first public CLI. Recommendation: keep them internal or behind an explicit experimental flag.
- Whether to use C++17 or C++20. Recommendation: C++17 for broader Linux compatibility.
- Whether Python packaging should initially be source-tree-only or installable via pyproject.toml. Recommendation: include pyproject.toml once the shared library layout is settled.
## Acceptance criteria for first implementation
- CMake configures without proprietary SDK dependencies.
- Unit tests pass without hardware.
- CLI probe works and reports no-device cleanly when absent.
- TCM codec tests include byte-exact known examples generated from the recovered frame rules.
- Python can import mag160c and call at least probe/dry-run TCM helpers.
- IR unsupported/protocol-unknown operations are explicit, not silent stubs.