优化无线/有线CAN,以及控制指令解析
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(cognitive_agent_can_read_trans LANGUAGES CXX)
|
||||
|
||||
find_package(Python3 REQUIRED COMPONENTS Interpreter)
|
||||
|
||||
set(CAN_PROTOCOL_SOURCE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../../config/can_protocol/semantic_protocol.json")
|
||||
set(CAN_PROTOCOL_GENERATOR
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../generate_can_semantic_protocol.py")
|
||||
set(CAN_PROTOCOL_CPP
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/generated/can_semantic_protocol.hpp")
|
||||
set(CAN_PROTOCOL_PYTHON
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../../src/cognitive_agent/generated/can_semantic_protocol.py")
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${CAN_PROTOCOL_CPP}" "${CAN_PROTOCOL_PYTHON}"
|
||||
COMMAND "${Python3_EXECUTABLE}" "${CAN_PROTOCOL_GENERATOR}"
|
||||
DEPENDS "${CAN_PROTOCOL_SOURCE}" "${CAN_PROTOCOL_GENERATOR}"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/../.."
|
||||
COMMENT "Generating shared CAN semantic protocol views"
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(generate_can_semantic_protocol
|
||||
DEPENDS "${CAN_PROTOCOL_CPP}" "${CAN_PROTOCOL_PYTHON}")
|
||||
|
||||
set(CANFD_ROOT "/home/orin/project511/project-code/canfd" CACHE PATH
|
||||
"Directory containing can_read_trans.cpp, zcan.h and libusbcanfd.so")
|
||||
|
||||
foreach(required_file can_read_trans.cpp zcan.h libusbcanfd.so)
|
||||
if(NOT EXISTS "${CANFD_ROOT}/${required_file}")
|
||||
message(FATAL_ERROR "Missing ${CANFD_ROOT}/${required_file}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
add_executable(can_read_trans main.cpp)
|
||||
add_dependencies(can_read_trans generate_can_semantic_protocol)
|
||||
target_compile_features(can_read_trans PRIVATE cxx_std_17)
|
||||
# The vendor zcan.h intentionally contains an anonymous packed structure.
|
||||
# Keep useful warnings without treating that SDK ABI declaration as our error.
|
||||
target_compile_options(can_read_trans PRIVATE -Wall -Wextra)
|
||||
target_compile_definitions(can_read_trans PRIVATE
|
||||
CANFD_LEGACY_SOURCE="${CANFD_ROOT}/can_read_trans.cpp"
|
||||
CANFD_PROJECT_DIR="${CANFD_ROOT}"
|
||||
)
|
||||
target_include_directories(can_read_trans PRIVATE "${CANFD_ROOT}")
|
||||
target_link_libraries(can_read_trans PRIVATE
|
||||
"${CANFD_ROOT}/libusbcanfd.so"
|
||||
pthread
|
||||
)
|
||||
set_target_properties(can_read_trans PROPERTIES
|
||||
BUILD_RPATH "${CANFD_ROOT}"
|
||||
INSTALL_RPATH "${CANFD_ROOT}"
|
||||
)
|
||||
|
||||
enable_testing()
|
||||
add_test(NAME can_read_trans_self_test COMMAND can_read_trans self-test)
|
||||
add_test(NAME can_read_trans_console_help COMMAND can_read_trans console --help)
|
||||
set_tests_properties(can_read_trans_console_help PROPERTIES
|
||||
PASS_REGULAR_EXPRESSION "console.*--execute"
|
||||
)
|
||||
@@ -0,0 +1,423 @@
|
||||
// Generated by tools/generate_can_semantic_protocol.py; do not edit.
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <charconv>
|
||||
#include <initializer_list>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace cognitive_agent_can_protocol {
|
||||
|
||||
inline constexpr std::string_view kProtocolVersion = "1.0.0";
|
||||
inline constexpr std::string_view kProtocolSourceSha256 = "cfb7a7eb28d14a82da70d44efece7f045a7a87a4cd549d7537dc00438c297e6e";
|
||||
|
||||
struct FrameMetadata {
|
||||
std::string_view rule_id;
|
||||
std::string_view key;
|
||||
unsigned frame_id;
|
||||
std::string_view format;
|
||||
bool brs;
|
||||
unsigned dlc;
|
||||
std::string_view template_kind;
|
||||
std::string_view base_payload_hex;
|
||||
std::string_view template_json;
|
||||
std::string_view repeat_kind;
|
||||
unsigned repeat_count;
|
||||
unsigned period_ms;
|
||||
std::string_view feedback_ids_csv;
|
||||
std::string_view feedback_policy;
|
||||
};
|
||||
|
||||
struct RuleMetadata {
|
||||
std::string_view id;
|
||||
int channel;
|
||||
std::string_view domain;
|
||||
std::string_view capabilities_csv;
|
||||
std::string_view help;
|
||||
std::string_view special_handler;
|
||||
};
|
||||
|
||||
struct StaticPayload {
|
||||
std::string_view rule_id;
|
||||
std::string_view frame_key;
|
||||
std::string_view selector;
|
||||
std::string_view payload_hex;
|
||||
};
|
||||
|
||||
inline constexpr std::array<FrameMetadata, 30> kFrames{{
|
||||
FrameMetadata{"can0.seat.position", "move", 768, "classic_can", false, 8, "can0_motion_fields", "00 10 00 00 00 00 00 00", "{\"base_payload_hex\":\"00 10 00 00 00 00 00 00\",\"fields\":{\"motor\":{\"bit_length\":2,\"start_bit\":8,\"values\":{\"backrest\":2,\"basin\":1,\"height\":1,\"horizontal\":0,\"legrest\":3,\"longitudinal\":0}},\"opcode\":{\"absolute\":1,\"bit_length\":8,\"home_zero\":11,\"start_bit\":0},\"param_a\":{\"bit_length\":16,\"home_key\":19282,\"scale\":100,\"start_bit\":32},\"param_b\":{\"absolute_timeout_ms\":30000,\"bit_length\":16,\"home_timeout_s\":120,\"start_bit\":48},\"protocol_version\":{\"bit_length\":2,\"constant\":1,\"start_bit\":12},\"sequence\":{\"bit_length\":16,\"runtime_allocated\":true,\"start_bit\":16}}}", "fixed", 1, 0, "0x301,0x302", "ack_then_motor_terminal"},
|
||||
FrameMetadata{"can0.seat.relative", "move", 768, "classic_can", false, 8, "can0_relative_closed_loop_or_timed_fallback", "03 10 00 00 00 00 00 00", "{\"base_payload_hex\":\"03 10 00 00 00 00 00 00\",\"fields\":{\"closed_loop_opcode\":1,\"direction_values\":{\"decrease\":2,\"increase\":1},\"timed_fallback_max_delta_pct\":15,\"timed_fallback_opcode\":3}}", "fixed", 1, 0, "0x301,0x302", "fresh_position_then_ack_then_motor_terminal"},
|
||||
FrameMetadata{"can0.seat.thermal", "thermal_mode", 974, "classic_can", false, 8, "can0_heating_mode_optional", "10 00 00 00 00 00 00 00", "{\"base_payload_hex\":\"10 00 00 00 00 00 00 00\",\"optional_when\":\"heating_on\"}", "fixed", 1, 0, "0x4FA", ""},
|
||||
FrameMetadata{"can0.seat.thermal", "thermal_control", 670, "classic_can", false, 8, "can0_thermal_bit_fields", "00 00 00 00 00 00 00 00", "{\"base_payload_hex\":\"00 00 00 00 00 00 00 00\"}", "fixed", 1, 0, "0x4FA", "thermal_state_match"},
|
||||
FrameMetadata{"can0.seat.dynamic", "dynamic", 768, "classic_can", false, 8, "can0_dynamic_fields", "0E 10 00 00 00 00 00 00", "{\"base_payload_hex\":\"0E 10 00 00 00 00 00 00\",\"fields\":{\"mode_values\":{\"cradle\":2,\"rocking-horse\":1,\"stop\":0},\"start_opcode\":14,\"stop_opcode\":15}}", "fixed", 1, 0, "0x301,0x306", "ack_then_dynamic_state"},
|
||||
FrameMetadata{"can1.door.rear_lock", "command", 811, "classic_can", false, 8, "bit_fields", "0A 00 00 00 00 00 00 00", "{\"base_payload_hex\":\"0A 00 00 00 00 00 00 00\",\"fields\":{\"state\":{\"bit_length\":2,\"start_bit\":14,\"values\":{\"lock\":1,\"unlock\":2}}}}", "fixed", 1, 0, "0x407", "rear_lock_state_match"},
|
||||
FrameMetadata{"can1.window.motion", "command", 1073, "can_fd", true, 8, "window_motion_bit_fields", "00 00 00 00 00 00 00 00", "{\"base_payload_hex\":\"00 00 00 00 00 00 00 00\",\"fields\":{\"motion_values\":{\"close\":2,\"open\":1,\"stop\":3},\"starts\":[48,56,51,59]}}", "fixed", 3, 100, "0x388", "window_state_match"},
|
||||
FrameMetadata{"can1.window.position", "command", 1073, "can_fd", true, 8, "window_feedback_closed_loop", "00 00 00 00 00 00 00 00", "{\"base_payload_hex\":\"00 00 00 00 00 00 00 00\"}", "special", 1, 0, "0x388", "fresh_position_then_motion_then_stop"},
|
||||
FrameMetadata{"can1.climate.power", "command", 478, "classic_can", false, 8, "bit_fields", "00 00 00 00 00 00 00 00", "{\"base_payload_hex\":\"00 00 00 00 00 00 00 00\",\"fields\":{\"state\":{\"bit_length\":2,\"start_bit\":28,\"values\":{\"off\":2,\"on\":1}}}}", "fixed", 3, 100, "0x404", "climate_power_match"},
|
||||
FrameMetadata{"can1.climate.fan", "command", 478, "classic_can", false, 8, "bit_fields", "00 00 00 00 00 00 00 00", "{\"base_payload_hex\":\"00 00 00 00 00 00 00 00\",\"fields\":{\"level\":{\"bit_length\":4,\"start_bit\":4}}}", "fixed", 3, 100, "0x404", "climate_fan_match"},
|
||||
FrameMetadata{"can1.climate.mode", "command", 478, "classic_can", false, 8, "bit_fields", "00 00 00 00 00 00 00 00", "{\"base_payload_hex\":\"00 00 00 00 00 00 00 00\",\"fields\":{\"mode\":{\"bit_length\":4,\"start_bit\":0,\"values\":{\"defrost\":5,\"face\":1,\"face-defrost\":7,\"face-feet\":2,\"face-feet-defrost\":6,\"feet\":3,\"feet-defrost\":4}}}}", "fixed", 3, 100, "0x404", "climate_mode_match"},
|
||||
FrameMetadata{"can1.climate.temperature", "front", 478, "classic_can", false, 8, "climate_temperature", "00 00 00 00 00 00 00 00", "{\"base_payload_hex\":\"00 00 00 00 00 00 00 00\",\"fields\":{\"starts\":{\"driver\":32,\"passenger\":40}}}", "fixed", 3, 100, "0x404", "climate_temperature_match"},
|
||||
FrameMetadata{"can1.climate.temperature", "rear", 478, "classic_can", false, 8, "climate_temperature", "00 00 00 00 00 00 00 00", "{\"base_payload_hex\":\"00 00 00 00 00 00 00 00\",\"fields\":{\"starts\":{\"rear\":48}}}", "fixed", 1, 100, "0x404", "climate_temperature_match"},
|
||||
FrameMetadata{"can1.climate.control", "command", 478, "classic_can", false, 8, "bit_fields", "00 00 00 00 00 00 00 00", "{\"base_payload_hex\":\"00 00 00 00 00 00 00 00\",\"fields\":{\"mode\":{\"bit_length\":2,\"start_bit\":16,\"values\":{\"auto\":2,\"manual\":1}}}}", "fixed", 3, 100, "0x404", "climate_control_match"},
|
||||
FrameMetadata{"can1.climate.cycle", "command", 478, "classic_can", false, 8, "bit_fields", "00 00 00 00 00 00 00 00", "{\"base_payload_hex\":\"00 00 00 00 00 00 00 00\",\"fields\":{\"mode\":{\"bit_length\":2,\"start_bit\":19,\"values\":{\"external\":2,\"internal\":1}}}}", "fixed", 3, 100, "0x404", "climate_cycle_match"},
|
||||
FrameMetadata{"can1.outlet.switch", "command", 1263, "can_fd", true, 8, "static_variant", "00 00 00 00 00 00 00 00", "{\"base_payload_hex\":\"00 00 00 00 00 00 00 00\",\"payload_variants\":{\"driver-left:close\":\"02 FF FF FF FF 02 FF FF\",\"driver-left:open\":\"02 FF FF FF FF 01 FF FF\",\"driver-right:close\":\"02 FF FF FF FF 08 FF FF\",\"driver-right:open\":\"02 FF FF FF FF 04 FF FF\",\"passenger-left:close\":\"03 FF FF FF FF 02 00 00\",\"passenger-left:open\":\"03 FF FF FF FF 01 00 00\",\"passenger-right:close\":\"03 FF FF FF FF 08 00 00\",\"passenger-right:open\":\"03 FF FF FF FF 04 00 00\"}}", "fixed", 3, 100, "0x4BB,0x37B", "outlet_switch_state_match"},
|
||||
FrameMetadata{"can1.outlet.percent", "command", 1263, "can_fd", true, 8, "outlet_calibrated_coordinates", "00 FF FF FF FF 00 00 00", "{\"base_payload_hex\":\"00 FF FF FF FF 00 00 00\"}", "fixed", 3, 100, "0x2F5,0x37B", "outlet_position_match"},
|
||||
FrameMetadata{"can1.outlet.position", "command", 1263, "can_fd", true, 8, "outlet_calibrated_coordinates", "00 FF FF FF FF 00 00 00", "{\"base_payload_hex\":\"00 FF FF FF FF 00 00 00\"}", "fixed", 3, 100, "0x2F5,0x37B", "outlet_position_match"},
|
||||
FrameMetadata{"can1.seat.driver_thermal", "command", 1073, "can_fd", true, 8, "driver_seat_thermal", "00 00 00 00 00 00 00 00", "{\"base_payload_hex\":\"00 00 00 00 00 00 00 00\"}", "fixed", 3, 100, "0x4FA", "driver_seat_thermal_match"},
|
||||
FrameMetadata{"can1.seat.driver_massage", "command", 1217, "can_fd", true, 8, "driver_seat_massage", "12 00 00 00 00 00 00 00", "{\"base_payload_hex\":\"12 00 00 00 00 00 00 00\"}", "fixed", 3, 100, "0x4FA", "driver_seat_massage_match"},
|
||||
FrameMetadata{"can1.seat.driver_axis", "motion", 1217, "can_fd", true, 8, "driver_seat_axis", "05 00 00 00 00 00 00 00", "{\"base_payload_hex\":\"05 00 00 00 00 00 00 00\"}", "duration_ceiling", 0, 100, "0x494", "activity_then_forced_stop"},
|
||||
FrameMetadata{"can1.seat.driver_axis", "stop", 1217, "can_fd", true, 8, "static", "05 00 00 00 00 00 00 00", "{\"base_payload_hex\":\"05 00 00 00 00 00 00 00\"}", "fixed", 3, 100, "0x494", "submitted_stop"},
|
||||
FrameMetadata{"can1.seat.driver_memory", "sequence", 1217, "can_fd", true, 8, "learned_driver_memory_pair", "16 00 00 00 00 00 00 00", "{\"base_payload_hex\":\"16 00 00 00 00 00 00 00\"}", "sequence", 20, 50, "0x494", "fresh_initialized_status_then_post_status"},
|
||||
FrameMetadata{"can1.light.indoor", "command", 1269, "can_fd", true, 8, "reading_light_all", "00 00 03 FF FF 00 00 00", "{\"base_payload_hex\":\"00 00 03 FF FF 00 00 00\"}", "fixed", 3, 100, "0x42E", "indoor_light_match"},
|
||||
FrameMetadata{"can1.light.reading_all", "command", 1269, "can_fd", true, 8, "reading_light_all", "00 00 03 FF FF 00 00 00", "{\"base_payload_hex\":\"00 00 03 FF FF 00 00 00\"}", "fixed", 3, 100, "0x42E", "indoor_light_match"},
|
||||
FrameMetadata{"can1.light.ambient_power", "command", 1020, "can_fd", true, 8, "static_variant", "00 00 00 00 00 00 00 00", "{\"base_payload_hex\":\"00 00 00 00 00 00 00 00\",\"payload_variants\":{\"off\":\"00 31 00 75 00 80 00 00\",\"on\":\"00 36 00 75 00 80 00 00\"}}", "fixed", 3, 100, "0x42E", "ambient_power_match"},
|
||||
FrameMetadata{"can1.light.ambient_color", "command", 1020, "can_fd", true, 8, "ambient_color", "00 30 00 75 00 00 00 00", "{\"base_payload_hex\":\"00 30 00 75 00 00 00 00\"}", "fixed", 3, 100, "0x42E", "ambient_color_match"},
|
||||
FrameMetadata{"can1.light.ambient_brightness", "command", 1020, "can_fd", true, 8, "ambient_brightness", "00 30 00 75 00 00 00 00", "{\"base_payload_hex\":\"00 30 00 75 00 00 00 00\"}", "fixed", 3, 100, "0x42E", "ambient_brightness_match"},
|
||||
FrameMetadata{"can1.shade.motion", "command", 1073, "classic_can", false, 8, "static_variant", "00 00 00 00 00 00 00 00", "{\"base_payload_hex\":\"00 00 00 00 00 00 00 00\",\"payload_variants\":{\"close\":\"20 00 00 00 00 00 00 00\",\"half\":\"30 00 00 00 00 00 00 00\",\"open\":\"10 00 00 00 00 00 00 00\",\"stop\":\"40 00 00 00 00 00 00 00\"}}", "fixed", 1, 0, "0x41A", "shade_position_or_stop_submission"},
|
||||
FrameMetadata{"can1.shade.position", "command", 1073, "classic_can", false, 8, "shade_feedback_closed_loop", "40 00 00 00 00 00 00 00", "{\"base_payload_hex\":\"40 00 00 00 00 00 00 00\"}", "special", 1, 0, "0x41A", "fresh_position_then_motion_then_stop"}
|
||||
}};
|
||||
|
||||
inline constexpr std::array<RuleMetadata, 27> kRules{{
|
||||
RuleMetadata{"can0.seat.position", 0, "seat", "seat.set_longitudinal_position,seat.set_cushion_tilt,seat.set_backrest_recline,seat.set_legrest_height", "seat passenger <horizontal|basin|backrest|legrest> position <0-100>", "can0_absolute_position"},
|
||||
RuleMetadata{"can0.seat.relative", 0, "seat", "seat.set_longitudinal_position,seat.set_cushion_tilt,seat.set_backrest_recline,seat.set_legrest_height", "seat passenger <axis> relative <direction> <1-100>", "can0_relative_position"},
|
||||
RuleMetadata{"can0.seat.thermal", 0, "seat", "seat.set_ventilation,seat.set_heating", "seat passenger <vent|heat> <off|low|medium|high>", "can0_thermal_preserve_peer_feature"},
|
||||
RuleMetadata{"can0.seat.dynamic", 0, "seat", "seat.start_dynamic_mode,seat.stop_dynamic_mode", "seat passenger dynamic <rocking-horse|cradle|stop>", "can0_dynamic_mode"},
|
||||
RuleMetadata{"can1.door.rear_lock", 1, "door", "door.set_rear_lock", "door rear <lock|unlock>", ""},
|
||||
RuleMetadata{"can1.window.motion", 1, "window", "window.set_motion", "window <all|front|rear|front-left|front-right|rear-left|rear-right> <open|close|stop>", ""},
|
||||
RuleMetadata{"can1.window.position", 1, "window", "window.set_position", "window <front-left|front-right|rear-left|rear-right> position <0-100>", "window_position"},
|
||||
RuleMetadata{"can1.climate.power", 1, "climate", "climate.set_power,climate.set_ac", "ac <on|off>", ""},
|
||||
RuleMetadata{"can1.climate.fan", 1, "climate", "climate.set_fan_level", "ac fan <1-7>", ""},
|
||||
RuleMetadata{"can1.climate.mode", 1, "climate", "climate.set_outlet_mode", "ac mode <face|face-feet|feet|feet-defrost|defrost|face-feet-defrost|face-defrost>", ""},
|
||||
RuleMetadata{"can1.climate.temperature", 1, "climate", "climate.set_temperature", "ac temp <driver|passenger|rear> <18-32>", ""},
|
||||
RuleMetadata{"can1.climate.control", 1, "climate", "climate.set_control_mode", "ac control <manual|auto>", ""},
|
||||
RuleMetadata{"can1.climate.cycle", 1, "climate", "climate.set_cycle_mode", "ac cycle <internal|external>", ""},
|
||||
RuleMetadata{"can1.outlet.switch", 1, "outlet", "outlet.set_switch_state", "outlet <driver-left|driver-right|passenger-left|passenger-right> <open|close>", ""},
|
||||
RuleMetadata{"can1.outlet.percent", 1, "outlet", "outlet.set_position_percent", "outlet <target> percent <0-100> <0-100>", "outlet_percent_to_calibrated_position"},
|
||||
RuleMetadata{"can1.outlet.position", 1, "outlet", "outlet.set_calibrated_position", "outlet <target> position <-128..126> <-128..126>", "outlet_calibrated_position"},
|
||||
RuleMetadata{"can1.seat.driver_thermal", 1, "seat", "seat.set_driver_ventilation_native,seat.set_driver_heating_native", "seat driver <vent|heat> <off|low|high>", ""},
|
||||
RuleMetadata{"can1.seat.driver_massage", 1, "seat", "seat.set_driver_massage_native", "seat driver massage <off|strength weak|medium|strong|mode wave|pulse|stretch|slack|relax>", ""},
|
||||
RuleMetadata{"can1.seat.driver_axis", 1, "seat", "seat.set_driver_axis_native", "seat driver <horizontal|height|backrest|basin> <direction|stop> [100-2000]", "driver_seat_axis"},
|
||||
RuleMetadata{"can1.seat.driver_memory", 1, "seat", "seat.set_driver_position_native,seat.recall_driver_memory_native", "seat driver position <1-3> | seat driver memory recall <1-3>", "driver_seat_memory_recall"},
|
||||
RuleMetadata{"can1.light.indoor", 1, "light", "interior_light.set_indoor_power", "light indoor <on|off>", ""},
|
||||
RuleMetadata{"can1.light.reading_all", 1, "light", "interior_light.set_reading_power", "light reading all <on|off>", ""},
|
||||
RuleMetadata{"can1.light.ambient_power", 1, "light", "ambient_light.set_power", "light ambient <on|off>", ""},
|
||||
RuleMetadata{"can1.light.ambient_color", 1, "light", "ambient_light.set_color_index", "light ambient color <1-127>", ""},
|
||||
RuleMetadata{"can1.light.ambient_brightness", 1, "light", "ambient_light.set_brightness_level", "light ambient brightness <1-5>", ""},
|
||||
RuleMetadata{"can1.shade.motion", 1, "roof", "roof.set_shade_motion", "shade <open|close|half|stop>", "shade_preset"},
|
||||
RuleMetadata{"can1.shade.position", 1, "roof", "roof.set_shade_position", "shade position <0-100>", "shade_position"}
|
||||
}};
|
||||
|
||||
inline constexpr std::array<StaticPayload, 14> kStaticPayloads{{
|
||||
StaticPayload{"can1.outlet.switch", "command", "driver-left:open", "02 FF FF FF FF 01 FF FF"},
|
||||
StaticPayload{"can1.outlet.switch", "command", "driver-left:close", "02 FF FF FF FF 02 FF FF"},
|
||||
StaticPayload{"can1.outlet.switch", "command", "driver-right:open", "02 FF FF FF FF 04 FF FF"},
|
||||
StaticPayload{"can1.outlet.switch", "command", "driver-right:close", "02 FF FF FF FF 08 FF FF"},
|
||||
StaticPayload{"can1.outlet.switch", "command", "passenger-left:open", "03 FF FF FF FF 01 00 00"},
|
||||
StaticPayload{"can1.outlet.switch", "command", "passenger-left:close", "03 FF FF FF FF 02 00 00"},
|
||||
StaticPayload{"can1.outlet.switch", "command", "passenger-right:open", "03 FF FF FF FF 04 00 00"},
|
||||
StaticPayload{"can1.outlet.switch", "command", "passenger-right:close", "03 FF FF FF FF 08 00 00"},
|
||||
StaticPayload{"can1.light.ambient_power", "command", "on", "00 36 00 75 00 80 00 00"},
|
||||
StaticPayload{"can1.light.ambient_power", "command", "off", "00 31 00 75 00 80 00 00"},
|
||||
StaticPayload{"can1.shade.motion", "command", "open", "10 00 00 00 00 00 00 00"},
|
||||
StaticPayload{"can1.shade.motion", "command", "close", "20 00 00 00 00 00 00 00"},
|
||||
StaticPayload{"can1.shade.motion", "command", "half", "30 00 00 00 00 00 00 00"},
|
||||
StaticPayload{"can1.shade.motion", "command", "stop", "40 00 00 00 00 00 00 00"}
|
||||
}};
|
||||
|
||||
inline const RuleMetadata* find_rule(std::string_view id) {
|
||||
for (const auto& item : kRules)
|
||||
if (item.id == id)
|
||||
return &item;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
inline const FrameMetadata* find_frame(std::string_view rule_id,
|
||||
std::string_view key = "command") {
|
||||
for (const auto& item : kFrames)
|
||||
if (item.rule_id == rule_id && item.key == key)
|
||||
return &item;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
inline const StaticPayload* find_static_payload(std::string_view rule_id,
|
||||
std::string_view selector,
|
||||
std::string_view key = "command") {
|
||||
for (const auto& item : kStaticPayloads)
|
||||
if (item.rule_id == rule_id && item.frame_key == key &&
|
||||
item.selector == selector)
|
||||
return &item;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
inline bool protocol_contains(std::string_view value,
|
||||
std::initializer_list<std::string_view> allowed) {
|
||||
for (const auto candidate : allowed)
|
||||
if (value == candidate)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
inline bool protocol_integer_in_range(std::string_view text, int minimum,
|
||||
int maximum) {
|
||||
int value = 0;
|
||||
const auto result = std::from_chars(text.data(), text.data() + text.size(), value);
|
||||
return result.ec == std::errc{} && result.ptr == text.data() + text.size() &&
|
||||
std::to_string(value) == text && value >= minimum && value <= maximum;
|
||||
}
|
||||
|
||||
inline const RuleMetadata* match_semantic_command(
|
||||
const std::vector<std::string>& words, int channel = -1) {
|
||||
if ((channel < 0 || channel == 0) &&
|
||||
((words.size() == 5 &&
|
||||
words[0] == "seat" &&
|
||||
words[1] == "passenger" &&
|
||||
protocol_contains(words[2], {"horizontal", "longitudinal", "basin", "height", "backrest", "legrest"}) &&
|
||||
words[3] == "position" &&
|
||||
protocol_integer_in_range(words[4], 0, 100))))
|
||||
return find_rule("can0.seat.position");
|
||||
if ((channel < 0 || channel == 0) &&
|
||||
((words.size() == 6 &&
|
||||
words[0] == "seat" &&
|
||||
words[1] == "passenger" &&
|
||||
protocol_contains(words[2], {"horizontal", "longitudinal"}) &&
|
||||
words[3] == "relative" &&
|
||||
protocol_contains(words[4], {"forward", "backward"}) &&
|
||||
protocol_integer_in_range(words[5], 1, 100)) ||
|
||||
(words.size() == 6 &&
|
||||
words[0] == "seat" &&
|
||||
words[1] == "passenger" &&
|
||||
protocol_contains(words[2], {"basin", "height"}) &&
|
||||
words[3] == "relative" &&
|
||||
protocol_contains(words[4], {"up", "down"}) &&
|
||||
protocol_integer_in_range(words[5], 1, 100)) ||
|
||||
(words.size() == 6 &&
|
||||
words[0] == "seat" &&
|
||||
words[1] == "passenger" &&
|
||||
words[2] == "backrest" &&
|
||||
words[3] == "relative" &&
|
||||
protocol_contains(words[4], {"forward", "backward"}) &&
|
||||
protocol_integer_in_range(words[5], 1, 100)) ||
|
||||
(words.size() == 6 &&
|
||||
words[0] == "seat" &&
|
||||
words[1] == "passenger" &&
|
||||
words[2] == "legrest" &&
|
||||
words[3] == "relative" &&
|
||||
protocol_contains(words[4], {"up", "down"}) &&
|
||||
protocol_integer_in_range(words[5], 1, 100))))
|
||||
return find_rule("can0.seat.relative");
|
||||
if ((channel < 0 || channel == 0) &&
|
||||
((words.size() == 4 &&
|
||||
words[0] == "seat" &&
|
||||
words[1] == "passenger" &&
|
||||
protocol_contains(words[2], {"vent", "heat"}) &&
|
||||
protocol_contains(words[3], {"off", "low", "medium", "high"}))))
|
||||
return find_rule("can0.seat.thermal");
|
||||
if ((channel < 0 || channel == 0) &&
|
||||
((words.size() == 4 &&
|
||||
words[0] == "seat" &&
|
||||
words[1] == "passenger" &&
|
||||
words[2] == "dynamic" &&
|
||||
protocol_contains(words[3], {"rocking-horse", "cradle", "stop"})) ||
|
||||
(words.size() == 5 &&
|
||||
words[0] == "seat" &&
|
||||
words[1] == "passenger" &&
|
||||
words[2] == "dynamic" &&
|
||||
words[3] == "start" &&
|
||||
protocol_contains(words[4], {"rocking-horse", "cradle"}))))
|
||||
return find_rule("can0.seat.dynamic");
|
||||
if ((channel < 0 || channel == 1) &&
|
||||
((words.size() == 3 &&
|
||||
words[0] == "door" &&
|
||||
words[1] == "rear" &&
|
||||
protocol_contains(words[2], {"lock", "unlock"}))))
|
||||
return find_rule("can1.door.rear_lock");
|
||||
if ((channel < 0 || channel == 1) &&
|
||||
((words.size() == 3 &&
|
||||
words[0] == "window" &&
|
||||
protocol_contains(words[1], {"all", "front", "rear", "front-left", "front-right", "rear-left", "rear-right"}) &&
|
||||
protocol_contains(words[2], {"open", "close", "stop"}))))
|
||||
return find_rule("can1.window.motion");
|
||||
if ((channel < 0 || channel == 1) &&
|
||||
((words.size() == 4 &&
|
||||
words[0] == "window" &&
|
||||
protocol_contains(words[1], {"front-left", "front-right", "rear-left", "rear-right"}) &&
|
||||
words[2] == "position" &&
|
||||
protocol_integer_in_range(words[3], 0, 100))))
|
||||
return find_rule("can1.window.position");
|
||||
if ((channel < 0 || channel == 1) &&
|
||||
((words.size() == 2 &&
|
||||
words[0] == "ac" &&
|
||||
protocol_contains(words[1], {"on", "off"}))))
|
||||
return find_rule("can1.climate.power");
|
||||
if ((channel < 0 || channel == 1) &&
|
||||
((words.size() == 3 &&
|
||||
words[0] == "ac" &&
|
||||
words[1] == "fan" &&
|
||||
protocol_integer_in_range(words[2], 1, 7))))
|
||||
return find_rule("can1.climate.fan");
|
||||
if ((channel < 0 || channel == 1) &&
|
||||
((words.size() == 3 &&
|
||||
words[0] == "ac" &&
|
||||
words[1] == "mode" &&
|
||||
protocol_contains(words[2], {"face", "face-feet", "feet", "feet-defrost", "defrost", "face-feet-defrost", "face-defrost"}))))
|
||||
return find_rule("can1.climate.mode");
|
||||
if ((channel < 0 || channel == 1) &&
|
||||
((words.size() == 4 &&
|
||||
words[0] == "ac" &&
|
||||
words[1] == "temp" &&
|
||||
protocol_contains(words[2], {"driver", "passenger", "rear"}) &&
|
||||
protocol_integer_in_range(words[3], 18, 32))))
|
||||
return find_rule("can1.climate.temperature");
|
||||
if ((channel < 0 || channel == 1) &&
|
||||
((words.size() == 3 &&
|
||||
words[0] == "ac" &&
|
||||
words[1] == "control" &&
|
||||
protocol_contains(words[2], {"manual", "auto"}))))
|
||||
return find_rule("can1.climate.control");
|
||||
if ((channel < 0 || channel == 1) &&
|
||||
((words.size() == 3 &&
|
||||
words[0] == "ac" &&
|
||||
words[1] == "cycle" &&
|
||||
protocol_contains(words[2], {"internal", "external"}))))
|
||||
return find_rule("can1.climate.cycle");
|
||||
if ((channel < 0 || channel == 1) &&
|
||||
((words.size() == 3 &&
|
||||
words[0] == "outlet" &&
|
||||
protocol_contains(words[1], {"driver-left", "driver-right", "passenger-left", "passenger-right"}) &&
|
||||
protocol_contains(words[2], {"open", "close"}))))
|
||||
return find_rule("can1.outlet.switch");
|
||||
if ((channel < 0 || channel == 1) &&
|
||||
((words.size() == 5 &&
|
||||
words[0] == "outlet" &&
|
||||
protocol_contains(words[1], {"driver-left", "driver-right", "passenger-left", "passenger-right"}) &&
|
||||
words[2] == "percent" &&
|
||||
protocol_integer_in_range(words[3], 0, 100) &&
|
||||
protocol_integer_in_range(words[4], 0, 100))))
|
||||
return find_rule("can1.outlet.percent");
|
||||
if ((channel < 0 || channel == 1) &&
|
||||
((words.size() == 5 &&
|
||||
words[0] == "outlet" &&
|
||||
protocol_contains(words[1], {"driver-left", "driver-right", "passenger-left", "passenger-right"}) &&
|
||||
words[2] == "position" &&
|
||||
protocol_integer_in_range(words[3], -128, 126) &&
|
||||
protocol_integer_in_range(words[4], -128, 126))))
|
||||
return find_rule("can1.outlet.position");
|
||||
if ((channel < 0 || channel == 1) &&
|
||||
((words.size() == 4 &&
|
||||
words[0] == "seat" &&
|
||||
words[1] == "driver" &&
|
||||
protocol_contains(words[2], {"vent", "heat"}) &&
|
||||
protocol_contains(words[3], {"off", "low", "high"}))))
|
||||
return find_rule("can1.seat.driver_thermal");
|
||||
if ((channel < 0 || channel == 1) &&
|
||||
((words.size() == 4 &&
|
||||
words[0] == "seat" &&
|
||||
words[1] == "driver" &&
|
||||
words[2] == "massage" &&
|
||||
words[3] == "off") ||
|
||||
(words.size() == 5 &&
|
||||
words[0] == "seat" &&
|
||||
words[1] == "driver" &&
|
||||
words[2] == "massage" &&
|
||||
words[3] == "strength" &&
|
||||
protocol_contains(words[4], {"weak", "medium", "strong"})) ||
|
||||
(words.size() == 5 &&
|
||||
words[0] == "seat" &&
|
||||
words[1] == "driver" &&
|
||||
words[2] == "massage" &&
|
||||
words[3] == "mode" &&
|
||||
protocol_contains(words[4], {"wave", "pulse", "stretch", "slack", "relax"}))))
|
||||
return find_rule("can1.seat.driver_massage");
|
||||
if ((channel < 0 || channel == 1) &&
|
||||
((words.size() == 4 &&
|
||||
words[0] == "seat" &&
|
||||
words[1] == "driver" &&
|
||||
protocol_contains(words[2], {"horizontal", "backrest"}) &&
|
||||
protocol_contains(words[3], {"forward", "backward"})) ||
|
||||
(words.size() == 5 &&
|
||||
words[0] == "seat" &&
|
||||
words[1] == "driver" &&
|
||||
protocol_contains(words[2], {"horizontal", "backrest"}) &&
|
||||
protocol_contains(words[3], {"forward", "backward"}) &&
|
||||
protocol_integer_in_range(words[4], 100, 2000)) ||
|
||||
(words.size() == 4 &&
|
||||
words[0] == "seat" &&
|
||||
words[1] == "driver" &&
|
||||
protocol_contains(words[2], {"height", "basin"}) &&
|
||||
protocol_contains(words[3], {"up", "down"})) ||
|
||||
(words.size() == 5 &&
|
||||
words[0] == "seat" &&
|
||||
words[1] == "driver" &&
|
||||
protocol_contains(words[2], {"height", "basin"}) &&
|
||||
protocol_contains(words[3], {"up", "down"}) &&
|
||||
protocol_integer_in_range(words[4], 100, 2000)) ||
|
||||
(words.size() == 4 &&
|
||||
words[0] == "seat" &&
|
||||
words[1] == "driver" &&
|
||||
protocol_contains(words[2], {"horizontal", "height", "backrest", "basin"}) &&
|
||||
words[3] == "stop")))
|
||||
return find_rule("can1.seat.driver_axis");
|
||||
if ((channel < 0 || channel == 1) &&
|
||||
((words.size() == 4 &&
|
||||
words[0] == "seat" &&
|
||||
words[1] == "driver" &&
|
||||
words[2] == "position" &&
|
||||
protocol_integer_in_range(words[3], 1, 3)) ||
|
||||
(words.size() == 5 &&
|
||||
words[0] == "seat" &&
|
||||
words[1] == "driver" &&
|
||||
words[2] == "memory" &&
|
||||
words[3] == "recall" &&
|
||||
protocol_integer_in_range(words[4], 1, 3))))
|
||||
return find_rule("can1.seat.driver_memory");
|
||||
if ((channel < 0 || channel == 1) &&
|
||||
((words.size() == 3 &&
|
||||
words[0] == "light" &&
|
||||
words[1] == "indoor" &&
|
||||
protocol_contains(words[2], {"on", "off"}))))
|
||||
return find_rule("can1.light.indoor");
|
||||
if ((channel < 0 || channel == 1) &&
|
||||
((words.size() == 4 &&
|
||||
words[0] == "light" &&
|
||||
words[1] == "reading" &&
|
||||
words[2] == "all" &&
|
||||
protocol_contains(words[3], {"on", "off"}))))
|
||||
return find_rule("can1.light.reading_all");
|
||||
if ((channel < 0 || channel == 1) &&
|
||||
((words.size() == 3 &&
|
||||
words[0] == "light" &&
|
||||
words[1] == "ambient" &&
|
||||
protocol_contains(words[2], {"on", "off"}))))
|
||||
return find_rule("can1.light.ambient_power");
|
||||
if ((channel < 0 || channel == 1) &&
|
||||
((words.size() == 4 &&
|
||||
words[0] == "light" &&
|
||||
words[1] == "ambient" &&
|
||||
words[2] == "color" &&
|
||||
protocol_integer_in_range(words[3], 1, 127))))
|
||||
return find_rule("can1.light.ambient_color");
|
||||
if ((channel < 0 || channel == 1) &&
|
||||
((words.size() == 4 &&
|
||||
words[0] == "light" &&
|
||||
words[1] == "ambient" &&
|
||||
words[2] == "brightness" &&
|
||||
protocol_integer_in_range(words[3], 1, 5))))
|
||||
return find_rule("can1.light.ambient_brightness");
|
||||
if ((channel < 0 || channel == 1) &&
|
||||
((words.size() == 2 &&
|
||||
words[0] == "shade" &&
|
||||
protocol_contains(words[1], {"open", "close", "half", "stop"}))))
|
||||
return find_rule("can1.shade.motion");
|
||||
if ((channel < 0 || channel == 1) &&
|
||||
((words.size() == 3 &&
|
||||
words[0] == "shade" &&
|
||||
words[1] == "position" &&
|
||||
protocol_integer_in_range(words[2], 0, 100))))
|
||||
return find_rule("can1.shade.position");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace cognitive_agent_can_protocol
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,534 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate the shared CAN semantic protocol and generate Python/C++ views."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import pprint
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_SOURCE = ROOT / "config" / "can_protocol" / "semantic_protocol.json"
|
||||
DEFAULT_PYTHON = (
|
||||
ROOT / "src" / "cognitive_agent" / "generated" / "can_semantic_protocol.py"
|
||||
)
|
||||
DEFAULT_CPP = (
|
||||
ROOT / "tools" / "can_read_trans" / "generated" / "can_semantic_protocol.hpp"
|
||||
)
|
||||
|
||||
|
||||
class ProtocolError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def _payload_bytes(value: str, *, where: str, dlc: int) -> bytes:
|
||||
try:
|
||||
payload = bytes.fromhex(value)
|
||||
except ValueError as exc:
|
||||
raise ProtocolError(f"{where}: invalid payload hex") from exc
|
||||
if len(payload) != dlc:
|
||||
raise ProtocolError(f"{where}: payload has {len(payload)} bytes, expected {dlc}")
|
||||
return payload
|
||||
|
||||
|
||||
def _frame_id(value: Any, *, where: str) -> int:
|
||||
if not isinstance(value, str) or not re.fullmatch(r"0x[0-9A-Fa-f]{1,3}", value):
|
||||
raise ProtocolError(f"{where}: frame ID must be 0x000..0x7FF")
|
||||
parsed = int(value, 16)
|
||||
if not 0 <= parsed <= 0x7FF:
|
||||
raise ProtocolError(f"{where}: only standard 11-bit IDs are supported")
|
||||
return parsed
|
||||
|
||||
|
||||
def load_and_validate(path: Path) -> dict[str, Any]:
|
||||
try:
|
||||
document = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
raise ProtocolError(f"cannot load {path}: {exc}") from exc
|
||||
if document.get("schema") != "cognitive-agent-can-semantic-protocol/v1":
|
||||
raise ProtocolError("unsupported or missing protocol schema")
|
||||
if not re.fullmatch(r"\d+\.\d+\.\d+", str(document.get("version", ""))):
|
||||
raise ProtocolError("version must be semantic x.y.z")
|
||||
rules = document.get("rules")
|
||||
if not isinstance(rules, list) or not rules:
|
||||
raise ProtocolError("rules must be a non-empty list")
|
||||
|
||||
seen_rules: set[str] = set()
|
||||
capability_channels: dict[str, set[int]] = {}
|
||||
for rule_index, rule in enumerate(rules):
|
||||
where = f"rules[{rule_index}]"
|
||||
rule_id = rule.get("id")
|
||||
if not isinstance(rule_id, str) or not re.fullmatch(r"can[01]\.[a-z0-9_.]+", rule_id):
|
||||
raise ProtocolError(f"{where}: invalid id")
|
||||
if rule_id in seen_rules:
|
||||
raise ProtocolError(f"{where}: duplicate id {rule_id}")
|
||||
seen_rules.add(rule_id)
|
||||
channel = rule.get("channel")
|
||||
if channel not in {0, 1} or not rule_id.startswith(f"can{channel}."):
|
||||
raise ProtocolError(f"{where}: channel does not match rule id")
|
||||
if not isinstance(rule.get("domain"), str) or not re.fullmatch(
|
||||
r"[a-z][a-z0-9_]*", rule["domain"]
|
||||
):
|
||||
raise ProtocolError(f"{where}: invalid domain")
|
||||
if not isinstance(rule.get("help"), str) or not rule["help"].strip():
|
||||
raise ProtocolError(f"{where}: help must be non-empty")
|
||||
capabilities = rule.get("capabilities")
|
||||
if not isinstance(capabilities, list) or not capabilities or not all(
|
||||
isinstance(item, str) and "." in item for item in capabilities
|
||||
):
|
||||
raise ProtocolError(f"{where}: capabilities must be non-empty names")
|
||||
for capability in capabilities:
|
||||
capability_channels.setdefault(capability, set()).add(channel)
|
||||
patterns = rule.get("patterns")
|
||||
if not isinstance(patterns, list) or not patterns:
|
||||
raise ProtocolError(f"{where}: patterns must be non-empty")
|
||||
for pattern_index, pattern in enumerate(patterns):
|
||||
pattern_where = f"{where}.patterns[{pattern_index}]"
|
||||
if not isinstance(pattern, list) or not pattern:
|
||||
raise ProtocolError(f"{pattern_where}: pattern must contain tokens")
|
||||
for token_index, token in enumerate(pattern):
|
||||
token_where = f"{pattern_where}[{token_index}]"
|
||||
if not isinstance(token, dict) or len(token) == 0:
|
||||
raise ProtocolError(f"{token_where}: token must be an object")
|
||||
kinds = {"literal", "enum", "integer"}.intersection(token)
|
||||
if len(kinds) != 1:
|
||||
raise ProtocolError(f"{token_where}: token needs one token kind")
|
||||
kind = next(iter(kinds))
|
||||
if kind == "literal" and not isinstance(token["literal"], str):
|
||||
raise ProtocolError(f"{token_where}: literal must be text")
|
||||
if kind == "enum":
|
||||
values = token.get("values")
|
||||
if not isinstance(values, list) or not values or not all(
|
||||
isinstance(item, str) for item in values
|
||||
):
|
||||
raise ProtocolError(f"{token_where}: enum values are invalid")
|
||||
if kind == "integer":
|
||||
minimum = token.get("minimum")
|
||||
maximum = token.get("maximum")
|
||||
if not isinstance(minimum, int) or not isinstance(maximum, int) or minimum > maximum:
|
||||
raise ProtocolError(f"{token_where}: integer range is invalid")
|
||||
|
||||
frames = rule.get("frames")
|
||||
if not isinstance(frames, list) or not frames:
|
||||
raise ProtocolError(f"{where}: frames must be non-empty")
|
||||
seen_keys: set[str] = set()
|
||||
for frame_index, frame in enumerate(frames):
|
||||
frame_where = f"{where}.frames[{frame_index}]"
|
||||
key = frame.get("key")
|
||||
if not isinstance(key, str) or not key or key in seen_keys:
|
||||
raise ProtocolError(f"{frame_where}: frame key is invalid or duplicate")
|
||||
seen_keys.add(key)
|
||||
_frame_id(frame.get("frame_id"), where=frame_where)
|
||||
frame_format = frame.get("format")
|
||||
if frame_format not in {"classic_can", "can_fd"}:
|
||||
raise ProtocolError(f"{frame_where}: unsupported frame format")
|
||||
if bool(frame.get("brs")) and frame_format != "can_fd":
|
||||
raise ProtocolError(f"{frame_where}: BRS requires CAN FD")
|
||||
dlc = frame.get("dlc")
|
||||
if not isinstance(dlc, int) or not 1 <= dlc <= 64:
|
||||
raise ProtocolError(f"{frame_where}: invalid DLC")
|
||||
_payload_bytes(frame.get("base_payload_hex", ""), where=frame_where, dlc=dlc)
|
||||
if not isinstance(frame.get("template_kind"), str) or not frame[
|
||||
"template_kind"
|
||||
]:
|
||||
raise ProtocolError(f"{frame_where}: template_kind is required")
|
||||
variants = frame.get("payload_variants", {})
|
||||
if not isinstance(variants, dict):
|
||||
raise ProtocolError(f"{frame_where}: payload_variants must be an object")
|
||||
for selector, payload in variants.items():
|
||||
if not isinstance(selector, str):
|
||||
raise ProtocolError(f"{frame_where}: payload selector must be text")
|
||||
_payload_bytes(payload, where=f"{frame_where}.{selector}", dlc=dlc)
|
||||
repeat = frame.get("repeat")
|
||||
if not isinstance(repeat, dict) or repeat.get("kind") not in {
|
||||
"fixed", "special", "duration_ceiling", "sequence"
|
||||
}:
|
||||
raise ProtocolError(f"{frame_where}: invalid repeat policy")
|
||||
count = repeat.get("count")
|
||||
period_ms = repeat.get("period_ms")
|
||||
if not isinstance(count, int) or count < 0 or not isinstance(period_ms, int) or period_ms < 0:
|
||||
raise ProtocolError(f"{frame_where}: invalid repeat count/period")
|
||||
if repeat["kind"] in {"fixed", "sequence"} and count < 1:
|
||||
raise ProtocolError(
|
||||
f"{frame_where}: {repeat['kind']} repeat count must be positive"
|
||||
)
|
||||
feedback_ids = frame.get("feedback_ids")
|
||||
if not isinstance(feedback_ids, list) or not feedback_ids:
|
||||
raise ProtocolError(f"{frame_where}: feedback_ids must be non-empty")
|
||||
for feedback_index, feedback_id in enumerate(feedback_ids):
|
||||
_frame_id(feedback_id, where=f"{frame_where}.feedback_ids[{feedback_index}]")
|
||||
|
||||
document["_capability_channels"] = {
|
||||
key: sorted(value) for key, value in sorted(capability_channels.items())
|
||||
}
|
||||
return document
|
||||
|
||||
|
||||
def _canonical_source(document: dict[str, Any]) -> bytes:
|
||||
source = {key: value for key, value in document.items() if not key.startswith("_")}
|
||||
return json.dumps(source, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
||||
|
||||
|
||||
def render_python(document: dict[str, Any]) -> str:
|
||||
digest = hashlib.sha256(_canonical_source(document)).hexdigest()
|
||||
source = {key: value for key, value in document.items() if not key.startswith("_")}
|
||||
literal = pprint.pformat(source, width=100, sort_dicts=False)
|
||||
return f'''# Generated by tools/generate_can_semantic_protocol.py; do not edit.
|
||||
"""Generated Python view of the shared CAN semantic protocol."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Iterable
|
||||
|
||||
PROTOCOL_SOURCE_SHA256 = "{digest}"
|
||||
PROTOCOL_MANIFEST: dict[str, Any] = {literal}
|
||||
PROTOCOL_VERSION = str(PROTOCOL_MANIFEST["version"])
|
||||
RULES: tuple[dict[str, Any], ...] = tuple(PROTOCOL_MANIFEST["rules"])
|
||||
RULE_BY_ID = {{str(rule["id"]): rule for rule in RULES}}
|
||||
CAN0_SUPPORTED_CAPABILITIES = frozenset(
|
||||
capability
|
||||
for rule in RULES
|
||||
if int(rule["channel"]) == 0
|
||||
for capability in rule["capabilities"]
|
||||
)
|
||||
CAN1_SUPPORTED_CAPABILITIES = frozenset(
|
||||
capability
|
||||
for rule in RULES
|
||||
if int(rule["channel"]) == 1
|
||||
for capability in rule["capabilities"]
|
||||
)
|
||||
|
||||
|
||||
def rule(rule_id: str) -> dict[str, Any]:
|
||||
try:
|
||||
return RULE_BY_ID[rule_id]
|
||||
except KeyError as exc:
|
||||
raise KeyError(f"unknown generated CAN protocol rule {{rule_id}}") from exc
|
||||
|
||||
|
||||
def frame(rule_id: str, key: str = "command") -> dict[str, Any]:
|
||||
for item in rule(rule_id)["frames"]:
|
||||
if item["key"] == key:
|
||||
return item
|
||||
raise KeyError(f"rule {{rule_id}} has no frame named {{key}}")
|
||||
|
||||
|
||||
def command_contract(rule_id: str, key: str = "command") -> dict[str, Any]:
|
||||
metadata = frame(rule_id, key)
|
||||
return {{
|
||||
"source_version": PROTOCOL_VERSION,
|
||||
"rule_id": rule_id,
|
||||
"frame_key": key,
|
||||
"frame_id": metadata["frame_id"],
|
||||
"format": metadata["format"],
|
||||
"brs": bool(metadata["brs"]),
|
||||
"dlc": int(metadata["dlc"]),
|
||||
"repeat": dict(metadata["repeat"]),
|
||||
"feedback_ids": list(metadata["feedback_ids"]),
|
||||
"feedback_policy": str(metadata.get("feedback_policy", "")),
|
||||
}}
|
||||
|
||||
|
||||
def payload_variant(rule_id: str, selector: str, key: str = "command") -> bytes:
|
||||
item = frame(rule_id, key)
|
||||
try:
|
||||
value = item["payload_variants"][selector]
|
||||
except KeyError as exc:
|
||||
raise KeyError(f"rule {{rule_id}} frame {{key}} has no payload {{selector}}") from exc
|
||||
return bytes.fromhex(str(value))
|
||||
|
||||
|
||||
def rules_for_capability(capability: str, channel: int | None = None) -> tuple[dict[str, Any], ...]:
|
||||
return tuple(
|
||||
item
|
||||
for item in RULES
|
||||
if capability in item["capabilities"]
|
||||
and (channel is None or int(item["channel"]) == channel)
|
||||
)
|
||||
|
||||
|
||||
def _matches_token(word: str, token: dict[str, Any]) -> bool:
|
||||
if "literal" in token:
|
||||
return word == token["literal"]
|
||||
if "enum" in token:
|
||||
return word in token["values"]
|
||||
try:
|
||||
value = int(word, 10)
|
||||
except ValueError:
|
||||
return False
|
||||
return str(value) == word or (word.startswith("-") and str(value) == word)
|
||||
|
||||
|
||||
def _matches_integer(word: str, token: dict[str, Any]) -> bool:
|
||||
try:
|
||||
value = int(word, 10)
|
||||
except ValueError:
|
||||
return False
|
||||
return token["minimum"] <= value <= token["maximum"] and str(value) == word
|
||||
|
||||
|
||||
def match_semantic_command(words: str | Iterable[str], channel: int | None = None) -> dict[str, Any] | None:
|
||||
normalized = tuple(
|
||||
part.lower()
|
||||
for part in (words.split() if isinstance(words, str) else words)
|
||||
)
|
||||
for item in RULES:
|
||||
if channel is not None and int(item["channel"]) != channel:
|
||||
continue
|
||||
for pattern in item["patterns"]:
|
||||
if len(pattern) != len(normalized):
|
||||
continue
|
||||
if all(
|
||||
_matches_integer(word, token) if "integer" in token else _matches_token(word, token)
|
||||
for word, token in zip(normalized, pattern)
|
||||
):
|
||||
return item
|
||||
return None
|
||||
|
||||
|
||||
def whitelist_help(channel: int) -> tuple[str, ...]:
|
||||
return tuple(str(item["help"]) for item in RULES if int(item["channel"]) == channel)
|
||||
'''
|
||||
|
||||
|
||||
def _cpp_escape(value: str) -> str:
|
||||
return value.replace("\\", "\\\\").replace('"', '\\"')
|
||||
|
||||
|
||||
def _cpp_condition(pattern: list[dict[str, Any]]) -> str:
|
||||
conditions = [f"words.size() == {len(pattern)}"]
|
||||
for index, token in enumerate(pattern):
|
||||
if "literal" in token:
|
||||
conditions.append(f'words[{index}] == "{_cpp_escape(token["literal"])}"')
|
||||
elif "enum" in token:
|
||||
values = ", ".join(f'"{_cpp_escape(value)}"' for value in token["values"])
|
||||
conditions.append(f"protocol_contains(words[{index}], {{{values}}})")
|
||||
else:
|
||||
conditions.append(
|
||||
f"protocol_integer_in_range(words[{index}], {token['minimum']}, {token['maximum']})"
|
||||
)
|
||||
return " &&\n ".join(conditions)
|
||||
|
||||
|
||||
def render_cpp(document: dict[str, Any]) -> str:
|
||||
digest = hashlib.sha256(_canonical_source(document)).hexdigest()
|
||||
rules = document["rules"]
|
||||
frames: list[tuple[str, dict[str, Any]]] = []
|
||||
payloads: list[tuple[str, str, str, str]] = []
|
||||
for rule in rules:
|
||||
for item in rule["frames"]:
|
||||
frames.append((rule["id"], item))
|
||||
for selector, payload in item.get("payload_variants", {}).items():
|
||||
payloads.append((rule["id"], item["key"], selector, payload))
|
||||
|
||||
frame_rows = []
|
||||
for rule_id, item in frames:
|
||||
repeat = item["repeat"]
|
||||
feedback_csv = ",".join(item["feedback_ids"])
|
||||
template_document = {
|
||||
key: item[key]
|
||||
for key in (
|
||||
"base_payload_hex",
|
||||
"fields",
|
||||
"payload_variants",
|
||||
"optional_when",
|
||||
)
|
||||
if key in item
|
||||
}
|
||||
template_json = json.dumps(
|
||||
template_document,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
)
|
||||
frame_rows.append(
|
||||
" FrameMetadata{"
|
||||
f'"{_cpp_escape(rule_id)}", "{_cpp_escape(item["key"])}", '
|
||||
f'{int(item["frame_id"], 16)}, "{item["format"]}", '
|
||||
f'{str(bool(item["brs"])).lower()}, {item["dlc"]}, '
|
||||
f'"{_cpp_escape(item["template_kind"])}", '
|
||||
f'"{_cpp_escape(item["base_payload_hex"])}", '
|
||||
f'"{_cpp_escape(template_json)}", '
|
||||
f'"{_cpp_escape(repeat["kind"])}", {repeat["count"]}, {repeat["period_ms"]}, '
|
||||
f'"{_cpp_escape(feedback_csv)}", "{_cpp_escape(item.get("feedback_policy", ""))}"'
|
||||
"}"
|
||||
)
|
||||
rule_rows = []
|
||||
for rule in rules:
|
||||
capabilities = ",".join(rule["capabilities"])
|
||||
rule_rows.append(
|
||||
" RuleMetadata{"
|
||||
f'"{_cpp_escape(rule["id"])}", {rule["channel"]}, '
|
||||
f'"{_cpp_escape(rule["domain"])}", "{_cpp_escape(capabilities)}", '
|
||||
f'"{_cpp_escape(rule["help"])}", "{_cpp_escape(rule.get("special_handler", ""))}"'
|
||||
"}"
|
||||
)
|
||||
payload_rows = [
|
||||
" StaticPayload{"
|
||||
f'"{_cpp_escape(rule_id)}", "{_cpp_escape(key)}", "{_cpp_escape(selector)}", '
|
||||
f'"{_cpp_escape(payload)}"'
|
||||
"}"
|
||||
for rule_id, key, selector, payload in payloads
|
||||
]
|
||||
match_blocks = []
|
||||
for rule in rules:
|
||||
conditions = [f"({ _cpp_condition(pattern) })" for pattern in rule["patterns"]]
|
||||
joined = " ||\n ".join(conditions)
|
||||
match_blocks.append(
|
||||
f' if ((channel < 0 || channel == {rule["channel"]}) &&\n'
|
||||
f" ({joined}))\n"
|
||||
f' return find_rule("{_cpp_escape(rule["id"])}");'
|
||||
)
|
||||
|
||||
return f'''// Generated by tools/generate_can_semantic_protocol.py; do not edit.
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <charconv>
|
||||
#include <initializer_list>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace cognitive_agent_can_protocol {{
|
||||
|
||||
inline constexpr std::string_view kProtocolVersion = "{document["version"]}";
|
||||
inline constexpr std::string_view kProtocolSourceSha256 = "{digest}";
|
||||
|
||||
struct FrameMetadata {{
|
||||
std::string_view rule_id;
|
||||
std::string_view key;
|
||||
unsigned frame_id;
|
||||
std::string_view format;
|
||||
bool brs;
|
||||
unsigned dlc;
|
||||
std::string_view template_kind;
|
||||
std::string_view base_payload_hex;
|
||||
std::string_view template_json;
|
||||
std::string_view repeat_kind;
|
||||
unsigned repeat_count;
|
||||
unsigned period_ms;
|
||||
std::string_view feedback_ids_csv;
|
||||
std::string_view feedback_policy;
|
||||
}};
|
||||
|
||||
struct RuleMetadata {{
|
||||
std::string_view id;
|
||||
int channel;
|
||||
std::string_view domain;
|
||||
std::string_view capabilities_csv;
|
||||
std::string_view help;
|
||||
std::string_view special_handler;
|
||||
}};
|
||||
|
||||
struct StaticPayload {{
|
||||
std::string_view rule_id;
|
||||
std::string_view frame_key;
|
||||
std::string_view selector;
|
||||
std::string_view payload_hex;
|
||||
}};
|
||||
|
||||
inline constexpr std::array<FrameMetadata, {len(frame_rows)}> kFrames{{{{
|
||||
{',\n'.join(frame_rows)}
|
||||
}}}};
|
||||
|
||||
inline constexpr std::array<RuleMetadata, {len(rule_rows)}> kRules{{{{
|
||||
{',\n'.join(rule_rows)}
|
||||
}}}};
|
||||
|
||||
inline constexpr std::array<StaticPayload, {len(payload_rows)}> kStaticPayloads{{{{
|
||||
{',\n'.join(payload_rows)}
|
||||
}}}};
|
||||
|
||||
inline const RuleMetadata* find_rule(std::string_view id) {{
|
||||
for (const auto& item : kRules)
|
||||
if (item.id == id)
|
||||
return &item;
|
||||
return nullptr;
|
||||
}}
|
||||
|
||||
inline const FrameMetadata* find_frame(std::string_view rule_id,
|
||||
std::string_view key = "command") {{
|
||||
for (const auto& item : kFrames)
|
||||
if (item.rule_id == rule_id && item.key == key)
|
||||
return &item;
|
||||
return nullptr;
|
||||
}}
|
||||
|
||||
inline const StaticPayload* find_static_payload(std::string_view rule_id,
|
||||
std::string_view selector,
|
||||
std::string_view key = "command") {{
|
||||
for (const auto& item : kStaticPayloads)
|
||||
if (item.rule_id == rule_id && item.frame_key == key &&
|
||||
item.selector == selector)
|
||||
return &item;
|
||||
return nullptr;
|
||||
}}
|
||||
|
||||
inline bool protocol_contains(std::string_view value,
|
||||
std::initializer_list<std::string_view> allowed) {{
|
||||
for (const auto candidate : allowed)
|
||||
if (value == candidate)
|
||||
return true;
|
||||
return false;
|
||||
}}
|
||||
|
||||
inline bool protocol_integer_in_range(std::string_view text, int minimum,
|
||||
int maximum) {{
|
||||
int value = 0;
|
||||
const auto result = std::from_chars(text.data(), text.data() + text.size(), value);
|
||||
return result.ec == std::errc{{}} && result.ptr == text.data() + text.size() &&
|
||||
std::to_string(value) == text && value >= minimum && value <= maximum;
|
||||
}}
|
||||
|
||||
inline const RuleMetadata* match_semantic_command(
|
||||
const std::vector<std::string>& words, int channel = -1) {{
|
||||
{chr(10).join(match_blocks)}
|
||||
return nullptr;
|
||||
}}
|
||||
|
||||
}} // namespace cognitive_agent_can_protocol
|
||||
'''
|
||||
|
||||
|
||||
def _write_or_check(path: Path, rendered: str, *, check: bool) -> bool:
|
||||
current = path.read_text(encoding="utf-8") if path.exists() else None
|
||||
if current == rendered:
|
||||
return True
|
||||
if check:
|
||||
print(f"generated file is stale: {path}", file=sys.stderr)
|
||||
return False
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(rendered, encoding="utf-8")
|
||||
print(f"generated {path.relative_to(ROOT)}")
|
||||
return True
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--source", type=Path, default=DEFAULT_SOURCE)
|
||||
parser.add_argument("--python-output", type=Path, default=DEFAULT_PYTHON)
|
||||
parser.add_argument("--cpp-output", type=Path, default=DEFAULT_CPP)
|
||||
parser.add_argument("--check", action="store_true")
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
document = load_and_validate(args.source)
|
||||
except ProtocolError as exc:
|
||||
print(f"CAN protocol error: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
ok_python = _write_or_check(
|
||||
args.python_output, render_python(document), check=args.check
|
||||
)
|
||||
ok_cpp = _write_or_check(args.cpp_output, render_cpp(document), check=args.check)
|
||||
return 0 if ok_python and ok_cpp else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -8,6 +8,16 @@ import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from generate_can_semantic_protocol import (
|
||||
DEFAULT_CPP as CAN_PROTOCOL_CPP,
|
||||
DEFAULT_PYTHON as CAN_PROTOCOL_PYTHON,
|
||||
DEFAULT_SOURCE as CAN_PROTOCOL_SOURCE,
|
||||
ProtocolError as CanProtocolError,
|
||||
load_and_validate as load_can_protocol,
|
||||
render_cpp as render_can_protocol_cpp,
|
||||
render_python as render_can_protocol_python,
|
||||
)
|
||||
|
||||
try:
|
||||
import yaml
|
||||
from jsonschema import Draft202012Validator, FormatChecker, RefResolver
|
||||
@@ -248,6 +258,27 @@ def validate_platform_resources(errors: list[str], warnings: list[str]) -> int:
|
||||
return count
|
||||
|
||||
|
||||
def validate_generated_can_protocol(errors: list[str]) -> int:
|
||||
try:
|
||||
document = load_can_protocol(CAN_PROTOCOL_SOURCE)
|
||||
expected = {
|
||||
CAN_PROTOCOL_PYTHON: render_can_protocol_python(document),
|
||||
CAN_PROTOCOL_CPP: render_can_protocol_cpp(document),
|
||||
}
|
||||
except CanProtocolError as exc:
|
||||
errors.append(f"{relative(CAN_PROTOCOL_SOURCE)}: {exc}")
|
||||
return 0
|
||||
for path, content in expected.items():
|
||||
if not path.is_file():
|
||||
errors.append(f"{relative(path)}: generated CAN protocol view is missing")
|
||||
elif path.read_text(encoding="utf-8") != content:
|
||||
errors.append(
|
||||
f"{relative(path)}: stale generated CAN protocol view; "
|
||||
"run make can-protocol-generate"
|
||||
)
|
||||
return len(document["rules"])
|
||||
|
||||
|
||||
def main() -> int:
|
||||
errors: list[str] = []
|
||||
warnings: list[str] = []
|
||||
@@ -255,6 +286,7 @@ def main() -> int:
|
||||
document_count = validate_documents(schemas, schema_store, errors)
|
||||
domain_count = validate_domains(errors)
|
||||
platform_count = validate_platform_resources(errors, warnings)
|
||||
protocol_rule_count = validate_generated_can_protocol(errors)
|
||||
|
||||
for warning in warnings:
|
||||
print(f"WARNING: {warning}")
|
||||
@@ -267,7 +299,8 @@ def main() -> int:
|
||||
print(
|
||||
"Repository validation passed: "
|
||||
f"{len(schemas)} schemas, {document_count} documents, "
|
||||
f"{domain_count} domains, {platform_count} platform(s)."
|
||||
f"{domain_count} domains, {platform_count} platform(s), "
|
||||
f"{protocol_rule_count} generated CAN protocol rule(s)."
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user