From a6d749f67933e7732fbf52b226a78b5c85f91a3b Mon Sep 17 00:00:00 2001 From: lgv Date: Thu, 25 Jun 2026 09:26:23 +0800 Subject: [PATCH] revert: rollback dev to fb304df94c --- CMakeLists.txt | 2 - .../config/agv_config/agv_config.pb.txt | 30 - .../biohead_config/biohead_config.pb.txt | 27 - .../config/camera_config/camera_config.pb.txt | 2 +- .../dexhand_config/dexhand_config.pb.txt | 6 +- .../monitor_config/monitor_config.pb.txt | 25 - .../config/robot_config/robot_config.pb.txt | 36 - .../config/server_config/server_config.pb.txt | 3 - .../config_helper/include/config_helper.h | 24 - .../config_helper/include/config_setting.h | 5 - .../config_helper/src/config_setting.cpp | 16 - .../common/utils/ffmpeg/include/FrameData.h | 53 - cmvr-es/data_center/CMakeLists.txt | 1 - cmvr-es/data_center/include/motors_info.h | 3 +- cmvr-es/data_center/src/motors_info.cpp | 156 +- cmvr-es/device_manager/CMakeLists.txt | 1 - .../device_manager/include/device_factory.h | 2 +- .../device_manager/include/device_manager.h | 2 +- cmvr-es/device_manager/src/device_factory.cpp | 56 +- cmvr-es/device_manager/src/device_manager.cpp | 66 +- cmvr-es/devices/CMakeLists.txt | 1 - cmvr-es/devices/agv/CMakeLists.txt | 2 - cmvr-es/devices/agv/abstract_agv.h | 881 +----- .../devices/agv/agv_src1100/CMakeLists.txt | 20 - .../agv/agv_src1100/include/agv_src1100.h | 307 -- .../agv/agv_src1100/src/agv_src1100.cpp | 2771 ----------------- cmvr-es/devices/biohead/abstract_biohead.h | 1 - .../biohead_esp32/include/biohead_esp32.h | 5 +- .../biohead_esp32/src/biohead_esp32.cpp | 213 -- cmvr-es/devices/canbus/abstract_canbus.h | 1 - .../socket/socket_can_client_raw.cc | 11 - .../can_client/socket/socket_can_client_raw.h | 4 - cmvr-es/devices/motor/CMakeLists.txt | 2 +- cmvr-es/devices/motor/abstract_motor.h | 1 - .../devices/motor/eyou_motor/CMakeLists.txt | 21 - .../ethercat/eyou_motor_ethercat_protocol.cpp | 747 ----- .../ethercat/eyou_motor_ethercat_protocol.h | 125 - .../ethercat/protocol/ec_master.cpp | 1143 ------- .../eyou_motor/ethercat/protocol/ec_master.h | 328 -- .../ethercat/protocol/ec_pdo_config.h | 75 - .../eyou_motor/ethercat/protocol/ec_types.h | 111 - .../ethercat/protocol/ec_xml_parser.cpp | 317 -- .../ethercat/protocol/ec_xml_parser.h | 41 - .../devices/motor/eyou_motor/eyou_motor.cpp | 5 - cmvr-es/devices/motor/eyou_motor/eyou_motor.h | 46 - .../devices/motor/motor_protocol_interface.h | 3 +- cmvr-es/devices/motor/ti5_motor/ti5_motor.h | 12 +- cmvr-es/devices/robot/abstract_robot.h | 1 - .../humanoid_robot/include/humanoid_robot.h | 9 +- .../humanoid_robot/src/humanoid_robot.cpp | 83 +- .../src/humanoid_robot_test.cpp | 18 - cmvr-es/main.cpp | 1 - cmvr-es/monitor/abstract_monitor.h | 1 - .../diskmonitor/include/disk_monitor.h | 6 +- .../monitor/diskmonitor/src/disk_monitor.cpp | 64 +- .../monitor_manager/include/monitor_factory.h | 15 +- .../monitor_manager/include/monitor_manager.h | 25 +- .../monitor_manager/src/monitor_factory.cpp | 28 +- .../monitor_manager/src/monitor_manager.cpp | 56 +- cmvr-es/service/CMakeLists.txt | 1 - .../service/grpc/include/grpc_agv_service.h | 278 -- cmvr-es/service/grpc/include/server_runner.h | 2 - cmvr-es/service/grpc/src/grpc_agv_service.cpp | 1208 ------- .../grpc/src/grpc_humanoid_robot_service.cpp | 2 +- cmvr-es/service/grpc/src/server_runner.cpp | 17 +- .../third_party/soem/include/soem/ec_base.h | 42 - .../third_party/soem/include/soem/ec_coe.h | 73 - .../third_party/soem/include/soem/ec_config.h | 31 - .../x86/third_party/soem/include/soem/ec_dc.h | 27 - .../third_party/soem/include/soem/ec_eoe.h | 213 -- .../third_party/soem/include/soem/ec_foe.h | 27 - .../third_party/soem/include/soem/ec_main.h | 624 ---- .../soem/include/soem/ec_options.h | 128 - .../third_party/soem/include/soem/ec_print.h | 30 - .../third_party/soem/include/soem/ec_soe.h | 121 - .../third_party/soem/include/soem/ec_type.h | 551 ---- .../third_party/soem/include/soem/nicdrv.h | 108 - .../x86/third_party/soem/include/soem/osal.h | 222 -- .../third_party/soem/include/soem/osal_defs.h | 46 - .../x86/third_party/soem/include/soem/soem.h | 28 - dependency/x86/third_party/soem/lib/libsoem.a | Bin 691060 -> 0 bytes protos/cmvr/api/agv_command.proto | 906 ------ protos/cmvr/api/agv_service.proto | 74 - .../cmvr/config/agv_config/agv_config.proto | 20 - .../biohead_config/biohead_config.proto | 44 - .../monitor_config/monitor_config.proto | 22 - .../config/robot_config/robot_config.proto | 88 - .../config/server_config/server_config.proto | 10 - request.txt | 1 - 89 files changed, 123 insertions(+), 12838 deletions(-) delete mode 100644 cmvr-es/common/config/agv_config/agv_config.pb.txt delete mode 100644 cmvr-es/common/config/biohead_config/biohead_config.pb.txt delete mode 100644 cmvr-es/common/config/monitor_config/monitor_config.pb.txt delete mode 100644 cmvr-es/common/config/robot_config/robot_config.pb.txt delete mode 100644 cmvr-es/common/config/server_config/server_config.pb.txt delete mode 100644 cmvr-es/common/utils/ffmpeg/include/FrameData.h delete mode 100644 cmvr-es/devices/agv/CMakeLists.txt delete mode 100644 cmvr-es/devices/agv/agv_src1100/CMakeLists.txt delete mode 100644 cmvr-es/devices/agv/agv_src1100/include/agv_src1100.h delete mode 100644 cmvr-es/devices/agv/agv_src1100/src/agv_src1100.cpp delete mode 100644 cmvr-es/devices/motor/eyou_motor/CMakeLists.txt delete mode 100644 cmvr-es/devices/motor/eyou_motor/ethercat/eyou_motor_ethercat_protocol.cpp delete mode 100644 cmvr-es/devices/motor/eyou_motor/ethercat/eyou_motor_ethercat_protocol.h delete mode 100644 cmvr-es/devices/motor/eyou_motor/ethercat/protocol/ec_master.cpp delete mode 100644 cmvr-es/devices/motor/eyou_motor/ethercat/protocol/ec_master.h delete mode 100644 cmvr-es/devices/motor/eyou_motor/ethercat/protocol/ec_pdo_config.h delete mode 100644 cmvr-es/devices/motor/eyou_motor/ethercat/protocol/ec_types.h delete mode 100644 cmvr-es/devices/motor/eyou_motor/ethercat/protocol/ec_xml_parser.cpp delete mode 100644 cmvr-es/devices/motor/eyou_motor/ethercat/protocol/ec_xml_parser.h delete mode 100644 cmvr-es/devices/motor/eyou_motor/eyou_motor.cpp delete mode 100644 cmvr-es/devices/motor/eyou_motor/eyou_motor.h delete mode 100644 cmvr-es/service/grpc/include/grpc_agv_service.h delete mode 100644 cmvr-es/service/grpc/src/grpc_agv_service.cpp delete mode 100644 dependency/x86/third_party/soem/include/soem/ec_base.h delete mode 100644 dependency/x86/third_party/soem/include/soem/ec_coe.h delete mode 100644 dependency/x86/third_party/soem/include/soem/ec_config.h delete mode 100644 dependency/x86/third_party/soem/include/soem/ec_dc.h delete mode 100644 dependency/x86/third_party/soem/include/soem/ec_eoe.h delete mode 100644 dependency/x86/third_party/soem/include/soem/ec_foe.h delete mode 100644 dependency/x86/third_party/soem/include/soem/ec_main.h delete mode 100644 dependency/x86/third_party/soem/include/soem/ec_options.h delete mode 100644 dependency/x86/third_party/soem/include/soem/ec_print.h delete mode 100644 dependency/x86/third_party/soem/include/soem/ec_soe.h delete mode 100644 dependency/x86/third_party/soem/include/soem/ec_type.h delete mode 100644 dependency/x86/third_party/soem/include/soem/nicdrv.h delete mode 100644 dependency/x86/third_party/soem/include/soem/osal.h delete mode 100644 dependency/x86/third_party/soem/include/soem/osal_defs.h delete mode 100644 dependency/x86/third_party/soem/include/soem/soem.h delete mode 100644 dependency/x86/third_party/soem/lib/libsoem.a delete mode 100644 protos/cmvr/api/agv_command.proto delete mode 100644 protos/cmvr/api/agv_service.proto delete mode 100644 protos/cmvr/config/agv_config/agv_config.proto delete mode 100644 protos/cmvr/config/biohead_config/biohead_config.proto delete mode 100644 protos/cmvr/config/monitor_config/monitor_config.proto delete mode 100644 protos/cmvr/config/robot_config/robot_config.proto delete mode 100644 protos/cmvr/config/server_config/server_config.proto diff --git a/CMakeLists.txt b/CMakeLists.txt index 77cc732c..9e100d3a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -122,8 +122,6 @@ target_link_libraries(cmvr_es PRIVATE cmvr_es::device::humanoid_robot cmvr_es::common cmvr_es::applications - cmvr_es::device::agv_src1100 - ) install(TARGETS cmvr_es RUNTIME DESTINATION bin) diff --git a/cmvr-es/common/config/agv_config/agv_config.pb.txt b/cmvr-es/common/config/agv_config/agv_config.pb.txt deleted file mode 100644 index e9c35fe1..00000000 --- a/cmvr-es/common/config/agv_config/agv_config.pb.txt +++ /dev/null @@ -1,30 +0,0 @@ -src1100_agvs { - id: "agv_src1100" - ip: "192.168.192.5" - port_status: 19204 - port_control: 19205 - port_nav: 19206 - port_config: 19207 - port_other: 19210 - port_push: 19302 - - - - enable: true -} - - -src1100_agvs { - id: "agv_src2200" - ip: "192.168.192.5" - port_status: 19204 - port_control: 19205 - port_nav: 19206 - port_config: 19207 - port_other: 19210 - port_push: 19301 - - - - enable: true -} diff --git a/cmvr-es/common/config/biohead_config/biohead_config.pb.txt b/cmvr-es/common/config/biohead_config/biohead_config.pb.txt deleted file mode 100644 index b2880e64..00000000 --- a/cmvr-es/common/config/biohead_config/biohead_config.pb.txt +++ /dev/null @@ -1,27 +0,0 @@ -bioheads { - id: "bio_head" - serial: "/dev/ttyUSB0" - ctrlFreq: 50 - enable: false - - eyebrow { - serial: "64:0~3" - offest: "90 90 90 90" - jLmtUp: "90 170 155 110" - jLmtLow: "20 77 90 20" - } - - eye { - serial: "64:4~9" - offest: "90 90 90 90 90 90" - jLmtUp: "90 150 165 90 120 115" - jLmtLow: "20 90 90 25 70 75" - } - - mouth { - serial: "65:0~9" - offest: "90 90 90 90 90 90 90 90 90 90" - jLmtUp: "150 110 130 140 100 105 110 125 90 95" - jLmtLow: "70 30 80 80 65 55 45 80 85 90" - } -} \ No newline at end of file diff --git a/cmvr-es/common/config/camera_config/camera_config.pb.txt b/cmvr-es/common/config/camera_config/camera_config.pb.txt index f35f4018..18b02c30 100644 --- a/cmvr-es/common/config/camera_config/camera_config.pb.txt +++ b/cmvr-es/common/config/camera_config/camera_config.pb.txt @@ -27,7 +27,7 @@ realsense_cameras { align_mode: ALIGN_MODE_COLOR buffer_size: 30 sync: false - enable: false + enable: true } realsense_cameras { diff --git a/cmvr-es/common/config/dexhand_config/dexhand_config.pb.txt b/cmvr-es/common/config/dexhand_config/dexhand_config.pb.txt index 4a38bd2f..e6783dd2 100644 --- a/cmvr-es/common/config/dexhand_config/dexhand_config.pb.txt +++ b/cmvr-es/common/config/dexhand_config/dexhand_config.pb.txt @@ -1,6 +1,6 @@ rh56dftp_dexhands { id: "hand1" - ip: "192.168.1.223" + ip: "192.168.1.213" port: 6000 poll_interval_ms: 10 enable: false @@ -11,7 +11,7 @@ rh56dftp_dexhands { ip: "192.168.1.224" port: 6000 poll_interval_ms: 10 - enable: false + enable: true } px_6ax_gen3 { @@ -32,5 +32,5 @@ px_6ax_gen3 { sensor_name: "Paxini Gen3末端压力" polling_read_mode: PX_6AX_GEN3_POLLING_READ_MODE_RESULTANT_FORCE auto_calibrate: false - enable: false + enable: true } diff --git a/cmvr-es/common/config/monitor_config/monitor_config.pb.txt b/cmvr-es/common/config/monitor_config/monitor_config.pb.txt deleted file mode 100644 index d5350f3c..00000000 --- a/cmvr-es/common/config/monitor_config/monitor_config.pb.txt +++ /dev/null @@ -1,25 +0,0 @@ -# 监控管理器配置 -monitor_manager { - disk_monitor { - id: "file_monitor" - freq: 1 - enable: false - - folders { - fileDir: "/home/share/assets/audio" - maxVolume: 1000 - } - folders { - fileDir: "/home/share/assets/image" - maxVolume: 1000 - } - folders { - fileDir: "/home/share/assets/video" - maxVolume: 1000 - } - folders { - fileDir: "../log" - maxVolume: 1000 - } - } -} \ No newline at end of file diff --git a/cmvr-es/common/config/robot_config/robot_config.pb.txt b/cmvr-es/common/config/robot_config/robot_config.pb.txt deleted file mode 100644 index 9e2a9327..00000000 --- a/cmvr-es/common/config/robot_config/robot_config.pb.txt +++ /dev/null @@ -1,36 +0,0 @@ -ethercat_robots { - id: "ec_robot_01" - dof: 6 - urdf: "/home/cmvr/Projects/cmvr-es/model/xiaoyan_description/dual_arm.urdf" - baseLink: "PELVIS_S" - jointNames: "L_SHOULDER_P,L_SHOULDER_R,L_SHOULDER_Y,L_ELBOW_R,L_WRIST_P,L_WRIST_Y,L_WRIST_R,R_SHOULDER_P,R_SHOULDER_R,R_SHOULDER_Y,R_ELBOW_R,R_WRIST_P,R_WRIST_Y,R_WRIST_R" - linkNames: "PELVIS_S,L_SHOULDER_P_S,L_SHOULDER_R_S,L_SHOULDER_Y_S,L_ELBOW_R_S,L_WRIST_P_S,L_WRIST_Y_S,L_WRIST_R_S,R_SHOULDER_P_S,R_SHOULDER_R_S,R_SHOULDER_Y_S,R_ELBOW_R_S,R_WRIST_P_S,R_WRIST_Y_S,R_WRIST_R_S,R_FINGER_TIP,R_CAM" - bufferSize: 100 - verbose: true - updFreq: 1000 - enable: true - - # EtherCAT主站配置 - master { - interface: "enp1s0" - cycle_time_us: 1000 - enable: true - } - - # 电机配置列表 - motors { - slave: 1 - jointName: "L_SHOULDER_P" - limitQLb: -3.14 - limitQUb: 3.14 - limitQd: 3.0 - } - - motors { - slave: 2 - jointName: "L_SHOULDER_R" - limitQLb: -3.14 - limitQUb: 3.14 - limitQd: 3.0 - } -} \ No newline at end of file diff --git a/cmvr-es/common/config/server_config/server_config.pb.txt b/cmvr-es/common/config/server_config/server_config.pb.txt deleted file mode 100644 index 78599a06..00000000 --- a/cmvr-es/common/config/server_config/server_config.pb.txt +++ /dev/null @@ -1,3 +0,0 @@ -grpc_server { - port: "50052" -} \ No newline at end of file diff --git a/cmvr-es/common/utils/config_helper/include/config_helper.h b/cmvr-es/common/utils/config_helper/include/config_helper.h index e7ae3404..7d189128 100644 --- a/cmvr-es/common/utils/config_helper/include/config_helper.h +++ b/cmvr-es/common/utils/config_helper/include/config_helper.h @@ -10,10 +10,6 @@ #include "cmvr/config/microphone_config/microphone_config.pb.h" #include "cmvr/config/speaker_config/speaker_conifg.pb.h" #include "cmvr/config/touch_screen_app_config/touch_screen_app_config.pb.h" -#include "cmvr/config/agv_config/agv_config.grpc.pb.h" -#include "cmvr/config/biohead_config/biohead_config.pb.h" -#include "cmvr/config/robot_config/robot_config.pb.h" -#include "cmvr/config/server_config/server_config.pb.h" #define GET_CONFIG(file, para) \ ([&]() -> bool { \ @@ -86,26 +82,6 @@ namespace cmvr return SET_CONFIG(config, touch_screen_app_config_file); } - static bool getAgvConfig(config::AGVConfig& config) - { - return GET_CONFIG(agv_config_file, config); - } - - - static bool getHeadConfig(config::HeadConfig& config) - { - return GET_CONFIG(biohead_config_file, config); - } - - static bool getRobotConfig(config::RobotConfig& config) - { - return GET_CONFIG(robot_config_file, config); - } - - static bool getServerConfig(config::ServerConfig& config) - { - return GET_CONFIG(server_config_file, config); - } private: // Make macros able to call these (macros call ::cmvr::ConfigHelper::xxx) template diff --git a/cmvr-es/common/utils/config_helper/include/config_setting.h b/cmvr-es/common/utils/config_helper/include/config_setting.h index 82c22b5f..5d58fb9e 100644 --- a/cmvr-es/common/utils/config_helper/include/config_setting.h +++ b/cmvr-es/common/utils/config_helper/include/config_setting.h @@ -6,8 +6,3 @@ DECLARE_string(dexhand_config_file); DECLARE_string(microphone_config_file); DECLARE_string(speaker_config_file); DECLARE_string(touch_screen_app_config_file); -DECLARE_string(agv_config_file); - -DECLARE_string(biohead_config_file); -DECLARE_string(robot_config_file); -DECLARE_string(server_config_file); diff --git a/cmvr-es/common/utils/config_helper/src/config_setting.cpp b/cmvr-es/common/utils/config_helper/src/config_setting.cpp index 041b4885..9e44092d 100644 --- a/cmvr-es/common/utils/config_helper/src/config_setting.cpp +++ b/cmvr-es/common/utils/config_helper/src/config_setting.cpp @@ -88,19 +88,3 @@ DEFINE_string(speaker_config_file, DEFINE_string(touch_screen_app_config_file, basePath() + "touch_screen_app_config/touch_screen_app_config.pb.txt", "The configuration file for TouchScreenApp"); - -DEFINE_string(agv_config_file, - basePath() + "agv_config/agv_config.pb.txt", - "AGV config file"); - -DEFINE_string(biohead_config_file, - basePath() + "biohead_config/biohead_config.pb.txt", - "The configuration file for BioHead"); - -DEFINE_string(robot_config_file, - basePath() + "robot_config/robot_config.pb.txt", - "The configuration file for Robot"); - -DEFINE_string(server_config_file, - basePath() + "server_config/server_config.pb.txt", - "The configuration file for Server"); diff --git a/cmvr-es/common/utils/ffmpeg/include/FrameData.h b/cmvr-es/common/utils/ffmpeg/include/FrameData.h deleted file mode 100644 index cd69682a..00000000 --- a/cmvr-es/common/utils/ffmpeg/include/FrameData.h +++ /dev/null @@ -1,53 +0,0 @@ -// FrameData.h -#pragma once - -#include - -extern "C" { -#include -#include -} - -namespace ffmpeg { - - struct FrameData { - AVFrame* av_frame = nullptr; // 编码用帧(YUV格式) - cv::Mat rgb_image; // OpenCV用图像(BGR格式) - int64_t pts = 0; // 时间戳 - int64_t frame_index = 0; // 帧序号 - - FrameData() = default; - - ~FrameData() { - if (av_frame) { - av_frame_free(&av_frame); - } - } - - // 禁止拷贝 - FrameData(const FrameData&) = delete; - FrameData& operator=(const FrameData&) = delete; - - // 允许移动 - FrameData(FrameData&& other) noexcept - : av_frame(other.av_frame), - rgb_image(std::move(other.rgb_image)), - pts(other.pts), - frame_index(other.frame_index) { - other.av_frame = nullptr; - } - - FrameData& operator=(FrameData&& other) noexcept { - if (this != &other) { - if (av_frame) av_frame_free(&av_frame); - av_frame = other.av_frame; - rgb_image = std::move(other.rgb_image); - pts = other.pts; - frame_index = other.frame_index; - other.av_frame = nullptr; - } - return *this; - } - }; - -} // namespace ffmpeg \ No newline at end of file diff --git a/cmvr-es/data_center/CMakeLists.txt b/cmvr-es/data_center/CMakeLists.txt index ff66818d..d0fda580 100644 --- a/cmvr-es/data_center/CMakeLists.txt +++ b/cmvr-es/data_center/CMakeLists.txt @@ -11,7 +11,6 @@ target_link_libraries(data_center PRIVATE cmvr_es::utils cmvr_es::device::canbus cmvr_es::device::ti5motor - cmvr_es::device::eyoumotor protobuf glog ) diff --git a/cmvr-es/data_center/include/motors_info.h b/cmvr-es/data_center/include/motors_info.h index ac892cfc..56c2de04 100644 --- a/cmvr-es/data_center/include/motors_info.h +++ b/cmvr-es/data_center/include/motors_info.h @@ -23,9 +23,8 @@ namespace cmvr { public: ~MotorsInfo(){}; + void init(const XmlNode &cfg); - void init(const cmvr::config::HumanRobotConfig& config); - void init(const cmvr::config::EthercatRobotConfig& config); static MotorsInfo *getInstance() { static MotorsInfo instance; diff --git a/cmvr-es/data_center/src/motors_info.cpp b/cmvr-es/data_center/src/motors_info.cpp index 7cfa7855..4b6a087c 100644 --- a/cmvr-es/data_center/src/motors_info.cpp +++ b/cmvr-es/data_center/src/motors_info.cpp @@ -3,8 +3,6 @@ // #include "data_center/include/motors_info.h" - -#include "motor/eyou_motor/eyou_motor.h" #include "motor/ti5_motor/canopen/ti5_motor_canopen_protocol.h" #include "motor/ti5_motor/ti5_motor.h" @@ -96,10 +94,7 @@ void MotorsInfo::init(const XmlNode &cfg) { }; // 创建 MotorManager - if (!motor_manager_) - { - motor_manager_ = std::make_shared(); - } + motor_manager_ = std::make_shared(); std::vector > tasks; @@ -156,155 +151,6 @@ void MotorsInfo::init(const XmlNode &cfg) { LOG(INFO) << "All enabled motors initialized successfully."; } -void MotorsInfo::init(const cmvr::config::HumanRobotConfig& config) { - const auto& can_manager = config.can_manager(); - - // 读取enable状态 - left_arm_enabled_ = can_manager.left_arm().enable(); - right_arm_enabled_ = can_manager.right_arm().enable(); - waist_enabled_ = can_manager.waist().enable(); - head_enabled_ = can_manager.head().enable(); - - // 局部变量存储电机配置 - std::vector l_motors_cfg; - std::vector r_motors_cfg; - std::vector waist_motors_cfg; - std::vector head_motors_cfg; - - // 左臂 - if (left_arm_enabled_) { - const auto& left_arm = can_manager.left_arm(); - for (const auto& motor : left_arm.motors()) { - l_motors_cfg.push_back(motor); - } - l_can_client_ = std::make_shared(left_arm); - l_can_sender_ = std::make_shared>(); - l_can_receiver_ = std::make_shared>(); - l_message_manager_ = std::make_shared>(); - } - - // 右臂 - if (right_arm_enabled_) { - const auto& right_arm = can_manager.right_arm(); - for (const auto& motor : right_arm.motors()) { - r_motors_cfg.push_back(motor); - } - r_can_client_ = std::make_shared(right_arm); - r_can_sender_ = std::make_shared>(); - r_can_receiver_ = std::make_shared>(); - r_message_manager_ = std::make_shared>(); - } - - // 腰部 - if (waist_enabled_) { - const auto& waist = can_manager.waist(); - for (const auto& motor : waist.motors()) { - waist_motors_cfg.push_back(motor); - } - waist_can_client_ = std::make_shared(waist); - waist_can_sender_ = std::make_shared>(); - waist_can_receiver_ = std::make_shared>(); - waist_message_manager_ = std::make_shared>(); - } - - // 头部 - if (head_enabled_) { - const auto& head = can_manager.head(); - for (const auto& motor : head.motors()) { - head_motors_cfg.push_back(motor); - } - head_can_client_ = std::make_shared(head); - head_can_sender_ = std::make_shared>(); - head_can_receiver_ = std::make_shared>(); - head_message_manager_ = std::make_shared>(); - } - - // 初始化 limbs - struct Limb { - std::string name; - bool enabled; - std::shared_ptr client; - std::shared_ptr> sender; - std::shared_ptr> receiver; - std::shared_ptr> message_manager; - std::vector motor_cfgs; - }; - - std::vector limbs{ - {"WAIST", waist_enabled_, waist_can_client_, waist_can_sender_, waist_can_receiver_, waist_message_manager_, waist_motors_cfg}, - {"LEFT_ARM", left_arm_enabled_, l_can_client_, l_can_sender_, l_can_receiver_, l_message_manager_, l_motors_cfg}, - {"RIGHT_ARM", right_arm_enabled_, r_can_client_, r_can_sender_, r_can_receiver_, r_message_manager_, r_motors_cfg}, - {"HEAD", head_enabled_, head_can_client_, head_can_sender_, head_can_receiver_, head_message_manager_, head_motors_cfg} - }; - - if (!motor_manager_) - { - motor_manager_ = std::make_shared(); - } - std::vector> tasks; - - for (auto &limb : limbs) { - if (!limb.enabled) continue; - - if (limb.client) limb.client->init(); - - if (limb.sender && limb.receiver && limb.client) { - auto ret = limb.sender->Init(limb.client.get(), false); - if (ret != ErrorCode::OK) - LOG(ERROR) << "Failed to init " << limb.name << " CAN sender."; - - ret = limb.receiver->Init(limb.client.get(), limb.message_manager.get(), false); - if (ret != ErrorCode::OK) - LOG(ERROR) << "Failed to init " << limb.name << " CAN receiver."; - - limb.client->start(); - ret = limb.sender->Start(); - if (ret != ErrorCode::OK) - LOG(ERROR) << "Failed to start " << limb.name << " CAN sender."; - - ret = limb.receiver->Start(); - if (ret != ErrorCode::OK) - LOG(ERROR) << "Failed to start " << limb.name << " CAN receiver."; - } - - std::shared_ptr protocol = nullptr; - if (limb.sender && limb.message_manager) { - protocol = std::make_shared(limb.sender, limb.message_manager); - } - - if (!limb.motor_cfgs.empty()) { - tasks.push_back(std::async(std::launch::async, [this, protocol, &limb] { - LOG(INFO) << "[Thread " << std::this_thread::get_id() << "] Start initializing " << limb.name << " motors..."; - for (const auto& motor_cfg : limb.motor_cfgs) { - auto motor = std::make_shared(motor_cfg); - if (protocol) motor->setProtocol(protocol); - motor->init(); - motor_manager_->addMotor(motor); - } - })); - } - } - - for (auto& task : tasks) task.get(); - LOG(INFO) << "All enabled motors initialized successfully."; -} - - -void MotorsInfo::init(const cmvr::config::EthercatRobotConfig& config) -{ - if (!motor_manager_) - { - motor_manager_ = std::make_shared(); - } - auto protocol = std::make_shared(config.master().interface()); - for (const auto& motor_cfg : config.motors()) { - auto motor = std::make_shared(motor_cfg); - if (protocol) motor->setProtocol(protocol); - motor->init(); - motor_manager_->addMotor(motor); - } -} - bool MotorsInfo::getJointsQ(const std::vector &joints_name, std::unordered_map &joint_qs) const { auto res{true}; diff --git a/cmvr-es/device_manager/CMakeLists.txt b/cmvr-es/device_manager/CMakeLists.txt index d74d86c3..0b97b456 100644 --- a/cmvr-es/device_manager/CMakeLists.txt +++ b/cmvr-es/device_manager/CMakeLists.txt @@ -15,7 +15,6 @@ target_link_libraries(device_manager PRIVATE cmvr_es::device::rh56dftp_dexhand cmvr_es::device::px_6ax_gen3 cmvr::device::head_esp32 - cmvr_es::device::agv_src1100 osqp cmvr_es::device::humanoid_robot # cmvr_es::device::aubo_robot diff --git a/cmvr-es/device_manager/include/device_factory.h b/cmvr-es/device_manager/include/device_factory.h index b46e3495..1fcce739 100644 --- a/cmvr-es/device_manager/include/device_factory.h +++ b/cmvr-es/device_manager/include/device_factory.h @@ -28,7 +28,7 @@ namespace cmvr::device { template std::shared_ptr createFromConfig(const ConfigType& cfg); private: - std::shared_ptr create_agv_(const XmlNode& cfg); + std::shared_ptr create_agv_(const XmlNode& cfg); std::shared_ptr create_battery_(const XmlNode& cfg); std::shared_ptr create_camera_(const XmlNode& cfg); std::shared_ptr create_dexhand_(const XmlNode& cfg); diff --git a/cmvr-es/device_manager/include/device_manager.h b/cmvr-es/device_manager/include/device_manager.h index 8f54823b..ee782e5d 100644 --- a/cmvr-es/device_manager/include/device_manager.h +++ b/cmvr-es/device_manager/include/device_manager.h @@ -16,7 +16,7 @@ namespace cmvr::device { using DeviceVariant = std::variant< - std::shared_ptr, + std::shared_ptr, std::shared_ptr, std::shared_ptr, std::shared_ptr, diff --git a/cmvr-es/device_manager/src/device_factory.cpp b/cmvr-es/device_manager/src/device_factory.cpp index 84262f35..fba92e51 100644 --- a/cmvr-es/device_manager/src/device_factory.cpp +++ b/cmvr-es/device_manager/src/device_factory.cpp @@ -15,13 +15,12 @@ //#include "robot/ti5_robot/ti5_robot.h" #include "cmvr/api/system_command.pb.h" #include "data_center/include/motors_info.h" -#include "devices/agv/agv_src1100/include/agv_src1100.h" //#include "devices/robot/aubo_robot/include/aubo_robot.h" using namespace std; using namespace cmvr::device; -template std::shared_ptr DeviceFactory::create(const XmlNode&); +template std::shared_ptr DeviceFactory::create(const XmlNode&); template std::shared_ptr DeviceFactory::create(const XmlNode&); template std::shared_ptr DeviceFactory::create(const XmlNode&); template std::shared_ptr DeviceFactory::create(const XmlNode&); @@ -33,7 +32,7 @@ template std::shared_ptr DeviceFactory::create template std::shared_ptr DeviceFactory::create(const XmlNode& cfg) { - if constexpr (std::is_same_v) { + if constexpr (std::is_same_v) { return create_agv_(cfg); } else if constexpr (std::is_same_v) { return create_battery_(cfg); @@ -58,7 +57,7 @@ std::shared_ptr DeviceFactory::create(const XmlNode& cfg) { } } -std::shared_ptr DeviceFactory::create_agv_(const XmlNode& cfg) { +std::shared_ptr DeviceFactory::create_agv_(const XmlNode& cfg) { return nullptr; } @@ -185,11 +184,6 @@ template std::shared_ptr DeviceFactory::createFromConfig DeviceFactory::createFromConfig(const config::PX6AXGen3& cfg); template std::shared_ptr DeviceFactory::createFromConfig(const config::FFMpegMicroPhoneConfig& cfg); template std::shared_ptr DeviceFactory::createFromConfig(const config::FFMpegSpeakerConfig& cfg); -template std::shared_ptr DeviceFactory::createFromConfig(const config::AGVsrc1100Config& cfg); -template std::shared_ptr DeviceFactory::createFromConfig(const config::BioHeadConfig& cfg); -template std::shared_ptr DeviceFactory::createFromConfig(const config::HumanRobotConfig& cfg); -template std::shared_ptr DeviceFactory::createFromConfig(const config::EthercatRobotConfig& cfg); - template std::shared_ptr DeviceFactory::createFromConfig(const ConfigType& cfg) { @@ -250,50 +244,6 @@ std::shared_ptr DeviceFactory::createFromConfig(const ConfigType& cf throw runtime_error("[DeviceFactory]: Unsupported microphone device type"); } } - else if constexpr (std::is_same_v) - { - if constexpr (std::is_same_v) - { - return std::make_shared(cfg); - } - else - { - LOG(ERROR) << "[DeviceFactory]: Unsupported microphone device type "; - throw runtime_error("[DeviceFactory]: Unsupported microphone device type"); - } - } - else if constexpr (std::is_same_v) - { - if constexpr (std::is_same_v) - { - auto motos_info = MotorsInfo::getInstance(); - motos_info->init(cfg); - return std::make_shared>(cfg); - } - else if constexpr (std::is_same_v) - { - auto motos_info = MotorsInfo::getInstance(); - motos_info->init(cfg); - return std::make_shared>(cfg); - } - else - { - LOG(ERROR) << "[DeviceFactory]: Unsupported microphone device type "; - throw runtime_error("[DeviceFactory]: Unsupported microphone device type"); - } - } - else if constexpr (std::is_same_v) - { - if constexpr (std::is_same_v) - { - return std::make_shared(cfg); - } - else - { - LOG(ERROR) << "[DeviceFactory]: Unsupported agv device type "; - throw runtime_error("[DeviceFactory]: Unsupported agv device type"); - } - } else { LOG(ERROR) << "[DeviceFactory]: Unsupported device type"; throw runtime_error("[DeviceFactory]: Unsupported device type"); diff --git a/cmvr-es/device_manager/src/device_manager.cpp b/cmvr-es/device_manager/src/device_manager.cpp index 60a8c824..805516f9 100644 --- a/cmvr-es/device_manager/src/device_manager.cpp +++ b/cmvr-es/device_manager/src/device_manager.cpp @@ -9,7 +9,7 @@ using namespace std; using namespace cmvr::device; using namespace cmvr::device; -template std::shared_ptr DeviceManager::getDevice(const std::string& device_id); +template std::shared_ptr DeviceManager::getDevice(const std::string& device_id); template std::shared_ptr DeviceManager::getDevice(const std::string& device_id); template std::shared_ptr DeviceManager::getDevice(const std::string& device_id); template std::shared_ptr DeviceManager::getDevice(const std::string& device_id); @@ -152,7 +152,7 @@ void DeviceManager::getDeviceList(std::list> string dev_type; std::visit([&](const auto& ptr) { using T = std::decay_t; - if constexpr (std::is_same_v>) { + if constexpr (std::is_same_v>) { dev_type = "AGV"; } else if constexpr (std::is_same_v>) { dev_type = "Battery"; @@ -223,12 +223,12 @@ void DeviceManager::init_devices_() { LOG(ERROR) << "[DeviceManager]: Duplicate AGV Device ID" << id; throw runtime_error("[DeviceManager]: Duplicate AGV Device ID" + id); } - auto device = dev_factory_->create(node); + auto device = dev_factory_->create(node); if (device == nullptr) { throw std::runtime_error("[DeviceManager]: Factory returned nullptr for device ID: " + id); } devices_[id] = device; - std::get>(devices_[id])->init(); + std::get>(devices_[id])->init(); LOG(INFO) << "[DeviceManager]: Init AGV " << id << " Success"; } @@ -312,24 +312,6 @@ void DeviceManager::init_devices_() { LOG(INFO) << "[DeviceManager]: Init Robot " << id << " Success"; } - // EYOU的先用新版的 - config::RobotConfig robot_config; - ConfigHelper::getRobotConfig(robot_config); - for (int i = 0; i < robot_config.ethercat_robots().size(); i++) { - auto robot = robot_config.ethercat_robots(i); - if (!robot.enable()) - continue; - auto id = robot.id(); - if (devices_.count(id)) { - LOG(ERROR) << "[DeviceManager]: Duplicate Robot Device ID" << id; - throw runtime_error("[DeviceManager]: Duplicate Robot Device ID" + id); - } - std::cout << "DexHand[" << i << "]: " << id << std::endl; - auto device = dev_factory_->createFromConfig(robot); - devices_[id] = device; - std::get>(devices_[id])->init(); - } - config::MicroPhoneConfig micro_phone_config; ConfigHelper::getMicroPhonesConfig(micro_phone_config); for (int i = 0; i < micro_phone_config.ffmpeg_microphones().size(); i++) { @@ -364,38 +346,22 @@ void DeviceManager::init_devices_() { std::get>(devices_[id])->init(); } - config::HeadConfig head_cfg; - ConfigHelper::getHeadConfig(head_cfg); - for (int i = 0; i < head_cfg.bioheads().size(); i++) { - auto head = head_cfg.bioheads(i); - if (!head.enable()) - continue; - auto id = head.id(); - if (devices_.count(id)) { - LOG(ERROR) << "[DeviceManager]: Duplicate Biohead Device ID" << id; - throw runtime_error("[DeviceManager]: Duplicate Biohead Device ID" + id); + + auto biohead_node = dmgr_node.getChild("BioHead"); + for (auto &node: biohead_node.getChildren()){ + string id = node.getAttrString("id"); + if (devices_.count(id)){ + LOG(ERROR) << "[DeviceManager]: Duplicate AbstractBiohead Device ID" << id; + throw runtime_error("[DeviceManager]: Duplicate AbstractBiohead Device ID" + id); + } + auto device = dev_factory_->create(node); + if (device == nullptr){ + throw std::runtime_error("[DeviceManager]: Factory returned nullptr for device ID: " + id); } - std::cout << "Biohead[" << i << "]: " << id << std::endl; - auto device = dev_factory_->createFromConfig(head); devices_[id] = device; std::get>(devices_[id])->init(); - } - config::AGVConfig agv_config; - ConfigHelper::getAgvConfig(agv_config); + LOG(INFO) << "[BioHead]: Init biohead " << id << " Success"; - for (int i = 0; i < agv_config.src1100_agvs().size(); i++) { - auto agv = agv_config.src1100_agvs(i); - if (!agv.enable()) - continue; - auto id = agv.id(); - if (devices_.count(id)) { - LOG(ERROR) << "[DeviceManager]: Duplicate agv Device ID" << id; - throw runtime_error("[DeviceManager]: Duplicate agv Device ID" + id); - } - std::cout << "AgvSrc1100[" << i << "]: " << id << std::endl; - auto device = dev_factory_->createFromConfig(agv); - devices_[id] = device; - std::get>(devices_[id])->init(); } } catch (const exception& e) { diff --git a/cmvr-es/devices/CMakeLists.txt b/cmvr-es/devices/CMakeLists.txt index 21191941..41c64db3 100644 --- a/cmvr-es/devices/CMakeLists.txt +++ b/cmvr-es/devices/CMakeLists.txt @@ -6,4 +6,3 @@ add_subdirectory(biohead) add_subdirectory(robot) add_subdirectory(canbus) add_subdirectory(motor) -add_subdirectory(agv) diff --git a/cmvr-es/devices/agv/CMakeLists.txt b/cmvr-es/devices/agv/CMakeLists.txt deleted file mode 100644 index 0d8b4bb9..00000000 --- a/cmvr-es/devices/agv/CMakeLists.txt +++ /dev/null @@ -1,2 +0,0 @@ -add_subdirectory(agv_src1100) - diff --git a/cmvr-es/devices/agv/abstract_agv.h b/cmvr-es/devices/agv/abstract_agv.h index 858e3973..a838fa89 100644 --- a/cmvr-es/devices/agv/abstract_agv.h +++ b/cmvr-es/devices/agv/abstract_agv.h @@ -1,869 +1,36 @@ // -// Created by xtkuang on 2025/5/6. +// Created by xtkuang on 2025/5/8. // -/** - * @file abstract_agv.h - * @brief AGV设备抽象基类定义,提供统一的接口规范和数据结构。 - * 所有具体AGV实现(如SRC-1100)需继承此类。 - * @note 本文件定义了大量与仙工API对应的结构体,用于状态查询、运动控制、导航任务等。 - */ + #ifndef CMVR_ES_ABSTRACT_AGV_H #define CMVR_ES_ABSTRACT_AGV_H #pragma once -#include -#include -#include -#include -#include -#include -#include -#include +#include "../abstract_device.h" -#include "devices/abstract_device.h" +namespace cmvr::device{ + class AbstractAGV: public AbstractDevice { + public: + explicit AbstractAGV(const XmlNode &config): AbstractDevice(config) {}; + ~AbstractAGV() override=default; -// 注意:这里只include,不使用 -#include "cmvr/config/agv_config/agv_config.pb.h" + virtual void getState(AGVState &state) {} -namespace cmvr::device { + // navigation + virtual void eStop() {} + virtual void goHome() {} + virtual void moveto(math::Pose2d &location, double speed_ratio) {} + virtual void setVelocity(math::Vec3 linear, math::Vec3 angular) {} -/** - * @brief AGV(自动导引车)设备的抽象基类 - * - * 继承自 AbstractDevice,定义了 AGV 设备通用的接口和行为, - * 包括状态管理、信息查询、运动控制、地图管理、导航任务等功能。 - * 所有具体 AGV 设备实现必须继承此类并实现其纯虚函数。 - */ -class AbstractAgv : public AbstractDevice { -public: - /** - * @brief AGV 设备运行状态枚举 - */ - enum class Status { - CREATED, ///< 已创建,未初始化 - INITIALIZED, ///< 已初始化,未运行 - RUNNING, ///< 运行中 - PAUSED, ///< 暂停 - STOPPED, ///< 已停止 - FAULT ///< 故障 + // map + virtual void initMap(float resolution, int width, int height) {} + virtual void updateMap() {} + virtual void saveMap(const std::string& file_path) {} + virtual void loadMap(const std::string& file_path) {} + + protected: + AGVState state_; }; +} - // ==================== 状态与信息结构体 ==================== - - /** - * @brief 机器人基本信息查询结果 - * - * 对应仙工 API 1000 (0x03E8) 的响应字段 - */ - struct AgvStatusInfo { - std::string id; ///< 设备唯一标识 - std::string vehicle_id; ///< 车辆编号(如 "agv_001") - std::string robot_note; ///< 机器人备注(用户自定义) - std::string version; ///< 软件版本号 - std::string model; ///< 车型(如 "SRC-1100") - std::string dsp_version; ///< DSP 固件版本 - std::string gyro_version; ///< 陀螺仪版本 - std::string map_version; ///< 地图版本 - std::string model_version; ///< 模型版本 - std::string netprotocol_version; ///< 网络协议版本 - std::string modbus_version; ///< Modbus 协议版本 - std::string current_map; ///< 当前加载的地图名称 - std::string current_map_md5; ///< 当前地图的 MD5 校验值 - std::string model_md5; ///< 模型文件 MD5 - std::string ssid; ///< 当前连接的 Wi-Fi SSID - int rssi = 0; ///< Wi-Fi 信号强度 (RSSI, 0~100) - std::string ap_addr; ///< 接入点 MAC 地址 - std::string current_ip; ///< 当前 IP 地址 - std::string mac; ///< 设备 MAC 地址 - std::string echoid_type; ///< 回音 ID 类型 - std::string echoid; ///< 回音 ID - int ret_code = 0; ///< 返回码,0 表示成功,非0 表示错误 - std::string err_msg; ///< 错误信息(失败时填充) - /** - * @brief 检查信息是否有效(ID 非空且返回码为 0) - */ - bool isValid() const { - return !id.empty() && ret_code == 0; - } - }; - - /** - * @brief 电池状态信息 - * - * 对应仙工 API 1007 (0x03EF) 的响应字段 - */ - struct BatteryStatus - { - double battery_level = 0.0; ///< 电池电量百分比 (0~100) - double battery_temp = 0.0; ///< 电池温度(摄氏度) - bool charging = false; ///< 是否正在充电 - double voltage = 0.0; ///< 当前电压(V) - double current = 0.0; ///< 当前电流(A) - double max_charge_voltage = -1.0; ///< 最大充电电压(-1 表示未知) - double max_charge_current = -1.0; ///< 最大充电电流(-1 表示未知) - bool manual_charge = false; ///< 是否连接手动充电器(仅 SRC-2000) - bool auto_charge = false; ///< 是否连接自动充电桩(仅 SRC-2000) - int32_t battery_cycle = 0; ///< 电池循环次数(从 BMS 读取,可能不准确) - std::string battery_user_data; ///< 电池用户自定义数据(字符串) - std::string extra; ///< 额外扩展字段(预留) - int32_t ret_code = 0; ///< 返回码,0 表示成功 - std::string create_on; ///< 数据创建时间戳(ISO 8601) - std::string err_msg; ///< 错误信息 - }; - - /** - * @brief 机器人位置信息 - * - * 对应仙工 API 1004 (0x03EC) 的响应字段 - */ - struct RobotLocation - { - double x = 0.0; ///< 世界坐标系 X 坐标(米) - double y = 0.0; ///< 世界坐标系 Y 坐标(米) - double angle = 0.0; ///< 朝向角(弧度),逆时针为正 - double confidence = 0.0; ///< 定位置信度(0~1) - std::string current_station; ///< 当前所在站点 ID(若在站点上) - std::string last_station; ///< 上一个经过的站点 ID - int32_t loc_method = 0; ///< 定位方法(0: 自然轮廓, 1: 反光柱, 2: 二维码, 3: 里程计, ...) - int32_t ret_code = 0; ///< 返回码,0 表示成功 - std::string create_on; ///< 数据创建时间戳 - std::string err_msg; ///< 错误信息 - }; - - /** - * @brief 地图下载结果 - * - * 对应仙工 API 4011 (0x0FAB) 的响应 - */ - struct DownloadMapResult - { - std::string map_name; ///< 地图名称 - std::string map_content; ///< 地图文件内容(JSON 字符串) - int32_t ret_code = 0; ///< 返回码,0 表示成功 - std::string create_on; ///< 数据创建时间戳 - std::string err_msg; ///< 错误信息 - }; - - /** - * @brief 单个地图文件信息 - */ - struct MapFileInfo - { - std::string name; ///< 文件名 - std::string modified; ///< 最后修改时间 - int64_t size = 0; ///< 文件大小(字节) - }; - - /** - * @brief 地图整体状态(当前地图、所有地图列表及文件详情) - * - * 对应仙工 API 1300 (0x0514) 的响应 - */ - struct MapStatus - { - std::string current_map; ///< 当前加载的地图名称 - std::string current_map_md5; ///< 当前地图的 MD5 - std::vector maps; ///< 所有地图名称列表 - std::vector map_files_info; ///< 每个地图文件的详细信息 - int32_t ret_code = 0; ///< 返回码,0 表示成功 - std::string create_on; ///< 数据创建时间戳 - std::string err_msg; ///< 错误信息 - }; - - /** - * @brief 上传地图结果 - * - * 对应仙工 API 4010 (0x0FAA) 的响应 - */ - struct UploadMapResult - { - int32_t ret_code = 0; ///< 返回码,0 表示成功 - std::string create_on; ///< 数据创建时间戳 - std::string err_msg; ///< 错误信息 - }; - - /** - * @brief 抢占控制权返回结果 - * - * 对应仙工 API 4005 (0x0FA5) 的响应 - */ - struct LockResult - { - int32_t ret_code = 0; ///< 返回码,0 表示成功 - std::string create_on; ///< 数据创建时间戳 - std::string err_msg; ///< 错误信息 - }; - - /** - * @brief 当前控制权信息 - * - * 对应仙工 API 1060 (0x0424) 的响应 - */ - struct CurrentLockStatus - { - bool locked = false; ///< 是否被锁定(有控制权持有者) - std::string ip; ///< 持有者 IP 地址 - int32_t port = 0; ///< 持有者端口 - uint8_t type = 0; ///< 持有者类型(0: 未知, 1: 调度系统, 2: 示教器, ...) - std::string nick_name; ///< 持有者昵称(自定义标识) - int64_t time_t = 0; ///< 锁定时间戳(Unix 秒) - std::string desc; ///< 描述信息 - int32_t ret_code = 0; ///< 返回码,0 表示成功 - std::string create_on; ///< 数据创建时间戳 - std::string err_msg; ///< 错误信息 - }; - - // ==================== 运动控制相关结构体 ==================== - - /** - * @brief 开环运动下发参数 - * - * 对应仙工 API 2010 (0x07DA) 的请求参数 - * 注意:多舵轮设备仅 vx/vy/w 生效,steer/real_steer 仅单舵轮设备有效。 - */ - struct MotionCtrlReq - { - double vx = 0.0; ///< 机器人坐标系 X 方向线速度(m/s) - double vy = 0.0; ///< 机器人坐标系 Y 方向线速度(m/s) - double w = 0.0; ///< 角速度(rad/s),逆时针为正 - double steer = 0.0; ///< 舵轮转向角(仅单舵轮,单位 rad) - double real_steer = 0.0; ///< 实际舵轮角度(反馈,仅单舵轮) - int64_t duration = -1; ///< 运动持续时间(毫秒),-1 表示一直运动直到下次指令 - }; - - /** - * @brief 运动控制返回结果 - */ - struct MotionCtrlRes - { - int ret_code = 0; ///< 返回码,0 表示成功 - std::string create_on; ///< 数据创建时间戳 - std::string err_msg; ///< 错误信息 - }; - - /** - * @brief 切换地图入参 - * - * 对应仙工 API 2022 (0x07E6) 的请求 - */ - struct LoadMapReq - { - std::string map_name; ///< 目标地图名称(必须已存在于机器人中) - }; - - /** - * @brief 切换地图返回结果 - */ - struct LoadMapRes - { - int ret_code = 0; ///< 返回码,0 表示成功 - std::string create_on; ///< 数据创建时间戳 - std::string err_msg; ///< 错误信息 - }; - - /** - * @brief 查询地图载入状态返回 - * - * 对应仙工 API 1022 (0x03FE) 的响应 - */ - struct QueryLoadMapStatusRes - { - int32_t loadmap_status = 0; ///< 地图加载状态:0=失败, 1=成功, 2=载入中 - int ret_code = 0; ///< 返回码,0 表示成功 - std::string create_on; ///< 数据创建时间戳 - std::string err_msg; ///< 错误信息 - }; - - /** - * @brief 单个站点信息 - * - * 对应仙工 API 1301 (0x0515) 响应中的单个站点对象 - */ - struct StationItem - { - std::string id; ///< 站点 ID(唯一标识) - std::string type; ///< 站点类型(如 "LocationMark", "ChargePoint", "ActionPoint") - double x = 0.0; ///< 站点 X 坐标(米) - double y = 0.0; ///< 站点 Y 坐标(米) - double r = 0.0; ///< 站点朝向角(弧度) - std::string desc; ///< 描述信息 - std::string executor; ///< 执行器信息(用于动作) - std::string prepoint; ///< 前置点 ID(若需要先到前置点) - std::string recfile; ///< 识别文件路径(用于视觉识别) - bool spin = false; ///< 是否允许自旋(原地旋转) - bool use_down_pgv = false; ///< 是否使用下视 PGV 二维码 - }; - - /** - * @brief 查询站点列表返回结果 - */ - struct QueryStationRes - { - std::vector stations; ///< 站点列表 - int ret_code = 0; ///< 返回码,0 表示成功 - std::string create_on; ///< 数据创建时间戳 - std::string err_msg; ///< 错误信息 - }; - - /** - * @brief 单段导航任务(用于指定路径导航) - * - * 对应仙工 API 3066 (0x0BFA) 中 move_task_list 的每个元素 - */ - struct MoveTaskItem - { - std::string task_id; ///< 任务 ID(必须全局唯一) - std::string source_id; ///< 起始站点 ID - std::string id; ///< 目标站点 ID - std::string operation; ///< 到达后执行的动作(如 "JackLoad", "WaitDI", 等) - double jack_height = 0.0; ///< 顶升高度(当 operation 为顶升相关时使用) - }; - - /** - * @brief 指定路径导航入参(多个导航段) - * - * 对应仙工 API 3066 的请求数据区 - */ - struct GoTargetListReq - { - std::vector move_task_list; ///< 导航任务序列(必须按顺序且相邻站点间有直接路径) - }; - - /** - * @brief 指定路径导航返回结果 - */ - struct GoTargetListRes - { - int ret_code = 0; ///< 返回码,0 表示成功 - std::string create_on; ///< 数据创建时间戳 - std::string err_msg; ///< 错误信息 - }; - - // ==================== 导航料箱信息 ==================== - /** - * @brief 导航任务中涉及的料箱信息 - */ - struct NavContainerItem - { - std::string container_name; ///< 料箱名称 - std::string desc; ///< 描述 - std::string goods_id; ///< 绑定的货物 ID - bool has_goods = false; ///< 是否已绑定货物 - }; - - // ==================== 1020 实时导航查询 ==================== - /** - * @brief 1020 实时导航查询入参 - * - * 对应仙工 API 1020 (0x03FC) - */ - struct RobotStatusTaskCurrentReq - { - bool simple = false; ///< 若为 true,只返回 task_status 字段 - }; - - /** - * @brief 1020 实时导航查询出参 - * - * 对应仙工 API 1020 的响应 - */ - struct RobotStatusTaskCurrentRes - { - int task_status = 0; ///< 任务状态:0=NONE, 1=WAITING, 2=RUNNING, 3=SUSPENDED, 4=COMPLETED, 5=FAILED, 6=CANCELED - int task_type = 0; ///< 任务类型:0=无, 1=自由导航到坐标, 2=自由导航到站点, 3=路径导航到站点, 7=平动转动, 100=其他 - std::string target_id; ///< 目标站点 ID(若 task_type 为 2 或 3) - std::vector target_point; ///< 目标坐标 [x, y, r](若 task_type 为 1) - std::vector finished_path; ///< 已经过的站点列表 - std::vector unfinished_path; ///< 尚未经过的站点列表 - std::string move_status_info; ///< 导航附加信息(如错误描述) - std::vector containers; ///< 料箱信息(若有) - int ret_code = 0; ///< 返回码,0 表示成功 - std::string create_on; ///< 数据创建时间戳 - std::string err_msg; ///< 错误信息 - }; - - // ==================== 1110 批量任务查询 ==================== - /** - * @brief 单条任务状态项 - */ - struct SingleTaskStatusItem - { - std::string task_id; ///< 任务 ID - int status = 0; ///< 状态码(同上述 task_status) - int type = 0; ///< 任务类型(同上述 task_type) - }; - - /** - * @brief 任务状态包(包含最近完成和所有未完成的任务) - * - * 对应仙工 API 1110 (0x0456) 的响应中的 task_status_package - */ - struct TaskStatusPackage - { - std::string closest_target; ///< 最近站点 ID - std::string source_name; ///< 起点站点名称 - std::string target_name; ///< 终点站点名称 - double percentage = 0.0; ///< 任务进度百分比(0~100) - double distance = 0.0; ///< 剩余距离(米) - std::string info; ///< 附加信息 - std::vector task_status_list; ///< 所有任务状态列表 - }; - - /** - * @brief 1110 批量查询任务入参 - */ - struct QueryTaskStatusPackageReq - { - std::vector task_ids; ///< 要查询的任务 ID 列表,若为空则查询所有未完成+最近一条完成 - }; - - /** - * @brief 1110 批量查询任务出参 - */ - struct QueryTaskStatusPackageRes - { - TaskStatusPackage task_status_package; ///< 任务状态包 - int ret_code = 0; ///< 返回码,0 表示成功 - std::string create_on; ///< 数据创建时间戳 - std::string err_msg; ///< 错误信息 - }; - - // ==================== DI / DO / 音频 / PGV / 脚本等通用类型 ==================== - /** - * @brief DI 点位信息 - */ - struct DIItem - { - int id = 0; ///< DI 编号 - bool status = false; ///< 状态(true=高电平, false=低电平) - }; - /** - * @brief DO 点位信息 - */ - struct DOItem - { - int id = 0; ///< DO 编号 - bool status = false; ///< 状态(true=高电平, false=低电平) - }; - /** - * @brief 音频播放参数 - */ - struct SoundArgs - { - std::string name; ///< 音频文件名 - int loop = 0; ///< 循环次数(0=播放一次, 1=循环播放) - int stop = 0; ///< 1=停止播放 - }; - /** - * @brief WaitDI 动作参数 - */ - struct WaitDIArgs - { - std::vector DI; ///< 需要等待的 DI 条件列表 - double timeout = 0.0; ///< 超时时间(秒),0 表示无限等待 - }; - /** - * @brief SetDO 动作参数 - */ - struct SetDOArgs - { - std::vector DO; ///< 要设置的 DO 列表 - }; - /** - * @brief PGV(二维码)二次定位参数 - */ - struct PgvParam - { - bool use_pgv = false; ///< 是否使用上视 PGV - bool use_down_pgv = false; ///< 是否使用下视 PGV - double pgv_adjust_dist = 0.0; ///< 最大调整半径(米) - double pgv_adjust_cx = 0.0; ///< 二维码坐标系下的 X 偏移(米) - double pgv_adjust_cy = 0.0; ///< 二维码坐标系下的 Y 偏移(米) - double pgv_x_adjust = 0.0; ///< 仅调整 X 方向误差(米) - }; - /** - * @brief 自由导航目标点(x, y, theta) - */ - struct FreeGoPoint - { - double x = 0.0; ///< X 坐标(米) - double y = 0.0; ///< Y 坐标(米) - double theta = 0.0; ///< 朝向角(弧度) - }; - /** - * @brief 脚本参数(通用) - */ - struct ScriptArgs - { - std::map str_kv; ///< 字符串键值对 - std::map num_kv; ///< 数字键值对 - std::vector do_list; ///< DO 列表(用于 setDO 等) - std::vector di_list; ///< DI 列表(用于 waitDI 等) - }; - - // ==================== 3051 单点导航 ==================== - /** - * @brief 3051 单点导航入参 - * - * 对应仙工 API 3051 (0x0BEB) 的请求数据区 - * - * @attention - * - **仅用于单车测试/验证,严禁用于多车调度场景**,否则会出现速度不连续、不跟随路径等危险。 - * - 下发此任务会取消当前正在执行的任务(不排队)。 - * - 成功下发后会自动清除指定的 warning/error 报错码。 - * - 支持自由导航(freeGo)和基于站点的路径导航两种模式。 - */ - struct RobotGoTargetReq - { - // -------- 必填字段 -------- - std::string source_id; ///< 起始站点 ID(若为 "SELF_POSITION" 表示从当前位置出发) - std::string id; ///< 目标站点 ID(若为 "SELF_POSITION" 表示原地执行 operation) - std::string task_id; ///< 任务 ID(非强制,但建议提供) - - // -------- 可选速度/角度控制 -------- - double angle = 0.0; ///< 目标点朝向角(弧度),仅对自由导航或目标站点有方向要求时使用 - std::string method; ///< 运动方式:"forward"(正走)或 "backward"(倒走) - double max_speed = 0.0; ///< 最大线速度(m/s),0 表示使用默认 - double max_wspeed = 0.0; ///< 最大角速度(rad/s) - double max_acc = 0.0; ///< 最大加速度(m/s²) - double max_wacc = 0.0; ///< 最大角加速度(rad/s²) - int64_t duration = 0; ///< 到达后等待时间(毫秒) - int32_t orientation = 0; ///< 全向车保持的角度(暂不使用) - bool spin = false; ///< 是否随动(用于托盘旋转) - int64_t delay = 0; ///< 延迟结束导航状态的时间(毫秒),缺省为 0 - int32_t start_rot_dir = 0; ///< 起步原地旋转方向:-1=顺时针, 0=就近, 1=逆时针 - int32_t end_rot_dir = 0; ///< 到点原地旋转方向(同上) - double reach_dist = 0.0; ///< 到点位置精度(米),默认由系统决定 - double reach_angle = 0.0; ///< 到点角度精度(弧度) - std::string skill_name; ///< 技能名称:"Action"(动作)或 "GotoSpecifiedPose"(路径导航) - - // -------- PGV 二次定位 -------- - PgvParam pgv; ///< PGV 参数 - - // -------- 设备操作(顶升/货叉/辊筒/牵引等) -------- - std::string operation; ///< 操作类型,如 "JackLoad", "ForkUnload", "RollerLoad", "HookLoad", "WaitDI", "SetDO", "sound", "Script" 等 - double jack_height = 0.0; ///< 顶升高度(米),用于 JackLoad/JackUnload/JackHeight - double start_height = 0.0; ///< 货叉起步前举升高度(米) - double end_height = 0.0; ///< 货叉到点后举升高度(米) - double fork_mid_height = 0.0; ///< 货叉行走过程中举升高度(米) - double fork_dist = 0.0; ///< 货叉前移距离(米),用于 ForkForward - std::string direction; ///< 方向:"left"/"right"/"front"/"back",用于辊筒或货叉 - bool recognize = false; ///< 是否启用视觉识别(用于货叉/顶升加载) - std::string recfile; ///< 识别文件路径(如 "shelf/s0002.shelf") - SoundArgs sounds_args; ///< 音频参数(当 operation="sound" 时) - WaitDIArgs wait_di_args; ///< WaitDI 参数 - SetDOArgs set_do_args; ///< SetDO 参数 - std::string script_name; ///< 脚本文件名(当 operation="Script" 时) - ScriptArgs script_args; ///< 脚本参数 - int32_t script_stage = 2; ///< 脚本执行阶段:0=导航前, 1=导航中, 2=导航后, 3=脚本控制导航 - - // -------- 原地旋转(特殊用法) -------- - double move_angle = 0.0; ///< 旋转角度(弧度),仅当 skill_name="GoByOdometer" 时使用 - double speed_w = 0.0; ///< 旋转角速度(rad/s) - int32_t loc_mode = 0; ///< 定位模式:1=激光定位, 0=里程定位 - - // -------- 自由导航(仅支持双轮差速底盘) -------- - FreeGoPoint freeGo; ///< 自由导航目标坐标(若提供此字段,则忽略 id 字段,使用坐标导航) - }; - - /** - * @brief 3051 单点导航出参 - */ - struct RobotGoTargetRes - { - int ret_code = 0; ///< 返回码,0 表示成功 - std::string create_on; ///< 数据创建时间戳 - std::string err_msg; ///< 错误信息 - }; - - //新增暂停导航响应结构体 - struct RobotTaskPauseRes { - int ret_code = 0; - std::string create_on; - std::string err_msg; - }; - - - // 继续导航响应结构体 - struct RobotTaskResumeRes { - int ret_code = 0; - std::string create_on; - std::string err_msg; - }; - - - // ==================== robot_control_stop 停止开环运动响应 ==================== - - // 0x07D0 robot_control_stop 停止开环运动返回 - struct RobotControlStopRes - { - int ret_code = 0; - std::string create_on; - std::string err_msg; - }; - - // 取消导航响应结构体 - struct RobotTaskCancelRes { - int ret_code = 0; - std::string create_on; - std::string err_msg; - }; - - // ==================== 类型别名(兼容旧代码) ==================== - using AgvState = AgvStatusInfo; - -public: - AbstractAgv() = default; - ~AbstractAgv() override = default; - - // 禁用拷贝,允许移动 - AbstractAgv(const AbstractAgv&) = delete; - AbstractAgv& operator=(const AbstractAgv&) = delete; - AbstractAgv(AbstractAgv&&) = default; - AbstractAgv& operator=(AbstractAgv&&) = default; - - // ==================== 纯虚接口 ==================== - - /** - * @brief 获取当前设备运行状态 - * @return Status 枚举值 - */ - virtual Status state() const = 0; - - /** - * @brief 获取最后一次发生的错误信息 - * @return 错误描述字符串 - */ - virtual std::string lastError() const = 0; - - /** - * @brief 启动设备(建立连接、初始化资源等) - */ - void start() override = 0; - - /** - * @brief 停止设备(断开连接、释放资源等) - */ - void stop() override = 0; - - /** - * @brief 周期性更新设备状态(由上层循环调用,可处理心跳、重连等) - */ - void update() override = 0; - - /** - * @brief 获取 AGV 状态(封装了 getStatusInfo) - * @param state 输出参数,填充 AgvState 结构 - */ - virtual void getState(AgvState& state) { - state = AgvState{}; - getStatusInfo(state); - } - - /** - * @brief 获取 AGV 基本信息(命令码 1000) - * @param info 输出参数,填充 AgvStatusInfo - */ - virtual void getStatusInfo(AgvStatusInfo& info) = 0; - - /** - * @brief 查询电池状态(命令码 1007) - * @param info 输出参数,填充 BatteryStatus - * @param simple 若为 true,只查询关键信息(可提高效率,具体取决于实现) - */ - virtual void getBatteryStatus(BatteryStatus& info, bool simple = false) = 0; - - /** - * @brief 查询机器人当前位置(命令码 1004) - * @param info 输出参数,填充 RobotLocation - */ - virtual void getRobotLocation(RobotLocation& info) = 0; - - /** - * @brief 下载指定名称的地图(命令码 4011) - * @param info 输出参数,填充 DownloadMapResult(包含地图内容) - * @param map_name 要下载的地图名称 - */ - virtual void downloadMap(DownloadMapResult& info, const std::string& map_name) = 0; - - /** - * @brief 查询地图状态(命令码 1300) - * @param info 输出参数,填充 MapStatus(当前地图、所有地图列表及文件信息) - */ - virtual void getMapStatus(MapStatus& info) = 0; - - /** - * @brief 上传地图(命令码 4010) - * @param info 输出参数,填充 UploadMapResult - * @param map_json 地图 JSON 字符串(完整的地图文件内容) - */ - virtual void uploadMap(UploadMapResult& info, const std::string& map_json) = 0; - - /** - * @brief 抢占机器人控制权(命令码 4005) - * @param info 输出参数,填充 LockResult - * @param nick_name 抢占者昵称(用于标识调用方) - */ - virtual void lockRobotControl(LockResult& info, const std::string& nick_name) = 0; - - /** - * @brief 下发开环速度运动指令(命令码 2010) - * - * 使用控制端口(19205)。下发后会强制中断当前自动导航任务。 - * 对于多舵轮设备,仅 vx/vy/w 生效。 - * - * @param res 输出参数,运动指令返回结果 - * @param req 输入参数,包含 vx/vy/w/steer/real_steer/duration - */ - virtual void robotMotionControl(MotionCtrlRes& res, const MotionCtrlReq& req) = 0; - - /** - * @brief 切换载入地图(命令码 2022) - * - * 使用控制端口(19205)。目标地图必须已在机器人中存储。 - * - * @param res 输出参数,执行结果 - * @param req 输入参数,目标地图名称 - */ - virtual void robotLoadMap(LoadMapRes& res, const LoadMapReq& req) = 0; - - /** - * @brief 查询当前导航状态(命令码 1020) - * - * 使用状态端口(19204)。 - * @param res 输出参数,导航全量状态 - * @param req 输入参数,simple=true 仅返回 task_status - */ - virtual void robotStatusTaskCurrent(RobotStatusTaskCurrentRes& res, const RobotStatusTaskCurrentReq& req) = 0; - - /** - * @brief 简单运动控制(设置速度) - * - * 默认实现抛出 logic_error,子类可重写。 - * @param vx X 方向速度 (m/s) - * @param vy Y 方向速度 (m/s) - * @param vw 角速度 (rad/s) - */ - virtual void move(double vx, double vy, double vw) { - throw std::logic_error("move not implemented"); - } - - /** - * @brief 停止运动 - * - * 默认实现抛出 logic_error,子类可重写。 - */ - virtual void stopMove() { - throw std::logic_error("stopMove not implemented"); - } - - /** - * @brief 查询当前机器人控制权持有者(命令码 1060) - * @param info 输出参数,填充 CurrentLockStatus - */ - virtual void getCurrentLockStatus(CurrentLockStatus& info) = 0; - - /** - * @brief 查询地图载入状态(命令码 1022) - * - * 使用状态端口(19204)。 - * @param res 输出参数,包含 loadmap_status(0=失败, 1=成功, 2=载入中) - * @note 载入中(2)时禁止执行重定位操作 - */ - virtual void queryLoadMapStatus(QueryLoadMapStatusRes& res) = 0; - - /** - * @brief 查询当前地图全部站点信息(命令码 1301) - * - * 使用状态端口(19204)。 - * @param res 输出参数,包含站点数组与执行结果 - */ - virtual void queryStationList(QueryStationRes& res) = 0; - - /** - * @brief 指定路径导航(命令码 3066) - * - * 使用导航端口(19206)。任务串行追加执行。 - * - * @param res 输出参数,下发任务结果(ret_code=0 仅表示接收成功,不代表执行完成) - * @param req 输入参数,多段站点导航序列(每个任务必须包含 task_id, source_id, id) - * @attention - * - source_id 和 id 之间必须有直接相连的线路,不可以跳点。 - * - 任务会排队执行,若前一个任务失败,后续任务自动取消。 - * - 适合多车调度场景,因为路径可预知、不会自主绕路。 - */ - virtual void robotGoTargetList(GoTargetListRes& res, const GoTargetListReq& req) = 0; - - /** - * @brief 批量查询任务状态(命令码 1110) - * - * 使用状态端口(19204)。 - * @param res 输出参数,包含所有任务状态包(已完成+未完成) - * @param req 输入参数,要查询的 task_id 列表;若为空,则查询所有未完成+最近一条完成的任务 - */ - virtual void robotStatusTaskPackage(QueryTaskStatusPackageRes& res, const QueryTaskStatusPackageReq& req) = 0; - - /** - * @brief 单点站点自动规划导航(命令码 3051) - * - * 使用导航端口(19206)。 - * @param res 输出参数,下发结果(ret_code=0 仅表示接收成功) - * @param req 完整导航参数(支持自由导航、固定路径导航、带设备操作等) - * @warning - * - **严禁用于多车调度场景**,仅限单车测试/任务链验证。 - * - 下发新任务会取消当前正在执行的任务(不排队)。 - * - 成功下发后会**自动清除**指定的 warning/error 报错码(详见协议文档)。 - * - 支持自由导航(freeGo),但仅限双轮差速底盘。 - */ - virtual void robotGoTarget(RobotGoTargetRes& res, const RobotGoTargetReq& req) = 0; - - - /** - * @brief 2000(0x07D0) robot_control_stop_req 停止开环速度运动指令 - * @param res 出参,执行结果 - */ - virtual void robotControlStop(RobotControlStopRes& res) = 0; - - - /** - * @brief 暂停当前导航任务(命令码 3001, 0x0BB9) - * @param res 输出参数,执行结果 - */ - virtual void robotTaskPause(RobotTaskPauseRes& res) = 0; - - - /** - * @brief 继续当前导航任务(命令码 3002, 0x0BBA) - * @param res 输出参数,执行结果 - */ - virtual void robotTaskResume(RobotTaskResumeRes& res) = 0; - - - /** - * @brief 取消当前导航任务(命令码 3003, 0x0BBB) - * @param res 输出参数,执行结果 - */ - virtual void robotTaskCancel(RobotTaskCancelRes& res) = 0; - - - - -protected: - // ==================== 保护成员变量 ==================== - std::string id_; ///< 设备 ID(唯一标识) - std::string ip_; ///< 设备 IP 地址 - - // 多端口配置(对应 proto 定义) - int port_status_{0}; ///< 状态查询端口(默认 19204) - int port_control_{0}; ///< 控制 API 端口(默认 19205) - int port_nav_{0}; ///< 导航 API 端口(默认 19206) - int port_config_{0}; ///< 配置 API 端口(默认 19207) - int port_other_{0}; ///< 其他 API 端口(默认 19210) - int port_push_{0}; ///< 推送端口(默认 19301) - - bool enable_{false}; ///< 设备启用标志 -}; - -} // namespace cmvr::device - -#endif // CMVR_ES_ABSTRACT_AGV_H \ No newline at end of file +#endif //CMVR_ES_ABSTRACT_AGV_H \ No newline at end of file diff --git a/cmvr-es/devices/agv/agv_src1100/CMakeLists.txt b/cmvr-es/devices/agv/agv_src1100/CMakeLists.txt deleted file mode 100644 index f18e7907..00000000 --- a/cmvr-es/devices/agv/agv_src1100/CMakeLists.txt +++ /dev/null @@ -1,20 +0,0 @@ -add_library(agv_src1100 SHARED src/agv_src1100.cpp) - -target_include_directories(agv_src1100 PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR} - ${CMAKE_CURRENT_SOURCE_DIR}/include - ${JSONCPP_INCLUDE_DIRS} -) - -add_library(cmvr_es::device::agv_src1100 ALIAS agv_src1100) - -# 关键修改:PRIVATE -> PUBLIC -target_link_libraries(agv_src1100 PUBLIC - jsoncpp - cmvr_es::common - cmvr_es::proto - glog - pthread -) - -install(TARGETS agv_src1100 LIBRARY DESTINATION lib) \ No newline at end of file diff --git a/cmvr-es/devices/agv/agv_src1100/include/agv_src1100.h b/cmvr-es/devices/agv/agv_src1100/include/agv_src1100.h deleted file mode 100644 index 13ab8d32..00000000 --- a/cmvr-es/devices/agv/agv_src1100/include/agv_src1100.h +++ /dev/null @@ -1,307 +0,0 @@ -// -// Created by linbo on 2025/6/20. -// -/** - * @file agv_src1100.h - * @brief 仙工智能 SRC-1100/2200 系列 AGV 控制器设备实现类的头文件。 - * 继承自 AbstractAgv,实现了所有纯虚接口,并管理多端口 TCP 连接。 - */ -#ifndef AGV_SRC1100_H -#define AGV_SRC1100_H -#pragma once - -#include "./agv/abstract_agv.h" -#include -#include -#include -#include "nlohmann/json.hpp" - -using json = nlohmann::json; - -namespace cmvr::device { - -/** - * @brief 仙工智能 SRC-1100/2200 系列 AGV 控制器设备实现类 - * - * 继承自 AbstractAgv,实现了与仙工智能 SRC 系列控制器通信的具体协议。 - * 支持多端口(状态、控制、导航、配置、推送)并发连接和指令交互。 - * - * @note 该类不是线程安全的,外部调用需自行保证同一设备实例的串行访问。 - */ -class AgvSrc1100 final : public AbstractAgv { -public: - // ==================== 构造 / 析构 ==================== - - /** - * @brief 从 XML 配置节点构造设备实例(已弃用或未使用,保留兼容) - * @param cfg XML 配置节点,包含设备 ID、IP、端口、启用标志等 - */ - explicit AgvSrc1100(const XmlNode& cfg); - - /** - * @brief 从 Protobuf 配置对象构造设备实例 - * @param cfg AGVsrc1100Config 配置对象,包含设备 ID、IP、端口、启用标志等 - */ - explicit AgvSrc1100(const config::AGVsrc1100Config& cfg); - - /** - * @brief 析构函数,自动停止设备并释放资源 - */ - ~AgvSrc1100() override; - - // ==================== 生命周期管理(通用) ==================== - - /** - * @brief 获取当前设备运行状态 - * @return Status 枚举值(CREATED / INITIALIZED / RUNNING / PAUSED / STOPPED / FAULT) - */ - Status state() const override; - - /** - * @brief 获取最后一次发生的错误信息 - * @return 错误描述字符串,若无错误则返回空字符串 - */ - std::string lastError() const override; - - /** - * @brief 启动设备,标记为运行状态 - * @note 所有 socket 连接应在构造时已完成,启动仅改变状态标志 - */ - void start() override; - - /** - * @brief 停止设备,关闭所有 TCP 连接并释放资源 - */ - void stop() override; - - /** - * @brief 周期性更新设备状态 - * @note 当前为空实现,可扩展心跳检测或重连逻辑 - */ - void update() override; - - // ==================== 端口 19204 – 机器人状态 API(允许 10 个连接) ==================== - // 功能:查询机器人各种状态信息(只读操作,不改变机器人状态) - - /** - * @brief 查询机器人基本信息(命令码 1000, robot_status_info_req) - * @param info 输出参数,填充 AgvStatusInfo - * @note 返回信息包括:版本、型号、地图名称、网络 IP、MAC、Wi-Fi 信号等 - */ - void getStatusInfo(AgvStatusInfo& info) override; - - /** - * @brief 查询电池状态(命令码 1007, robot_status_battery_req) - * @param info 输出参数,填充 BatteryStatus - * @param simple 若为 true,仅返回关键电量信息(当前实现未区分) - * @note 返回信息包括:电量百分比、温度、充放电状态、电压、电流等 - */ - void getBatteryStatus(BatteryStatus& info, bool simple = false) override; - - /** - * @brief 查询机器人当前位置(命令码 1004, robot_status_loc_req) - * @param info 输出参数,填充 RobotLocation - * @note 返回信息包括:世界坐标系 X/Y 坐标、朝向角、定位置信度、当前站点 - */ - void getRobotLocation(RobotLocation& info) override; - - /** - * @brief 查询地图状态(命令码 1300, robot_status_map_req) - * @param info 输出参数,填充 MapStatus - * @note 返回信息包括:当前载入的地图名称、所有存储的地图列表、文件详情 - */ - void getMapStatus(MapStatus& info) override; - - /** - * @brief 查询当前控制权持有者(命令码 1060, robot_status_current_lock_req) - * @param info 输出参数,填充 CurrentLockStatus - * @note 返回信息包括:是否被锁定、持有者 IP/端口/昵称、锁定时间等 - */ - void getCurrentLockStatus(CurrentLockStatus& info) override; - - /** - * @brief 查询地图载入状态(命令码 1022, robot_status_loadmap_req) - * @param res 输出参数,包含 loadmap_status - * @note loadmap_status: 0=失败, 1=成功, 2=载入中(载入中禁止重定位) - */ - void queryLoadMapStatus(QueryLoadMapStatusRes& res) override; - - /** - * @brief 查询当前地图站点列表(命令码 1301, robot_status_station_req) - * @param res 输出参数,填充 QueryStationRes,包含所有站点信息 - * @note 返回信息包括:站点 ID、类型、坐标、朝向角、属性等 - */ - void queryStationList(QueryStationRes& res) override; - - /** - * @brief 查询当前导航状态(命令码 1020, robot_status_task_req) - * @param res 输出参数,填充 RobotStatusTaskCurrentRes - * @param req 输入参数,simple=true 时只返回 task_status - * @note 返回信息包括:任务状态、任务类型、目标站点/坐标、已走/未走路径 - */ - void robotStatusTaskCurrent(RobotStatusTaskCurrentRes& res, const RobotStatusTaskCurrentReq& req) override; - - /** - * @brief 批量查询任务状态(命令码 1110, robot_status_task_status_package_req) - * @param res 输出参数,填充 QueryTaskStatusPackageRes - * @param req 输入参数,task_ids 列表;若为空则查询所有未完成 + 最近一条已完成的任务 - * @note 返回信息包括:任务列表、每个任务的 ID/状态/类型、进度百分比、剩余距离等 - */ - void robotStatusTaskPackage(QueryTaskStatusPackageRes& res, const QueryTaskStatusPackageReq& req) override; - - // ==================== 端口 19205 – 机器人控制 API(允许 5 个连接) ==================== - // 功能:下发控制指令,改变机器人运动或状态(非导航类指令) - - /** - * @brief 下发开环速度运动指令(命令码 2010, robot_control_motion_req) - * @param res 输出参数,执行结果 - * @param req 输入参数,包含 vx/vy/w/steer/real_steer/duration - * @warning 下发此指令会立即取消当前正在执行的导航任务 - * @note 多舵轮设备仅 vx/vy/w 生效,steer/real_steer 仅单舵轮设备有效 - */ - void robotMotionControl(MotionCtrlRes& res, const MotionCtrlReq& req) override; - - /** - * @brief 切换载入地图(命令码 2022, robot_control_loadmap_req) - * @param res 输出参数,执行结果 - * @param req 输入参数,目标地图名称 - * @note 目标地图必须已存在于机器人中,否则切换失败 - */ - void robotLoadMap(LoadMapRes& res, const LoadMapReq& req) override; - - // ==================== 端口 19206 – 机器人导航 API(允许 5 个连接) ==================== - // 功能:下发导航任务 - - /** - * @brief 指定路径导航(命令码 3066, robot_task_gotargetlist_req) - * @param res 输出参数,下发结果(ret_code=0 仅表示指令被接收,不表示执行完成) - * @param req 输入参数,包含 move_task_list(站点序列) - * @attention - * - 每个任务必须含 task_id、source_id、id 三个必填字段 - * - source_id 和 id 之间必须有直接相连的线路,不可跳点 - * - 任务会排队执行,前一个任务失败时后续任务自动取消 - * - 适合多车调度场景 - */ - void robotGoTargetList(GoTargetListRes& res, const GoTargetListReq& req) override; - - /** - * @brief 单点站点自动规划导航(命令码 3051, robot_task_gotarget_req) - * @param res 输出参数,下发结果(ret_code=0 仅表示指令被接收) - * @param req 输入参数,支持自由导航(freeGo)和基于站点的路径导航两种模式 - * @warning - * - **严禁用于多车调度场景**,仅限单车测试/任务链验证 - * - 下发新任务会取消当前正在执行的任务(不排队) - * - 成功下发后会自动清除指定的 warning/error 报错码 - * - 自由导航(freeGo)仅支持双轮差速底盘 - */ - void robotGoTarget(RobotGoTargetRes& res, const RobotGoTargetReq& req) override; - - // ==================== 端口 19207 – 机器人配置 API(允许 5 个连接) ==================== - // 功能:配置类操作(地图上传/下载、控制权管理、参数修改等) - - /** - * @brief 抢占机器人控制权(命令码 4005, robot_config_lock_req) - * @param info 输出参数,填充 LockResult - * @param nick_name 抢占者昵称,用于标识调用方 - * @note 抢占成功后调用方获得独占控制权,其他方仅可查询状态 - */ - void lockRobotControl(LockResult& info, const std::string& nick_name) override; - - /** - * @brief 上传地图(命令码 4010, robot_config_uploadmap_req) - * @param info 输出参数,填充 UploadMapResult - * @param map_json 地图 JSON 字符串(完整的地图文件内容) - * @note 地图数据较大时会自动分片发送,需确保 JSON 格式正确 - */ - void uploadMap(UploadMapResult& info, const std::string& map_json) override; - - /** - * @brief 下载指定地图(命令码 4011, robot_config_downloadmap_req) - * @param info 输出参数,填充 DownloadMapResult(包含地图 JSON 内容) - * @param map_name 要下载的地图名称 - * @note 下载的地图内容以 JSON 字符串形式存储在 info.map_content 中 - */ - void downloadMap(DownloadMapResult& info, const std::string& map_name) override; - - // ==================== 端口 19210 – 其他 API(允许 5 个连接) ==================== - // 功能:外设控制(顶升/货叉/辊筒/音频/IO 等) - // 当前未实现具体方法,预留扩展 - - // ==================== 端口 19301 – 机器人推送 API(允许 10 个连接) ==================== - // 功能:接收机器人主动推送的实时状态数据 - // 相关配置方法(如 9300)可能在此,当前未实现 - - - // ---------- 新增任务控制接口(3001/3002/3003)及停止运动(2000) ---------- - void robotTaskPause(RobotTaskPauseRes& res) override; - void robotTaskResume(RobotTaskResumeRes& res) override; - void robotTaskCancel(RobotTaskCancelRes& res) override; - void robotControlStop(RobotControlStopRes& res) override; - - -private: - // ==================== 私有通信辅助函数 ==================== - - /** - * @brief 异步连接指定端口 - * @param sock 输出参数,连接成功后存储 socket 文件描述符 - * @param port 目标端口号(19204/19205/19206/19207/19210/19301) - * @note 在独立线程中执行,连接成功后将 sock 设置为有效值 - */ - void asyncConnect(int& sock, int port); - - /** - * @brief 发送请求并接收完整响应 - * @param sock 已连接的 socket 文件描述符 - * @param header 16 字节协议帧头 - * @return true 表示收发成功,current_json_ 中存储响应 JSON;false 表示失败 - * @note 内部先发送帧头,然后循环接收直到超时,最后提取 JSON 部分 - */ - bool sendAndRecv(int sock, const uint8_t* header); - - /** - * @brief 清空 socket 接收缓冲区中的残留数据 - * @param sock 目标 socket 文件描述符 - * @note 采用非阻塞方式读取并丢弃所有可读数据,防止粘包干扰 - */ - void flushSocket(int sock); - - /** - * @brief 设置 socket 接收超时时间 - * @param sock 目标 socket 文件描述符 - * @param timeout_ms 超时时间(毫秒),0 表示取消超时 - */ - void setSocketTimeout(int sock, int timeout_ms); - - /** - * @brief 解析 JSON 字符串并做异常捕获 - * @param json_str 输入的 JSON 字符串 - * @param root 输出参数,解析后的 json 对象 - * @param err_msg 输出参数,解析失败时的错误信息 - * @return true 表示解析成功,false 表示解析失败 - */ - bool parseJson(const std::string& json_str, json& root, std::string& err_msg); - - - // ==================== 成员变量 ==================== - - mutable std::mutex mutex_; ///< 保护状态和错误信息的互斥锁 - std::atomic status_{Status::CREATED}; ///< 当前设备运行状态(原子变量) - std::string last_error_; ///< 最后一次错误信息 - std::string current_json_; ///< 最近一次响应的 JSON 内容(由 sendAndRecv 填充) - - // ---------- 各端口 Socket 文件描述符 ---------- - int sock_status_ = -1; ///< 19204 – 状态 API 端口 - int sock_control_ = -1; ///< 19205 – 控制 API 端口 - int sock_nav_ = -1; ///< 19206 – 导航 API 端口 - int sock_config_ = -1; ///< 19207 – 配置 API 端口 - int sock_other_ = -1; ///< 19210 – 其他 API 端口 - int sock_push_ = -1; ///< 19301 – 推送 API 端口 - - std::vector connect_threads_; ///< 各端口异步连接线程 - mutable std::mutex connect_mutex_; ///< 保护 connect_threads_ 的互斥锁 -}; - -} // namespace cmvr::device - -#endif // AGV_SRC1100_H \ No newline at end of file diff --git a/cmvr-es/devices/agv/agv_src1100/src/agv_src1100.cpp b/cmvr-es/devices/agv/agv_src1100/src/agv_src1100.cpp deleted file mode 100644 index 558e621b..00000000 --- a/cmvr-es/devices/agv/agv_src1100/src/agv_src1100.cpp +++ /dev/null @@ -1,2771 +0,0 @@ -// ============================================================================ -// 文件名: agv_src1100.cpp -// 描述: 仙工智能 SRC-1100/2200 系列 AGV 控制器设备实现 -// 端口分类: -// 19204 - 机器人状态 API(查询位置/电池/任务/地图/控制权等) -// 19205 - 机器人控制 API(开环运动/切换地图等) -// 19206 - 机器人导航 API(路径导航/指定路径导航等) -// 19207 - 机器人配置 API(地图上传下载/控制权抢占等) -// 19210 - 其他 API(外设控制/音频/IO 等)【未实现】 -// 19301 - 机器人推送 API(主动状态推送)【未实现】 -// ============================================================================ - -#include "../include/agv_src1100.h" -#include -#include -#include -#include -#include -#include -#include -#include -// #include "json/json.h" -#include "nlohmann/json.hpp" -using json = nlohmann::json; -namespace cmvr::device -{ - // ============================================================================ - // 构造函数 / 析构函数 - // ============================================================================ - - /** - * @brief 从 Protobuf 配置对象构造设备实例 - * @param cfg AGVsrc1100Config 配置对象 - * @note 连接所有端口:19204/19205/19206/19207/19210/19301 - */ - AgvSrc1100::AgvSrc1100(const config::AGVsrc1100Config& cfg) : AbstractAgv() { - // 打印构造日志,便于调试追踪 - LOG(INFO) << "[AgvSrc1100 Create] this_addr=" << this - << " cfg_id=" << cfg.id() - << " cfg_ip=" << cfg.ip() - << " cfg_port_config=" << cfg.port_config(); - - // 从配置中读取设备标识信息 - id_ = cfg.id(); // 设备 ID - ip_ = cfg.ip(); // 设备 IP 地址 - enable_ = cfg.enable(); // 是否启用 - - // 初始化各端口号(对应仙工协议的不同服务端口) - port_status_ = cfg.port_status(); // 19204 – 状态查询 - port_control_ = cfg.port_control(); // 19205 – 控制指令 - port_nav_ = cfg.port_nav(); // 19206 – 导航任务 - port_config_ = cfg.port_config(); // 19207 – 配置操作 - port_other_ = cfg.port_other(); // 19210 – 其他外设 - port_push_ = cfg.port_push(); // 19301 – 数据推送 - - // 如果设备启用,则异步连接所有端口 - if (enable_) { - connect_threads_.emplace_back(&AgvSrc1100::asyncConnect, this, std::ref(sock_status_), port_status_); - connect_threads_.emplace_back(&AgvSrc1100::asyncConnect, this, std::ref(sock_control_), port_control_); - connect_threads_.emplace_back(&AgvSrc1100::asyncConnect, this, std::ref(sock_nav_), port_nav_); - connect_threads_.emplace_back(&AgvSrc1100::asyncConnect, this, std::ref(sock_config_), port_config_); - connect_threads_.emplace_back(&AgvSrc1100::asyncConnect, this, std::ref(sock_other_), port_other_); - connect_threads_.emplace_back(&AgvSrc1100::asyncConnect, this, std::ref(sock_push_), port_push_); - } - - LOG(INFO) << "port_status=" << port_status_ - << ", port_control=" << port_control_ - << ", port_nav=" << port_nav_ - << ", port_config=" << port_config_ - << ", port_other=" << port_other_ - << ", port_push=" << port_push_; - } - - /** - * @brief 析构函数:停止设备并等待所有连接线程结束 - */ - AgvSrc1100::~AgvSrc1100() { - stop(); // 关闭所有 socket - // 等待所有异步连接线程结束 - for (auto& t : connect_threads_) { - if (t.joinable()) t.join(); - } - } - - // ============================================================================ - // 生命周期管理(通用接口) - // ============================================================================ - - /** - * @brief 获取当前设备运行状态 - * @return Status 枚举值 - */ - AbstractAgv::Status AgvSrc1100::state() const { - return status_; - } - - /** - * @brief 获取最后一次发生的错误信息 - * @return 错误描述字符串 - */ - std::string AgvSrc1100::lastError() const { - std::lock_guard lock(mutex_); - return last_error_; - } - - /** - * @brief 启动设备:标记为运行状态 - * @note socket 连接已在构造时完成,此处仅改变状态标志 - */ - void AgvSrc1100::start() { - status_ = Status::RUNNING; - } - - /** - * @brief 停止设备:关闭所有 TCP 连接并释放资源 - */ - void AgvSrc1100::stop() { - // 定义 lambda 关闭单个 socket 并置为 -1 - auto closeSocket = [](int& s) { - if (s >= 0) { close(s); s = -1; } - }; - // 关闭所有端口的 socket - closeSocket(sock_status_); - closeSocket(sock_control_); - closeSocket(sock_nav_); - closeSocket(sock_config_); - closeSocket(sock_other_); - closeSocket(sock_push_); - status_ = Status::STOPPED; - } - - /** - * @brief 周期性更新设备状态 - * @note 当前为空实现,预留扩展(如心跳检测、自动重连等) - */ - void AgvSrc1100::update() {} - - // ============================================================================ - // 私有通信辅助函数 - // ============================================================================ - - /** - * @brief 清空 socket 接收缓冲区残留数据,防止粘包 - * @param sock 目标 socket - * @note 非阻塞读取并丢弃所有可读数据 - */ - void AgvSrc1100::flushSocket(int sock) { - uint8_t tmp[128]; // 临时缓冲区 - ssize_t rlen; - // MSG_DONTWAIT: 非阻塞模式,读到没有数据时立即返回 - while ((rlen = recv(sock, tmp, sizeof(tmp), MSG_DONTWAIT)) > 0); - } - - /** - * @brief 设置 socket 接收超时时间 - * @param sock 目标 socket - * @param timeout_ms 超时时间(毫秒),0 表示取消超时 - */ - void AgvSrc1100::setSocketTimeout(int sock, int timeout_ms) { - struct timeval tv; - tv.tv_sec = timeout_ms / 1000; // 秒 - tv.tv_usec = (timeout_ms % 1000) * 1000; // 微秒 - // SO_RCVTIMEO: 设置接收超时 - setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); - } - - /** - * @brief 异步连接指定端口 - * @param sock 输出参数,连接成功后的 socket 描述符 - * @param port 目标端口号 - * @note 在独立线程中执行,连接失败时设置错误状态 - */ - void AgvSrc1100::asyncConnect(int& sock, int port) { - // 创建 TCP socket - int s = socket(AF_INET, SOCK_STREAM, 0); - if (s < 0) { - std::lock_guard lock(mutex_); - last_error_ = "socket create failed"; - status_ = Status::FAULT; - return; - } - - // 配置目标地址 - struct sockaddr_in addr{}; - addr.sin_family = AF_INET; - addr.sin_port = htons(port); - inet_pton(AF_INET, ip_.c_str(), &addr.sin_addr); // IP 地址转换 - - // 发起 TCP 连接 - if (connect(s, (struct sockaddr*)&addr, sizeof(addr)) < 0) { - close(s); // 连接失败,关闭 socket - std::lock_guard lock(mutex_); - last_error_ = "connect failed port:" + std::to_string(port); - status_ = Status::FAULT; - return; - } - - // 连接成功,保存 socket 描述符 - { - std::lock_guard lock(mutex_); - sock = s; - status_ = Status::INITIALIZED; - } - } - - /** - * @brief 发送请求并接收完整响应 - * @param sock 已连接的 socket - * @param header 16 字节协议帧头(包含命令码等) - * @return true 表示成功,current_json_ 存储响应 JSON - * @note 适用于无负载或简单查询命令(如 1000/1004/1007) - */ - bool AgvSrc1100::sendAndRecv(int sock, const uint8_t* header) { - // 加锁,防止多个线程同时操作 current_json_ - std::lock_guard lock(mutex_); - - if (sock < 0) { - last_error_ = "socket not connected"; - return false; - } - - // 清空残留数据,防止读取到上一次的响应 - flushSocket(sock); - - // 发送 16 字节协议帧头 - if (send(sock, header, 16, MSG_NOSIGNAL) != 16) { - last_error_ = "send header failed"; - return false; - } - - // 设置接收超时 100ms,避免无限阻塞 - setSocketTimeout(sock, 100); - - const int RECV_BUF_LEN = 16384; - uint8_t recvBuf[RECV_BUF_LEN] = {0}; - int totalRecv = 0; - - // 循环接收数据,直到读空或缓冲区满 - while (totalRecv < RECV_BUF_LEN) - { - int ret = recv(sock, recvBuf + totalRecv, RECV_BUF_LEN - totalRecv, 0); - if (ret <= 0) break; // 读空或超时,退出循环 - totalRecv += ret; - } - - // 取消超时设置 - setSocketTimeout(sock, 0); - - if (totalRecv <= 0) { - last_error_ = "no data received"; - return false; - } - - // 将接收到的数据转为字符串 - std::string rawData(reinterpret_cast(recvBuf), totalRecv); - // 查找 JSON 起始和结束位置 - size_t jsonStart = rawData.find('{'); - size_t jsonEnd = rawData.rfind('}'); - - // 提取 JSON 内容,兜底处理不完整数据 - if (jsonStart == std::string::npos) - { - last_error_ = "can not find '{'"; - return false; - } - if (jsonEnd == std::string::npos) - { - // 找不到 },截取到末尾并补上 } - current_json_ = rawData.substr(jsonStart) + "}"; - } - else - { - current_json_ = rawData.substr(jsonStart, jsonEnd - jsonStart + 1); - } - - fflush(stdout); - last_error_ = "ok"; - return true; - } - - /** - * @brief 解析 JSON 字符串并做异常捕获 - * @param json_str 输入的 JSON 字符串 - * @param root 输出参数,解析后的 json 对象 - * @param err_msg 输出参数,解析失败时的错误信息 - * @return true 表示解析成功,false 表示失败 - */ - bool AgvSrc1100::parseJson(const std::string& json_str, json& root, std::string& err_msg) - { - root.clear(); - try - { - root = json::parse(json_str); // 使用 nlohmann/json 解析 - } - catch (json::parse_error& e) // 捕获解析异常 - { - err_msg = "JSON parse error: " + std::string(e.what()); - return false; - } - return true; - } - - // ============================================================================ - // 端口 19204 – 机器人状态 API(允许 10 个连接) - // 功能:查询机器人各种状态信息(只读操作) - // ============================================================================ - - // -------------------------------------------------------------------------- - // 1000 - robot_status_info_req 查询机器人基本信息 - // -------------------------------------------------------------------------- - void AgvSrc1100::getStatusInfo(AgvStatusInfo& info) { - int sock = sock_status_; - if (sock < 0) { - info.ret_code = -1; - info.err_msg = "socket not connected"; - return; - } - - // 构造 16 字节协议帧头 - // 格式: 0x5A 0x01 0x00 0x01 + 长度(4字节) + 命令码(2字节) + 保留(6字节) - // 命令码 0x03E8 = 1000 - uint8_t send_buf[16] = { - 0x5A, 0x01, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, - 0x03, 0xE8, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00 - }; - - // 发送请求并接收响应 - if (!sendAndRecv(sock, send_buf)) { - info.ret_code = -2; - info.err_msg = last_error_; - return; - } - - // 从成员变量中读取响应 JSON - std::string json_str; - { - std::lock_guard lk(mutex_); - json_str = current_json_; - } - - // 兜底修复 JSON 格式(补全花括号) - if (!json_str.empty()) - { - if (json_str.front() != '{') json_str = "{" + json_str; - if (json_str.back() != '}') json_str += "}"; - } - - if (json_str.empty()) { - info.ret_code = -4; - info.err_msg = "empty json data"; - return; - } - - // 解析 JSON - json root; - std::string err; - if (!parseJson(json_str, root, err)) { - info.ret_code = -3; - info.err_msg = err; - return; - } - - // 提取字段,不存在的字段使用默认值 - info.id = root.value("id", ""); - info.vehicle_id = root.value("vehicle_id", ""); - info.version = root.value("version", ""); - info.model = root.value("model", ""); - info.dsp_version = root.value("dsp_version", ""); - info.current_ip = root.value("current_ip", ""); - info.mac = root.value("MAC", ""); - info.rssi = root.value("rssi", 0); - info.ret_code = root.value("ret_code", 0); - info.err_msg = root.value("err_msg", ""); - } - - - // -------------------------------------------------------------------------- - // 1007 - robot_status_battery_req 查询电池状态 - // -------------------------------------------------------------------------- - void AgvSrc1100::getBatteryStatus(BatteryStatus& info, bool simple) - { - int sock = sock_status_; - if (sock < 0) - { - info.ret_code = -1; - info.err_msg = "socket not connected"; - return; - } - - // 命令码 0x03EF = 1007 - uint8_t send_buf[16] = { - 0x5A, 0x01, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, - 0x03, 0xEF, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00 - }; - - if (!sendAndRecv(sock, send_buf)) - { - info.ret_code = -2; - info.err_msg = last_error_; - return; - } - - std::string json_str; - { - std::lock_guard lk(mutex_); - json_str = current_json_; - } - - if (!json_str.empty()) - { - if (json_str.front() != '{') json_str = "{" + json_str; - if (json_str.back() != '}') json_str += "}"; - } - - if (json_str.empty()) - { - info.ret_code = -4; - info.err_msg = "empty json data"; - return; - } - - json root; - std::string err; - if (!parseJson(json_str, root, err)) - { - info.ret_code = -3; - info.err_msg = err; - return; - } - - // 提取电池相关字段 - info.battery_level = root.value("battery_level", 0.0); - info.battery_temp = root.value("battery_temp", 0.0); - info.charging = root.value("charging", false); - info.voltage = root.value("voltage", 0.0); - info.current = root.value("current", 0.0); - info.max_charge_voltage = root.value("max_charge_voltage", -1.0); - info.max_charge_current = root.value("max_charge_current", -1.0); - info.manual_charge = root.value("manual_charge", false); - info.auto_charge = root.value("auto_charge", false); - info.battery_cycle = root.value("battery_cycle", 0); - info.battery_user_data = root.value("battery_user_data", ""); - info.extra = root.value("extra", ""); - info.ret_code = root.value("ret_code", 0); - info.create_on = root.value("create_on", ""); - info.err_msg = root.value("err_msg", ""); - } - - - // -------------------------------------------------------------------------- - // 1004 - robot_status_loc_req 查询机器人位置 - // -------------------------------------------------------------------------- - void AgvSrc1100::getRobotLocation(RobotLocation& info) - { - int sock = sock_status_; - if (sock < 0) - { - info.ret_code = -1; - info.err_msg = "socket not connected"; - return; - } - - // 命令码 0x03EC = 1004 - uint8_t send_buf[16] = { - 0x5A, 0x01, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, - 0x03, 0xEC, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00 - }; - - if (!sendAndRecv(sock, send_buf)) - { - info.ret_code = -2; - info.err_msg = last_error_; - return; - } - - std::string json_str; - { - std::lock_guard lk(mutex_); - json_str = current_json_; - } - - if (!json_str.empty()) - { - if (json_str.front() != '{') json_str = "{" + json_str; - if (json_str.back() != '}') json_str += "}"; - } - - if (json_str.empty()) - { - info.ret_code = -4; - info.err_msg = "empty json data"; - return; - } - - json root; - std::string err; - if (!parseJson(json_str, root, err)) - { - info.ret_code = -3; - info.err_msg = err; - return; - } - - // 提取位置相关字段 - info.x = root.value("x", 0.0); - info.y = root.value("y", 0.0); - info.angle = root.value("angle", 0.0); - info.confidence = root.value("confidence", 0.0); - info.current_station = root.value("current_station", ""); - info.last_station = root.value("last_station", ""); - info.loc_method = root.value("loc_method", 0); - info.ret_code = root.value("ret_code", 0); - info.create_on = root.value("create_on", ""); - info.err_msg = root.value("err_msg", ""); - } - - - // -------------------------------------------------------------------------- - // 1300 - robot_status_map_req 查询地图状态 - // -------------------------------------------------------------------------- - void AgvSrc1100::getMapStatus(MapStatus& info) - { - int sock = sock_status_; - if (sock < 0) - { - info.ret_code = -1; - info.err_msg = "socket not connected"; - return; - } - - // 命令码 0x0514 = 1300 - uint8_t send_buf[16] = { - 0x5A, 0x01, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, - 0x05, 0x14, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00 - }; - - if (!sendAndRecv(sock, send_buf)) - { - info.ret_code = -2; - info.err_msg = last_error_; - return; - } - - std::string json_str; - { - std::lock_guard lk(mutex_); - json_str = current_json_; - } - - if (!json_str.empty()) - { - if (json_str.front() != '{') json_str = "{" + json_str; - if (json_str.back() != '}') json_str += "}"; - } - - if (json_str.empty()) - { - info.ret_code = -4; - info.err_msg = "empty json data"; - return; - } - - json root; - std::string err; - if (!parseJson(json_str, root, err)) - { - info.ret_code = -3; - info.err_msg = err; - return; - } - - // 提取地图字段 - info.current_map = root.value("current_map", ""); - info.current_map_md5 = root.value("current_map_md5", ""); - info.ret_code = root.value("ret_code", 0); - info.create_on = root.value("create_on", ""); - info.err_msg = root.value("err_msg", ""); - - // 解析地图名称数组 - if (root.contains("maps") && root["maps"].is_array()) - { - for (auto& item : root["maps"]) - { - info.maps.push_back(item.get()); - } - } - - // 解析地图文件详情数组 - if (root.contains("map_files_info") && root["map_files_info"].is_array()) - { - for (auto& item : root["map_files_info"]) - { - MapFileInfo fileInfo; - fileInfo.name = item.value("name", ""); - fileInfo.modified = item.value("modified", ""); - fileInfo.size = item.value("size", 0LL); - info.map_files_info.push_back(fileInfo); - } - } - } - - - // -------------------------------------------------------------------------- - // 1022 - robot_status_loadmap_req 查询地图载入状态 - // -------------------------------------------------------------------------- - void AgvSrc1100::queryLoadMapStatus(QueryLoadMapStatusRes& res) - { - int sock = sock_status_; - if (sock < 0) - { - res.ret_code = -1; - res.err_msg = "socket not connected"; - return; - } - - // 命令码 0x03FE = 1022,无负载,长度=0 - uint16_t payload_len = 0; - uint8_t send_buf[16] = { - 0x5A, 0x01, 0x00, 0x01, - 0x00, 0x00, - static_cast((payload_len >> 8) & 0xFF), // 长度高字节 - static_cast(payload_len & 0xFF), // 长度低字节 - 0x03, 0xFE, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - }; - - flushSocket(sock); - setSocketTimeout(sock, 1000); // 1秒超时 - - // 发送帧头(有重试机制) - int total_sent = 0; - int retry_cnt = 3; - while (total_sent < 16 && retry_cnt > 0) - { - int ret = send(sock, send_buf + total_sent, 16 - total_sent, MSG_NOSIGNAL); - if (ret <= 0) - { - retry_cnt--; - usleep(10000); - continue; - } - total_sent += ret; - } - if (total_sent != 16) - { - setSocketTimeout(sock, 0); - res.ret_code = -2; - res.err_msg = "send frame header failed"; - return; - } - - // 接收响应 - const int recv_buf_len = 65536; - std::vector recv_buf(recv_buf_len); - std::string raw_data; - int recv_ret = 0; - while ((recv_ret = recv(sock, recv_buf.data(), recv_buf_len, 0)) > 0) - { - raw_data.append(reinterpret_cast(recv_buf.data()), recv_ret); - } - setSocketTimeout(sock, 0); - - if (raw_data.empty()) - { - res.ret_code = -2; - res.err_msg = "no response data"; - return; - } - - // 提取 JSON - size_t json_start = raw_data.find('{'); - size_t json_end = raw_data.rfind('}'); - std::string json_str; - if (json_start == std::string::npos) - { - res.ret_code = -4; - res.err_msg = "json not found in response"; - return; - } - if (json_end == std::string::npos) - { - json_str = raw_data.substr(json_start) + "}"; - } - else - { - json_str = raw_data.substr(json_start, json_end - json_start + 1); - } - - // 解析 JSON - json root; - std::string parse_err; - if (!parseJson(json_str, root, parse_err)) - { - res.ret_code = -3; - res.err_msg = "json parse error: " + parse_err; - return; - } - - // loadmap_status: 0=失败, 1=成功, 2=载入中 - res.loadmap_status = root.value("loadmap_status", 0); - res.ret_code = root.value("ret_code", 0); - res.create_on = root.value("create_on", ""); - res.err_msg = root.value("err_msg", ""); - } - - - // -------------------------------------------------------------------------- - // 1301 - robot_status_station_req 查询站点列表 - // -------------------------------------------------------------------------- - void AgvSrc1100::queryStationList(QueryStationRes& res) - { - int sock = sock_status_; - if (sock < 0) - { - res.ret_code = -1; - res.err_msg = "socket not connected"; - return; - } - - // 命令码 0x0515 = 1301,无负载,长度=0 - uint16_t payload_len = 0; - uint8_t send_buf[16] = { - 0x5A, 0x01, 0x00, 0x01, - 0x00, 0x00, - static_cast((payload_len >> 8) & 0xFF), - static_cast(payload_len & 0xFF), - 0x05, 0x15, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - }; - - flushSocket(sock); - setSocketTimeout(sock, 1000); - - int total_sent = 0; - int retry_cnt = 3; - while (total_sent < 16 && retry_cnt > 0) - { - int ret = send(sock, send_buf + total_sent, 16 - total_sent, MSG_NOSIGNAL); - if (ret <= 0) - { - retry_cnt--; - usleep(10000); - continue; - } - total_sent += ret; - } - if (total_sent != 16) - { - setSocketTimeout(sock, 0); - res.ret_code = -2; - res.err_msg = "send frame header failed"; - return; - } - - const int recv_buf_len = 65536; - std::vector recv_buf(recv_buf_len); - std::string raw_data; - int recv_ret = 0; - while ((recv_ret = recv(sock, recv_buf.data(), recv_buf_len, 0)) > 0) - { - raw_data.append(reinterpret_cast(recv_buf.data()), recv_ret); - } - setSocketTimeout(sock, 0); - - if (raw_data.empty()) - { - res.ret_code = -2; - res.err_msg = "no response data"; - return; - } - - size_t json_start = raw_data.find('{'); - size_t json_end = raw_data.rfind('}'); - std::string json_str; - if (json_start == std::string::npos) - { - res.ret_code = -4; - res.err_msg = "json not found in response"; - return; - } - if (json_end == std::string::npos) - { - json_str = raw_data.substr(json_start) + "}"; - } - else - { - json_str = raw_data.substr(json_start, json_end - json_start + 1); - } - - json root; - std::string parse_err; - if (!parseJson(json_str, root, parse_err)) - { - res.ret_code = -3; - res.err_msg = "json parse error: " + parse_err; - return; - } - - res.ret_code = root.value("ret_code", 0); - res.create_on = root.value("create_on", ""); - res.err_msg = root.value("err_msg", ""); - - // 解析站点数组 - if (root.contains("stations") && root["stations"].is_array()) - { - for (auto& item : root["stations"]) - { - StationItem st; - st.id = item.value("id", ""); - st.type = item.value("type", ""); - st.x = item.value("x", 0.0); - st.y = item.value("y", 0.0); - st.r = item.value("r", 0.0); - st.desc = item.value("desc", ""); - st.executor = item.value("executor", ""); - st.prepoint = item.value("prepoint", ""); - st.recfile = item.value("recfile", ""); - - // 兼容 bool 和字符串 "true"/"false" - if (item.contains("spin")) - { - if (item["spin"].is_boolean()) - st.spin = item["spin"].get(); - else if (item["spin"].is_string()) - st.spin = (item["spin"].get() == "true"); - } - if (item.contains("use_down_pgv")) - { - if (item["use_down_pgv"].is_boolean()) - st.use_down_pgv = item["use_down_pgv"].get(); - else if (item["use_down_pgv"].is_string()) - st.use_down_pgv = (item["use_down_pgv"].get() == "true"); - } - - res.stations.push_back(st); - } - } - } - - - // -------------------------------------------------------------------------- - // 1060 - robot_status_current_lock_req 查询当前控制权所有者 - // -------------------------------------------------------------------------- - void AgvSrc1100::getCurrentLockStatus(CurrentLockStatus& info) - { - int sock = sock_status_; - if (sock < 0) - { - info.ret_code = -1; - info.err_msg = "socket not connected"; - return; - } - - // 命令码 0x0424 = 1060,无负载,长度=0 - uint16_t payload_len = 0; - uint8_t send_buf[16] = { - 0x5A, 0x01, 0x00, 0x01, - 0x00, 0x00, - static_cast((payload_len >> 8) & 0xFF), - static_cast(payload_len & 0xFF), - 0x04, 0x24, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - }; - - flushSocket(sock); - setSocketTimeout(sock, 1000); - - int total_sent = 0; - int retry = 3; - while (total_sent < 16 && retry > 0) - { - int ret = send(sock, send_buf + total_sent, 16 - total_sent, 0); - if (ret <= 0) - { - retry--; - usleep(10000); - continue; - } - total_sent += ret; - } - if (total_sent != 16) - { - setSocketTimeout(sock, 0); - info.ret_code = -2; - info.err_msg = "send frame header failed"; - return; - } - - const int RECV_BUF_LEN = 65536; - std::vector recv_buf(RECV_BUF_LEN); - std::string raw_data; - int recv_ret = 0; - while ((recv_ret = recv(sock, recv_buf.data(), RECV_BUF_LEN, 0)) > 0) - { - raw_data.append(reinterpret_cast(recv_buf.data()), recv_ret); - } - setSocketTimeout(sock, 0); - - if (raw_data.empty()) - { - info.ret_code = -2; - info.err_msg = "no response data"; - return; - } - - size_t json_start = raw_data.find('{'); - size_t json_end = raw_data.rfind('}'); - std::string json_str; - if (json_start == std::string::npos) - { - info.ret_code = -4; - info.err_msg = "json not found in response"; - return; - } - if (json_end == std::string::npos) - { - json_str = raw_data.substr(json_start) + "}"; - } - else - { - json_str = raw_data.substr(json_start, json_end - json_start + 1); - } - - json root; - std::string err; - if (!parseJson(json_str, root, err)) - { - info.ret_code = -3; - info.err_msg = "json parse error: " + err; - return; - } - - // 提取控制权信息 - info.locked = root.value("locked", false); - info.ip = root.value("ip", ""); - info.port = root.value("port", 0); - info.type = static_cast(root.value("type", 0)); - info.nick_name = root.value("nick_name", ""); - info.time_t = root.value("time_t", 0LL); - info.desc = root.value("desc", ""); - info.ret_code = root.value("ret_code", 0); - info.create_on = root.value("create_on", ""); - info.err_msg = root.value("err_msg", ""); - } - - - // -------------------------------------------------------------------------- - // 1020 - robot_status_task_req 查询当前导航状态 - // -------------------------------------------------------------------------- - void AgvSrc1100::robotStatusTaskCurrent(RobotStatusTaskCurrentRes& res, const RobotStatusTaskCurrentReq& req) - { - int sock = sock_status_; - LOG(INFO) << "robotStatusTaskCurrent sock fd=" << sock; - if (sock < 0) - { - res.ret_code = -1; - res.err_msg = "status port socket invalid"; - return; - } - - // 组装请求 JSON: {"simple": true/false} - json send_json; - send_json["simple"] = req.simple; - std::string payload = send_json.dump(); - uint16_t payload_len = static_cast(payload.size()); - - // 命令码 0x03FC = 1020 - static uint8_t frame_header[16] = { - 0x5A,0x01,0x00,0x01, - 0x00,0x00, - 0,0, - 0x03,0xFC, - 0,0,0,0,0,0 - }; - frame_header[6] = static_cast((payload_len >> 8) & 0xFF); - frame_header[7] = static_cast(payload_len & 0xFF); - - flushSocket(sock); - setSocketTimeout(sock, 3000); - - // 发送帧头 - int sent = 0; - int retry = 5; - while (sent < 16 && retry > 0) - { - int w = send(sock, frame_header + sent, 16 - sent, MSG_NOSIGNAL); - if (w <= 0) - { - retry--; - usleep(20000); - continue; - } - sent += w; - } - if (sent != 16) - { - res.ret_code = -2; - res.err_msg = "send frame header failed"; - return; - } - - // 分片发送 payload - const int slice = 1024; - size_t off = 0; - size_t total = payload.size(); - const char* data = payload.data(); - while (off < total) - { - int wlen = static_cast(std::min((size_t)slice, total - off)); - int w = send(sock, data + off, wlen, MSG_NOSIGNAL); - if (w <= 0) - { - res.ret_code = -2; - res.err_msg = "send payload failed"; - return; - } - off += w; - } - - // 接收响应 - std::string raw; - uint8_t buf[65536]; - int rlen = 0; - while ((rlen = recv(sock, buf, sizeof(buf), MSG_NOSIGNAL)) > 0) - { - raw.append((char*)buf, rlen); - } - if (raw.empty()) - { - res.ret_code = -2; - res.err_msg = "no response data"; - return; - } - - // 提取 JSON - size_t json_begin = raw.find('{'); - if (json_begin == std::string::npos) - { - res.ret_code = -4; - res.err_msg = "response no json"; - return; - } - std::string json_str = raw.substr(json_begin); - if (json_str.back() != '}') - json_str += "}"; - - json root; - try - { - root = json::parse(json_str); - } - catch (json::parse_error& e) - { - res.ret_code = -3; - res.err_msg = std::string("json parse err:") + e.what(); - return; - } - - // 提取公共字段 - res.ret_code = root.contains("ret_code") ? root["ret_code"].get() : 0; - res.create_on = root.contains("create_on") ? root["create_on"].get() : ""; - res.err_msg = root.contains("err_msg") ? root["err_msg"].get() : ""; - - // 提取导航状态字段 - // task_status: 0=NONE, 1=WAITING, 2=RUNNING, 3=SUSPENDED, 4=COMPLETED, 5=FAILED, 6=CANCELED - res.task_status = root.contains("task_status") ? root["task_status"].get() : 0; - // task_type: 0=无, 1=自由导航到坐标, 2=自由导航到站点, 3=路径导航到站点 - res.task_type = root.contains("task_type") ? root["task_type"].get() : 0; - res.target_id = root.contains("target_id") ? root["target_id"].get() : ""; - res.move_status_info = root.contains("move_status_info") ? root["move_status_info"].get() : ""; - - // 提取目标坐标 [x, y, r] - if (root.contains("target_point") && root["target_point"].is_array()) - { - for (auto& val : root["target_point"]) - res.target_point.push_back(val.get()); - } - - // 提取已经过的站点列表 - if (root.contains("finished_path") && root["finished_path"].is_array()) - { - for (auto& s : root["finished_path"]) - res.finished_path.push_back(s.get()); - } - - // 提取尚未经过的站点列表 - if (root.contains("unfinished_path") && root["unfinished_path"].is_array()) - { - for (auto& s : root["unfinished_path"]) - res.unfinished_path.push_back(s.get()); - } - - // 提取料箱信息 - if (root.contains("containers") && root["containers"].is_array()) - { - for (auto& item : root["containers"]) - { - NavContainerItem c; - c.container_name = item.contains("container_name") ? item["container_name"].get() : ""; - c.desc = item.contains("desc") ? item["desc"].get() : ""; - c.goods_id = item.contains("goods_id") ? item["goods_id"].get() : ""; - c.has_goods = item.contains("has_goods") ? item["has_goods"].get() : false; - res.containers.push_back(c); - } - } - } - - - // -------------------------------------------------------------------------- - // 1110 - robot_status_task_status_package_req 批量查询任务状态 - // -------------------------------------------------------------------------- - void AgvSrc1100::robotStatusTaskPackage(QueryTaskStatusPackageRes& res, const QueryTaskStatusPackageReq& req) - { - int sock = sock_status_; - LOG(INFO) << "robotStatusTaskPackage sock fd=" << sock; - if (sock < 0) - { - res.ret_code = -1; - res.err_msg = "status port socket invalid"; - return; - } - - // 强制携带 task_ids 字段,空数组也生成,避免设备报 60002 错误 - json send_json; - json arr = json::array(); - for (auto& tid : req.task_ids) - arr.push_back(tid); - send_json["task_ids"] = arr; - - std::string payload = send_json.dump(); - uint16_t payload_len = static_cast(payload.size()); - - // 命令码 0x0456 = 1110 - static uint8_t frame_header[16] = { - 0x5A,0x01,0x00,0x01, - 0x00,0x00, - 0,0, - 0x04,0x56, - 0,0,0,0,0,0 - }; - frame_header[6] = static_cast((payload_len >> 8) & 0xFF); - frame_header[7] = static_cast(payload_len & 0xFF); - - flushSocket(sock); - setSocketTimeout(sock, 3000); - - // 发送帧头 - int sent = 0; - int retry = 5; - while (sent < 16 && retry > 0) - { - int w = send(sock, frame_header + sent, 16 - sent, MSG_NOSIGNAL); - if (w <= 0) - { - retry--; - usleep(20000); - continue; - } - sent += w; - } - if (sent != 16) - { - res.ret_code = -2; - res.err_msg = "send frame header failed"; - return; - } - - // 分片发送 payload - const int slice = 1024; - size_t off = 0; - size_t total = payload.size(); - const char* data = payload.data(); - while (off < total) - { - int wlen = static_cast(std::min((size_t)slice, total - off)); - int w = send(sock, data + off, wlen, MSG_NOSIGNAL); - if (w <= 0) - { - res.ret_code = -2; - res.err_msg = "send payload failed"; - return; - } - off += w; - } - - // 接收响应 - std::string raw; - uint8_t buf[65536]; - int rlen = 0; - while ((rlen = recv(sock, buf, sizeof(buf), MSG_NOSIGNAL)) > 0) - { - raw.append((char*)buf, rlen); - } - if (raw.empty()) - { - res.ret_code = -2; - res.err_msg = "no response data"; - return; - } - - // 提取 JSON - size_t json_begin = raw.find('{'); - if (json_begin == std::string::npos) - { - res.ret_code = -4; - res.err_msg = "response no json"; - return; - } - std::string json_str = raw.substr(json_begin); - if (json_str.back() != '}') - json_str += "}"; - - json root; - try - { - root = json::parse(json_str); - } - catch (json::parse_error& e) - { - res.ret_code = -3; - res.err_msg = std::string("json parse err:") + e.what(); - return; - } - - // 提取公共字段 - res.ret_code = root.contains("ret_code") ? root["ret_code"].get() : 0; - res.create_on = root.contains("create_on") ? root["create_on"].get() : ""; - res.err_msg = root.contains("err_msg") ? root["err_msg"].get() : ""; - - // 解析 task_status_package 对象 - if (root.contains("task_status_package")) - { - auto pkg_json = root["task_status_package"]; - auto& pkg_out = res.task_status_package; - - pkg_out.closest_target = pkg_json.contains("closest_target") ? pkg_json["closest_target"].get() : ""; - pkg_out.source_name = pkg_json.contains("source_name") ? pkg_json["source_name"].get() : ""; - pkg_out.target_name = pkg_json.contains("target_name") ? pkg_json["target_name"].get() : ""; - pkg_out.percentage = pkg_json.contains("percentage") ? pkg_json["percentage"].get() : 0.0; - pkg_out.distance = pkg_json.contains("distance") ? pkg_json["distance"].get() : 0.0; - pkg_out.info = pkg_json.contains("info") ? pkg_json["info"].get() : ""; - - // 解析任务状态列表 - if (pkg_json.contains("task_status_list") && pkg_json["task_status_list"].is_array()) - { - for (auto& item : pkg_json["task_status_list"]) - { - SingleTaskStatusItem st; - st.task_id = item.contains("task_id") ? item["task_id"].get() : ""; - st.status = item.contains("status") ? item["status"].get() : 0; - st.type = item.contains("type") ? item["type"].get() : 0; - pkg_out.task_status_list.push_back(st); - } - } - } - } - - - // ============================================================================ - // 端口 19205 – 机器人控制 API(允许 5 个连接) - // 功能:下发控制指令,改变机器人运动或状态 - // ============================================================================ - - // -------------------------------------------------------------------------- - // 2010 - robot_control_motion_req 开环速度运动控制 - // -------------------------------------------------------------------------- - void AgvSrc1100::robotMotionControl(MotionCtrlRes& res, const MotionCtrlReq& req) - { - int sock = sock_control_; - if (sock < 0) - { - res.ret_code = -1; - res.err_msg = "socket not connected"; - return; - } - - // 组装请求 JSON,只包含非零有效字段 - json send_json; - if (req.vx != 0.0) send_json["vx"] = req.vx; // X 方向速度 - if (req.vy != 0.0) send_json["vy"] = req.vy; // Y 方向速度 - if (req.w != 0.0) send_json["w"] = req.w; // 角速度 - if (req.steer != 0.0) send_json["steer"] = req.steer; // 舵角(单舵轮) - if (req.real_steer != 0.0) send_json["real_steer"] = req.real_steer; // 实际舵角 - if (req.duration >= 0) send_json["duration"] = req.duration; // 运动持续时间 - - std::string payload = send_json.dump(); - uint16_t payload_len = static_cast(payload.size()); - - // 命令码 0x07DA = 2010 - uint8_t send_buf[16] = { - 0x5A, 0x01, 0x00, 0x01, - 0x00, 0x00, - static_cast((payload_len >> 8) & 0xFF), - static_cast(payload_len & 0xFF), - 0x07, 0xDA, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - }; - - flushSocket(sock); - setSocketTimeout(sock, 1000); - - int total_sent = 0; - int retry_cnt = 3; - while (total_sent < 16 && retry_cnt > 0) - { - int ret = send(sock, send_buf + total_sent, 16 - total_sent, MSG_NOSIGNAL); - if (ret <= 0) - { - retry_cnt--; - usleep(10000); - continue; - } - total_sent += ret; - } - if (total_sent != 16) - { - setSocketTimeout(sock, 0); - res.ret_code = -2; - res.err_msg = "send frame header failed"; - return; - } - - // 分片发送 payload - const int slice_size = 4096; - size_t offset = 0; - size_t total_size = payload.size(); - const char* data_ptr = payload.data(); - while (offset < total_size) - { - int send_len = std::min((size_t)slice_size, total_size - offset); - int ret = send(sock, data_ptr + offset, send_len, MSG_NOSIGNAL); - if (ret <= 0) - { - setSocketTimeout(sock, 0); - res.ret_code = -2; - res.err_msg = "send payload failed"; - return; - } - offset += ret; - usleep(500); - } - - // 接收响应 - const int recv_buf_len = 65536; - std::vector recv_buf(recv_buf_len); - std::string raw_data; - int recv_ret = 0; - while ((recv_ret = recv(sock, recv_buf.data(), recv_buf_len, 0)) > 0) - { - raw_data.append(reinterpret_cast(recv_buf.data()), recv_ret); - } - setSocketTimeout(sock, 0); - - if (raw_data.empty()) - { - res.ret_code = -2; - res.err_msg = "no response data"; - return; - } - - // 提取并解析 JSON - size_t json_start = raw_data.find('{'); - size_t json_end = raw_data.rfind('}'); - std::string json_str; - if (json_start == std::string::npos) - { - res.ret_code = -4; - res.err_msg = "json not found in response"; - return; - } - if (json_end == std::string::npos) - { - json_str = raw_data.substr(json_start) + "}"; - } - else - { - json_str = raw_data.substr(json_start, json_end - json_start + 1); - } - - json root; - std::string parse_err; - if (!parseJson(json_str, root, parse_err)) - { - res.ret_code = -3; - res.err_msg = "json parse error: " + parse_err; - return; - } - - res.ret_code = root.value("ret_code", 0); - res.create_on = root.value("create_on", ""); - res.err_msg = root.value("err_msg", ""); - } - - - // -------------------------------------------------------------------------- - // 2022 - robot_control_loadmap_req 切换载入地图 - // -------------------------------------------------------------------------- - void AgvSrc1100::robotLoadMap(LoadMapRes& res, const LoadMapReq& req) - { - int sock = sock_control_; - if (sock < 0) - { - res.ret_code = -1; - res.err_msg = "socket not connected"; - return; - } - - // 组装请求 JSON: {"map_name":"xxx"} - json send_json; - send_json["map_name"] = req.map_name; - std::string payload = send_json.dump(); - uint16_t payload_len = static_cast(payload.size()); - - // 命令码 0x07E6 = 2022 - uint8_t send_buf[16] = { - 0x5A, 0x01, 0x00, 0x01, - 0x00, 0x00, - static_cast((payload_len >> 8) & 0xFF), - static_cast(payload_len & 0xFF), - 0x07, 0xE6, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - }; - - flushSocket(sock); - setSocketTimeout(sock, 1000); - - // 发送帧头 - int total_sent = 0; - int retry_cnt = 3; - while (total_sent < 16 && retry_cnt > 0) - { - int ret = send(sock, send_buf + total_sent, 16 - total_sent, MSG_NOSIGNAL); - if (ret <= 0) - { - retry_cnt--; - usleep(10000); - continue; - } - total_sent += ret; - } - if (total_sent != 16) - { - setSocketTimeout(sock, 0); - res.ret_code = -2; - res.err_msg = "send frame header failed"; - return; - } - - // 分片发送 payload - const int slice_size = 4096; - size_t offset = 0; - size_t total_size = payload.size(); - const char* data_ptr = payload.data(); - while (offset < total_size) - { - int send_len = std::min((size_t)slice_size, total_size - offset); - int ret = send(sock, data_ptr + offset, send_len, MSG_NOSIGNAL); - if (ret <= 0) - { - setSocketTimeout(sock, 3000); - res.ret_code = -2; - res.err_msg = "send payload failed"; - return; - } - offset += ret; - usleep(500); - } - - // 接收响应 - const int recv_buf_len = 65536; - std::vector recv_buf(recv_buf_len); - std::string raw_data; - int recv_ret = 0; - while ((recv_ret = recv(sock, recv_buf.data(), recv_buf_len, 0)) > 0) - { - raw_data.append(reinterpret_cast(recv_buf.data()), recv_ret); - } - setSocketTimeout(sock, 0); - - if (raw_data.empty()) - { - res.ret_code = -2; - res.err_msg = "no response data"; - return; - } - - // 提取并解析 JSON - size_t json_start = raw_data.find('{'); - size_t json_end = raw_data.rfind('}'); - std::string json_str; - if (json_start == std::string::npos) - { - res.ret_code = -4; - res.err_msg = "json not found in response"; - return; - } - if (json_end == std::string::npos) - { - json_str = raw_data.substr(json_start) + "}"; - } - else - { - json_str = raw_data.substr(json_start, json_end - json_start + 1); - } - - json root; - std::string parse_err; - if (!parseJson(json_str, root, parse_err)) - { - res.ret_code = -3; - res.err_msg = "json parse error: " + parse_err; - return; - } - - res.ret_code = root.value("ret_code", 0); - res.create_on = root.value("create_on", ""); - res.err_msg = root.value("err_msg", ""); - LOG(INFO) << "recv raw_data len=" << raw_data.size() << " content=" << raw_data; - } - - - // -------------------------------------------------------------------------- - // 2000 - robot_control_stop_req 停止开环运动 命令码0x07D0,应答码12000(0x2EE0) - // 下发零速度开环指令,终止当前持续运动 - // -------------------------------------------------------------------------- - // -------------------------------------------------------------------------- - // 2000 - robot_control_stop_req 停止开环运动 命令码0x07D0,应答码12000(0x2EE0) - // 下发零速度开环指令,终止当前持续运动 - // -------------------------------------------------------------------------- - - void AgvSrc1100::robotControlStop(RobotControlStopRes& res) - { - int sock = sock_control_; - if (sock < 0) { - res.ret_code = -1; - res.err_msg = "control socket disconnected"; - return; - } - - // 业务负载长度为 0 - uint32_t payload_len = 0; - - // 标准 16 字节帧头(长度 4 字节,命令码 0x07D0,保留 6 字节) - uint8_t send_buf[16] = { - 0x5A, 0x01, 0x00, 0x01, - static_cast((payload_len >> 24) & 0xFF), - static_cast((payload_len >> 16) & 0xFF), - static_cast((payload_len >> 8) & 0xFF), - static_cast(payload_len & 0xFF), - 0x07, 0xD0, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - }; - - flushSocket(sock); - setSocketTimeout(sock, 1000); // 适当增加超时 - - // 发送帧头(重试机制) - int total_sent = 0; - int retry = 3; - while (total_sent < 16 && retry-- > 0) { - int ret = send(sock, send_buf + total_sent, 16 - total_sent, MSG_NOSIGNAL); - if (ret <= 0) { - usleep(10000); - continue; - } - total_sent += ret; - } - if (total_sent != 16) { - setSocketTimeout(sock, 0); - res.ret_code = -2; - res.err_msg = "send header failed"; - return; - } - - // 无 payload,无需发送额外数据 - - // 接收响应(使用较大缓冲区) - const int RECV_BUF = 131072; - std::vector recv_buf(RECV_BUF); - std::string raw_data; - int rlen; - while ((rlen = recv(sock, recv_buf.data(), RECV_BUF, MSG_NOSIGNAL)) > 0) { - raw_data.append(reinterpret_cast(recv_buf.data()), rlen); - } - setSocketTimeout(sock, 0); - - if (raw_data.empty()) { - res.ret_code = -2; - res.err_msg = "no response from device"; - return; - } - - // 提取 JSON(容错补全) - size_t json_start = raw_data.find('{'); - if (json_start == std::string::npos) { - res.ret_code = -4; - res.err_msg = "no JSON in response"; - return; - } - std::string json_str = raw_data.substr(json_start); - // 补全缺失的 '}' - int left = std::count(json_str.begin(), json_str.end(), '{'); - int right = std::count(json_str.begin(), json_str.end(), '}'); - while (right < left) { json_str += "}"; right++; } - - json root; - std::string parse_err; - if (!parseJson(json_str, root, parse_err)) { - res.ret_code = -3; - res.err_msg = "JSON parse error: " + parse_err; - return; - } - - res.ret_code = root.value("ret_code", 0); - res.create_on = root.value("create_on", ""); - res.err_msg = root.value("err_msg", ""); - } - - - // ============================================================================ - // 端口 19206 – 机器人导航 API(允许 5 个连接) - // 功能:下发导航任务 - // ============================================================================ - - // -------------------------------------------------------------------------- - // 3066 - robot_task_gotargetlist_req 指定路径导航(多点) - // 功能:下发多段连续站点导航任务序列,任务串行排队执行 - // 必填字段:task_id / source_id / id - // -------------------------------------------------------------------------- - void AgvSrc1100::robotGoTargetList(GoTargetListRes& res, const GoTargetListReq& req) - { - int sock = sock_nav_; - LOG(INFO) << "robotGoTargetList sock fd=" << sock; - if (sock < 0) - { - res.ret_code = -1; - res.err_msg = "nav socket invalid"; - return; - } - - // 组装请求 JSON: {"move_task_list": [{"task_id":"xxx","source_id":"xxx","id":"xxx"}, ...]} - json send_json; - json arr = json::array(); - for (const auto& task : req.move_task_list) - { - json obj; - obj["task_id"] = task.task_id; - obj["source_id"] = task.source_id; - obj["id"] = task.id; - if (!task.operation.empty()) - { - obj["operation"] = task.operation; - obj["jack_height"] = task.jack_height; - } - arr.push_back(obj); - } - send_json["move_task_list"] = arr; - std::string payload = send_json.dump(); - uint16_t payload_len = static_cast(payload.size()); - LOG(INFO) << "nav payload length:" << payload_len; - - // 命令码 0x0BFA = 3066 - static uint8_t send_buf[16] = { - 0x5A, 0x01, 0x00, 0x01, - 0x00, 0x00, - 0,0, - 0x0B, 0xFA, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - }; - send_buf[6] = static_cast((payload_len >> 8) & 0xFF); - send_buf[7] = static_cast(payload_len & 0xFF); - - flushSocket(sock); - setSocketTimeout(sock, 3000); - - // 发送帧头,带重试 - int total_sent = 0; - int retry_cnt = 5; - while (total_sent < 16 && retry_cnt > 0) - { - int ret = send(sock, send_buf + total_sent, 16 - total_sent, MSG_NOSIGNAL); - if (ret <= 0) - { - LOG(ERROR) << "send header failed, ret=" << ret - << ", errno=" << errno - << " (" << strerror(errno) << ")"; - retry_cnt--; - usleep(20000); - continue; - } - total_sent += ret; - } - if (total_sent != 16) - { - setSocketTimeout(sock, 0); - res.ret_code = -2; - res.err_msg = "send frame header failed"; - return; - } - - // 分片发送 payload(1024 字节一片) - const int slice_size = 1024; - size_t offset = 0; - size_t total_size = payload.size(); - const char* data_ptr = payload.data(); - while (offset < total_size) - { - int send_len = std::min((size_t)slice_size, total_size - offset); - int ret = send(sock, data_ptr + offset, send_len, MSG_NOSIGNAL); - if (ret <= 0) - { - LOG(ERROR) << "send payload failed, errno=" << errno << " (" << strerror(errno) << ")"; - setSocketTimeout(sock, 0); - res.ret_code = -2; - res.err_msg = "send payload failed"; - return; - } - offset += ret; - } - - // 接收响应 - const int recv_buf_len = 65536; - std::vector recv_buf(recv_buf_len); - std::string raw_data; - int recv_ret = 0; - while ((recv_ret = recv(sock, recv_buf.data(), recv_buf_len, MSG_NOSIGNAL)) > 0) - { - raw_data.append(reinterpret_cast(recv_buf.data()), recv_ret); - } - setSocketTimeout(sock, 0); - - if (raw_data.empty()) - { - res.ret_code = -2; - res.err_msg = "no response data"; - return; - } - - // 提取并解析 JSON - size_t json_start = raw_data.find('{'); - if (json_start == std::string::npos) - { - res.ret_code = -4; - res.err_msg = "json not found in response"; - return; - } - std::string json_str = raw_data.substr(json_start); - if (json_str.back() != '}') - { - json_str += "}"; - } - - json root; - std::string parse_err; - try - { - root = json::parse(json_str); - } - catch (json::parse_error& e) - { - res.ret_code = -3; - res.err_msg = "json parse error: " + std::string(e.what()); - return; - } - - res.ret_code = root.contains("ret_code") ? root["ret_code"].get() : 0; - res.create_on = root.contains("create_on") ? root["create_on"].get() : ""; - res.err_msg = root.contains("err_msg") ? root["err_msg"].get() : ""; - } - - - // -------------------------------------------------------------------------- - // 3051 - robot_task_gotarget_req 单点路径导航(自动规划) - // 功能:给定目标站点,机器人自动规划路径前往 - // 注意:仅用于单车测试,严禁用于多车调度场景 - // -------------------------------------------------------------------------- - void AgvSrc1100::robotGoTarget(RobotGoTargetRes& res, const RobotGoTargetReq& req) - { - int sock = sock_nav_; - LOG(INFO) << "robotGoTarget control sock fd=" << sock; - if (sock < 0) - { - res.ret_code = -1; - res.err_msg = "control port socket invalid"; - return; - } - - // 组装请求 JSON(根据协议 3051 的字段结构) - json send_json; - // 必填字段 - send_json["source_id"] = req.source_id; - send_json["id"] = req.id; - if (!req.task_id.empty()) - send_json["task_id"] = req.task_id; - - // 基础运动参数(只添加非默认值) - if (req.angle != 0) send_json["angle"] = req.angle; - if (!req.method.empty()) send_json["method"] = req.method; - if (req.max_speed != 0) send_json["max_speed"] = req.max_speed; - if (req.max_wspeed != 0) send_json["max_wspeed"] = req.max_wspeed; - if (req.max_acc != 0) send_json["max_acc"] = req.max_acc; - if (req.max_wacc != 0) send_json["max_wacc"] = req.max_wacc; - if (req.duration > 0) send_json["duration"] = req.duration; - if (req.orientation != 0) send_json["orientation"] = req.orientation; - if (req.spin) send_json["spin"] = true; - if (req.delay > 0) send_json["delay"] = req.delay; - if (req.start_rot_dir != 0) send_json["start_rot_dir"] = req.start_rot_dir; - if (req.end_rot_dir != 0) send_json["end_rot_dir"] = req.end_rot_dir; - if (req.reach_dist != 0) send_json["reach_dist"] = req.reach_dist; - if (req.reach_angle != 0) send_json["reach_angle"] = req.reach_angle; - if (!req.skill_name.empty()) send_json["skill_name"] = req.skill_name; - - // PGV 二次定位参数 - auto& pgv = req.pgv; - if (pgv.use_pgv) send_json["use_pgv"] = true; - if (pgv.use_down_pgv) send_json["use_down_pgv"] = true; - if (pgv.pgv_adjust_dist != 0) send_json["pgv_adjust_dist"] = pgv.pgv_adjust_dist; - if (pgv.pgv_adjust_cx != 0) send_json["pgv_adjust_cx"] = pgv.pgv_adjust_cx; - if (pgv.pgv_adjust_cy != 0) send_json["pgv_adjust_cy"] = pgv.pgv_adjust_cy; - if (pgv.pgv_x_adjust != 0) send_json["pgv_x_adjust"] = pgv.pgv_x_adjust; - - // 设备操作(顶升/货叉/辊筒/牵引/音频/等待DI/设置DO/脚本) - if (!req.operation.empty()) - { - send_json["operation"] = req.operation; - - // 顶升设备 JackLoad/JackUnload/JackHeight - if (req.operation.substr(0,4) == "Jack") - { - if (req.jack_height != 0) send_json["jack_height"] = req.jack_height; - } - // 货叉设备 ForkLoad/ForkUnload/ForkHeight/ForkForward - else if (req.operation.substr(0,4) == "Fork") - { - if (req.start_height != 0) send_json["start_height"] = req.start_height; - if (req.end_height != 0) send_json["end_height"] = req.end_height; - if (req.fork_mid_height != 0) send_json["fork_mid_height"] = req.fork_mid_height; - if (req.fork_dist != 0) send_json["fork_dist"] = req.fork_dist; - } - // 辊筒设备 RollerLoad/RollerUnload/RollerRoll/... - else if (req.operation.substr(0,6) == "Roller") - { - if (!req.direction.empty()) send_json["direction"] = req.direction; - } - // 牵引设备 HookLoad/HookUnload - else if (req.operation.substr(0,4) == "Hook") - { - if (req.recognize) send_json["recognize"] = true; - } - // 音频设备 sound - else if (req.operation == "sound") - { - json sound; - auto& sa = req.sounds_args; - if (!sa.name.empty()) sound["name"] = sa.name; - if (sa.loop != 0) sound["loop"] = sa.loop; - if (sa.stop != 0) sound["stop"] = sa.stop; - send_json["sounds_args"] = sound; - } - // 等待 DI 触发 - else if (req.operation == "WaitDI") - { - json args_obj; - json di_arr = json::array(); - for (auto& di : req.wait_di_args.DI) - { - json di_item; - di_item["id"] = di.id; - di_item["status"] = di.status; - di_arr.push_back(di_item); - } - args_obj["DI"] = di_arr; - if (req.wait_di_args.timeout > 0) args_obj["timeout"] = req.wait_di_args.timeout; - send_json["args"] = args_obj; - } - // 控制 DO 开关 - else if (req.operation == "SetDO") - { - json args_obj; - json do_arr = json::array(); - for (auto& d : req.set_do_args.DO) - { - json do_item; - do_item["id"] = d.id; - do_item["status"] = d.status; - do_arr.push_back(do_item); - } - args_obj["DO"] = do_arr; - send_json["args"] = args_obj; - } - } - - // 视觉识别标志 - if (req.recognize) send_json["recognize"] = true; - if (!req.recfile.empty()) send_json["recfile"] = req.recfile; - - // 脚本执行 - if (!req.script_name.empty()) - { - send_json["script_name"] = req.script_name; - send_json["script_stage"] = req.script_stage; - json s_arg; - for (auto& kv : req.script_args.str_kv) s_arg[kv.first] = kv.second; - for (auto& kv : req.script_args.num_kv) s_arg[kv.first] = kv.second; - json do_arr = json::array(); - for (auto& d : req.script_args.do_list) - { - json o; o["id"] = d.id; o["status"] = d.status; do_arr.push_back(o); - } - if (!do_arr.empty()) s_arg["DO"] = do_arr; - json di_arr = json::array(); - for (auto& di : req.script_args.di_list) - { - json o; o["id"] = di.id; o["status"] = di.status; di_arr.push_back(o); - } - if (!di_arr.empty()) s_arg["DI"] = di_arr; - send_json["script_args"] = s_arg; - } - - // 原地旋转(GoByOdometer) - if (req.move_angle != 0) - { - send_json["move_angle"] = req.move_angle; - send_json["speed_w"] = req.speed_w; - send_json["loc_mode"] = req.loc_mode; - } - - // 自由导航(仅双轮差速底盘) - if (req.freeGo.x != 0 || req.freeGo.y != 0) - { - json fg; - fg["x"] = req.freeGo.x; - fg["y"] = req.freeGo.y; - fg["theta"] = req.freeGo.theta; - send_json["freeGo"] = fg; - } - - std::string payload = send_json.dump(); - uint16_t payload_len = static_cast(payload.size()); - - // 命令码 0x0BEB = 3051 - uint8_t frame_header[16] = { - 0x5A,0x01,0x00,0x01, - 0x00,0x00, - 0,0, - 0x0B,0xEB, - 0,0,0,0,0,0 - }; - frame_header[6] = static_cast((payload_len >> 8) & 0xFF); - frame_header[7] = static_cast(payload_len & 0xFF); - - flushSocket(sock); - setSocketTimeout(sock, 3000); - - // 发送帧头 - int sent = 0; - int retry = 5; - while (sent < 16 && retry > 0) - { - int w = send(sock, frame_header + sent, 16 - sent, MSG_NOSIGNAL); - if (w <= 0) - { - retry--; - usleep(20000); - continue; - } - sent += w; - } - if (sent != 16) - { - res.ret_code = -2; - res.err_msg = "send frame header failed"; - return; - } - - // 分片发送 payload - const int slice = 1024; - size_t off = 0; - size_t total = payload.size(); - const char* data = payload.data(); - while (off < total) - { - int wlen = static_cast(std::min((size_t)slice, total - off)); - int w = send(sock, data + off, wlen, MSG_NOSIGNAL); - if (w <= 0) - { - res.ret_code = -2; - res.err_msg = "send payload failed"; - return; - } - off += w; - } - - // 接收响应 - std::string raw; - uint8_t buf[65536]; - int rlen = 0; - while ((rlen = recv(sock, buf, sizeof(buf), MSG_NOSIGNAL)) > 0) - { - raw.append((char*)buf, rlen); - } - if (raw.empty()) - { - res.ret_code = -2; - res.err_msg = "no response data"; - return; - } - - // 提取并解析 JSON - size_t json_start = raw.find('{'); - if (json_start == std::string::npos) - { - res.ret_code = -4; - res.err_msg = "response no valid json"; - return; - } - std::string json_str = raw.substr(json_start); - if (json_str.back() != '}') json_str += "}"; - - json root; - try - { - root = json::parse(json_str); - } - catch (json::parse_error& e) - { - res.ret_code = -3; - res.err_msg = std::string("json parse err:") + e.what(); - return; - } - - res.ret_code = root.contains("ret_code") ? root["ret_code"].get() : 0; - res.create_on = root.contains("create_on") ? root["create_on"].get() : ""; - res.err_msg = root.contains("err_msg") ? root["err_msg"].get() : ""; - } - - - // ============================================================================ - // 端口 19207 – 机器人配置 API(允许 5 个连接) - // 功能:配置类操作(地图上传下载、控制权管理等) - // ============================================================================ - - // -------------------------------------------------------------------------- - // 4005 - robot_config_lock_req 抢占控制权 - // 功能:向机器人申请独占控制权,成功后获得操作权限 - // -------------------------------------------------------------------------- - void AgvSrc1100::lockRobotControl(LockResult& info, const std::string& nick_name) - { - int sock = sock_config_; - if (sock < 0) - { - info.ret_code = -1; - info.err_msg = "socket not connected"; - return; - } - - // 组装请求 JSON: {"nick_name":"xxx"} - std::string req_json = R"({"nick_name":")" + nick_name + R"("})"; - uint16_t payload_len = static_cast(req_json.size()); - - // 命令码 0x0FA5 = 4005 - uint8_t send_buf[16] = { - 0x5A, 0x01, 0x00, 0x01, - 0x00, 0x00, - static_cast((payload_len >> 8) & 0xFF), - static_cast(payload_len & 0xFF), - 0x0F, 0xA5, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - }; - - flushSocket(sock); - setSocketTimeout(sock, 1000); - - // 发送帧头 - int total_sent = 0; - int retry = 3; - while (total_sent < 16 && retry > 0) - { - int ret = send(sock, send_buf + total_sent, 16 - total_sent, 0); - if (ret <= 0) - { - retry--; - usleep(10000); - continue; - } - total_sent += ret; - } - if (total_sent != 16) - { - setSocketTimeout(sock, 0); - info.ret_code = -2; - info.err_msg = "send frame header failed"; - return; - } - - // 分片发送 JSON 负载 - const int slice_size = 4096; - size_t offset = 0; - size_t total_size = req_json.size(); - const char* data = req_json.c_str(); - while (offset < total_size) - { - int send_len = std::min((size_t)slice_size, total_size - offset); - int ret = send(sock, data + offset, send_len, 0); - if (ret <= 0) - { - setSocketTimeout(sock, 0); - info.ret_code = -2; - info.err_msg = "send payload failed"; - return; - } - offset += ret; - usleep(500); - } - - // 接收响应 - const int recv_buf_len = 65536; - std::vector recv_buf(recv_buf_len); - std::string raw_data; - int recv_ret = 0; - while ((recv_ret = recv(sock, recv_buf.data(), recv_buf_len, 0)) > 0) - { - raw_data.append(reinterpret_cast(recv_buf.data()), recv_ret); - } - setSocketTimeout(sock, 0); - - if (raw_data.empty()) - { - info.ret_code = -2; - info.err_msg = "no response data"; - return; - } - - // 提取并解析 JSON - size_t json_start = raw_data.find('{'); - size_t json_end = raw_data.rfind('}'); - std::string json_str; - if (json_start == std::string::npos) - { - info.ret_code = -4; - info.err_msg = "json not found in response"; - return; - } - if (json_end == std::string::npos) - { - json_str = raw_data.substr(json_start) + "}"; - } - else - { - json_str = raw_data.substr(json_start, json_end - json_start + 1); - } - - json root; - std::string err; - if (!parseJson(json_str, root, err)) - { - info.ret_code = -3; - info.err_msg = "json parse error: " + err; - return; - } - - info.ret_code = root.value("ret_code", 0); - info.create_on = root.value("create_on", ""); - info.err_msg = root.value("err_msg", ""); - } - - - - // ============================================================================ - // 3003 - robot_task_cancel_req 取消当前导航任务 (导航端口 19206) - // ============================================================================ - void AgvSrc1100::robotTaskCancel(RobotTaskCancelRes& res) { - int sock = sock_nav_; - if (sock < 0) { - res.ret_code = -1; - res.err_msg = "nav socket disconnected"; - return; - } - - uint32_t payload_len = 0; - uint8_t send_buf[16] = { - 0x5A, 0x01, 0x00, 0x01, - static_cast((payload_len >> 24) & 0xFF), - static_cast((payload_len >> 16) & 0xFF), - static_cast((payload_len >> 8) & 0xFF), - static_cast(payload_len & 0xFF), - 0x0B, 0xBB, // 命令码 3003 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - }; - - flushSocket(sock); - setSocketTimeout(sock, 1000); - - int total_sent = 0; - int retry = 3; - while (total_sent < 16 && retry-- > 0) { - int ret = send(sock, send_buf + total_sent, 16 - total_sent, MSG_NOSIGNAL); - if (ret <= 0) { - usleep(10000); - continue; - } - total_sent += ret; - } - if (total_sent != 16) { - setSocketTimeout(sock, 0); - res.ret_code = -2; - res.err_msg = "send header failed"; - return; - } - - // 接收响应(先读 16 字节头,再按长度读 JSON) - uint8_t recv_header[16]; - size_t header_read = 0; - while (header_read < 16) { - int ret = recv(sock, recv_header + header_read, 16 - header_read, MSG_NOSIGNAL); - if (ret <= 0) { - setSocketTimeout(sock, 0); - res.ret_code = -2; - res.err_msg = "recv header failed or timeout"; - return; - } - header_read += ret; - } - - uint32_t data_len = (recv_header[4] << 24) | - (recv_header[5] << 16) | - (recv_header[6] << 8) | - recv_header[7]; - - std::string json_str; - if (data_len > 0) { - json_str.resize(data_len); - size_t total_read = 0; - while (total_read < data_len) { - int ret = recv(sock, &json_str[total_read], data_len - total_read, MSG_NOSIGNAL); - if (ret <= 0) { - setSocketTimeout(sock, 0); - res.ret_code = -2; - res.err_msg = "recv payload failed or timeout"; - return; - } - total_read += ret; - } - } - setSocketTimeout(sock, 0); - - if (json_str.empty()) { - res.ret_code = -4; - res.err_msg = "empty json data"; - return; - } - - // 容错提取 JSON - size_t json_start = json_str.find('{'); - if (json_start == std::string::npos) { - res.ret_code = -4; - res.err_msg = "no JSON found in payload"; - return; - } - size_t json_end = json_str.rfind('}'); - if (json_end != std::string::npos) { - json_str = json_str.substr(json_start, json_end - json_start + 1); - } else { - json_str = json_str.substr(json_start) + "}"; - } - - json root; - std::string parse_err; - if (!parseJson(json_str, root, parse_err)) { - res.ret_code = -3; - res.err_msg = "JSON parse error: " + parse_err; - return; - } - - res.ret_code = root.value("ret_code", 0); - res.create_on = root.value("create_on", ""); - res.err_msg = root.value("err_msg", ""); - - LOG(INFO) << "robotTaskCancel success, ret_code=" << res.ret_code; - } - - // ============================================================================ - // 3002 - robot_task_resume_req 继续当前导航任务 (导航端口 19206) - // ============================================================================ - void AgvSrc1100::robotTaskResume(RobotTaskResumeRes& res) { - int sock = sock_nav_; - if (sock < 0) { - res.ret_code = -1; - res.err_msg = "nav socket disconnected"; - return; - } - - uint32_t payload_len = 0; - uint8_t send_buf[16] = { - 0x5A, 0x01, 0x00, 0x01, - static_cast((payload_len >> 24) & 0xFF), - static_cast((payload_len >> 16) & 0xFF), - static_cast((payload_len >> 8) & 0xFF), - static_cast(payload_len & 0xFF), - 0x0B, 0xBA, // 命令码 3002 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - }; - - flushSocket(sock); - setSocketTimeout(sock, 1000); - - // 发送帧头(带重试) - int total_sent = 0; - int retry = 3; - while (total_sent < 16 && retry-- > 0) { - int ret = send(sock, send_buf + total_sent, 16 - total_sent, MSG_NOSIGNAL); - if (ret <= 0) { - usleep(10000); - continue; - } - total_sent += ret; - } - if (total_sent != 16) { - setSocketTimeout(sock, 0); - res.ret_code = -2; - res.err_msg = "send header failed"; - return; - } - - // 接收响应(先读 16 字节头,再按长度读 JSON) - uint8_t recv_header[16]; - size_t header_read = 0; - while (header_read < 16) { - int ret = recv(sock, recv_header + header_read, 16 - header_read, MSG_NOSIGNAL); - if (ret <= 0) { - setSocketTimeout(sock, 0); - res.ret_code = -2; - res.err_msg = "recv header failed or timeout"; - return; - } - header_read += ret; - } - - uint32_t data_len = (recv_header[4] << 24) | - (recv_header[5] << 16) | - (recv_header[6] << 8) | - recv_header[7]; - - std::string json_str; - if (data_len > 0) { - json_str.resize(data_len); - size_t total_read = 0; - while (total_read < data_len) { - int ret = recv(sock, &json_str[total_read], data_len - total_read, MSG_NOSIGNAL); - if (ret <= 0) { - setSocketTimeout(sock, 0); - res.ret_code = -2; - res.err_msg = "recv payload failed or timeout"; - return; - } - total_read += ret; - } - } - setSocketTimeout(sock, 0); - - if (json_str.empty()) { - res.ret_code = -4; - res.err_msg = "empty json data"; - return; - } - - // 容错提取 JSON - size_t json_start = json_str.find('{'); - if (json_start == std::string::npos) { - res.ret_code = -4; - res.err_msg = "no JSON found in payload"; - return; - } - size_t json_end = json_str.rfind('}'); - if (json_end != std::string::npos) { - json_str = json_str.substr(json_start, json_end - json_start + 1); - } else { - json_str = json_str.substr(json_start) + "}"; - } - - json root; - std::string parse_err; - if (!parseJson(json_str, root, parse_err)) { - res.ret_code = -3; - res.err_msg = "JSON parse error: " + parse_err; - return; - } - - res.ret_code = root.value("ret_code", 0); - res.create_on = root.value("create_on", ""); - res.err_msg = root.value("err_msg", ""); - - LOG(INFO) << "robotTaskResume success, ret_code=" << res.ret_code; - } - - - - // ============================================================================ - // 3001 - robot_task_pause_req 暂停当前导航任务 (控制端口 19205) - // ============================================================================ - void AgvSrc1100::robotTaskPause(RobotTaskPauseRes& res) { - int sock = sock_nav_; // 使用控制端口 - if (sock < 0) { - res.ret_code = -1; - res.err_msg = "control socket disconnected"; - return; - } - - // 业务负载长度为 0(无 JSON 数据) - uint32_t payload_len = 0; - - // 标准 16 字节帧头(长度 4 字节,命令码 0x0BB9,保留 6 字节) - uint8_t send_buf[16] = { - 0x5A, 0x01, 0x00, 0x01, - static_cast((payload_len >> 24) & 0xFF), - static_cast((payload_len >> 16) & 0xFF), - static_cast((payload_len >> 8) & 0xFF), - static_cast(payload_len & 0xFF), - 0x0B, 0xB9, // 命令码 3001 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - }; - - flushSocket(sock); - setSocketTimeout(sock, 1000); // 安全超时 - - // 发送帧头(带重试) - int total_sent = 0; - int retry = 3; - while (total_sent < 16 && retry-- > 0) { - int ret = send(sock, send_buf + total_sent, 16 - total_sent, MSG_NOSIGNAL); - if (ret <= 0) { - usleep(10000); - continue; - } - total_sent += ret; - } - if (total_sent != 16) { - setSocketTimeout(sock, 0); - res.ret_code = -2; - res.err_msg = "send header failed"; - return; - } - - // 接收响应(先读 16 字节帧头,再按长度读 JSON) - uint8_t recv_header[16]; - size_t header_read = 0; - while (header_read < 16) { - int ret = recv(sock, recv_header + header_read, 16 - header_read, MSG_NOSIGNAL); - if (ret <= 0) { - setSocketTimeout(sock, 0); - res.ret_code = -2; - res.err_msg = "recv header failed or timeout"; - return; - } - header_read += ret; - } - - // 解析数据区长度(大端 4 字节) - uint32_t data_len = (recv_header[4] << 24) | - (recv_header[5] << 16) | - (recv_header[6] << 8) | - recv_header[7]; - - // 读取数据区(JSON) - std::string json_str; - if (data_len > 0) { - json_str.resize(data_len); - size_t total_read = 0; - while (total_read < data_len) { - int ret = recv(sock, &json_str[total_read], data_len - total_read, MSG_NOSIGNAL); - if (ret <= 0) { - setSocketTimeout(sock, 0); - res.ret_code = -2; - res.err_msg = "recv payload failed or timeout"; - return; - } - total_read += ret; - } - } - setSocketTimeout(sock, 0); - - if (json_str.empty()) { - res.ret_code = -4; - res.err_msg = "empty json data"; - return; - } - - // 容错提取 JSON(可能包含前缀/后缀) - size_t json_start = json_str.find('{'); - if (json_start == std::string::npos) { - res.ret_code = -4; - res.err_msg = "no JSON found in payload"; - return; - } - size_t json_end = json_str.rfind('}'); - if (json_end != std::string::npos) { - json_str = json_str.substr(json_start, json_end - json_start + 1); - } else { - json_str = json_str.substr(json_start) + "}"; - } - - // 解析 JSON - json root; - std::string parse_err; - if (!parseJson(json_str, root, parse_err)) { - res.ret_code = -3; - res.err_msg = "JSON parse error: " + parse_err; - return; - } - - res.ret_code = root.value("ret_code", 0); - res.create_on = root.value("create_on", ""); - res.err_msg = root.value("err_msg", ""); - - LOG(INFO) << "robotTaskPause success, ret_code=" << res.ret_code; - } - - - - - - // -------------------------------------------------------------------------- - // 4010 - robot_config_uploadmap_req 上传地图 - // 功能:将地图文件上传到机器人 - // -------------------------------------------------------------------------- - void AgvSrc1100::uploadMap(UploadMapResult& info, const std::string& map_json) - { - int sock = sock_config_; - if (sock < 0) - { - info.ret_code = -1; - info.err_msg = "socket not connected"; - return; - } - - // 清空残留数据,设置超时 - flushSocket(sock); - setSocketTimeout(sock, 1000); - - // 计算负载长度(地图 JSON 数据) - uint16_t payload_len = static_cast(map_json.size()); - // 命令码 0x0FAA = 4010 - uint8_t send_buf[16] = { - 0x5A, 0x01, 0x00, 0x00, - 0x00, 0x00, - static_cast((payload_len >> 8) & 0xFF), - static_cast(payload_len & 0xFF), - 0x0F, 0xAA, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - }; - - // 发送帧头(带重试) - int totalSent = 0; - int retries = 3; - while (totalSent < 16 && retries-- > 0) - { - int ret = send(sock, send_buf + totalSent, 16 - totalSent, 0); - if (ret <= 0) - { - usleep(10000); - continue; - } - totalSent += ret; - } - if (totalSent != 16) - { - setSocketTimeout(sock, 0); - info.ret_code = -2; - info.err_msg = "send frame header failed"; - return; - } - - // 分片发送地图数据(4096 字节一片) - const int SLICE_SIZE = 4096; - size_t offset = 0; - size_t totalSize = map_json.size(); - const char* data = map_json.c_str(); - - while (offset < totalSize) - { - int sendLen = std::min((size_t)SLICE_SIZE, totalSize - offset); - int ret = send(sock, data + offset, sendLen, 0); - if (ret <= 0) - { - setSocketTimeout(sock, 0); - info.ret_code = -2; - info.err_msg = "send map payload failed"; - return; - } - offset += ret; - usleep(500); // 短暂延时,防止发送过快 - } - - // 接收响应 - const int RECV_BUF_LEN = 65536; - std::vector recvBuf(RECV_BUF_LEN); - std::string rawData; - int recvRet = 0; - while ((recvRet = recv(sock, recvBuf.data(), RECV_BUF_LEN, 0)) > 0) - { - rawData.append(reinterpret_cast(recvBuf.data()), recvRet); - } - setSocketTimeout(sock, 0); - - if (rawData.empty()) - { - info.ret_code = -2; - info.err_msg = "no response data"; - return; - } - - // 提取并解析 JSON - size_t jsonStart = rawData.find('{'); - size_t jsonEnd = rawData.rfind('}'); - std::string json_str; - if (jsonStart == std::string::npos) - { - info.ret_code = -4; - info.err_msg = "json not found"; - return; - } - if (jsonEnd == std::string::npos) - { - json_str = rawData.substr(jsonStart) + "}"; - } - else - { - json_str = rawData.substr(jsonStart, jsonEnd - jsonStart + 1); - } - - json root; - std::string err; - if (!parseJson(json_str, root, err)) - { - info.ret_code = -3; - info.err_msg = "json parse error: " + err; - return; - } - - info.ret_code = root.value("ret_code", 0); - info.create_on = root.value("create_on", ""); - info.err_msg = root.value("err_msg", ""); - } - - - // -------------------------------------------------------------------------- - // 4011 - robot_config_downloadmap_req 下载地图 - // 功能:从机器人下载指定地图文件 - // -------------------------------------------------------------------------- - void AgvSrc1100::downloadMap(DownloadMapResult& info, const std::string& map_name) - { - int sock = sock_config_; - if (sock < 0) - { - info.ret_code = -1; - info.err_msg = "socket not connected"; - return; - } - - // 组装请求 JSON: {"map_name":"xxx"} - std::string req_json = R"({"map_name":")" + map_name + R"("})"; - uint8_t payload_len = static_cast(req_json.size()); - - // 命令码 0x0FAB = 4011 - uint8_t send_buf[16] = { - 0x5A, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, payload_len, - 0x0F, 0xAB, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - }; - - flushSocket(sock); - - // 发送帧头 - if (send(sock, send_buf, 16, MSG_NOSIGNAL) != 16) - { - info.ret_code = -2; - info.err_msg = "send frame header failed"; - return; - } - // 发送 JSON 负载 - if (send(sock, req_json.data(), req_json.size(), MSG_NOSIGNAL) != (ssize_t)req_json.size()) - { - info.ret_code = -2; - info.err_msg = "send payload failed"; - return; - } - - // 接收地图数据(地图文件可能较大,使用 65536 字节缓冲区) - const int RECV_BUF_LEN = 65536; - std::vector recvBuf(RECV_BUF_LEN); - std::string rawData; - int ret = 0; - - setSocketTimeout(sock, 500); // 延长超时适配大地图 - while ((ret = recv(sock, recvBuf.data(), RECV_BUF_LEN, 0)) > 0) - { - rawData.append(reinterpret_cast(recvBuf.data()), ret); - } - setSocketTimeout(sock, 0); - - if (rawData.empty()) - { - info.ret_code = -2; - info.err_msg = "no response data"; - return; - } - - // 提取并解析 JSON - size_t jsonStart = rawData.find('{'); - size_t jsonEnd = rawData.rfind('}'); - std::string json_str; - - if (jsonStart == std::string::npos) - { - info.ret_code = -4; - info.err_msg = "json not found"; - return; - } - if (jsonEnd == std::string::npos) - { - json_str = rawData.substr(jsonStart) + "}"; - } - else - { - json_str = rawData.substr(jsonStart, jsonEnd - jsonStart + 1); - } - - json root; - std::string err; - if (!parseJson(json_str, root, err)) - { - info.ret_code = -3; - info.err_msg = err; - return; - } - - info.map_name = map_name; - info.ret_code = root.value("ret_code", 0); - info.create_on = root.value("create_on", ""); - info.err_msg = root.value("err_msg", ""); - // 成功时保存地图内容 - info.map_content = (0 == info.ret_code) ? json_str : ""; - } - - - // ============================================================================ - // 端口 19210 – 其他 API(允许 5 个连接) - // 功能:外设控制(顶升/货叉/辊筒/音频/IO 等) - // 当前未实现具体方法,预留扩展 - // ============================================================================ - - - // ============================================================================ - // 端口 19301 – 机器人推送 API(允许 10 个连接) - // 功能:接收机器人主动推送的实时状态数据 - // 当前未实现具体方法,预留扩展 - // ============================================================================ - - - - -} \ No newline at end of file diff --git a/cmvr-es/devices/biohead/abstract_biohead.h b/cmvr-es/devices/biohead/abstract_biohead.h index 09b774ff..93e0eb20 100644 --- a/cmvr-es/devices/biohead/abstract_biohead.h +++ b/cmvr-es/devices/biohead/abstract_biohead.h @@ -58,7 +58,6 @@ namespace cmvr::device { // 抽象头部类 class AbstractBiohead : public AbstractDevice { public: - AbstractBiohead() = default; explicit AbstractBiohead(const XmlNode &config) : AbstractDevice(config) {} ~AbstractBiohead() override = default; diff --git a/cmvr-es/devices/biohead/biohead_esp32/include/biohead_esp32.h b/cmvr-es/devices/biohead/biohead_esp32/include/biohead_esp32.h index 08c0953f..52b34d5d 100644 --- a/cmvr-es/devices/biohead/biohead_esp32/include/biohead_esp32.h +++ b/cmvr-es/devices/biohead/biohead_esp32/include/biohead_esp32.h @@ -7,7 +7,7 @@ #include #include #include -#include "cmvr/config/biohead_config/biohead_config.pb.h" + namespace cmvr::device { struct ServoChannel { @@ -18,7 +18,6 @@ namespace cmvr::device { class BioHeadRobot : public AbstractBiohead { public: explicit BioHeadRobot(const XmlNode &config); - BioHeadRobot(const cmvr::config::BioHeadConfig& config); ~BioHeadRobot() override = default; // 接口实现 @@ -67,7 +66,7 @@ namespace cmvr::device { std::shared_ptr speak_thread_; std::atomic speak_running_{false}; - cmvr::config::BioHeadConfig config_; + }; diff --git a/cmvr-es/devices/biohead/biohead_esp32/src/biohead_esp32.cpp b/cmvr-es/devices/biohead/biohead_esp32/src/biohead_esp32.cpp index 8ff9e3b3..c3b67887 100644 --- a/cmvr-es/devices/biohead/biohead_esp32/src/biohead_esp32.cpp +++ b/cmvr-es/devices/biohead/biohead_esp32/src/biohead_esp32.cpp @@ -26,219 +26,6 @@ BioHeadRobot::BioHeadRobot(const XmlNode &config) : AbstractBiohead(config) { } -BioHeadRobot::BioHeadRobot(const cmvr::config::BioHeadConfig& config) : config_(config) { - // 从config中读取串口参数 - port_name_ = config_.serial(); - - serial_ = std::make_shared(); - if (!serial_->open(port_name_, 115200)) { - LOG(ERROR) << "[BioHeadRobot] Failed to open serial port: " << port_name_; - } else { - serial_->wakeupESP32(port_name_); - LOG(INFO) << "[BioHeadRobot] Serial port opened and ESP32 woken up."; - } - - // 解析眉毛配置 - if (config_.has_eyebrow()) { - const auto& eyebrow = config_.eyebrow(); - std::string serial_str = eyebrow.serial(); - std::string offset_str = eyebrow.offest(); - std::string min_angles_str = eyebrow.jlmtlow(); - std::string max_angles_str = eyebrow.jlmtup(); - - // 解析偏移角 - std::vector offsets; - std::istringstream oss(offset_str); - double val; - while (oss >> val) offsets.push_back(val); - - // 解析角度限制 - std::vector min_angles; - if (!min_angles_str.empty()) { - std::istringstream min_ss(min_angles_str); - while (min_ss >> val) min_angles.push_back(val); - } - - std::vector max_angles; - if (!max_angles_str.empty()) { - std::istringstream max_ss(max_angles_str); - while (max_ss >> val) max_angles.push_back(val); - } - - // 解析 channel,如 64:0~3 - size_t colon = serial_str.find(':'); - if (colon != std::string::npos) { - uint8_t addr = std::stoi(serial_str.substr(0, colon)); - std::string range = serial_str.substr(colon + 1); - size_t tilde = range.find('~'); - if (tilde != std::string::npos) { - int ch_start = std::stoi(range.substr(0, tilde)); - int ch_end = std::stoi(range.substr(tilde + 1)); - int channel_count = ch_end - ch_start + 1; - - // 添加舵机通道 - for (int ch = ch_start; ch <= ch_end; ++ch) { - channels_.push_back({addr, static_cast(ch)}); - } - - // 添加角度限制 - if (min_angles.size() == 1) { - min_angles_.insert(min_angles_.end(), channel_count, min_angles[0]); - } else if (min_angles.size() >= channel_count) { - min_angles_.insert(min_angles_.end(), min_angles.begin(), min_angles.begin() + channel_count); - } else { - min_angles_.insert(min_angles_.end(), channel_count, 0.0); - } - - if (max_angles.size() == 1) { - max_angles_.insert(max_angles_.end(), channel_count, max_angles[0]); - } else if (max_angles.size() >= channel_count) { - max_angles_.insert(max_angles_.end(), max_angles.begin(), max_angles.begin() + channel_count); - } else { - max_angles_.insert(max_angles_.end(), channel_count, 180.0); - } - } - } - } - - // 解析眼睛配置 - if (config_.has_eye()) { - const auto& eye = config_.eye(); - std::string serial_str = eye.serial(); - std::string offset_str = eye.offest(); - std::string min_angles_str = eye.jlmtlow(); - std::string max_angles_str = eye.jlmtup(); - - // 解析偏移角 - std::vector offsets; - std::istringstream oss(offset_str); - double val; - while (oss >> val) offsets.push_back(val); - - // 解析角度限制 - std::vector min_angles; - if (!min_angles_str.empty()) { - std::istringstream min_ss(min_angles_str); - while (min_ss >> val) min_angles.push_back(val); - } - - std::vector max_angles; - if (!max_angles_str.empty()) { - std::istringstream max_ss(max_angles_str); - while (max_ss >> val) max_angles.push_back(val); - } - - // 解析 channel,如 64:4~9 - size_t colon = serial_str.find(':'); - if (colon != std::string::npos) { - uint8_t addr = std::stoi(serial_str.substr(0, colon)); - std::string range = serial_str.substr(colon + 1); - size_t tilde = range.find('~'); - if (tilde != std::string::npos) { - int ch_start = std::stoi(range.substr(0, tilde)); - int ch_end = std::stoi(range.substr(tilde + 1)); - int channel_count = ch_end - ch_start + 1; - - // 添加舵机通道 - for (int ch = ch_start; ch <= ch_end; ++ch) { - channels_.push_back({addr, static_cast(ch)}); - } - - // 添加角度限制 - if (min_angles.size() == 1) { - min_angles_.insert(min_angles_.end(), channel_count, min_angles[0]); - } else if (min_angles.size() >= channel_count) { - min_angles_.insert(min_angles_.end(), min_angles.begin(), min_angles.begin() + channel_count); - } else { - min_angles_.insert(min_angles_.end(), channel_count, 0.0); - } - - if (max_angles.size() == 1) { - max_angles_.insert(max_angles_.end(), channel_count, max_angles[0]); - } else if (max_angles.size() >= channel_count) { - max_angles_.insert(max_angles_.end(), max_angles.begin(), max_angles.begin() + channel_count); - } else { - max_angles_.insert(max_angles_.end(), channel_count, 180.0); - } - } - } - } - - // 解析嘴巴配置 - if (config_.has_mouth()) { - const auto& mouth = config_.mouth(); - std::string serial_str = mouth.serial(); - std::string offset_str = mouth.offest(); - std::string min_angles_str = mouth.jlmtlow(); - std::string max_angles_str = mouth.jlmtup(); - - // 解析偏移角 - std::vector offsets; - std::istringstream oss(offset_str); - double val; - while (oss >> val) offsets.push_back(val); - - // 解析角度限制 - std::vector min_angles; - if (!min_angles_str.empty()) { - std::istringstream min_ss(min_angles_str); - while (min_ss >> val) min_angles.push_back(val); - } - - std::vector max_angles; - if (!max_angles_str.empty()) { - std::istringstream max_ss(max_angles_str); - while (max_ss >> val) max_angles.push_back(val); - } - - // 解析 channel,如 65:0~9 - size_t colon = serial_str.find(':'); - if (colon != std::string::npos) { - uint8_t addr = std::stoi(serial_str.substr(0, colon)); - std::string range = serial_str.substr(colon + 1); - size_t tilde = range.find('~'); - if (tilde != std::string::npos) { - int ch_start = std::stoi(range.substr(0, tilde)); - int ch_end = std::stoi(range.substr(tilde + 1)); - int channel_count = ch_end - ch_start + 1; - - // 添加舵机通道 - for (int ch = ch_start; ch <= ch_end; ++ch) { - channels_.push_back({addr, static_cast(ch)}); - } - - // 添加角度限制 - if (min_angles.size() == 1) { - min_angles_.insert(min_angles_.end(), channel_count, min_angles[0]); - } else if (min_angles.size() >= channel_count) { - min_angles_.insert(min_angles_.end(), min_angles.begin(), min_angles.begin() + channel_count); - } else { - min_angles_.insert(min_angles_.end(), channel_count, 0.0); - } - - if (max_angles.size() == 1) { - max_angles_.insert(max_angles_.end(), channel_count, max_angles[0]); - } else if (max_angles.size() >= channel_count) { - max_angles_.insert(max_angles_.end(), max_angles.begin(), max_angles.begin() + channel_count); - } else { - max_angles_.insert(max_angles_.end(), channel_count, 180.0); - } - } - } - } - - // 确保所有向量大小一致 - if (min_angles_.size() != channels_.size()) { - min_angles_.resize(channels_.size(), 0.0); - } - - if (max_angles_.size() != channels_.size()) { - max_angles_.resize(channels_.size(), 180.0); - } - - current_joints_.resize(channels_.size(), 90.0); - last_joints_ = current_joints_; -} void BioHeadRobot::parseXmlConfig(const XmlNode &node) { diff --git a/cmvr-es/devices/canbus/abstract_canbus.h b/cmvr-es/devices/canbus/abstract_canbus.h index 6fce7f5e..ca0399d0 100644 --- a/cmvr-es/devices/canbus/abstract_canbus.h +++ b/cmvr-es/devices/canbus/abstract_canbus.h @@ -54,7 +54,6 @@ namespace cmvr::device { class AbstractCanbus : public AbstractDevice { public: - AbstractCanbus() = default; AbstractCanbus(const XmlNode &cfg) : AbstractDevice(cfg) {} ~AbstractCanbus() {} /** diff --git a/cmvr-es/devices/canbus/can_client/socket/socket_can_client_raw.cc b/cmvr-es/devices/canbus/can_client/socket/socket_can_client_raw.cc index 28779a06..aef0af2c 100644 --- a/cmvr-es/devices/canbus/can_client/socket/socket_can_client_raw.cc +++ b/cmvr-es/devices/canbus/can_client/socket/socket_can_client_raw.cc @@ -35,17 +35,6 @@ namespace cmvr { // LOG(ERROR) << "[SocketCanClientRaw] ([SocketCanClientRaw]): Failed to parse XML: " << e.what(); } - SocketCanClientRaw::SocketCanClientRaw(const cmvr::config::CanDeviceConfig& config):config_(config) - { - auto channel_id = config_.channelid(); - port_ = static_cast(channel_id); - interface_ = CANCardParameter::NATIVE; - - enable_can_err_check_ = false; - - - } - void SocketCanClientRaw::init() { // if (!parameter.has_channel_id()) { diff --git a/cmvr-es/devices/canbus/can_client/socket/socket_can_client_raw.h b/cmvr-es/devices/canbus/can_client/socket/socket_can_client_raw.h index 04d57c86..3de5b42c 100644 --- a/cmvr-es/devices/canbus/can_client/socket/socket_can_client_raw.h +++ b/cmvr-es/devices/canbus/can_client/socket/socket_can_client_raw.h @@ -26,7 +26,6 @@ #include "gflags/gflags.h" #include "../../abstract_canbus.h" #include "canbus/common/canbus_consts.h" -#include "cmvr/config/robot_config/robot_config.pb.h" namespace cmvr { namespace device { @@ -37,7 +36,6 @@ namespace cmvr { class SocketCanClientRaw final : public AbstractCanbus { public: explicit SocketCanClientRaw(const XmlNode &cfg); - SocketCanClientRaw(const cmvr::config::CanDeviceConfig& config); ~SocketCanClientRaw(); void init() override; void start() override; @@ -75,8 +73,6 @@ namespace cmvr { // bool enable_can_err_check_{false}; - - cmvr::config::CanDeviceConfig config_; }; } } diff --git a/cmvr-es/devices/motor/CMakeLists.txt b/cmvr-es/devices/motor/CMakeLists.txt index 3b7669c9..532924d2 100644 --- a/cmvr-es/devices/motor/CMakeLists.txt +++ b/cmvr-es/devices/motor/CMakeLists.txt @@ -1,5 +1,5 @@ add_subdirectory(ti5_motor) -add_subdirectory(eyou_motor) + # -------------------------------------------------------- # Unit test # -------------------------------------------------------- diff --git a/cmvr-es/devices/motor/abstract_motor.h b/cmvr-es/devices/motor/abstract_motor.h index bb89a415..710f88d2 100644 --- a/cmvr-es/devices/motor/abstract_motor.h +++ b/cmvr-es/devices/motor/abstract_motor.h @@ -41,7 +41,6 @@ namespace cmvr::device{ } JointStatus; public: - AbstractMotor() = default; explicit AbstractMotor(const XmlNode &config): AbstractDevice(config) {} AbstractMotor(const XmlNode &config,uint8_t node_id): AbstractDevice(config) ,node_id_(node_id) {} ~AbstractMotor() override = default; diff --git a/cmvr-es/devices/motor/eyou_motor/CMakeLists.txt b/cmvr-es/devices/motor/eyou_motor/CMakeLists.txt deleted file mode 100644 index 9f00953a..00000000 --- a/cmvr-es/devices/motor/eyou_motor/CMakeLists.txt +++ /dev/null @@ -1,21 +0,0 @@ -add_library(eyoumotor SHARED - ${CMAKE_CURRENT_SOURCE_DIR}/ethercat/protocol/ec_xml_parser.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/ethercat/protocol/ec_master.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/ethercat/eyou_motor_ethercat_protocol.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/eyou_motor.cpp - -) - - -target_include_directories(eyoumotor PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) - -add_library(cmvr_es::device::eyoumotor ALIAS eyoumotor) - -target_link_libraries(eyoumotor - PRIVATE - -lsoem - protobuf - glog -) - -install(TARGETS eyoumotor LIBRARY DESTINATION lib) \ No newline at end of file diff --git a/cmvr-es/devices/motor/eyou_motor/ethercat/eyou_motor_ethercat_protocol.cpp b/cmvr-es/devices/motor/eyou_motor/ethercat/eyou_motor_ethercat_protocol.cpp deleted file mode 100644 index 26636505..00000000 --- a/cmvr-es/devices/motor/eyou_motor/ethercat/eyou_motor_ethercat_protocol.cpp +++ /dev/null @@ -1,747 +0,0 @@ -// -// Created by cmvr on 2026/3/30. -// - -#include "eyou_motor_ethercat_protocol.h" -#include "canbus/canopen/register.h" -#include "motor/ti5_motor/canopen/protocol/ti5_motor_tpdo1.h" -#include "motor/ti5_motor/canopen/protocol/ti5_motor_tpdo2.h" -#include "motor/ti5_motor/canopen/protocol/ti5_motor_sdo_response.h" -#include "canbus/canopen/nmt_response_protocol.h" - -#include -#include -#include - -using namespace cmvr::device; - -// 辅助函数:获取操作模式字符串 -static const char* getOpModeString(int8_t mode) { - switch (mode) { - case 1: return "Profile Position (PP)"; - case 2: return "Velocity (VL)"; - case 3: return "Profile Velocity (PV)"; - case 4: return "Profile Torque (PT)"; - case 6: return "Homing (HM)"; - case 8: return "Cyclic Sync Position (CSP)"; - case 9: return "Cyclic Sync Velocity (CSV)"; - case 10: return "Cyclic Sync Torque (CST)"; - default: return "Unknown"; - } -} - -// 构造函数 -EYouEthercatProtocol::EYouEthercatProtocol(std::string ifName, const std::string& xmlPath) - : interface_name_(ifName), - xml_config_path_(xmlPath), - initialized_(false), - slave_count_(0), - use_fixed_pdo_(xmlPath.empty()) { - - comm_proto = CommProto::ETHERCAT; - - try { - // 初始化message_manager - message_manager_ = std::make_shared>(); - - // 创建 EcMaster 实例 - master_ = std::make_unique(); - - // 初始化主站 - int cycleTimeUs = 1000; // 1ms周期 - int slaveCount = master_->init(interface_name_.c_str(), cycleTimeUs); - - if (slaveCount < 0) { - throw std::runtime_error("[EYouEthercatProtocol] master init failed"); - } - - slave_count_ = slaveCount; - - EcSlaveConfig config = createFixedSlaveConfig(1); - std::vector rxPdos = config.availableRxPdos; - std::vector txPdos = config.availableTxPdos; - - if (rxPdos.empty() || txPdos.empty()) { - LOG(ERROR) << "[EYouEthercatProtocol] No available PDOs found\n"; - throw std::runtime_error("[EYouEthercatProtocol] No available PDOs found "); - } - const EcPdo* rxPdo = &config.modules[0].rxPdos[0]; - const EcPdo* txPdo = &config.modules[0].txPdos[0]; - for (int slaveId = 1; slaveId <= slave_count_; slaveId++) { - if (master_->configurePdo(slaveId, rxPdo, txPdo) != 0) { - throw std::runtime_error("[EYouEthercatProtocol] PDO configuration failed for slave " + std::to_string(slaveId)); - } - } - master_->setupMappingAndDc(); - - // 等待配置生效 - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - // 进入OP状态 - if (master_->gotoOperational() != 0) { - throw std::runtime_error("[EYouEthercatProtocol] OPERATIONAL state failed"); - } - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - if (!master_->isOperational()) { - LOG(WARNING) << "[EYouEthercatProtocol] Master reports not operational"; - } else { - LOG(INFO) << "[EYouEthercatProtocol] EtherCAT in OPERATIONAL state"; - } - - // 初始化从站状态 - for (int i = 1; i <= slave_count_; i++) { - node_states_[i] = NodeState(); - uint16_t status = master_->getStatusWord(i); - LOG(INFO) << "[EYouEthercatProtocol] Slave " << i << " initial status word: 0x" << std::hex << status << std::dec; - } - - initialized_ = true; - LOG(INFO) << "[EYouEthercatProtocol] Initialized successfully"; - - // 添加NMT响应协议 - for (int i = 1; i <= slave_count_; i++) { - message_manager_->AddRecvProtocolData, false>(i); - } - - } catch (const std::exception& e) { - LOG(ERROR) << "[EYouEthercatProtocol] Constructor failed: " << e.what(); - throw std::runtime_error(e.what()); - } -} - -// 析构函数 -EYouEthercatProtocol::~EYouEthercatProtocol() { - if (master_) { - // 失能所有电机 - for (auto& node : node_states_) { - if (node.second.enabled) { - master_->disable(node.first); - LOG(INFO) << "[EYouEthercatProtocol] Disabled motor " << node.first; - } - } - - master_->shutdown(); - LOG(INFO) << "[EYouEthercatProtocol] Master shutdown successfully"; - } -} - -// 执行电机使能序列 -bool EYouEthercatProtocol::performEnableSequence(uint8_t node_id) { - if (node_id > slave_count_) { - LOG(ERROR) << "[EYouEthercatProtocol] Node " << node_id << " out of range"; - return false; - } - // 读取状态字 - uint16_t status = master_->getStatusWord(node_id); - LOG(INFO) << "[EYouEthercatProtocol] Node " << node_id << " status: 0x" << std::hex << status << std::dec; - - // 如果处于故障状态,先复位 - if (status & 0x0008) { - LOG(INFO) << "[EYouEthercatProtocol] Fault detected, resetting..."; - master_->faultReset(node_id); - std::this_thread::sleep_for(std::chrono::milliseconds(200)); - status = master_->getStatusWord(node_id); - } - - // 如果处于 Switch On Disabled 状态,执行使能序列 - if (status & 0x0040) { - LOG(INFO) << "[EYouEthercatProtocol] Node " << (int)node_id << " in Switch On Disabled, enabling..."; - - // 获取当前位置并同步目标位置(防止抖动) - int32_t actualPos = master_->getActualPosition(node_id); - master_->setTargetPosition(node_id, actualPos); - master_->setModeOfOperation(node_id,EcMaster::EcOperationMode_ProfilePosition); - // Shutdown - master_->setControlWord(node_id, 0x06); - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - - // Switch On - master_->setControlWord(node_id, 0x07); - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - - // Enable Operation - master_->setControlWord(node_id, 0x0F); - std::this_thread::sleep_for(std::chrono::milliseconds(200)); - - status = master_->getStatusWord(node_id); - } - - // 验证使能成功 - bool enabled = (status & 0x0004) != 0; - if (enabled) { - LOG(INFO) << "[EYouEthercatProtocol] Node " << node_id << " enabled successfully"; - } else { - LOG(ERROR) << "[EYouEthercatProtocol] Node " << node_id << " enable failed, status: 0x" - << std::hex << status << std::dec; - } - - return enabled; -} - -// 操作模式转换 (msgs::RunMode -> EcOperationMode) -int8_t EYouEthercatProtocol::runModeToEcMode(msgs::RunMode mode) { - switch (mode) { - case msgs::RUN_MODE_PROFILE_POSITION: - return EcMaster::EcOperationMode_ProfilePosition; // 1 - - case msgs::RUN_MODE_PROFILE_VELOCITY: - return EcMaster::EcOperationMode_ProfileVelocity; // 3 - - case msgs::RUN_MODE_TORQUE: // 需要确认是否有这个枚举 - return EcMaster::EcOperationMode_ProfileTorque; // 4 - - case msgs::RUN_MODE_INTERPOLATED_POSITION: - return EcMaster::EcOperationMode_InterpolatedPosition; // 7 - - case msgs::RUN_MODE_CYCLIC_SYNC_POSITION: - return EcMaster::EcOperationMode_CyclicSyncPosition; // 8 - - case msgs::RUN_MODE_CYCLIC_SYNC_VELOCITY: - return EcMaster::EcOperationMode_CyclicSyncVelocity; // 9 - - case msgs::RUN_MODE_CYCLIC_SYNC_CURRENT: // 名称统一 - return EcMaster::EcOperationMode_CyclicSyncTorque; // 10 - - default: - return 0; - } -} - -// 操作模式转换 (EcOperationMode -> msgs::RunMode) -cmvr::msgs::RunMode EYouEthercatProtocol::ecModeToRunMode(int8_t mode) { - switch (mode) { - case EcMaster::EcOperationMode_ProfilePosition: - return msgs::RUN_MODE_PROFILE_POSITION; - - case EcMaster::EcOperationMode_ProfileVelocity: - return msgs::RUN_MODE_PROFILE_VELOCITY; - - case EcMaster::EcOperationMode_ProfileTorque: - return msgs::RUN_MODE_TORQUE; // 需要确认枚举存在 - - case EcMaster::EcOperationMode_InterpolatedPosition: - return msgs::RUN_MODE_INTERPOLATED_POSITION; - - case EcMaster::EcOperationMode_CyclicSyncPosition: - return msgs::RUN_MODE_CYCLIC_SYNC_POSITION; - - case EcMaster::EcOperationMode_CyclicSyncVelocity: - return msgs::RUN_MODE_CYCLIC_SYNC_VELOCITY; - - case EcMaster::EcOperationMode_CyclicSyncTorque: - return msgs::RUN_MODE_CYCLIC_SYNC_CURRENT; - - default: - return msgs::RUN_MODE_UNSPECIFIED; - } -} - -// 初始化节点 -bool EYouEthercatProtocol::initNode(uint8_t node_id) { - try { - if (node_id > slave_count_) { - LOG(ERROR) << "[EYouEthercatProtocol] Node " << node_id << " out of range (max: " << slave_count_ << ")"; - return false; - } - - // 执行使能序列 - if (!performEnableSequence(node_id)) { - LOG(ERROR) << "[EYouEthercatProtocol] Failed to enable node " << node_id; - return false; - } - - node_states_[node_id].initialized = true; - node_states_[node_id].enabled = true; - node_states_[node_id].current_mode = msgs::RUN_MODE_PROFILE_POSITION; - - LOG(INFO) << "[EYouEthercatProtocol] Node " << node_id << " initialized successfully"; - return true; - - } catch (const std::exception& e) { - LOG(ERROR) << "[EYouEthercatProtocol] Failed to init node " << node_id << ": " << e.what(); - return false; - } -} - -// 设置运行模式 -void EYouEthercatProtocol::setMode(uint8_t node_id, msgs::RunMode mode) { - - - int8_t ec_mode = runModeToEcMode(mode); - master_->setModeOfOperation(node_id, ec_mode); - - // 验证模式设置 - std::this_thread::sleep_for(std::chrono::milliseconds(20)); - int8_t actual_mode = master_->getModeOfOperationDisplay(node_id); - - if (actual_mode == ec_mode) { - node_states_[node_id].current_mode = mode; - LOG(INFO) << "[EYouEthercatProtocol] Node " << (int)node_id << " mode set to " - << getOpModeString(ec_mode); - } else { - LOG(WARNING) << "[EYouEthercatProtocol] Node " << node_id << " mode set failed, " - << "expected " << getOpModeString(ec_mode) - << ", got " << getOpModeString(actual_mode); - } -} - -// 设置目标位置和速度 -void EYouEthercatProtocol::setTarget(uint8_t node_id, double angle_rad, double vel) { - auto pos_cmd = angleToPos(angle_rad); - auto speed = static_cast(std::abs(velToSpeed(vel))); - - msgs::RunMode current_mode = getMode(node_id); - - switch (current_mode) { - case msgs::RUN_MODE_PROFILE_POSITION: { - // 先设置轮廓速度 - master_->setProfileVelocity(node_id, speed); - // 设置目标位置 - master_->setTargetPosition(node_id, pos_cmd); - node_states_[node_id].target_position = pos_cmd; - - // 构建控制字:使能状态 + 立即更新标志 - uint16_t ctlword = 0x0F | 0x20; // 0x0F使能, 0x20立即更新 - - // 触发位置更新 - master_->setControlWord(node_id, ctlword); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - - // 启动新位置命令 - master_->setControlWord(node_id, ctlword | 0x10); - break; - } - case msgs::RUN_MODE_CYCLIC_SYNC_POSITION: - master_->setTargetPosition(node_id, pos_cmd); - node_states_[node_id].target_position = pos_cmd; - break; - default: - LOG(WARNING) << "[EYouEthercatProtocol] setTarget(pos,vel) called in unsupported mode for node " << node_id; - break; - } -} - -// 设置目标速度 -void EYouEthercatProtocol::setTarget(uint8_t node_id, double vel) { - auto speed = static_cast(velToSpeed(vel)); - - - - msgs::RunMode current_mode = getMode(node_id); - - switch (current_mode) { - case msgs::RUN_MODE_VELOCITY: - case msgs::RUN_MODE_PROFILE_VELOCITY: - case msgs::RUN_MODE_CYCLIC_SYNC_VELOCITY: - master_->setTargetVelocity(node_id, speed); - break; - default: - LOG(WARNING) << "[EYouEthercatProtocol] setTarget(vel) called in unsupported mode for node " << node_id; - break; - } -} - -// 设置目标位置 -void EYouEthercatProtocol::setQ(uint8_t node_id, double angle_rad) { - auto pos_cmd = angleToPos(angle_rad); - - - - msgs::RunMode current_mode = getMode(node_id); - - switch (current_mode) { - case msgs::RUN_MODE_CYCLIC_SYNC_POSITION: - { - master_->setTargetPosition(node_id, pos_cmd); - node_states_[node_id].target_position = pos_cmd; - break; - } - case msgs::RUN_MODE_PROFILE_POSITION: - { - // 1. 设置目标位置 - master_->setTargetPosition(node_id, pos_cmd); - node_states_[node_id].target_position = pos_cmd; - - // 2. 构建控制字:使能状态 + 立即更新标志 - uint16_t ctlword = 0x0F | 0x20; // 0x0F使能, 0x20立即更新 - - // 3. 触发位置更新 - master_->setControlWord(node_id, ctlword); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - - // 4. 启动新位置命令 - master_->setControlWord(node_id, ctlword | 0x10); - break; - } - default: - { - LOG(WARNING) << "[EYouEthercatProtocol] setQ called in unsupported mode for node " << node_id; - break; - } - } -} - -// 设置位置限制 -void EYouEthercatProtocol::setLimitQ(uint8_t node_id, double ub, double lb) { - - - int32_t lb_val = angleToPos(lb); - int32_t ub_val = angleToPos(ub); - - // 开启软件位置限制 (0x4C494D54 = "LIMT") - uint32_t limitEnable = 0x4C494D54; - if (master_->writeSdo(node_id, 0x2003, 0x00, &limitEnable, EcDataType::Uint32, 100) != 0) { - LOG(ERROR) << "[EYouEthercatProtocol] Failed to enable software position limit for node " << node_id; - } - - // 设置软件位置限制 - if (master_->writeSdo(node_id, 0x607D, 0x01, &lb_val, EcDataType::Int32, 100) != 0) { - LOG(ERROR) << "[EYouEthercatProtocol] Failed to set lower position limit for node " << node_id; - } - - if (master_->writeSdo(node_id, 0x607D, 0x02, &ub_val, EcDataType::Int32, 100) != 0) { - LOG(ERROR) << "[EYouEthercatProtocol] Failed to set upper position limit for node " << node_id; - } - - LOG(INFO) << "[EYouEthercatProtocol] Position limits set for node " << node_id - << ": [" << lb << ", " << ub << "] rad"; -} - -// 设置速度限制 -void EYouEthercatProtocol::setLimitQd(uint8_t node_id, double qd) { - - - auto speed = static_cast(std::abs(velToSpeed(qd))); - - // 设置最大速度限制 - if (master_->writeSdo(node_id, 0x607F, 0x00, &speed, EcDataType::Uint32, 100) != 0) { - LOG(ERROR) << "[EYouEthercatProtocol] Failed to set max speed limit for node " << node_id; - } - - if (master_->writeSdo(node_id, 0x6081, 0x00, &speed, EcDataType::Uint32, 100) != 0) { - LOG(ERROR) << "[EYouEthercatProtocol] Failed to set profile speed for node " << node_id; - } - - LOG(INFO) << "[EYouEthercatProtocol] Speed limit set for node " << node_id << ": " << qd << " rad/s"; -} - -// 设置加速度限制 -void EYouEthercatProtocol::setLimitQdd(uint8_t node_id, double u_qdd, double l_qdd) { - - - // u_qdd: rad/s² (最大加速度) - // l_qdd: rad/s² (最大减速度) - // 转换为电机加速度(脉冲/秒²) - // 圈数/秒² = 弧度/秒² / 2π - // 脉冲/秒² = 圈数/秒² * GearRatio * PULSE_PER_REV - auto accel = static_cast((std::abs(u_qdd) / (2.0 * M_PI)) * GearRatio * PULSE_PER_REV); - auto decel = static_cast((std::abs(l_qdd) / (2.0 * M_PI)) * GearRatio * PULSE_PER_REV); - - // 通过SDO写入加速度和减速度限制 - if (master_->writeSdo(node_id, 0x6083, 0x00, &accel, EcDataType::Uint32, 100) != 0) { - LOG(ERROR) << "[EYouEthercatProtocol] Failed to set acceleration limit for node " << node_id; - } - - if (master_->writeSdo(node_id, 0x6084, 0x00, &decel, EcDataType::Uint32, 100) != 0) { - LOG(ERROR) << "[EYouEthercatProtocol] Failed to set deceleration limit for node " << node_id; - } - - LOG(INFO) << "[EYouEthercatProtocol] Acceleration limits set for node " << node_id - << ": accel=" << u_qdd << ", decel=" << l_qdd << " rad/s²"; -} - -// 零位校准 -bool EYouEthercatProtocol::calibrateZeroQ(uint8_t node_id) { - - - try { - int32_t pos; - int32_t offset = 0; - int32_t rev = 0; - - // 读取当前位置 - if (master_->readSdo(node_id, 0x6064, 0x00, &pos, EcDataType::Int32, 20000) != 0) { - LOG(ERROR) << "[EYouEthercatProtocol] read actual position failed"; - return false; - } - LOG(INFO) << "[EYouEthercatProtocol] Actual position: " << pos; - - // 读取home offset - if (master_->readSdo(node_id, 0x607C, 0x00, &offset, EcDataType::Int32, 20000) != 0) { - LOG(ERROR) << "[EYouEthercatProtocol] read home offset failed"; - return false; - } - LOG(INFO) << "[EYouEthercatProtocol] Home offset before: " << offset; - - // 先将home offset设为0 - offset = 0; - if (master_->writeSdo(node_id, 0x607C, 0x00, &offset, EcDataType::Int32, 20000) != 0) { - LOG(ERROR) << "[EYouEthercatProtocol] write home offset 0 failed"; - return false; - } - LOG(INFO) << "[EYouEthercatProtocol] Home offset set to 0"; - - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - - // 读取实际位置 - if (master_->readSdo(node_id, 0x6064, 0x00, &pos, EcDataType::Int32, 20000) != 0) { - LOG(ERROR) << "[EYouEthercatProtocol] read actual position after offset reset failed"; - return false; - } - LOG(INFO) << "[EYouEthercatProtocol] Actual position after offset reset: " << pos; - - // 获取轴转数,超圈处理 - if (master_->readSdo(node_id, 0x6091, 0x02, &rev, EcDataType::Int32, 20000) != 0) { - LOG(ERROR) << "[EYouEthercatProtocol] read gear ratio shaft revolution failed"; - return false; - } - LOG(INFO) << "[EYouEthercatProtocol] Gear ratio shaft revolution: " << rev; - - // 处理超出编码器范围的情况 - long long r = rev; - int temp = static_cast(r / 2); - if (pos > temp || pos < -temp) { - pos = pos % static_cast(r); - if (pos > temp || pos < -temp) { - if (pos > temp) { - pos = pos - static_cast(r); - } else if (pos < -temp) { - pos = pos + static_cast(r); - } - } - LOG(WARNING) << "[EYouEthercatProtocol] Position out of encoder range, adjusted to " << pos; - } - - // 写home offset偏移 - offset = -pos; - if (master_->writeSdo(node_id, 0x607C, 0x00, &offset, EcDataType::Int32, 20000) != 0) { - LOG(ERROR) << "[EYouEthercatProtocol] write home offset failed"; - return false; - } - LOG(INFO) << "[EYouEthercatProtocol] Home offset set to: " << offset; - - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - - // 读取实际位置 - if (master_->readSdo(node_id, 0x6064, 0x00, &pos, EcDataType::Int32, 20000) != 0) { - LOG(ERROR) << "[EYouEthercatProtocol] read actual position after calibration failed"; - return false; - } - LOG(INFO) << "[EYouEthercatProtocol] Actual position after calibration: " << pos; - - // 保存参数 - uint32_t saveValue = 0x65766173; // "evas" - if (master_->writeSdo(node_id, 0x1010, 0x01, &saveValue, EcDataType::Uint32, 20000) != 0) { - LOG(ERROR) << "[EYouEthercatProtocol] save parameters failed"; - return false; - } - LOG(INFO) << "[EYouEthercatProtocol] Parameters saved"; - - LOG(INFO) << "[EYouEthercatProtocol] Motor " << node_id << " calibrated successfully"; - return true; - - } catch (const std::exception& e) { - LOG(ERROR) << "[EYouEthercatProtocol] Calibration failed for node " << node_id << ": " << e.what(); - return false; - } -} - -// 刹车(快速停机) -void EYouEthercatProtocol::brake(uint8_t node_id) { - - - // 发送快速停机命令 - uint16_t ctlword = 0x0002; // Quick Stop - master_->setControlWord(node_id, ctlword); - - LOG(INFO) << "[EYouEthercatProtocol] Brake (quick stop) applied to motor " << node_id; -} - -// 检查是否到达目标位置 -bool EYouEthercatProtocol::reachedTargetQ(uint8_t node_id) { - uint16_t status_word = getStatusWord(node_id); - // 检查状态字的bit 10(目标到达位) - return (status_word & 0x0400) != 0; -} - -// 获取当前位置 -double EYouEthercatProtocol::getQ(uint8_t node_id) { - - int32_t position = master_->getActualPosition(node_id); - return posToAngle(position); -} - -// 获取当前速度 -double EYouEthercatProtocol::getQd(uint8_t node_id) { - - int32_t velocity = master_->getActualVelocity(node_id); - return speedToVel(velocity); -} - -// 设置速度 -void EYouEthercatProtocol::setQd(uint8_t node_id, double qd) { - auto speed = static_cast(std::abs(velToSpeed(qd))); - - - - msgs::RunMode current_mode = getMode(node_id); - - switch (current_mode) { - case msgs::RUN_MODE_CYCLIC_SYNC_POSITION: - case msgs::RUN_MODE_PROFILE_POSITION: - master_->setProfileVelocity(node_id, speed); - last_Qd_[node_id] = speed; - break; - case msgs::RUN_MODE_PROFILE_VELOCITY: - case msgs::RUN_MODE_CYCLIC_SYNC_VELOCITY: - master_->setTargetVelocity(node_id, static_cast(speed)); - break; - default: - LOG(WARNING) << "[EYouEthercatProtocol] setQd called in unsupported mode for node " << node_id; - break; - } -} - -// 设置加速度 -void EYouEthercatProtocol::setQdd(uint8_t node_id, double qdd) { - // qdd: rad/s² (加速度) - // 转换为电机加速度(脉冲/秒²) - uint32_t accel = static_cast((std::abs(qdd) / (2.0 * M_PI)) * GearRatio * PULSE_PER_REV); - - if (last_Qdd_[node_id] != accel) { - - master_->setProfileAcceleration(node_id, accel); - master_->setProfileDeceleration(node_id, accel); - last_Qdd_[node_id] = accel; - } -} - -// 力矩关闭 -void EYouEthercatProtocol::torqueOff(uint8_t node_id) { - - auto ret = master_->disable(node_id); - node_states_[node_id].enabled = false; - LOG(INFO) << "[EYouEthercatProtocol] Torque off for motor " << node_id << ", ret = " << ret; -} - -// 获取运行模式 -cmvr::msgs::RunMode EYouEthercatProtocol::getMode(uint8_t node_id) { - - int8_t mode = master_->getModeOfOperationDisplay(node_id); - return ecModeToRunMode(mode); -} - -// 配置轮廓参数 -void EYouEthercatProtocol::configProfile(uint8_t node_id, uint32_t speed, uint32_t accel, uint32_t decel) { - - master_->setProfileVelocity(node_id, speed); - master_->setProfileAcceleration(node_id, accel); - master_->setProfileDeceleration(node_id, decel); - LOG(INFO) << "[EYouEthercatProtocol] Profile configured for node " << node_id - << ": speed=" << speed << ", accel=" << accel << ", decel=" << decel; -} - -// 获取力矩 -double EYouEthercatProtocol::getTorque(uint8_t node_id) { - - int16_t torque = master_->getActualTorque(node_id); - // 返回实际力矩值(取决于具体驱动器,这里假设是额定力矩的倍数) - return torque / 1000.0; -} - -// 获取状态字 -uint16_t EYouEthercatProtocol::getStatusWord(uint8_t node_id) { - - uint16_t status_word = master_->getStatusWord(node_id); - node_states_[node_id].status_word = status_word; - return status_word; -} - -// 检查电机是否使能 -bool EYouEthercatProtocol::isEnabled(uint8_t node_id) { - uint16_t status_word = getStatusWord(node_id); - // 检查状态字的bit 2(操作使能)和bit 3(故障) - return (status_word & 0x000C) == 0x0008; -} - -// 检查是否有故障 -bool EYouEthercatProtocol::isFault(uint8_t node_id) { - uint16_t status_word = getStatusWord(node_id); - return (status_word & 0x0008) != 0; -} - -// 故障复位 -void EYouEthercatProtocol::faultReset(uint8_t node_id) { - if (isFault(node_id)) { - - if (master_->faultReset(node_id) == 0) { - LOG(INFO) << "[EYouEthercatProtocol] Fault reset for motor " << node_id; - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } else { - LOG(ERROR) << "[EYouEthercatProtocol] Failed to reset fault for motor " << node_id; - } - } -} - -// 等待目标到达 -bool EYouEthercatProtocol::waitForTargetReached(uint8_t node_id, int timeout_ms) { - return waitUntil([this, node_id]() { return reachedTargetQ(node_id); }, timeout_ms); -} - -// 获取RobotDetail数据 -std::unique_ptr EYouEthercatProtocol::GetRobotDetail() { - if (message_manager_) { - auto data_ptr = std::make_unique(); - message_manager_->GetSensorData(data_ptr.get()); - return data_ptr; - } - return std::make_unique(); -} - -// 辅助函数实现(弧度制版本) -double EYouEthercatProtocol::posToAngle(int32_t pos) const { - // 脉冲数 -> 弧度 - // 圈数 = 脉冲数 / (GearRatio * PULSE_PER_REV) - // 弧度 = 圈数 * 2π - return (pos * 2.0 * M_PI) / (GearRatio * PULSE_PER_REV); -} - -int32_t EYouEthercatProtocol::angleToPos(double angle) const { - // 弧度 -> 脉冲数 - // 圈数 = 弧度 / 2π - // 脉冲数 = 圈数 * GearRatio * PULSE_PER_REV - return static_cast((angle / (2.0 * M_PI)) * GearRatio * PULSE_PER_REV); -} - -double EYouEthercatProtocol::velToSpeed(double vel) const { - // vel: rad/s (角速度) - // 输出:电机速度(脉冲/秒) - // 圈数/秒 = vel / 2π - // 脉冲/秒 = 圈数/秒 * GearRatio * PULSE_PER_REV - return (vel / (2.0 * M_PI)) * GearRatio * PULSE_PER_REV; -} - -double EYouEthercatProtocol::speedToVel(int32_t speed) const { - // speed: 电机速度(脉冲/秒) - // 输出:rad/s - // 圈数/秒 = speed / (GearRatio * PULSE_PER_REV) - // 弧度/秒 = 圈数/秒 * 2π - return (speed * 2.0 * M_PI) / (GearRatio * PULSE_PER_REV); -} - -bool EYouEthercatProtocol::waitUntil(std::function condition, int timeout_ms) { - auto start = std::chrono::steady_clock::now(); - while (!condition()) { - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - if (std::chrono::steady_clock::now() - start > std::chrono::milliseconds(timeout_ms)) { - return false; - } - } - return true; -} - -void EYouEthercatProtocol::updateNodeState(uint8_t node_id) { - - // 更新状态字 - node_states_[node_id].status_word = master_->getStatusWord(node_id); - // 可以根据需要更新其他状态信息 -} \ No newline at end of file diff --git a/cmvr-es/devices/motor/eyou_motor/ethercat/eyou_motor_ethercat_protocol.h b/cmvr-es/devices/motor/eyou_motor/ethercat/eyou_motor_ethercat_protocol.h deleted file mode 100644 index 2b14279c..00000000 --- a/cmvr-es/devices/motor/eyou_motor/ethercat/eyou_motor_ethercat_protocol.h +++ /dev/null @@ -1,125 +0,0 @@ -// -// Created by cmvr on 2026/3/30. -// - -#pragma once -#include "cmvr/msgs/motor.pb.h" -#include "motor/motor_protocol_interface.h" -#include "../../../canbus/abstract_canbus.h" -#include "canbus/can_comm/can_receiver.h" -#include "canbus/can_comm/can_sender.h" -#include "canbus/can_comm/message_manager.h" -#include "cmvr/msgs/error_code.pb.h" -#include "cmvr/msgs/robot_detail.pb.h" - -// 替换原有的 eu_ethercat 头文件 -#include "protocol/ec_master.h" -#include "protocol/ec_xml_parser.h" -#include "protocol/ec_pdo_config.h" - -#include -#include -#include -#include -#include -#include -#include - -namespace cmvr { - namespace device { - - // 节点状态结构体 - struct NodeState { - bool enabled = false; - bool initialized = false; - msgs::RunMode current_mode = msgs::RUN_MODE_UNSPECIFIED; - uint16_t status_word = 0; - int32_t position_offset = 0; - int32_t target_position = 0; - }; - - class EYouEthercatProtocol : public MotorProtocolInterface { - public: - explicit EYouEthercatProtocol(std::string ifName, const std::string& xmlPath = ""); - ~EYouEthercatProtocol() override; - - // 基础接口实现 - bool initNode(uint8_t node_id) override; - void setMode(uint8_t node_id, msgs::RunMode mode) override; - void setTarget(uint8_t node_id, double angle_rad, double vel) override; - void setTarget(uint8_t node_id, double vel) override; - void setQ(uint8_t node_id, double angle_rad) override; - void setLimitQ(uint8_t node_id, double ub, double lb) override; - void setLimitQd(uint8_t node_id, double qd) override; - void setLimitQdd(uint8_t node_id, double u_qdd, double l_qdd) override; - bool calibrateZeroQ(uint8_t node_id) override; - void brake(uint8_t node_id) override; - bool reachedTargetQ(uint8_t node_id) override; - - double getQ(uint8_t node_id) override; - double getQd(uint8_t node_id) override; - - void setQd(uint8_t node_id, double qd) override; - void setQdd(uint8_t node_id, double qdd) override; - - void torqueOff(uint8_t node_id) override; - msgs::RunMode getMode(uint8_t node_id) override; - - // 配置接口 - void configProfile(uint8_t node_id, uint32_t speed, uint32_t accel, uint32_t decel); - - // 扩展功能接口 - double getTorque(uint8_t node_id); - uint16_t getStatusWord(uint8_t node_id); - bool isEnabled(uint8_t node_id); - bool isFault(uint8_t node_id); - void faultReset(uint8_t node_id); - bool waitForTargetReached(uint8_t node_id, int timeout_ms); - - // 数据获取接口 - std::unique_ptr GetRobotDetail(); - - int getSlaveCount() const { return slave_count_; } - - private: - // 常量定义 - static constexpr double GearRatio = 101.0; - static constexpr double RADTODEG = 180.0 / M_PI; - static constexpr double PULSE_PER_REV = 65536.0; - - // 辅助函数 - double posToAngle(int32_t pos) const; - int32_t angleToPos(double angle) const; - double velToSpeed(double vel) const; - double speedToVel(int32_t speed) const; - - bool waitUntil(std::function condition, int timeout_ms); - void updateNodeState(uint8_t node_id); - bool performEnableSequence(uint8_t node_id); - - int8_t runModeToEcMode(msgs::RunMode mode); - msgs::RunMode ecModeToRunMode(int8_t mode); - - // 成员变量 - std::unordered_map node_states_; - std::unordered_map last_Qd_; - std::unordered_map last_Qdd_; - - std::shared_ptr> can_sender_; - std::shared_ptr> message_manager_; - - std::string interface_name_; - std::string xml_config_path_; - bool initialized_; - int slave_count_; - - std::unique_ptr master_; - std::unique_ptr parser_; - EcSlaveConfig slave_config_; - bool use_fixed_pdo_; - - std::mutex master_mutex_; - }; - - } // namespace device -} // namespace cmvr \ No newline at end of file diff --git a/cmvr-es/devices/motor/eyou_motor/ethercat/protocol/ec_master.cpp b/cmvr-es/devices/motor/eyou_motor/ethercat/protocol/ec_master.cpp deleted file mode 100644 index afac1e5c..00000000 --- a/cmvr-es/devices/motor/eyou_motor/ethercat/protocol/ec_master.cpp +++ /dev/null @@ -1,1143 +0,0 @@ -// ec_master.cpp -#include "ec_master.h" -#include - -static float pgain = 0.01f; -static float igain = 0.00002f; -static int64_t syncoffset = 500000; -static int64_t timeerror = 0; - -static void add_time_ns(ec_timet* ts, int64_t addtime) { - ec_timet addts; - addts.tv_nsec = addtime % 1000000000; - addts.tv_sec = (addtime - addts.tv_nsec) / 1000000000; - osal_timespecadd(ts, &addts, ts); -} - -// DC 同步 PI 计算 -static void ec_sync(int64_t reftime, int64_t cycletime, int64_t *offsettime) { - static int64_t integral = 0; - int64_t delta; - delta = (reftime - syncoffset) % cycletime; - if (delta > (cycletime / 2)) { - delta = delta - cycletime; - } - timeerror = -delta; - integral += timeerror; - *offsettime = (int64_t)((timeerror * pgain) + (integral * igain)); -} - -// ==================== 构造和析构 ==================== - -EcMaster::EcMaster() { - memset(&ctx, 0, sizeof(ctx)); - memset(IOmap, 0, sizeof(IOmap)); - - slaveCount = 0; - expectedWKC = 0; - currentgroup = 0; - inOp = false; - isRunning = false; - mappingdone = 0; - dorun = 0; - cyclicWkcErrorCount = 0; - cycleTimeNs = 1000000; - cyclicThread = nullptr; - checkThread = nullptr; - anyPdoDirty = false; -} - -EcMaster::~EcMaster() { - shutdown(); - // shadowBuffers 的析构函数会自动清理内存 -} - -// ==================== 初始化和状态控制 ==================== - -int EcMaster::init(const char* ifName, int cycleTimeUs) { - cycleTimeNs = cycleTimeUs * 1000; - - if (!ecx_init(&ctx, ifName)) { - printf("Failed to init EtherCAT on %s\n", ifName); - return -EC_FAILED_INIT; - } - - ecx_config_init(&ctx); - slaveCount = ctx.slavecount; - - if (slaveCount <= 0) { - printf("No slave found\n"); - return -EC_FAILED_NOSLAVE; - } - - printf("%d slaves found\n", slaveCount); - - isRunning = true; - mappingdone = 0; - dorun = 0; - inOp = false; - cyclicWkcErrorCount = 0; - anyPdoDirty = false; - - osal_thread_create_rt(&cyclicThread, 128000, reinterpret_cast(cyclicThreadFunc), this); - - return slaveCount; -} - -int EcMaster::setState(uint16_t slaveId, uint8_t targetState) { - if (slaveId > (uint16_t)ctx.slavecount) return -1; - - int oldManual = ctx.manualstatechange; - ctx.manualstatechange = 1; - - ctx.slavelist[slaveId].state = targetState; - ecx_writestate(&ctx, slaveId); - - int ret = ecx_statecheck(&ctx, slaveId, targetState, EC_TIMEOUTSTATE); - ctx.manualstatechange = oldManual; - - if (ret != targetState) { - printf("Slave %d failed to enter state %d (current: %d)\n", slaveId, targetState, ret); - return -1; - } - - return 0; -} - -int EcMaster::gotoOperational() { - while (!mappingdone) { - osal_usleep(100); - } - - dorun = 1; - - printf("Waiting for PDO exchange to stabilize...\n"); - osal_usleep(500000); - - // ========== 逐个处理每个从站 ========== - int allOperational = 1; - for (int i = 1; i <= ctx.slavecount; i++) { - printf("\n--- Processing slave %d ---\n", i); - - // 1. 检查并清除错误状态 - if (ctx.slavelist[i].state == (EC_STATE_SAFE_OP + EC_STATE_ERROR)) { - printf(" Clearing error on slave %d\n", i); - ctx.slavelist[i].state = (EC_STATE_SAFE_OP + EC_STATE_ACK); - ecx_writestate(&ctx, i); - osal_usleep(100000); - - // 刷新状态 - ecx_readstate(&ctx); - printf(" Slave %d state after error clear: 0x%02X\n", i, ctx.slavelist[i].state); - } - - // 2. 请求进入 OP 状态 - printf(" Requesting OPERATIONAL state for slave %d...\n", i); - ctx.slavelist[i].state = EC_STATE_OPERATIONAL; - ecx_writestate(&ctx, i); - - // 3. 等待该从站进入 OP 状态 - printf(" Waiting for slave %d to enter OPERATIONAL...\n", i); - - int retry = 0; - const int maxRetry = 200; // 最多等待2秒 (200 * 10ms) - int currentState = 0; - - while (retry < maxRetry) { - osal_usleep(10000); // 10ms - ecx_readstate(&ctx); - currentState = ctx.slavelist[i].state; - - if (currentState == EC_STATE_OPERATIONAL) { - printf(" ✓ Slave %d entered OPERATIONAL after %d ms\n", i, retry * 10); - break; - } - - // 如果状态异常,提前退出 - if (currentState == EC_STATE_INIT || currentState == EC_STATE_PRE_OP) { - printf(" ✗ Slave %d regressed to state 0x%02X\n", i, currentState); - break; - } - - retry++; - } - - // 4. 检查结果 - if (currentState == EC_STATE_OPERATIONAL) { - printf(" ✓ Slave %d is OPERATIONAL\n", i); - } else { - printf(" ✗ Slave %d failed to enter OPERATIONAL, state: 0x%02X\n", i, currentState); - - // 读取错误码 - uint16_t alStatusCode = 0; - int psize = 2; - ecx_SDOread(&ctx, i, 0x0134, 0, FALSE, &psize, &alStatusCode, EC_TIMEOUTRXM); - printf(" AL Status Code: 0x%04X\n", alStatusCode); - - allOperational = 0; - } - } - - // 5. 检查是否所有从站都进入了 OP - if (!allOperational) { - printf("\n✗ Failed to enter OPERATIONAL\n"); - return -1; - } - - printf("\n✓ All slaves are OPERATIONAL\n"); - - // 6. 启动 check 线程 - printf("Starting check thread for runtime monitoring...\n"); - osal_thread_create(&checkThread, 128000, reinterpret_cast(checkThreadFunc), this); - - osal_usleep(200000); - inOp = true; - printf("EtherCAT OPERATIONAL (all slaves)\n"); - return 0; -} - -void EcMaster::shutdown() { - printf("Shutting down...\n"); - - if (inOp) { - inOp = false; - dorun = 0; - osal_usleep(100000); - } - - for (int i = 1; i <= slaveCount; i++) { - printf(" Resetting slave %d to INIT...\n", i); - ctx.slavelist[i].state = EC_STATE_INIT; - ecx_writestate(&ctx, i); - osal_usleep(50000); - ecx_statecheck(&ctx, i, EC_STATE_INIT, 500000); - } - - isRunning = false; - osal_usleep(100000); - - shadowBuffers.clear(); - - printf("Shutdown complete\n"); -} - -// ==================== 影子缓冲区管理 ==================== - -EcMaster::SlaveShadowBuffer* EcMaster::getShadowBuffer(uint16_t slaveId) { - auto it = shadowBuffers.find(slaveId); - if (it != shadowBuffers.end()) { - return &it->second; - } - return nullptr; -} - -void EcMaster::syncSlaveOutputs(uint16_t slaveId) { - auto* shadow = getShadowBuffer(slaveId); - if (!shadow || !shadow->dirty || !shadow->data) return; - - auto outIt = outputs.find(slaveId); - auto sizeIt = outputSizes.find(slaveId); - if (outIt == outputs.end() || outIt->second == nullptr) return; - if (sizeIt == outputSizes.end()) return; - - size_t copySize = std::min(shadow->size, (size_t)sizeIt->second); - memcpy(outIt->second, shadow->data, copySize); - shadow->dirty = false; -} - -void EcMaster::syncAllOutputs() { - if (!anyPdoDirty.load()) return; - - for (auto& pair : shadowBuffers) { - uint16_t slaveId = pair.first; - SlaveShadowBuffer& shadow = pair.second; - - if (!shadow.dirty || !shadow.data) continue; - - auto outIt = outputs.find(slaveId); - auto sizeIt = outputSizes.find(slaveId); - if (outIt == outputs.end() || outIt->second == nullptr) continue; - if (sizeIt == outputSizes.end()) continue; - - size_t copySize = std::min(shadow.size, (size_t)sizeIt->second); - memcpy(outIt->second, shadow.data, copySize); - shadow.dirty = false; - } - - anyPdoDirty = false; -} - -// ==================== 线程函数 ==================== - -OSAL_THREAD_FUNC_RT EcMaster::cyclicThreadFunc(void* arg) { - static_cast(arg)->runCyclic(); -} - -OSAL_THREAD_FUNC EcMaster::checkThreadFunc(void* arg) { - static_cast(arg)->runCheck(); -} - -void EcMaster::runCyclic() { - ec_timet ts; - int ht; - static int64_t toff = 0; - int cycleCount = 0; - int printInterval = 500; - - printf("[Cyclic Thread] Thread started, waiting for mappingdone...\n"); - - while (!mappingdone && isRunning) { - osal_usleep(100); - } - - if (!isRunning) { - printf("[Cyclic Thread] Exiting - master not running\n"); - return; - } - - printf("[Cyclic Thread] Mapping done, initializing timing...\n"); - - osal_get_monotonic_time(&ts); - ht = (ts.tv_nsec / 1000000) + 1; - ts.tv_nsec = ht * 1000000; - - printf("[Cyclic Thread] Entering main cyclic loop (cycleTime=%lld ns)\n", - (long long)cycleTimeNs); - - int receiveSuccessCount = 0; - int receiveFailCount = 0; - int lastPrintCycle = 0; - - ecx_send_processdata(&ctx); - - while (isRunning) { - add_time_ns(&ts, cycleTimeNs + toff); - osal_monotonic_sleep(&ts); - - cycleCount++; - - if (dorun) { - // ========== 同步所有从站的影子缓冲区到 outputs ========== - if (anyPdoDirty.load()) { - for (auto& pair : shadowBuffers) { - uint16_t slaveId = pair.first; - SlaveShadowBuffer& shadow = pair.second; - - if (!shadow.dirty || !shadow.data) continue; - - auto outIt = outputs.find(slaveId); - auto sizeIt = outputSizes.find(slaveId); - if (outIt == outputs.end() || outIt->second == nullptr) continue; - if (sizeIt == outputSizes.end()) continue; - - size_t copySize = std::min(shadow.size, (size_t)sizeIt->second); - memcpy(outIt->second, shadow.data, copySize); - shadow.dirty = false; - } - anyPdoDirty = false; - } - - ecx_send_processdata(&ctx); - - int wkc = ecx_receive_processdata(&ctx, EC_TIMEOUTRET); - - if (wkc != expectedWKC) { - cyclicWkcErrorCount++; - receiveFailCount++; - if (cyclicWkcErrorCount <= 5 || cyclicWkcErrorCount % 100 == 0) { - printf("[Cyclic] WARNING: WKC mismatch! Expected=%d, Got=%d, ErrorCount=%d\n", - expectedWKC, wkc, cyclicWkcErrorCount.load()); - } - } else { - receiveSuccessCount++; - } - - if (ctx.slavelist[0].hasdc && (wkc > 0)) { - ec_sync(ctx.DCtime, cycleTimeNs, &toff); - } - - ecx_mbxhandler(&ctx, 0, 4); - - // if (cycleCount - lastPrintCycle >= printInterval) { - // lastPrintCycle = cycleCount; - // - // printf("\n========== PDO Data Exchange Status ==========\n"); - // printf("Cycle: %d | Success: %d | Fail: %d | WKC Error: %d\n", - // cycleCount, receiveSuccessCount, receiveFailCount, cyclicWkcErrorCount.load()); - // printf("DC time: %lld | toff: %lld\n", (long long)ctx.DCtime, (long long)toff); - // - // for (int slaveIdx = 1; slaveIdx <= slaveCount; slaveIdx++) { - // ec_slavet* slave = &ctx.slavelist[slaveIdx]; - // - // printf("\n[Slave %d] State: 0x%02X | AL Status: 0x%04X\n", - // slaveIdx, slave->state, slave->ALstatuscode); - // - // if (slave->inputs && slave->Ibytes > 0) { - // printf(" Inputs (%d bytes): ", slave->Ibytes); - // for (int i = 0; i < slave->Ibytes && i < 16; i++) { - // printf("%02X ", ((uint8_t*)slave->inputs)[i]); - // } - // if (slave->Ibytes > 16) printf("..."); - // printf("\n"); - // - // if (slave->Ibytes >= 2) { - // uint16_t statusWord = *(uint16_t*)slave->inputs; - // printf(" -> Status Word: 0x%04X", statusWord); - // if (statusWord == 0xFFFF) { - // printf(" (INVALID - All ones)"); - // } else if (statusWord == 0) { - // printf(" (INVALID - All zeros)"); - // } else { - // printf(" ["); - // if (statusWord & 0x0001) printf("ReadyToSwitchOn "); - // if (statusWord & 0x0002) printf("SwitchedOn "); - // if (statusWord & 0x0004) printf("OperationEnabled "); - // if (statusWord & 0x0008) printf("Fault "); - // if (statusWord & 0x0010) printf("VoltageEnabled "); - // if (statusWord & 0x0020) printf("QuickStop "); - // if (statusWord & 0x0040) printf("SwitchOnDisabled "); - // if (statusWord & 0x0080) printf("Warning "); - // printf("]"); - // } - // printf("\n"); - // - // if (slave->Ibytes >= 6) { - // int32_t actualPosition = *(int32_t*)(slave->inputs + 2); - // printf(" -> Actual Position: %d\n", actualPosition); - // } - // } - // } - // - // if (slave->outputs && slave->Obytes > 0) { - // printf(" Outputs (%d bytes): ", slave->Obytes); - // for (int i = 0; i < slave->Obytes && i < 16; i++) { - // printf("%02X ", ((uint8_t*)slave->outputs)[i]); - // } - // if (slave->Obytes > 16) printf("..."); - // printf("\n"); - // - // if (slave->Obytes >= 2) { - // uint16_t controlWord = *(uint16_t*)slave->outputs; - // printf(" <- Control Word: 0x%04X\n", controlWord); - // } - // } - // } - // - // printf("================================================\n\n"); - // - // receiveSuccessCount = 0; - // receiveFailCount = 0; - // } - - } else { - if (cycleCount % 1000 == 0) { - printf("[Cyclic] Waiting for dorun=1, cycle=%d\n", cycleCount); - } - } - } - - printf("[Cyclic Thread] Thread exiting\n"); -} - -void EcMaster::runCheck() { - while (isRunning) { - if (dorun && (cyclicWkcErrorCount > 2 || ctx.grouplist[currentgroup].docheckstate)) { - ctx.grouplist[currentgroup].docheckstate = FALSE; - ecx_readstate(&ctx); - - for (int slaveix = 1; slaveix <= ctx.slavecount; slaveix++) { - ec_slavet *slave = &ctx.slavelist[slaveix]; - - if ((slave->group == currentgroup) && (slave->state != EC_STATE_OPERATIONAL)) { - ctx.grouplist[currentgroup].docheckstate = TRUE; - - if (slave->state == (EC_STATE_SAFE_OP + EC_STATE_ERROR)) { - printf("ERROR : slave %d is in SAFE_OP + ERROR, attempting ack.\n", slaveix); - slave->state = (EC_STATE_SAFE_OP + EC_STATE_ACK); - ecx_writestate(&ctx, slaveix); - } - else if (slave->state == EC_STATE_SAFE_OP) { - printf("WARNING : slave %d is in SAFE_OP, change to OPERATIONAL.\n", slaveix); - slave->state = EC_STATE_OPERATIONAL; - if (slave->mbxhandlerstate == ECT_MBXH_LOST) - slave->mbxhandlerstate = ECT_MBXH_CYCLIC; - ecx_writestate(&ctx, slaveix); - } - else if (slave->state > EC_STATE_NONE) { - if (ecx_reconfig_slave(&ctx, slaveix, EC_TIMEOUTMON) >= EC_STATE_PRE_OP) { - slave->islost = FALSE; - printf("MESSAGE : slave %d reconfigured\n", slaveix); - } - } - else if (!slave->islost) { - ecx_statecheck(&ctx, slaveix, EC_STATE_OPERATIONAL, EC_TIMEOUTRET); - if (slave->state == EC_STATE_NONE) { - slave->islost = TRUE; - slave->mbxhandlerstate = ECT_MBXH_LOST; - if (slave->Ibytes) { - memset(slave->inputs, 0x00, slave->Ibytes); - } - printf("ERROR : slave %d lost\n", slaveix); - } - } - } - - if (slave->islost) { - if (slave->state <= EC_STATE_INIT) { - if (ecx_recover_slave(&ctx, slaveix, EC_TIMEOUTMON)) { - slave->islost = FALSE; - printf("MESSAGE : slave %d recovered\n", slaveix); - } - } - else { - slave->islost = FALSE; - printf("MESSAGE : slave %d found\n", slaveix); - } - } - } - - if (!ctx.grouplist[currentgroup].docheckstate) { - printf("OK : all slaves resumed OPERATIONAL.\n"); - } - } - - osal_usleep(10000); - } -} - -// ==================== PDO 配置 ==================== - -int EcMaster::configurePdo(uint16_t slaveId, const EcPdo* rxPdo, const EcPdo* txPdo) { - uint8_t zero = 0; - int wkc = 0; - - // ========== 步骤1: 进入 PRE-OP ========== - printf("Step 1: Entering PRE-OP...\n"); - if (setState(slaveId, EC_STATE_PRE_OP) != 0) { - printf("Failed to enter PRE-OP\n"); - return -1; - } - printf("Slave %d in PRE-OP\n", slaveId); - osal_usleep(50000); - - // ========== 步骤2: 禁用全局 PDO 分配 ========== - printf("Step 2: Disabling global PDO assignment...\n"); - wkc = ecx_SDOwrite(&ctx, slaveId, 0x1C12, 0x00, FALSE, 1, &zero, EC_TIMEOUTRXM); - printf("ecx_SDOwrite 0x1C12, wkc = %d\n", wkc); - - wkc = ecx_SDOwrite(&ctx, slaveId, 0x1C13, 0x00, FALSE, 1, &zero, EC_TIMEOUTRXM); - printf("ecx_SDOwrite 0x1C13, wkc = %d\n", wkc); - - // ========== 计算输出大小(用于分配影子缓冲区) ========== - int outputSize = 0; - if (rxPdo) { - for (const auto& entry : rxPdo->entries) { - outputSize += (entry.bitlen + 7) / 8; - } - printf("Calculated output size: %d bytes\n", outputSize); - } - - // ========== 步骤3: 配置 RxPDO ========== - if (rxPdo && rxPdo->mapCount > 0) { - printf("Step 3: Configuring RxPDO 0x%04X\n", rxPdo->index); - - wkc = ecx_SDOwrite(&ctx, slaveId, rxPdo->index, 0x00, FALSE, 1, &zero, EC_TIMEOUTRXM); - if (wkc <= 0) { - printf("Failed to disable RxPDO 0x%04X\n", rxPdo->index); - return -1; - } - - uint8_t subidx = 1; - for (const auto& entry : rxPdo->entries) { - uint32_t mapValue = entry.makeMapValue(); - wkc = ecx_SDOwrite(&ctx, slaveId, rxPdo->index, subidx++, FALSE, 4, &mapValue, EC_TIMEOUTRXM); - if (wkc <= 0) { - printf("Failed to write RxPDO entry 0x%04X:%02X\n", entry.index, entry.subindex); - return -1; - } - printf(" Entry: 0x%04X:%02X (%d bits)\n", entry.index, entry.subindex, entry.bitlen); - } - - uint8_t mapCount = rxPdo->mapCount; - wkc = ecx_SDOwrite(&ctx, slaveId, rxPdo->index, 0x00, FALSE, 1, &mapCount, EC_TIMEOUTRXM); - if (wkc <= 0) { - printf("Failed to enable RxPDO 0x%04X\n", rxPdo->index); - return -1; - } - printf("RxPDO 0x%04X configured successfully\n", rxPdo->index); - } - - // ========== 步骤4: 配置 TxPDO ========== - if (txPdo && txPdo->mapCount > 0) { - printf("Step 4: Configuring TxPDO 0x%04X\n", txPdo->index); - - wkc = ecx_SDOwrite(&ctx, slaveId, txPdo->index, 0x00, FALSE, 1, &zero, EC_TIMEOUTRXM); - if (wkc <= 0) { - printf("Failed to disable TxPDO 0x%04X\n", txPdo->index); - return -1; - } - - uint8_t subidx = 1; - for (const auto& entry : txPdo->entries) { - uint32_t mapValue = entry.makeMapValue(); - wkc = ecx_SDOwrite(&ctx, slaveId, txPdo->index, subidx++, FALSE, 4, &mapValue, EC_TIMEOUTRXM); - if (wkc <= 0) { - printf("Failed to write TxPDO entry 0x%04X:%02X\n", entry.index, entry.subindex); - return -1; - } - printf(" Entry: 0x%04X:%02X (%d bits)\n", entry.index, entry.subindex, entry.bitlen); - } - - uint8_t mapCount = txPdo->mapCount; - wkc = ecx_SDOwrite(&ctx, slaveId, txPdo->index, 0x00, FALSE, 1, &mapCount, EC_TIMEOUTRXM); - if (wkc <= 0) { - printf("Failed to enable TxPDO 0x%04X\n", txPdo->index); - return -1; - } - printf("TxPDO 0x%04X configured successfully\n", txPdo->index); - } - - // ========== 步骤5: 应用 PDO 分配 ========== - printf("Step 5: Applying PDO assignment...\n"); - - if (rxPdo && rxPdo->mapCount > 0) { - uint8_t rpdo_map_count = 1; - wkc = ecx_SDOwrite(&ctx, slaveId, 0x1C12, 0x01, FALSE, 2, &rxPdo->index, EC_TIMEOUTRXM); - if (wkc <= 0) { - printf("Failed to write RxPDO index to 0x1C12:01\n"); - return -1; - } - - wkc = ecx_SDOwrite(&ctx, slaveId, 0x1C12, 0x00, FALSE, 1, &rpdo_map_count, EC_TIMEOUTRXM); - if (wkc <= 0) { - printf("Failed to set RxPDO count\n"); - return -1; - } - printf("RxPDO assigned to 0x1C12\n"); - } - - if (txPdo && txPdo->mapCount > 0) { - uint8_t tpdo_map_count = 1; - wkc = ecx_SDOwrite(&ctx, slaveId, 0x1C13, 0x01, FALSE, 2, &txPdo->index, EC_TIMEOUTRXM); - if (wkc <= 0) { - printf("Failed to write TxPDO index to 0x1C13:01\n"); - return -1; - } - - wkc = ecx_SDOwrite(&ctx, slaveId, 0x1C13, 0x00, FALSE, 1, &tpdo_map_count, EC_TIMEOUTRXM); - if (wkc <= 0) { - printf("Failed to set TxPDO count\n"); - return -1; - } - printf("TxPDO assigned to 0x1C13\n"); - } - - // ========== 步骤6: 为这个从站分配影子缓冲区 ========== - if (outputSize > 0) { - // 移除旧的影子缓冲区(如果存在) - auto it = shadowBuffers.find(slaveId); - if (it != shadowBuffers.end()) { - shadowBuffers.erase(it); - } - // 创建新的影子缓冲区 - shadowBuffers.emplace(std::piecewise_construct, - std::forward_as_tuple(slaveId), - std::forward_as_tuple(outputSize)); - printf("Shadow buffer allocated for slave %d: %d bytes\n", slaveId, outputSize); - } - - // ========== 步骤7: 设置 PDO 偏移 ========== - setupPdoOffsets(slaveId, rxPdo, txPdo); - - printf("PDO configuration for slave %d completed\n", slaveId); - return 0; -} - -// ========== setupMappingAndDc - 只需要做映射和DC配置 ========== -int EcMaster::setupMappingAndDc() { - printf("\n========== Step 6: Finalizing mapping and DC configuration ==========\n"); - - // 配置映射 - ctx.manualstatechange = 0; - ecx_config_map_group(&ctx, IOmap, 0); - - // 设置 mappingdone 标志 - mappingdone = 1; - - // 验证并保存所有从站的 PDO 数据指针和偏移 - for (int i = 1; i <= ctx.slavecount; i++) { - inputs[i] = ctx.slavelist[i].inputs; - outputs[i] = ctx.slavelist[i].outputs; - inputSizes[i] = ctx.slavelist[i].Ibytes; - outputSizes[i] = ctx.slavelist[i].Obytes; - - if (outputs[i]) { - outputOffsets[i] = (uint8_t*)outputs[i] - IOmap; - printf("Slave %d: Out offset=%d, size=%d bytes\n", - i, outputOffsets[i], outputSizes[i]); - } - printf("Slave %d: In=%d bytes, Out=%d bytes\n", i, inputSizes[i], outputSizes[i]); - - printf(" SM2 (Outputs): StartAddr=0x%04X, Length=%d\n", - ctx.slavelist[i].SM[2].StartAddr, ctx.slavelist[i].SM[2].SMlength); - printf(" SM3 (Inputs): StartAddr=0x%04X, Length=%d\n", - ctx.slavelist[i].SM[3].StartAddr, ctx.slavelist[i].SM[3].SMlength); - } - - expectedWKC = (ctx.grouplist[0].outputsWKC * 2) + ctx.grouplist[0].inputsWKC; - printf("Expected WKC: %d\n", expectedWKC); - - // 配置分布式时钟 - ecx_configdc(&ctx); - - // 添加所有 CoE 从站到周期邮箱处理 - for (int i = 1; i <= ctx.slavecount; i++) { - if (ctx.slavelist[i].CoEdetails > 0) { - ecx_slavembxcyclic(&ctx, i); - printf("Slave %d added to cyclic mailbox handler\n", i); - } - } - - // 验证影子缓冲区大小是否匹配实际输出大小 - for (int i = 1; i <= ctx.slavecount; i++) { - auto it = shadowBuffers.find(i); - if (it != shadowBuffers.end()) { - size_t shadowSize = it->second.size; - int actualSize = outputSizes[i]; - if (shadowSize != (size_t)actualSize) { - printf("WARNING: Slave %d shadow buffer size (%zu) != actual output size (%d)\n", - i, shadowSize, actualSize); - // 重新分配匹配的大小 - it->second = SlaveShadowBuffer(actualSize); - printf(" Reallocated shadow buffer to %d bytes\n", actualSize); - } - } else if (outputSizes[i] > 0) { - // 如果没有影子缓冲区但有输出数据,创建一个 - shadowBuffers.emplace(std::piecewise_construct, - std::forward_as_tuple(i), - std::forward_as_tuple(outputSizes[i])); - printf("Created shadow buffer for slave %d: %d bytes\n", i, outputSizes[i]); - } - } - - printf("Mapping and DC configuration completed\n"); - return 0; -} - -int EcMaster::setupPdoOffsets(uint16_t slaveId, const EcPdo* rxPdo, const EcPdo* txPdo) { - EcPdoOffsets offsets; - memset(&offsets, 0xFF, sizeof(offsets)); - - // ========== 解析 RxPDO (Outputs) ========== - if (rxPdo) { - uint16_t offset = 0; - printf("RxPDO 0x%04X mapping (%zu entries):\n", rxPdo->index, rxPdo->entries.size()); - - for (const auto& entry : rxPdo->entries) { - uint8_t bytes = (entry.bitlen + 7) / 8; - printf(" Entry 0x%04X:%02X -> offset %d (%d bytes)\n", - entry.index, entry.subindex, offset, bytes); - - switch (entry.index) { - case 0x6040: // Control Word - offsets.controlWord = offset; - printf(" *** Control Word at offset %d ***\n", offset); - break; - case 0x607A: // Target Position - offsets.targetPosition = offset; - break; - case 0x60FF: // Target Velocity - offsets.targetVelocity = offset; - break; - case 0x6071: // Target Torque - offsets.targetTorque = offset; - break; - case 0x6083: // Profile Acceleration - offsets.profileAcceleration = offset; - printf(" *** Profile Acceleration at offset %d ***\n", offset); - break; - case 0x6084: // Profile Deceleration - offsets.profileDeceleration = offset; - printf(" *** Profile Deceleration at offset %d ***\n", offset); - break; - case 0x6081: // Profile Velocity - offsets.profileVelocity = offset; - printf(" *** Profile Velocity at offset %d ***\n", offset); - break; - case 0x6087: // Torque Slope - offsets.torqueSlope = offset; - break; - case 0x6060: // Mode of Operation - offsets.modeOfOperation = offset; - break; - default: - printf(" *** Unknown entry 0x%04X:%02X ***\n", entry.index, entry.subindex); - break; - } - offset += bytes; - } - } - - // ========== 解析 TxPDO (Inputs) ========== - if (txPdo) { - uint16_t offset = 0; - printf("\nTxPDO 0x%04X mapping (%zu entries):\n", txPdo->index, txPdo->entries.size()); - - for (const auto& entry : txPdo->entries) { - uint8_t bytes = (entry.bitlen + 7) / 8; - printf(" Entry 0x%04X:%02X -> offset %d (%d bytes)\n", - entry.index, entry.subindex, offset, bytes); - - switch (entry.index) { - case 0x6041: // Status Word - offsets.statusWord = offset; - printf(" *** Status Word at offset %d ***\n", offset); - break; - case 0x6064: // Actual Position - offsets.actualPosition = offset; - break; - case 0x606C: // Actual Velocity - offsets.actualVelocity = offset; - break; - case 0x6077: // Actual Torque - offsets.actualTorque = offset; - break; - case 0x6061: // Mode of Operation Display - offsets.modeOfOperationDisplay = offset; - break; - case 0x603F: // Error Code - offsets.errorCode = offset; - break; - case 0x6078: // Actual Current - offsets.actualCurrent = offset; - printf(" *** Actual Current at offset %d ***\n", offset); - break; - default: - printf(" *** Unknown entry 0x%04X:%02X ***\n", entry.index, entry.subindex); - break; - } - offset += bytes; - } - } - - pdoOffsets[slaveId] = offsets; - - // ========== 打印摘要 ========== - printf("\n=== PDO Offset Summary ===\n"); - printf("RxPDO (Outputs):\n"); - printf(" Control Word: %d %s\n", offsets.controlWord, - offsets.controlWord == 0xFFFF ? "(INVALID)" : ""); - printf(" Target Position: %d %s\n", offsets.targetPosition, - offsets.targetPosition == 0xFFFF ? "(INVALID)" : ""); - printf(" Target Velocity: %d %s\n", offsets.targetVelocity, - offsets.targetVelocity == 0xFFFF ? "(INVALID)" : ""); - printf(" Target Torque: %d %s\n", offsets.targetTorque, - offsets.targetTorque == 0xFFFF ? "(INVALID)" : ""); - printf(" Profile Velocity: %d %s\n", offsets.profileVelocity, - offsets.profileVelocity == 0xFFFF ? "(NOT IN PDO)" : ""); - printf(" Profile Acc: %d %s\n", offsets.profileAcceleration, - offsets.profileAcceleration == 0xFFFF ? "(NOT IN PDO)" : ""); - printf(" Profile Dec: %d %s\n", offsets.profileDeceleration, - offsets.profileDeceleration == 0xFFFF ? "(NOT IN PDO)" : ""); - printf(" Mode of Operation: %d %s\n", offsets.modeOfOperation, - offsets.modeOfOperation == 0xFFFF ? "(INVALID)" : ""); - - printf("\nTxPDO (Inputs):\n"); - printf(" Status Word: %d %s\n", offsets.statusWord, - offsets.statusWord == 0xFFFF ? "(INVALID)" : ""); - printf(" Actual Position: %d %s\n", offsets.actualPosition, - offsets.actualPosition == 0xFFFF ? "(INVALID)" : ""); - printf(" Actual Velocity: %d %s\n", offsets.actualVelocity, - offsets.actualVelocity == 0xFFFF ? "(INVALID)" : ""); - printf(" Actual Torque: %d %s\n", offsets.actualTorque, - offsets.actualTorque == 0xFFFF ? "(INVALID)" : ""); - printf(" Mode Display: %d %s\n", offsets.modeOfOperationDisplay, - offsets.modeOfOperationDisplay == 0xFFFF ? "(INVALID)" : ""); - printf(" Error Code: %d %s\n", offsets.errorCode, - offsets.errorCode == 0xFFFF ? "(NOT IN PDO)" : ""); - printf(" Actual Current: %d %s\n", offsets.actualCurrent, - offsets.actualCurrent == 0xFFFF ? "(NOT IN PDO)" : ""); - printf("=================================\n"); - - if (offsets.controlWord == 0xFFFF || offsets.statusWord == 0xFFFF) { - printf("ERROR: Critical PDO offsets not configured!\n"); - return -1; - } - - return 0; -} - -// ==================== PDO 访问接口 ==================== - -void EcMaster::setControlWord(uint16_t slaveId, uint16_t value) { - if (inOp) { - writePdo(slaveId, pdoOffsets[slaveId].controlWord, value); - } else { - writeSdo(slaveId, 0x6040, 0, &value, EcDataType::Uint16, 100); - } -} - -uint16_t EcMaster::getStatusWord(uint16_t slaveId) { - if (inOp) { - return readPdo(slaveId, pdoOffsets[slaveId].statusWord); - } - uint16_t value; - readSdo(slaveId, 0x6041, 0, &value, EcDataType::Uint16, 100); - return value; -} - -void EcMaster::setTargetPosition(uint16_t slaveId, int32_t value) { - if (inOp) { - writePdo(slaveId, pdoOffsets[slaveId].targetPosition, value); - } else { - writeSdo(slaveId, 0x607A, 0, &value, EcDataType::Int32, 100); - } -} - -int32_t EcMaster::getActualPosition(uint16_t slaveId) { - if (inOp) { - return readPdo(slaveId, pdoOffsets[slaveId].actualPosition); - } - int32_t value; - readSdo(slaveId, 0x6064, 0, &value, EcDataType::Int32, 100); - return value; -} - -void EcMaster::setTargetVelocity(uint16_t slaveId, int32_t value) { - if (inOp) { - writePdo(slaveId, pdoOffsets[slaveId].targetVelocity, value); - } else { - writeSdo(slaveId, 0x60FF, 0, &value, EcDataType::Int32, 100); - } -} - -int32_t EcMaster::getActualVelocity(uint16_t slaveId) { - if (inOp) { - return readPdo(slaveId, pdoOffsets[slaveId].actualVelocity); - } - int32_t value; - readSdo(slaveId, 0x606C, 0, &value, EcDataType::Int32, 100); - return value; -} - -void EcMaster::setTargetTorque(uint16_t slaveId, int16_t value) { - if (inOp) { - writePdo(slaveId, pdoOffsets[slaveId].targetTorque, value); - } else { - writeSdo(slaveId, 0x6071, 0, &value, EcDataType::Int16, 100); - } -} - -int16_t EcMaster::getActualTorque(uint16_t slaveId) { - if (inOp) { - return readPdo(slaveId, pdoOffsets[slaveId].actualTorque); - } - int16_t value; - readSdo(slaveId, 0x6077, 0, &value, EcDataType::Int16, 100); - return value; -} - -void EcMaster::setModeOfOperation(uint16_t slaveId, int8_t value) { - if (inOp) { - writePdo(slaveId, pdoOffsets[slaveId].modeOfOperation, value); - } else { - writeSdo(slaveId, 0x6060, 0, &value, EcDataType::Int8, 100); - } -} - -int8_t EcMaster::getModeOfOperationDisplay(uint16_t slaveId) { - if (inOp) { - return readPdo(slaveId, pdoOffsets[slaveId].modeOfOperationDisplay); - } - int8_t value; - readSdo(slaveId, 0x6061, 0, &value, EcDataType::Int8, 100); - return value; -} - -void EcMaster::setProfileVelocity(uint16_t slaveId, uint32_t value) { - if (inOp) { - writePdo(slaveId, pdoOffsets[slaveId].profileVelocity, value); - } -} - -void EcMaster::setProfileAcceleration(uint16_t slaveId, uint32_t value) { - if (inOp) { - writePdo(slaveId, pdoOffsets[slaveId].profileAcceleration, value); - } -} - -void EcMaster::setProfileDeceleration(uint16_t slaveId, uint32_t value) { - if (inOp) { - writePdo(slaveId, pdoOffsets[slaveId].profileDeceleration, value); - } -} - -int16_t EcMaster::getCurrentValue(uint16_t slaveId) { - if (inOp) { - return readPdo(slaveId, pdoOffsets[slaveId].actualCurrent); - } - int16_t value; - readSdo(slaveId, 0x6078, 0, &value, EcDataType::Int16, 100); - return value; -} - -uint16_t EcMaster::getErrorCode(uint16_t slaveId) { - if (inOp) { - auto it = pdoOffsets.find(slaveId); - if (it != pdoOffsets.end() && it->second.errorCode != 0xFFFF) { - return readPdo(slaveId, it->second.errorCode); - } - } - uint16_t value = 0; - readSdo(slaveId, 0x603F, 0, &value, EcDataType::Uint16, 100); - return value; -} - -// ==================== 电机控制高级接口 ==================== - -int EcMaster::enable(uint16_t slaveId) { - if (inOp) { - setControlWord(slaveId, 0x0006); - osal_usleep(10000); - setControlWord(slaveId, 0x0007); - osal_usleep(10000); - setControlWord(slaveId, 0x000F); - } else { - uint16_t val = 0x0006; - writeSdo(slaveId, 0x6040, 0, &val, EcDataType::Uint16, 100); - osal_usleep(10000); - val = 0x0007; - writeSdo(slaveId, 0x6040, 0, &val, EcDataType::Uint16, 100); - osal_usleep(10000); - val = 0x000F; - writeSdo(slaveId, 0x6040, 0, &val, EcDataType::Uint16, 100); - } - osal_usleep(10000); - return 0; -} - -int EcMaster::disable(uint16_t slaveId) { - if (inOp) { - setControlWord(slaveId, 0x0007); - osal_usleep(10000); - setControlWord(slaveId, 0x0000); - } else { - uint16_t val = 0x0007; - writeSdo(slaveId, 0x6040, 0, &val, EcDataType::Uint16, 100); - osal_usleep(10000); - val = 0x0000; - writeSdo(slaveId, 0x6040, 0, &val, EcDataType::Uint16, 100); - } - return 0; -} - -int EcMaster::faultReset(uint16_t slaveId) { - if (inOp) { - setControlWord(slaveId, 0x0080); - osal_usleep(10000); - setControlWord(slaveId, 0x0000); - } else { - uint16_t val = 0x0080; - writeSdo(slaveId, 0x6040, 0, &val, EcDataType::Uint16, 100); - osal_usleep(10000); - val = 0x0000; - writeSdo(slaveId, 0x6040, 0, &val, EcDataType::Uint16, 100); - } - return 0; -} - -// ==================== SDO 读写 ==================== - -int EcMaster::readSdo(uint16_t slaveId, uint16_t index, uint8_t subindex, void* value, EcDataType type, int timeoutMs) { - int size = 0; - switch (type) { - case EcDataType::Boolean: - case EcDataType::Int8: - case EcDataType::Uint8: - size = 1; break; - case EcDataType::Int16: - case EcDataType::Uint16: - size = 2; break; - case EcDataType::Int32: - case EcDataType::Uint32: - case EcDataType::Real32: - size = 4; break; - case EcDataType::Real64: - size = 8; break; - default: return -1; - } - int wkc = ecx_SDOread(&ctx, slaveId, index, subindex, FALSE, &size, value, timeoutMs); - return wkc > 0 ? 0 : -1; -} - -int EcMaster::writeSdo(uint16_t slaveId, uint16_t index, uint8_t subindex, void* value, EcDataType type, int timeoutMs) { - int size = 0; - switch (type) { - case EcDataType::Boolean: - case EcDataType::Int8: - case EcDataType::Uint8: - size = 1; break; - case EcDataType::Int16: - case EcDataType::Uint16: - size = 2; break; - case EcDataType::Int32: - case EcDataType::Uint32: - case EcDataType::Real32: - size = 4; break; - case EcDataType::Real64: - size = 8; break; - default: return -1; - } - int wkc = ecx_SDOwrite(&ctx, slaveId, index, subindex, FALSE, size, value, timeoutMs); - return wkc > 0 ? 0 : -1; -} - -// ==================== SM/FMMU 配置校验 ==================== - -void EcMaster::verifySmConfiguration(uint16_t slaveId) { - if (slaveId > (uint16_t)ctx.slavecount) { - printf("Invalid slave ID: %d\n", slaveId); - return; - } - - ec_slavet* slave = &ctx.slavelist[slaveId]; - - printf("\n========== SM Configuration for Slave %d ==========\n", slaveId); - - printf("PDO Mapping Sizes:\n"); - printf(" Outputs (Obytes): %d bytes\n", slave->Obytes); - printf(" Inputs (Ibytes): %d bytes\n", slave->Ibytes); - - printf("\nSync Manager Configuration:\n"); - const char* sm_names[] = {"MBoxOut", "MBoxIn", "Outputs", "Inputs"}; - for (int sm = 0; sm < 4; sm++) { - printf(" SM%d (%s): StartAddr=0x%04X, Length=%d bytes\n", - sm, sm_names[sm], - slave->SM[sm].StartAddr, - slave->SM[sm].SMlength); - } - - printf("\nFMMU Configuration:\n"); - printf(" Outputs mapped to IOmap at offset: %td\n", - (uint8_t*)slave->outputs - IOmap); - printf(" Inputs mapped to IOmap at offset: %td\n", - (uint8_t*)slave->inputs - IOmap); - - // 打印影子缓冲区信息 - auto it = shadowBuffers.find(slaveId); - if (it != shadowBuffers.end()) { - printf("\nShadow Buffer:\n"); - printf(" Size: %zu bytes\n", it->second.size); - printf(" Dirty: %s\n", it->second.dirty ? "true" : "false"); - } else { - printf("\nShadow Buffer: Not allocated\n"); - } - - printf("==================================================\n"); -} - -void EcMaster::verifyAllSlavesSmConfiguration() { - for (int i = 1; i <= ctx.slavecount; i++) { - verifySmConfiguration((uint16_t)i); - } -} \ No newline at end of file diff --git a/cmvr-es/devices/motor/eyou_motor/ethercat/protocol/ec_master.h b/cmvr-es/devices/motor/eyou_motor/ethercat/protocol/ec_master.h deleted file mode 100644 index e4b67ad0..00000000 --- a/cmvr-es/devices/motor/eyou_motor/ethercat/protocol/ec_master.h +++ /dev/null @@ -1,328 +0,0 @@ -// ec_master.h -#ifndef EC_MASTER_H -#define EC_MASTER_H - -#include "ec_types.h" -#include -#include -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif -#include "soem/soem.h" -#include "soem/osal.h" -#ifdef __cplusplus -} -#endif - -// 错误码定义 -#define EC_SUCCESS 0 -#define EC_FAILED_INIT 1 -#define EC_FAILED_NOSLAVE 2 - -#ifndef EC_TIMEOUTMON -#define EC_TIMEOUTMON 500 -#endif - -#ifndef EC_TIMEOUTRET -#define EC_TIMEOUTRET 2000 -#endif - -#ifndef EC_TIMEOUTSTATE -#define EC_TIMEOUTSTATE 2000 -#endif - -#ifndef EC_TIMEOUTRXM -#define EC_TIMEOUTRXM 2000 -#endif - -// EtherCAT 主站类 -class EcMaster { - -public: - // 操作模式枚举(符合 CiA 402 标准) - enum EcOperationMode : int8_t { - // 轮廓模式 (Profile Modes) - EcOperationMode_ProfilePosition = 1, // PP 模式 - EcOperationMode_ProfileVelocity = 3, // PV 模式 - EcOperationMode_ProfileTorque = 4, // PT 模式 - - // 插补模式 - EcOperationMode_InterpolatedPosition = 7, // IP 模式 - - // 循环同步模式 (Cyclic Sync Modes) - EcOperationMode_CyclicSyncPosition = 8, // CSP 模式 - EcOperationMode_CyclicSyncVelocity = 9, // CSV 模式 - EcOperationMode_CyclicSyncTorque = 10, // CST 模式 - }; - -public: - EcMaster(); - ~EcMaster(); - - EcMaster(const EcMaster&) = delete; - EcMaster& operator=(const EcMaster&) = delete; - - // 初始化 - int init(const char* ifName, int cycleTimeUs = 1000); - void close() { shutdown(); } - int getSlaveCount() const { return slaveCount; } - bool isOperational() const { return inOp; } - - // 状态控制 - int setState(uint16_t slaveId, uint8_t targetState); - int preOperational(uint16_t slaveId) { return setState(slaveId, EC_STATE_PRE_OP); } - int safeOperational(uint16_t slaveId) { return setState(slaveId, EC_STATE_SAFE_OP); } - int operational(uint16_t slaveId) { return setState(slaveId, EC_STATE_OPERATIONAL); } - int gotoOperational(); - void shutdown(); - - // PDO 配置 - int configurePdo(uint16_t slaveId, const EcPdo* rxPdo, const EcPdo* txPdo); - int setupMappingAndDc(); // 配置所有从站的映射和DC - int setupPdoOffsets(uint16_t slaveId, const EcPdo* rxPdo, const EcPdo* txPdo); - - // PDO 数据访问(高性能,零拷贝) - void setControlWord(uint16_t slaveId, uint16_t value); - uint16_t getStatusWord(uint16_t slaveId); - void setTargetPosition(uint16_t slaveId, int32_t value); - int32_t getActualPosition(uint16_t slaveId); - void setTargetVelocity(uint16_t slaveId, int32_t value); - int32_t getActualVelocity(uint16_t slaveId); - void setTargetTorque(uint16_t slaveId, int16_t value); - int16_t getActualTorque(uint16_t slaveId); - void setModeOfOperation(uint16_t slaveId, int8_t value); - int8_t getModeOfOperationDisplay(uint16_t slaveId); - - void setProfileVelocity(uint16_t slaveId, uint32_t value); - void setProfileAcceleration(uint16_t slaveId, uint32_t value); - void setProfileDeceleration(uint16_t slaveId, uint32_t value); - int16_t getCurrentValue(uint16_t slaveId); - uint16_t getErrorCode(uint16_t slaveId); - - // 电机控制高级接口 - int enable(uint16_t slaveId); - int disable(uint16_t slaveId); - int faultReset(uint16_t slaveId); - - // 周期操作 - int sendOutputs() { return ecx_send_processdata(&ctx); } - int receiveInputs() { return ecx_receive_processdata(&ctx, EC_TIMEOUTRET); } - - // 影子缓冲区同步接口 - void syncSlaveOutputs(uint16_t slaveId); // 同步指定从站的输出 - void syncAllOutputs(); // 同步所有从站的输出 - - // 原始数据访问 - uint8_t* getInputs(uint16_t slaveId) { - auto it = inputs.find(slaveId); - return (it != inputs.end()) ? it->second : nullptr; - } - uint8_t* getOutputs(uint16_t slaveId) { - auto it = outputs.find(slaveId); - return (it != outputs.end()) ? it->second : nullptr; - } - int getInputSize(uint16_t slaveId) { - auto it = inputSizes.find(slaveId); - return (it != inputSizes.end()) ? it->second : 0; - } - int getOutputSize(uint16_t slaveId) { - auto it = outputSizes.find(slaveId); - return (it != outputSizes.end()) ? it->second : 0; - } - - // SDO 读写(仅用于配置阶段) - int readSdo(uint16_t slaveId, uint16_t index, uint8_t subindex, void* value, EcDataType type, int timeoutMs = 100); - int writeSdo(uint16_t slaveId, uint16_t index, uint8_t subindex, void* value, EcDataType type, int timeoutMs = 100); - - // SM/FMMU 配置校验(调试用) - void verifySmConfiguration(uint16_t slaveId); - void verifyAllSlavesSmConfiguration(); - - // 模板便捷接口 - template - int readSdo(uint16_t slaveId, uint16_t index, uint8_t subindex, T* value, int timeoutMs = 100) { - EcDataType type; - if constexpr (std::is_same_v) type = EcDataType::Int8; - else if constexpr (std::is_same_v) type = EcDataType::Uint8; - else if constexpr (std::is_same_v) type = EcDataType::Int16; - else if constexpr (std::is_same_v) type = EcDataType::Uint16; - else if constexpr (std::is_same_v) type = EcDataType::Int32; - else if constexpr (std::is_same_v) type = EcDataType::Uint32; - else if constexpr (std::is_same_v) type = EcDataType::Real32; - else if constexpr (std::is_same_v) type = EcDataType::Real64; - else return -1; - return readSdo(slaveId, index, subindex, value, type, timeoutMs); - } - - template - int writeSdo(uint16_t slaveId, uint16_t index, uint8_t subindex, T* value, int timeoutMs = 100) { - EcDataType type; - if constexpr (std::is_same_v) type = EcDataType::Int8; - else if constexpr (std::is_same_v) type = EcDataType::Uint8; - else if constexpr (std::is_same_v) type = EcDataType::Int16; - else if constexpr (std::is_same_v) type = EcDataType::Uint16; - else if constexpr (std::is_same_v) type = EcDataType::Int32; - else if constexpr (std::is_same_v) type = EcDataType::Uint32; - else if constexpr (std::is_same_v) type = EcDataType::Real32; - else if constexpr (std::is_same_v) type = EcDataType::Real64; - else return -1; - return writeSdo(slaveId, index, subindex, value, type, timeoutMs); - } - -private: - // ========== 影子缓冲区结构(每个从站独立) ========== - struct SlaveShadowBuffer { - uint8_t* data = nullptr; // 影子缓冲区数据指针 - size_t size = 0; // 缓冲区大小 - bool dirty = false; // 是否有待同步的数据 - - SlaveShadowBuffer() = default; - - explicit SlaveShadowBuffer(size_t sz) : size(sz) { - if (sz > 0) { - data = new uint8_t[sz]; - memset(data, 0, sz); - } - } - - ~SlaveShadowBuffer() { - delete[] data; - data = nullptr; - } - - // 禁止拷贝 - SlaveShadowBuffer(const SlaveShadowBuffer&) = delete; - SlaveShadowBuffer& operator=(const SlaveShadowBuffer&) = delete; - - // 允许移动 - SlaveShadowBuffer(SlaveShadowBuffer&& other) noexcept - : data(other.data), size(other.size), dirty(other.dirty) { - other.data = nullptr; - other.size = 0; - other.dirty = false; - } - - SlaveShadowBuffer& operator=(SlaveShadowBuffer&& other) noexcept { - if (this != &other) { - delete[] data; - data = other.data; - size = other.size; - dirty = other.dirty; - other.data = nullptr; - other.size = 0; - other.dirty = false; - } - return *this; - } - - // 重新分配缓冲区 - void reallocate(size_t newSize) { - if (data) { - delete[] data; - data = nullptr; - } - size = newSize; - if (newSize > 0) { - data = new uint8_t[newSize]; - memset(data, 0, newSize); - } - dirty = false; - } - }; - - // SOEM 上下文 - ecx_contextt ctx; - uint8_t IOmap[4096]; - - // 每个从站的影子缓冲区映射 - std::map shadowBuffers; - std::atomic anyPdoDirty{false}; // 是否有任何影子缓冲区有更新 - - int slaveCount = 0; - int expectedWKC = 0; - int currentgroup = 0; - - // 状态标志 - std::atomic inOp{false}; - std::atomic isRunning{false}; - std::atomic mappingdone{0}; - std::atomic dorun{0}; - std::atomic cyclicWkcErrorCount{0}; - - int64_t cycleTimeNs = 1000000; - - OSAL_THREAD_HANDLE cyclicThread; - OSAL_THREAD_HANDLE checkThread; - - // PDO 数据指针映射 - std::map inputs; - std::map outputs; - std::map inputSizes; - std::map outputSizes; - std::map outputOffsets; - - // PDO 偏移映射 - std::map pdoOffsets; - - // 辅助函数 - template - void writePdo(uint16_t slaveId, uint16_t offset, T value); - - template - T readPdo(uint16_t slaveId, uint16_t offset); - - // 获取从站的影子缓冲区 - SlaveShadowBuffer* getShadowBuffer(uint16_t slaveId); - - // 线程函数 - static OSAL_THREAD_FUNC_RT cyclicThreadFunc(void* arg); - static OSAL_THREAD_FUNC checkThreadFunc(void* arg); - void runCyclic(); - void runCheck(); -}; - -// ==================== 模板实现(必须放在头文件中) ==================== - -template -void EcMaster::writePdo(uint16_t slaveId, uint16_t offset, T value) { - if (inOp) { - auto* shadow = getShadowBuffer(slaveId); - if (shadow && shadow->data) { - if (offset + sizeof(T) <= shadow->size) { - *(T*)(shadow->data + offset) = value; - shadow->dirty = true; - anyPdoDirty = true; - return; - } - } - } - - // 降级:直接写 IOmap(配置阶段使用) - auto outIt = outputs.find(slaveId); - if (outIt != outputs.end() && outIt->second) { - auto sizeIt = outputSizes.find(slaveId); - if (sizeIt != outputSizes.end() && offset + sizeof(T) <= (uint16_t)sizeIt->second) { - *(T*)(outIt->second + offset) = value; - } - } -} - -template -T EcMaster::readPdo(uint16_t slaveId, uint16_t offset) { - auto it = pdoOffsets.find(slaveId); - if (it == pdoOffsets.end() || offset == 0xFFFF) return 0; - - auto inIt = inputs.find(slaveId); - if (inIt != inputs.end() && inIt->second) { - auto sizeIt = inputSizes.find(slaveId); - if (sizeIt != inputSizes.end() && offset + sizeof(T) <= (uint16_t)sizeIt->second) { - return *(T*)(inIt->second + offset); - } - } - return 0; -} - -#endif // EC_MASTER_H \ No newline at end of file diff --git a/cmvr-es/devices/motor/eyou_motor/ethercat/protocol/ec_pdo_config.h b/cmvr-es/devices/motor/eyou_motor/ethercat/protocol/ec_pdo_config.h deleted file mode 100644 index 7f8a919d..00000000 --- a/cmvr-es/devices/motor/eyou_motor/ethercat/protocol/ec_pdo_config.h +++ /dev/null @@ -1,75 +0,0 @@ -// ec_pdo_config.h - 固定 PDO 配置(替代 XML 解析) -#ifndef EC_PDO_CONFIG_H -#define EC_PDO_CONFIG_H - -#include "ec_types.h" - -// 创建固定的 RxPDO 配置(对应 XML 中的 #x1600) -inline EcPdo createFixedRxPdo() { - EcPdo rxPdo; - rxPdo.index = 0x1600; - rxPdo.mapCount = 10; // 9个有效条目 + 1个结束标志 - rxPdo.name = "Outputs"; - rxPdo.isRxPdo = true; - - // 按XML顺序填充 Entry - rxPdo.entries.push_back({0x6040, 0, 16, "Control Word", EcDataType::Uint16}); - rxPdo.entries.push_back({0x607A, 0, 32, "Target Position", EcDataType::Int32}); - rxPdo.entries.push_back({0x60FF, 0, 32, "Target Velocity", EcDataType::Int32}); - rxPdo.entries.push_back({0x6071, 0, 16, "Target Torque", EcDataType::Int16}); - rxPdo.entries.push_back({0x6083, 0, 32, "Profile Acceleration", EcDataType::Uint32}); - rxPdo.entries.push_back({0x6084, 0, 32, "Profile Deceleration", EcDataType::Uint32}); - rxPdo.entries.push_back({0x6081, 0, 32, "Profile Velocity", EcDataType::Uint32}); - rxPdo.entries.push_back({0x6087, 0, 32, "Torque Slope", EcDataType::Uint32}); - rxPdo.entries.push_back({0x6060, 0, 8, "Modes Of Operation", EcDataType::Int8}); - rxPdo.entries.push_back({0x0000, 0, 8, "End Marker", EcDataType::Uint8}); - - return rxPdo; -} - -// 创建固定的 TxPDO 配置(对应 XML 中的 #x1a00) -inline EcPdo createFixedTxPdo() { - EcPdo txPdo; - txPdo.index = 0x1A00; - txPdo.mapCount = 8; // 7个有效条目 + 1个结束标志 - txPdo.name = "Inputs"; - txPdo.isRxPdo = false; - - // 按XML顺序填充 Entry - txPdo.entries.push_back({0x6041, 0, 16, "Status Word", EcDataType::Uint16}); - txPdo.entries.push_back({0x6064, 0, 32, "Actual Position", EcDataType::Int32}); - txPdo.entries.push_back({0x606C, 0, 32, "Actual Velocity", EcDataType::Int32}); - txPdo.entries.push_back({0x6077, 0, 16, "Actual Torque", EcDataType::Int16}); - txPdo.entries.push_back({0x6061, 0, 8, "Mode Of Operation Display", EcDataType::Int8}); - txPdo.entries.push_back({0x603F, 0, 16, "Error Code", EcDataType::Uint16}); - txPdo.entries.push_back({0x6078, 0, 16, "Actual Current Value", EcDataType::Int16}); - txPdo.entries.push_back({0x0000, 0, 8, "End Marker", EcDataType::Uint8}); - - return txPdo; -} - -// 创建完整的从站配置 - 可以直接替换 XML 解析结果 -inline EcSlaveConfig createFixedSlaveConfig(uint16_t slaveId) { - EcSlaveConfig config; - config.slaveId = slaveId; - config.productCode = 0x00000000; - config.revisionNo = 0; - config.name = "Axis (csv,csp,cst)"; - - EcModule module; - module.moduleIdent = 0x119800; // 对应 XML 中的 ModuleIdent="#x119800" - module.name = "dynamic switchbewteen csp/csv"; - - module.rxPdos.push_back(createFixedRxPdo()); - module.txPdos.push_back(createFixedTxPdo()); - - // 记录可用的PDO索引(对应 parser.getAvailableRxPdos/getAvailableTxPdos 的返回值) - config.availableRxPdos.push_back(0x1600); - config.availableTxPdos.push_back(0x1A00); - - config.modules.push_back(module); - - return config; -} - -#endif // EC_PDO_CONFIG_H \ No newline at end of file diff --git a/cmvr-es/devices/motor/eyou_motor/ethercat/protocol/ec_types.h b/cmvr-es/devices/motor/eyou_motor/ethercat/protocol/ec_types.h deleted file mode 100644 index 174f2460..00000000 --- a/cmvr-es/devices/motor/eyou_motor/ethercat/protocol/ec_types.h +++ /dev/null @@ -1,111 +0,0 @@ -// ec_types.h - 公共类型定义 -#ifndef EC_TYPES_H -#define EC_TYPES_H - -#include -#include -#include - -// 数据类型枚举 -enum class EcDataType : uint8_t { - Boolean = 0x01, - Int8 = 0x02, - Int16 = 0x03, - Int32 = 0x04, - Uint8 = 0x05, - Uint16 = 0x06, - Uint32 = 0x07, - Real32 = 0x08, - Real64 = 0x09 -}; - -// 从站状态枚举 -enum class EcState : uint8_t { - Init = 0x01, - PreOp = 0x02, - SafeOp = 0x04, - Operational = 0x08 -}; - -// 操作模式枚举 -enum class EcOperateMode : int8_t { - ProfilePosition = 1, - Velocity = 2, - ProfileVelocity = 3, - ProfileTorque = 4, - Homing = 6, - CyclicSyncPosition = 8, - CyclicSyncVelocity = 9, - CyclicSyncTorque = 10 -}; - -// PDO 条目 -struct EcPdoEntry { - uint16_t index = 0; - uint8_t subindex = 0; - uint8_t bitlen = 0; - std::string name; - EcDataType dataType = EcDataType::Uint8; - - uint32_t makeMapValue() const { - return ((uint32_t)index << 16) | ((uint32_t)subindex << 8) | bitlen; - } -}; - -// PDO -struct EcPdo { - uint16_t index = 0; - uint8_t mapCount = 0; - std::vector entries; - std::string name; - bool isRxPdo = false; - - void updateMapCount() { - mapCount = entries.size(); - } -}; - -// 模块信息 -struct EcModule { - uint32_t moduleIdent = 0; - std::string name; - std::vector rxPdos; - std::vector txPdos; -}; - -// 从站配置信息 -struct EcSlaveConfig { - uint16_t slaveId = 0; - uint32_t productCode = 0; - uint32_t revisionNo = 0; - std::string name; - std::vector modules; - std::vector availableRxPdos; - std::vector availableTxPdos; -}; - -// PDO 偏移映射 -// ec_types.h 或 ec_master.h 中 -struct EcPdoOffsets { - // RxPDO 偏移量 (主站 -> 从站) - uint16_t controlWord = 0xFFFF; // 0x6040 - uint16_t targetPosition = 0xFFFF; // 0x607A - uint16_t targetVelocity = 0xFFFF; // 0x60FF - uint16_t targetTorque = 0xFFFF; // 0x6071 - uint16_t profileAcceleration = 0xFFFF; // 0x6083 (PP模式) - uint16_t profileDeceleration = 0xFFFF; // 0x6084 (PP模式) - uint16_t profileVelocity = 0xFFFF; // 0x6081 (PP模式) - uint16_t torqueSlope = 0xFFFF; // 0x6087 (PT模式) - uint16_t modeOfOperation = 0xFFFF; // 0x6060 - - // TxPDO 偏移量 (从站 -> 主站) - uint16_t statusWord = 0xFFFF; // 0x6041 - uint16_t actualPosition = 0xFFFF; // 0x6064 - uint16_t actualVelocity = 0xFFFF; // 0x606C - uint16_t actualTorque = 0xFFFF; // 0x6077 - uint16_t modeOfOperationDisplay = 0xFFFF;// 0x6061 - uint16_t errorCode = 0xFFFF; // 0x603F - uint16_t actualCurrent = 0xFFFF; // 0x6078 当前电流值 -}; - -#endif // EC_TYPES_H \ No newline at end of file diff --git a/cmvr-es/devices/motor/eyou_motor/ethercat/protocol/ec_xml_parser.cpp b/cmvr-es/devices/motor/eyou_motor/ethercat/protocol/ec_xml_parser.cpp deleted file mode 100644 index c721e3fe..00000000 --- a/cmvr-es/devices/motor/eyou_motor/ethercat/protocol/ec_xml_parser.cpp +++ /dev/null @@ -1,317 +0,0 @@ -// ec_xml_parser.cpp -#include "ec_xml_parser.h" -#include "rapidxml/xml_parser.h" -#include -#include -#include - -using namespace rapidxml; - -static EcDataType stringToDataType(const std::string& typeName) { - if (typeName == "BOOL") return EcDataType::Boolean; - if (typeName == "SINT") return EcDataType::Int8; - if (typeName == "INT") return EcDataType::Int16; - if (typeName == "DINT") return EcDataType::Int32; - if (typeName == "USINT") return EcDataType::Uint8; - if (typeName == "UINT") return EcDataType::Uint16; - if (typeName == "UDINT") return EcDataType::Uint32; - if (typeName == "REAL") return EcDataType::Real32; - return EcDataType::Uint8; -} - -static uint32_t parseHexValue(const std::string& str) { - if (str.empty()) return 0; - if (str[0] == '#' && str[1] == 'x') { - return strtoul(str.c_str() + 2, nullptr, 16); - } - return strtoul(str.c_str(), nullptr, 16); -} - -class EcXmlParser::Impl { -public: - std::map slaveConfigs; - std::unique_ptr xmlDoc; - - bool loadXmlFile(const std::string& path) { - try { - xmlDoc = std::make_unique(path); - return true; - } catch (const std::exception& e) { - printf("Failed to load XML file: %s, error: %s\n", path.c_str(), e.what()); - return false; - } - } - - void parsePdoEntry(const XmlNode& entryNode, EcPdoEntry& entry) { - if (entryNode.hasChild("Index")) { - XmlNode idxNode = entryNode.getChild("Index"); - entry.index = (uint16_t)parseHexValue(idxNode.getNodeValue()); - } - if (entryNode.hasChild("SubIndex")) { - XmlNode subIdxNode = entryNode.getChild("SubIndex"); - entry.subindex = (uint8_t)atoi(subIdxNode.getNodeValue().c_str()); - } - if (entryNode.hasChild("BitLen")) { - XmlNode bitLenNode = entryNode.getChild("BitLen"); - entry.bitlen = (uint8_t)atoi(bitLenNode.getNodeValue().c_str()); - } - if (entryNode.hasChild("Name")) { - XmlNode nameEntryNode = entryNode.getChild("Name"); - entry.name = nameEntryNode.getNodeValue(); - } - if (entryNode.hasChild("DataType")) { - XmlNode dataTypeNode = entryNode.getChild("DataType"); - entry.dataType = stringToDataType(dataTypeNode.getNodeValue()); - } - } - - void parsePdoMapping(const XmlNode& pdoNode, bool isRxPdo, EcModule& module) { - if (!pdoNode.hasChild("Index")) { - printf("Warning: PDO missing Index node, skipping\n"); - return; - } - - XmlNode indexNode = pdoNode.getChild("Index"); - uint16_t index = (uint16_t)parseHexValue(indexNode.getNodeValue()); - - EcPdo pdo; - pdo.index = index; - pdo.isRxPdo = isRxPdo; - - if (pdoNode.hasChild("Name")) { - XmlNode nameNode = pdoNode.getChild("Name"); - pdo.name = nameNode.getNodeValue(); - } - - std::vector entries = pdoNode.getChildren("Entry"); - for (const auto& entryNode : entries) { - EcPdoEntry entry; - parsePdoEntry(entryNode, entry); - pdo.entries.push_back(entry); - } - - pdo.updateMapCount(); - - if (isRxPdo) { - module.rxPdos.push_back(pdo); - } else { - module.txPdos.push_back(pdo); - } - } - - void parseModule(const XmlNode& moduleNode, EcSlaveConfig& config) { - if (!moduleNode.hasChild("Type")) { - printf("Warning: Module missing Type node, skipping\n"); - return; - } - - auto typeNode = moduleNode.getChild("Type"); - std::string moduleIdentStr = typeNode.getAttrDefault("ModuleIdent", ""); - if (moduleIdentStr.empty()) { - printf("Warning: Module missing ModuleIdent attribute, skipping\n"); - return; - } - - EcModule module; - module.moduleIdent = (uint32_t)parseHexValue(moduleIdentStr); - - if (moduleNode.hasChild("Name")) { - XmlNode nameNode = moduleNode.getChild("Name"); - module.name = nameNode.getNodeValue(); - } - - if (moduleNode.hasChild("RxPdo")) { - auto rxPdoNode = moduleNode.getChild("RxPdo"); - parsePdoMapping(rxPdoNode, true, module); - } - - if (moduleNode.hasChild("TxPdo")) { - auto txPdoNode = moduleNode.getChild("TxPdo"); - parsePdoMapping(txPdoNode, false, module); - } - - config.modules.push_back(module); - - for (const auto& pdo : module.rxPdos) { - config.availableRxPdos.push_back(pdo.index); - } - for (const auto& pdo : module.txPdos) { - config.availableTxPdos.push_back(pdo.index); - } - - printf(" Parsed module: 0x%08X - %s\n", module.moduleIdent, module.name.c_str()); - printf(" RxPDOs: %zu, TxPDOs: %zu\n", module.rxPdos.size(), module.txPdos.size()); - } - - bool parseSlaveConfigById(uint16_t slaveId, EcSlaveConfig& outConfig) { - if (!xmlDoc) return false; - - XmlNode rootNode = xmlDoc->getRoot(); - if (rootNode.getNodeName() != "EtherCATInfo") { - printf("Root node is not EtherCATInfo\n"); - return false; - } - - if (!rootNode.hasChild("Descriptions")) { - printf("No Descriptions node found\n"); - return false; - } - XmlNode descNode = rootNode.getChild("Descriptions"); - - outConfig = EcSlaveConfig(); - outConfig.slaveId = slaveId; - - if (descNode.hasChild("Devices")) { - XmlNode devicesNode = descNode.getChild("Devices"); - if (devicesNode.hasChild("Device")) { - XmlNode deviceNode = devicesNode.getChild("Device"); - if (deviceNode.hasChild("Type")) { - XmlNode typeNode = deviceNode.getChild("Type"); - std::string productCode = typeNode.getAttrDefault("ProductCode", ""); - if (!productCode.empty()) { - outConfig.productCode = (uint32_t)parseHexValue(productCode); - } - std::string revision = typeNode.getAttrDefault("RevisionNo", ""); - if (!revision.empty()) { - outConfig.revisionNo = (uint32_t)atoi(revision.c_str()); - } - } - if (deviceNode.hasChild("Name")) { - XmlNode nameNode = deviceNode.getChild("Name"); - outConfig.name = nameNode.getNodeValue(); - } - } - } - - if (!descNode.hasChild("Modules")) { - printf("Warning: No Modules node found under Descriptions\n"); - return !outConfig.modules.empty(); - } - - XmlNode modulesNode = descNode.getChild("Modules"); - printf("Found Modules node under Descriptions, parsing PDO configurations...\n"); - - std::vector moduleNodes = modulesNode.getChildren("Module"); - if (moduleNodes.empty()) { - printf("Warning: No Module element found under Modules\n"); - return !outConfig.modules.empty(); - } - - for (const auto& moduleNode : moduleNodes) { - parseModule(moduleNode, outConfig); - } - - printf("Total %zu modules parsed\n", moduleNodes.size()); - return true; - } -}; - -// ==================== EcXmlParser 公共接口 ==================== - -EcXmlParser::EcXmlParser() : pImpl(std::make_unique()) {} -EcXmlParser::~EcXmlParser() = default; - -bool EcXmlParser::loadXmlFile(const std::string& xmlPath) { - return pImpl->loadXmlFile(xmlPath); -} - -bool EcXmlParser::parseSlaveConfig(uint16_t slaveId, EcSlaveConfig& outConfig) { - if (!pImpl->parseSlaveConfigById(slaveId, outConfig)) { - return false; - } - pImpl->slaveConfigs[slaveId] = outConfig; - return true; -} - -EcSlaveConfig* EcXmlParser::getSlaveConfig(uint16_t slaveId) { - auto it = pImpl->slaveConfigs.find(slaveId); - return (it != pImpl->slaveConfigs.end()) ? &it->second : nullptr; -} - -const EcSlaveConfig* EcXmlParser::getSlaveConfig(uint16_t slaveId) const { - auto it = pImpl->slaveConfigs.find(slaveId); - return (it != pImpl->slaveConfigs.end()) ? &it->second : nullptr; -} - -std::vector EcXmlParser::getAvailableRxPdos(uint16_t slaveId) const { - auto it = pImpl->slaveConfigs.find(slaveId); - return (it != pImpl->slaveConfigs.end()) ? it->second.availableRxPdos : std::vector(); -} - -std::vector EcXmlParser::getAvailableTxPdos(uint16_t slaveId) const { - auto it = pImpl->slaveConfigs.find(slaveId); - return (it != pImpl->slaveConfigs.end()) ? it->second.availableTxPdos : std::vector(); -} - -const EcPdo* EcXmlParser::findPdo(uint16_t slaveId, uint16_t pdoIndex, bool isRxPdo) const { - auto it = pImpl->slaveConfigs.find(slaveId); - if (it == pImpl->slaveConfigs.end()) return nullptr; - - for (const auto& module : it->second.modules) { - const auto& pdos = isRxPdo ? module.rxPdos : module.txPdos; - for (const auto& pdo : pdos) { - if (pdo.index == pdoIndex) return &pdo; - } - } - return nullptr; -} - -const EcModule* EcXmlParser::findModuleByIndex(uint16_t slaveId, uint16_t pdoIndex, bool isRxPdo) const { - auto it = pImpl->slaveConfigs.find(slaveId); - if (it == pImpl->slaveConfigs.end()) return nullptr; - - for (const auto& module : it->second.modules) { - const auto& pdos = isRxPdo ? module.rxPdos : module.txPdos; - for (const auto& pdo : pdos) { - if (pdo.index == pdoIndex) return &module; - } - } - return nullptr; -} - -const EcModule* EcXmlParser::findModuleByIdent(uint16_t slaveId, uint32_t moduleIdent) const { - auto it = pImpl->slaveConfigs.find(slaveId); - if (it == pImpl->slaveConfigs.end()) return nullptr; - - for (const auto& module : it->second.modules) { - if (module.moduleIdent == moduleIdent) return &module; - } - return nullptr; -} - -void EcXmlParser::printSlaveInfo(uint16_t slaveId) const { - auto it = pImpl->slaveConfigs.find(slaveId); - if (it == pImpl->slaveConfigs.end()) { - printf("Slave %d not configured\n", slaveId); - return; - } - - const EcSlaveConfig& config = it->second; - printf("\n========== Slave %d Configuration ==========\n", slaveId); - printf("Name: %s\n", config.name.c_str()); - printf("Product Code: 0x%08X\n", config.productCode); - printf("Revision: %u\n", config.revisionNo); - - for (const auto& module : config.modules) { - printf("\n[Module 0x%08X] %s\n", module.moduleIdent, module.name.c_str()); - for (const auto& pdo : module.rxPdos) { - printf(" RxPDO 0x%04X - %s (%d entries)\n", pdo.index, pdo.name.c_str(), pdo.mapCount); - for (const auto& entry : pdo.entries) { - printf(" -> 0x%04X:%02X (%d bits) - %s\n", - entry.index, entry.subindex, entry.bitlen, entry.name.c_str()); - } - } - for (const auto& pdo : module.txPdos) { - printf(" TxPDO 0x%04X - %s (%d entries)\n", pdo.index, pdo.name.c_str(), pdo.mapCount); - for (const auto& entry : pdo.entries) { - printf(" <- 0x%04X:%02X (%d bits) - %s\n", - entry.index, entry.subindex, entry.bitlen, entry.name.c_str()); - } - } - } - printf("==========================================\n"); -} - -void EcXmlParser::clear() { - pImpl->slaveConfigs.clear(); -} \ No newline at end of file diff --git a/cmvr-es/devices/motor/eyou_motor/ethercat/protocol/ec_xml_parser.h b/cmvr-es/devices/motor/eyou_motor/ethercat/protocol/ec_xml_parser.h deleted file mode 100644 index c7b72536..00000000 --- a/cmvr-es/devices/motor/eyou_motor/ethercat/protocol/ec_xml_parser.h +++ /dev/null @@ -1,41 +0,0 @@ -// ec_xml_parser.h -#ifndef EC_XML_PARSER_H -#define EC_XML_PARSER_H - -#include "ec_types.h" -#include -#include -#include - -// XML PDO配置解析器类(仅负责解析XML) -class EcXmlParser { -public: - EcXmlParser(); - ~EcXmlParser(); - - bool loadXmlFile(const std::string& xmlPath); - bool parseSlaveConfig(uint16_t slaveId, EcSlaveConfig& outConfig); - - // 获取已解析的配置 - EcSlaveConfig* getSlaveConfig(uint16_t slaveId); - const EcSlaveConfig* getSlaveConfig(uint16_t slaveId) const; - - // 查询接口 - std::vector getAvailableRxPdos(uint16_t slaveId) const; - std::vector getAvailableTxPdos(uint16_t slaveId) const; - const EcPdo* findPdo(uint16_t slaveId, uint16_t pdoIndex, bool isRxPdo) const; - const EcModule* findModuleByIndex(uint16_t slaveId, uint16_t pdoIndex, bool isRxPdo) const; - const EcModule* findModuleByIdent(uint16_t slaveId, uint32_t moduleIdent) const; - - // 打印信息 - void printSlaveInfo(uint16_t slaveId) const; - - // 清空所有配置 - void clear(); - -private: - class Impl; - std::unique_ptr pImpl; -}; - -#endif // EC_XML_PARSER_H \ No newline at end of file diff --git a/cmvr-es/devices/motor/eyou_motor/eyou_motor.cpp b/cmvr-es/devices/motor/eyou_motor/eyou_motor.cpp deleted file mode 100644 index d9e2f043..00000000 --- a/cmvr-es/devices/motor/eyou_motor/eyou_motor.cpp +++ /dev/null @@ -1,5 +0,0 @@ -// -// Created by cmvr on 2026/3/30. -// - -#include "eyou_motor.h" \ No newline at end of file diff --git a/cmvr-es/devices/motor/eyou_motor/eyou_motor.h b/cmvr-es/devices/motor/eyou_motor/eyou_motor.h deleted file mode 100644 index da4d8ae3..00000000 --- a/cmvr-es/devices/motor/eyou_motor/eyou_motor.h +++ /dev/null @@ -1,46 +0,0 @@ -// -// Created by cmvr on 2026/3/30. -// -#pragma once -#include "../abstract_motor.h" -#include "motor/motor_protocol_interface.h" -#include "motor/eyou_motor/ethercat/eyou_motor_ethercat_protocol.h" - -namespace cmvr { - namespace device { - class EYouMotor : public AbstractMotor { - public: - EYouMotor(const XmlNode &config,uint8_t node_id):AbstractMotor(config,node_id){} - EYouMotor(const XmlNode &config):AbstractMotor(config) { - info_.id = config.getAttrDefault("slave", -1); - info_.joint_name = config.getAttrString("jointName"); - info_.limitQ = config.getAttrDefault("limitQ", 3.14f); - info_.limitQd = config.getAttrDefault("limitQd", 0.5f); - node_id_ = info_.id; - } - - EYouMotor(const cmvr::config::EthercatMotorConfig& config) : AbstractMotor() { - info_.id = config.slave(); - info_.joint_name = config.jointname(); - info_.limitQ = config.limitqlb(); - info_.limitQd = config.limitqd(); - node_id_ = info_.id; - } - /** - * 电机初始化在具体的类中完成 - */ - void init() override { - if (!protocol_) { - throw std::runtime_error("Protocol not set for motor"); - } - if (protocol_->comm_proto == MotorProtocolInterface::CommProto::ETHERCAT ) { - auto ethercat_protocol = std::dynamic_pointer_cast(protocol_); - ethercat_protocol->initNode(node_id_); - ethercat_protocol->setQdd(node_id_,1); - } - } - }; - - - } -} diff --git a/cmvr-es/devices/motor/motor_protocol_interface.h b/cmvr-es/devices/motor/motor_protocol_interface.h index 6b49aa06..634a715c 100644 --- a/cmvr-es/devices/motor/motor_protocol_interface.h +++ b/cmvr-es/devices/motor/motor_protocol_interface.h @@ -15,8 +15,7 @@ namespace cmvr { public: enum class CommProto : uint8_t { CANOPEN = 1, - ETHERCAT = 2, - CUSTOM = 3 + CUSTOM = 2 }; virtual ~MotorProtocolInterface() = default; diff --git a/cmvr-es/devices/motor/ti5_motor/ti5_motor.h b/cmvr-es/devices/motor/ti5_motor/ti5_motor.h index 65f44ddc..cfc9bde9 100644 --- a/cmvr-es/devices/motor/ti5_motor/ti5_motor.h +++ b/cmvr-es/devices/motor/ti5_motor/ti5_motor.h @@ -1,12 +1,11 @@ // // Created by lgv on 2025/8/1. // -#pragma once #include "../abstract_motor.h" #include "motor/motor_protocol_interface.h" #include "motor/ti5_motor/canopen/ti5_motor_canopen_protocol.h" -#include "cmvr/config/robot_config/robot_config.pb.h" + /** * class: Ti5Motor 完成电机硬件的相关工作,软件相关的在protocol 中完成 */ @@ -23,15 +22,6 @@ namespace cmvr { node_id_ = info_.id; } - Ti5Motor(const cmvr::config::MotorConfig& config) : AbstractMotor() { - info_.id = config.id(); - info_.joint_name = config.jointname(); - info_.limitQ = config.limitqlb(); - info_.limitQd = config.limitqd(); - node_id_ = info_.id; - } - - /** * 电机初始化在具体的类中完成 */ diff --git a/cmvr-es/devices/robot/abstract_robot.h b/cmvr-es/devices/robot/abstract_robot.h index 91daf0ae..5759c3af 100644 --- a/cmvr-es/devices/robot/abstract_robot.h +++ b/cmvr-es/devices/robot/abstract_robot.h @@ -41,7 +41,6 @@ namespace cmvr::device{ class AbstractRobot: public AbstractDevice { public: - AbstractRobot() = default; explicit AbstractRobot(const XmlNode &config): AbstractDevice(config) {} ~AbstractRobot() override=default; diff --git a/cmvr-es/devices/robot/humanoid_robot/include/humanoid_robot.h b/cmvr-es/devices/robot/humanoid_robot/include/humanoid_robot.h index 9cd2d11d..7770997b 100644 --- a/cmvr-es/devices/robot/humanoid_robot/include/humanoid_robot.h +++ b/cmvr-es/devices/robot/humanoid_robot/include/humanoid_robot.h @@ -35,8 +35,6 @@ #include "planner/joint_space_planner/include/joint_space_planner_creator.h" #include "planner/joint_space_planner/include/joint_space_planner.h" #include "ik_solver/include/pinocchio_dls_ik_solver.h" -#include "cmvr/config/robot_config/robot_config.pb.h" - namespace cmvr::device{ @@ -51,10 +49,12 @@ namespace cmvr::device{ ROBOT_ERROR } RobotStateMachine; +// typedef enum { +// PPM_MODE, PVM_MODE, PTM_MODE, CSP_MODE, CSV_MODE, CSC_MODE +// } RobotRunningMode; + public: explicit HumanoidRobot(const XmlNode& cfg); - HumanoidRobot(const cmvr::config::HumanRobotConfig& config); - HumanoidRobot(const cmvr::config::EthercatRobotConfig& config); ~HumanoidRobot(); void init() override; @@ -232,7 +232,6 @@ namespace cmvr::device{ std::atomic is_head_busy_{false}, is_waist_busy_{false}; - std::string urdf_path_ = ""; }; diff --git a/cmvr-es/devices/robot/humanoid_robot/src/humanoid_robot.cpp b/cmvr-es/devices/robot/humanoid_robot/src/humanoid_robot.cpp index 9cc52666..0146c053 100644 --- a/cmvr-es/devices/robot/humanoid_robot/src/humanoid_robot.cpp +++ b/cmvr-es/devices/robot/humanoid_robot/src/humanoid_robot.cpp @@ -49,87 +49,6 @@ HumanoidRobot::HumanoidRobot(const XmlNode &cfg) : AbstractRobot(cfg) { } } - -template -HumanoidRobot::HumanoidRobot(const cmvr::config::HumanRobotConfig& config) { - try { - id_ = config.id(); - urdf_path_ = config.urdf(); - dof_ = DOF; - if (!pathExists(config.urdf())) { - throw runtime_error("urdf file does not exist"); - } - auto rcfg = cmvr::dyn::LoadRobotFromURDF( - config.urdf(), config.baselink()); - m_robot_ = std::make_shared >(rcfg); - - // 解析jointNames字符串,按逗号分隔 - joint_names_ = splitString(config.jointnames(), ","); - // 解析linkNames字符串,按逗号分隔 - link_names_ = splitString(config.linknames(), ","); - - if (joint_names_.size() != dof_) { - throw runtime_error("joint names size mismatched with dof"); - } - - m_state_ = m_robot_->MakeState(link_names_, joint_names_); - m_cctrl_ = make_shared >(m_robot_); - upd_freq_ = config.updfreq(); - CSP_buffer_ = make_shared >(config.buffersize()); - CSV_buffer_ = make_shared >(config.buffersize()); - CSC_buffer_ = make_shared >(config.buffersize()); - - upd_freq_ = 1000; - upd_timer_ = make_shared(); - upd_timer_->start(chrono::nanoseconds(1000 / upd_freq_ * 1000), - [this] { update_state_(); }); - rsm_.store(ROBOT_READY); - } catch (exception &e) { - LOG(ERROR) << "HumanoidRobot init failed, id=" << id_; - throw runtime_error(e.what()); - } -} - -template -HumanoidRobot::HumanoidRobot(const cmvr::config::EthercatRobotConfig& config) { - try { - id_ = config.id(); - urdf_path_ = config.urdf(); - dof_ = DOF; - if (!pathExists(config.urdf())) { - throw runtime_error("urdf file does not exist"); - } - auto rcfg = cmvr::dyn::LoadRobotFromURDF( - config.urdf(), config.baselink()); - m_robot_ = std::make_shared >(rcfg); - - // 解析jointNames字符串,按逗号分隔 - joint_names_ = splitString(config.jointnames(), ","); - // 解析linkNames字符串,按逗号分隔 - link_names_ = splitString(config.linknames(), ","); - - if (joint_names_.size() != dof_) { - throw runtime_error("joint names size mismatched with dof"); - } - - m_state_ = m_robot_->MakeState(link_names_, joint_names_); - m_cctrl_ = make_shared >(m_robot_); - upd_freq_ = config.updfreq(); - CSP_buffer_ = make_shared >(config.buffersize()); - CSV_buffer_ = make_shared >(config.buffersize()); - CSC_buffer_ = make_shared >(config.buffersize()); - - upd_freq_ = 1000; - upd_timer_ = make_shared(); - upd_timer_->start(chrono::nanoseconds(1000 / upd_freq_ * 1000), - [this] { update_state_(); }); - rsm_.store(ROBOT_READY); - } catch (exception &e) { - LOG(ERROR) << "HumanoidRobot init failed, id=" << id_; - throw runtime_error(e.what()); - } -} - template void HumanoidRobot::init() { motor_manager_ = MotorsInfo::getInstance()->getMotorManager(); @@ -140,7 +59,7 @@ void HumanoidRobot::init() { std::vector(7, 1.0)); - ik_solver_ = std::make_shared("/home/cmvr/Projects/cmvr-es/model/xiaoyan_description/dual_arm.urdf", + ik_solver_ = std::make_shared("/home/lgv/cmvr/cmvr-es/model/xiaoyan_description/dual_arm.urdf", "PELVIS_S", "R_WRIST_R_S", "R_FINGER_TIP_FIXED"); diff --git a/cmvr-es/devices/robot/humanoid_robot/src/humanoid_robot_test.cpp b/cmvr-es/devices/robot/humanoid_robot/src/humanoid_robot_test.cpp index ed16d61d..d1b47e4e 100644 --- a/cmvr-es/devices/robot/humanoid_robot/src/humanoid_robot_test.cpp +++ b/cmvr-es/devices/robot/humanoid_robot/src/humanoid_robot_test.cpp @@ -990,21 +990,3 @@ TEST(HumanoidRobotTest,IBVSWithRealRobotTrackedPoint) { EXPECT_GT(ok_steps, 0) << "No successful tracked-point IBVS control steps."; } - - -TEST(HumanoidRobotTest, EYouTest) { - auto& dmgr = DeviceManager::getInstance(); - auto robot_abs = dmgr.getDevice("ec_robot_01"); - auto robot = std::dynamic_pointer_cast>(robot_abs); - ASSERT_NE(robot, nullptr) << "hc01 is not HumanoidRobot<14>"; - - std::vector init_cmd = { - {"L_SHOULDER_P", 0.1} - }; - // robot->moveJ(init_cmd, 0.1, 1.0); - - // robot->calibrateZeroQ("L_SHOULDER_P"); - - robot->torqueOff("L_SHOULDER_P"); - std::this_thread::sleep_for(std::chrono::milliseconds(1000)); -} diff --git a/cmvr-es/main.cpp b/cmvr-es/main.cpp index c5531829..ab0f76d3 100644 --- a/cmvr-es/main.cpp +++ b/cmvr-es/main.cpp @@ -9,7 +9,6 @@ #include #include "service/grpc/include/server_runner.h" #include "utils/base/include/logger.h" - using namespace cmvr::service; int main(int argc, char* argv[]) { std::string config_path; diff --git a/cmvr-es/monitor/abstract_monitor.h b/cmvr-es/monitor/abstract_monitor.h index 244e5722..6b707e9c 100644 --- a/cmvr-es/monitor/abstract_monitor.h +++ b/cmvr-es/monitor/abstract_monitor.h @@ -13,7 +13,6 @@ namespace cmvr::monitor{ class AbstractMonitor { public: - AbstractMonitor() = default; explicit AbstractMonitor(const XmlNode &cfg){ cfg_ = cfg; freq_ = cfg_.getAttrDefault("freq", 1); diff --git a/cmvr-es/monitor/diskmonitor/include/disk_monitor.h b/cmvr-es/monitor/diskmonitor/include/disk_monitor.h index a22256b3..89664362 100644 --- a/cmvr-es/monitor/diskmonitor/include/disk_monitor.h +++ b/cmvr-es/monitor/diskmonitor/include/disk_monitor.h @@ -8,21 +8,17 @@ #include #include "../../abstract_monitor.h" -#include "cmvr/config/monitor_config/monitor_config.pb.h" namespace cmvr::monitor { class DiskMonitor final : public AbstractMonitor{ public: explicit DiskMonitor(const XmlNode& config); - explicit DiskMonitor(const cmvr::config::DiskMonitorConfig& config); // 新增proto构造函数 ~DiskMonitor(); protected: bool check_() override; bool execution_() override; private: - void initFromProto(const cmvr::config::DiskMonitorConfig& config); // 新增初始化方法 - std::unordered_map folders_; std::vector dirsToClean_; // 需要清理的目录列表 mutable std::mutex mutex_; // 保护共享资源的互斥锁 @@ -31,4 +27,6 @@ namespace cmvr::monitor { }; } + + #endif //FOLDER_MONITOR_H \ No newline at end of file diff --git a/cmvr-es/monitor/diskmonitor/src/disk_monitor.cpp b/cmvr-es/monitor/diskmonitor/src/disk_monitor.cpp index 9f4497cf..20630615 100644 --- a/cmvr-es/monitor/diskmonitor/src/disk_monitor.cpp +++ b/cmvr-es/monitor/diskmonitor/src/disk_monitor.cpp @@ -14,6 +14,7 @@ using namespace cmvr::monitor; namespace fs = std::filesystem; using namespace std::chrono; + // 文件信息结构体 struct FileInfo { std::string path; @@ -37,40 +38,51 @@ bool deleteOldestFile(const std::string& directoryPath) { struct dirent* entry; struct stat fileStat; + // 遍历目录中的每个条目 while ((entry = readdir(dir)) != nullptr) { + // 跳过 "." 和 ".." if (std::string(entry->d_name) == "." || std::string(entry->d_name) == "..") { continue; } + // 构建完整路径 std::string filePath = directoryPath + "/" + entry->d_name; + // 获取文件状态信息 if (stat(filePath.c_str(), &fileStat) == -1) { std::cerr << "Failed to get file status: " << filePath << std::endl; continue; } + // 只处理普通文件 if (S_ISREG(fileStat.st_mode)) { FileInfo fileInfo; fileInfo.path = filePath; + + // 优先使用 st_birthtime (如果可用),否则使用 st_ctime #ifdef HAVE_STRUCT_STAT_ST_BIRTHTIME fileInfo.creationTime = fileStat.st_birthtime; #else fileInfo.creationTime = fileStat.st_ctime; #endif + files.push_back(fileInfo); } } closedir(dir); + // 如果没有找到文件,返回失败 if (files.empty()) { std::cout << "No files found in directory: " << directoryPath << std::endl; return false; } + // 按创建时间排序 std::sort(files.begin(), files.end(), compareByCreationTime); - std::string oldestFilePath = files[0].path; + // 删除最早创建的文件 + std::string oldestFilePath = files[0].path; if (unlink(oldestFilePath.c_str()) == -1) { std::cerr << "Failed to delete file: " << oldestFilePath << std::endl; return false; @@ -80,11 +92,11 @@ bool deleteOldestFile(const std::string& directoryPath) { return true; } + bool pathExists(const std::string& path) { struct stat buffer; return (stat(path.c_str(), &buffer) == 0); } - int countFilesInDirectory(const std::string& path) { DIR* dir = opendir(path.c_str()); if (!dir) { @@ -95,7 +107,9 @@ int countFilesInDirectory(const std::string& path) { int count = 0; struct dirent* entry; + // 遍历目录中的每个条目 while ((entry = readdir(dir)) != nullptr) { + // 跳过 "." 和 ".." if (std::string(entry->d_name) != "." && std::string(entry->d_name) != "..") { count++; } @@ -105,8 +119,8 @@ int countFilesInDirectory(const std::string& path) { return count; } -// XML构造函数 DiskMonitor::DiskMonitor(const XmlNode& config) : AbstractMonitor(config){ + try { for (auto &node: config.getChildren()){ string fileDir = node.getAttrString("fileDir"); @@ -124,42 +138,13 @@ DiskMonitor::DiskMonitor(const XmlNode& config) : AbstractMonitor(config){ throw runtime_error("[DiskMonitor] (DiskMonitor): Failed "+ string(e.what())); } } - -// Proto构造函数 -DiskMonitor::DiskMonitor(const cmvr::config::DiskMonitorConfig& config) : AbstractMonitor() { - try { - initFromProto(config); - is_initialized_ = true; - LOG(INFO) << "[DiskMonitor]: Init DiskMonitor from proto Success"; - } - catch (const exception& e) { - LOG(ERROR) << "[DiskMonitor] (DiskMonitor): Failed " << e.what(); - throw runtime_error("[DiskMonitor] (DiskMonitor): Failed " + string(e.what())); - } -} - -// Proto初始化方法 -void DiskMonitor::initFromProto(const cmvr::config::DiskMonitorConfig& config) { - // 设置监控ID和频率(通过AbstractMonitor) - // 假设AbstractMonitor有setId和setFreq方法,如果没有需要添加 - // 这里根据你的AbstractMonitor实现来调整 - - for (const auto& folder : config.folders()) { - string fileDir = folder.filedir(); - if (!pathExists(fileDir)) { - throw runtime_error("[DiskMonitor] (initFromProto): path not exists: " + fileDir); - } - int maxVolume = folder.maxvolume(); - folders_[fileDir] = maxVolume; - } -} - DiskMonitor::~DiskMonitor() { folders_.clear(); } - bool DiskMonitor::check_() { + // 检查当前目录下的文件数量是否超过最大值 bool allOk = true; + // 遍历所有监控的文件夹 for (const auto& folder : folders_) { const std::string& path = folder.first; size_t maxFiles = folder.second; @@ -169,21 +154,28 @@ bool DiskMonitor::check_() { continue; } int nFileCount = countFilesInDirectory(path); - if (nFileCount > maxFiles) { + if (nFileCount > maxFiles) + { allOk = false; dirsToClean_.push_back(path); std::cout << "path need remove oldest file: " << path << std::endl; } + + } catch (const fs::filesystem_error& e) { std::cerr << "Filesystem error: " << e.what() << '\n'; } catch (const std::exception& e) { std::cerr << "Unexpected error: " << e.what() << '\n'; } } + return allOk; } + bool DiskMonitor::execution_() { + + // 删除创建时间最早的文件 std::lock_guard lock(mutex_); bool success = true; @@ -191,6 +183,6 @@ bool DiskMonitor::execution_() { deleteOldestFile(path); } - dirsToClean_.clear(); + dirsToClean_.clear(); // 清理完成后清空列表 return success; } \ No newline at end of file diff --git a/cmvr-es/monitor_manager/include/monitor_factory.h b/cmvr-es/monitor_manager/include/monitor_factory.h index cdd879a3..a0dbc900 100644 --- a/cmvr-es/monitor_manager/include/monitor_factory.h +++ b/cmvr-es/monitor_manager/include/monitor_factory.h @@ -5,8 +5,8 @@ #ifndef MONITOR_FACTORY_H #define MONITOR_FACTORY_H -#include "monitor/abstract_monitor.h" -#include "cmvr/config/monitor_config/monitor_config.pb.h" + +#include "../../monitor/abstract_monitor.h" namespace cmvr::monitor { @@ -14,17 +14,14 @@ namespace cmvr::monitor { public: MonitorFactory() = default; - - // 原有的XML方式 template std::shared_ptr create(const XmlNode& cfg); std::shared_ptr create(const XmlNode& cfg); - - // 新增proto方式 - template - std::shared_ptr create(const cmvr::config::DiskMonitorConfig& config); - std::shared_ptr create(const cmvr::config::DiskMonitorConfig& config); }; + + } + + #endif //MONITOR_FACTORY_H \ No newline at end of file diff --git a/cmvr-es/monitor_manager/include/monitor_manager.h b/cmvr-es/monitor_manager/include/monitor_manager.h index 422ece1f..8c53a401 100644 --- a/cmvr-es/monitor_manager/include/monitor_manager.h +++ b/cmvr-es/monitor_manager/include/monitor_manager.h @@ -15,38 +15,23 @@ namespace cmvr::monitor { public: MonitorManager(const MonitorManager&) = delete; MonitorManager& operator=(const MonitorManager&) = delete; - - // 原有的XML方式 static MonitorManager& getInstance(const XmlNode &cfg); - - // 新增proto方式 - static MonitorManager& getInstance(const cmvr::config::MonitorManagerConfig& config); - static MonitorManager& getInstance(); static void destroyInstance(); private: - // 原有的XML方式 - explicit MonitorManager(const XmlNode &cfg); - - // 新增proto方式 - explicit MonitorManager(const cmvr::config::MonitorManagerConfig& config); - - void init_monitors(); // XML方式 - void init_monitors_from_proto(); // Proto方式 - + explicit MonitorManager (const XmlNode &cfg); + void init_monitors(); private: static std::once_flag init_flag_; static std::shared_ptr instance_; - // 两种配置方式,只有一个有效 - bool use_proto_ = false; - XmlNode cfg_; // XML配置 - cmvr::config::MonitorManagerConfig proto_cfg_; // Proto配置 - + XmlNode cfg_; std::shared_ptr monitor_factory_; std::unordered_map> monitors_; }; } + + #endif //MONITOR_MANAGER_H \ No newline at end of file diff --git a/cmvr-es/monitor_manager/src/monitor_factory.cpp b/cmvr-es/monitor_manager/src/monitor_factory.cpp index e770f294..821373e2 100644 --- a/cmvr-es/monitor_manager/src/monitor_factory.cpp +++ b/cmvr-es/monitor_manager/src/monitor_factory.cpp @@ -3,12 +3,12 @@ // #include "../include/monitor_factory.h" + #include "../../monitor/diskmonitor/include/disk_monitor.h" using namespace std; using namespace cmvr::monitor; -// XML方式的模板特化 template std::shared_ptr MonitorFactory::create(const XmlNode&); template @@ -17,30 +17,8 @@ std::shared_ptr MonitorFactory::create(const XmlNode& cfg) { return std::make_shared(cfg); } else { - LOG(ERROR) << "[MonitorFactory]: Unsupported monitor type"; - throw runtime_error("[MonitorFactory]: Unsupported monitor type"); + LOG(ERROR) << "[DeviceFactory]: Unsupported device type"; + throw runtime_error("[DeviceFactory]: Unsupported device type"); } } -// Proto方式的模板特化 -template std::shared_ptr MonitorFactory::create(const cmvr::config::DiskMonitorConfig&); - -template -std::shared_ptr MonitorFactory::create(const cmvr::config::DiskMonitorConfig& config) { - if constexpr (std::is_same_v) { - return std::make_shared(config); - } - else { - LOG(ERROR) << "[MonitorFactory]: Unsupported monitor type"; - throw runtime_error("[MonitorFactory]: Unsupported monitor type"); - } -} - -std::shared_ptr MonitorFactory::create(const XmlNode& cfg) { - // 保留原有实现 - return nullptr; -} - -std::shared_ptr MonitorFactory::create(const cmvr::config::DiskMonitorConfig& config) { - return std::make_shared(config); -} \ No newline at end of file diff --git a/cmvr-es/monitor_manager/src/monitor_manager.cpp b/cmvr-es/monitor_manager/src/monitor_manager.cpp index dde9b386..2dd9e2cf 100644 --- a/cmvr-es/monitor_manager/src/monitor_manager.cpp +++ b/cmvr-es/monitor_manager/src/monitor_manager.cpp @@ -3,7 +3,8 @@ // #include "../include/monitor_manager.h" -#include "monitor/diskmonitor/include/disk_monitor.h" + +#include "../../monitor/diskmonitor/include/disk_monitor.h" using namespace std; using namespace cmvr::monitor; @@ -11,19 +12,11 @@ using namespace cmvr::monitor; std::shared_ptr MonitorManager::instance_ = nullptr; std::once_flag MonitorManager::init_flag_; -// XML方式构造函数 -MonitorManager::MonitorManager(const XmlNode& cfg) : use_proto_(false), cfg_(cfg) { +MonitorManager::MonitorManager(const XmlNode& cfg) { monitor_factory_ = make_shared(); + cfg_ = cfg; init_monitors(); } - -// Proto方式构造函数 -MonitorManager::MonitorManager(const cmvr::config::MonitorManagerConfig& config) : use_proto_(true), proto_cfg_(config) { - monitor_factory_ = make_shared(); - init_monitors_from_proto(); -} - -// XML方式单例 MonitorManager& MonitorManager::getInstance(const XmlNode& cfg) { std::call_once(init_flag_, [&cfg] { instance_.reset(new MonitorManager(cfg)); @@ -31,17 +24,9 @@ MonitorManager& MonitorManager::getInstance(const XmlNode& cfg) { return *instance_; } -// Proto方式单例 -MonitorManager& MonitorManager::getInstance(const cmvr::config::MonitorManagerConfig& config) { - std::call_once(init_flag_, [&config] { - instance_.reset(new MonitorManager(config)); - }); - return *instance_; -} - MonitorManager& MonitorManager::getInstance() { if (!instance_) { - throw std::runtime_error("[MonitorManager] (getInstance): MonitorManager not initialized. Call getInstance() first."); + throw std::runtime_error("[MonitorManager] (getInstance): MonitorManager not initialized. Call getInstance(const XmlNode&) first."); } return *instance_; } @@ -50,14 +35,13 @@ void MonitorManager::destroyInstance() { instance_.reset(); } -// XML方式初始化 void MonitorManager::init_monitors() { - try { + try{ auto diskmonitor_node = cfg_.getChild("DiskMonitor"); auto id = diskmonitor_node.getAttrString("id"); auto monitor = monitor_factory_->create(diskmonitor_node); if (monitor == nullptr) { - throw std::runtime_error("[MonitorManager]: Factory returned nullptr for monitor ID: " + id); + throw std::runtime_error("[DeviceManager]: Factory returned nullptr for device ID: " + id); } monitors_[id] = monitor; } @@ -65,31 +49,5 @@ void MonitorManager::init_monitors() { LOG(ERROR) << e.what(); throw runtime_error(e.what()); } -} -// Proto方式初始化 -void MonitorManager::init_monitors_from_proto() { - try { - if (!proto_cfg_.has_disk_monitor()) { - LOG(WARNING) << "[MonitorManager]: No disk_monitor config found"; - return; - } - - const auto& disk_monitor_cfg = proto_cfg_.disk_monitor(); - if (!disk_monitor_cfg.enable()) { - LOG(INFO) << "[MonitorManager]: Disk monitor is disabled"; - return; - } - - auto id = disk_monitor_cfg.id(); - auto monitor = monitor_factory_->create(disk_monitor_cfg); - if (monitor == nullptr) { - throw std::runtime_error("[MonitorManager]: Factory returned nullptr for monitor ID: " + id); - } - monitors_[id] = monitor; - } - catch (const exception& e) { - LOG(ERROR) << e.what(); - throw runtime_error(e.what()); - } } \ No newline at end of file diff --git a/cmvr-es/service/CMakeLists.txt b/cmvr-es/service/CMakeLists.txt index 98ea4199..4ef50063 100644 --- a/cmvr-es/service/CMakeLists.txt +++ b/cmvr-es/service/CMakeLists.txt @@ -9,7 +9,6 @@ add_library(service grpc/src/grpc_humanoid_robot_service.cpp grpc/src/grpc_hlc_service.cpp grpc/src/server_runner.cpp - grpc/src/grpc_agv_service.cpp ) target_include_directories(service PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/cmvr-es/service/grpc/include/grpc_agv_service.h b/cmvr-es/service/grpc/include/grpc_agv_service.h deleted file mode 100644 index 02253dda..00000000 --- a/cmvr-es/service/grpc/include/grpc_agv_service.h +++ /dev/null @@ -1,278 +0,0 @@ -// -// Created by xtkuang on 2025/6/1. -// -/** - * @file grpc_agv_service.h - * @brief gRPC AGV 服务实现类的头文件。 - * 继承自自动生成的 api::AgvService::Service,提供所有 AGV 相关 RPC 接口的具体实现。 - * 通过 DeviceManager 获取对应的 AGV 设备实例(如 AgvSrc1100),将请求转发至设备层。 - * @author xtkuang - * @date 2025-06-01 - */ - -#ifndef GRPC_AGV_SERVICE_H -#define GRPC_AGV_SERVICE_H - -#include "cmvr/api/agv_service.grpc.pb.h" -#include "utils/base/include/grpc_utils.h" -#include "device_manager/include/device_manager.h" - -namespace cmvr::service { - -/** - * @brief gRPC AGV 服务实现类。 - * 负责将 gRPC 请求转换为对具体 AGV 设备的调用,并统一处理异常、日志和时间戳。 - */ -class gRPCAGVServiceImpl final : public api::AgvService::Service { -public: - /** - * @brief 构造函数,获取 DeviceManager 单例引用。 - */ - gRPCAGVServiceImpl(); - - /** - * @brief 默认析构函数。 - */ - ~gRPCAGVServiceImpl() override = default; - - // ===================== 基本状态查询接口 ===================== - - /** - * @brief 获取 AGV 基本信息(命令码 1000)。 - * @param context gRPC 上下文(未使用)。 - * @param request 请求消息,包含设备 ID(header.device_id)。 - * @param response 响应消息,填充 AGV 状态信息(版本、型号、IP、MAC 等)。 - * @return 始终返回 grpc::Status::OK,错误信息通过 response.header 返回。 - */ - grpc::Status GetStatusInfo(grpc::ServerContext* context, - const api::GetAgvStatusInfoCommand_Request* request, - api::GetAgvStatusInfoCommand_Feedback* response) override; - - /** - * @brief 查询电池状态(命令码 1007)。 - * @param context gRPC 上下文(未使用)。 - * @param request 请求消息,包含设备 ID 和可选的 simple 标志。 - * @param response 响应消息,填充电池电量、温度、充放电状态等。 - * @return 始终返回 grpc::Status::OK,错误信息通过 response.header 返回。 - */ - grpc::Status GetBatteryStatus(grpc::ServerContext* context, - const api::RobotStatusBatteryCommand_Request* request, - api::RobotStatusBatteryCommand_Feedback* response) override; - - /** - * @brief 查询机器人当前位置(命令码 1004)。 - * @param context gRPC 上下文(未使用)。 - * @param request 请求消息,包含设备 ID。 - * @param response 响应消息,填充坐标、朝向角、置信度、当前站点等。 - * @return 始终返回 grpc::Status::OK,错误信息通过 response.header 返回。 - */ - grpc::Status GetRobotLocation(grpc::ServerContext* context, - const api::RobotStatusLocCommand_Request* request, - api::RobotStatusLocCommand_Feedback* response) override; - - // ===================== 地图管理接口 ===================== - - /** - * @brief 下载指定地图(命令码 4011)。 - * @param context gRPC 上下文(未使用)。 - * @param request 请求消息,包含设备 ID 和地图名称。 - * @param response 响应消息,返回地图 JSON 内容或错误码。 - * @return 始终返回 grpc::Status::OK,错误信息通过 response.header 返回。 - */ - grpc::Status RobotConfigDownloadMap(grpc::ServerContext* context, - const api::RobotConfigDownloadMapCommand_Request* request, - api::RobotConfigDownloadMapCommand_Feedback* response) override; - - /** - * @brief 查询地图状态(命令码 1300)。 - * @param context gRPC 上下文(未使用)。 - * @param request 请求消息,包含设备 ID。 - * @param response 响应消息,返回当前地图名称、所有地图列表及文件详情。 - * @return 始终返回 grpc::Status::OK,错误信息通过 response.header 返回。 - */ - grpc::Status GetMapStatus(grpc::ServerContext* context, - const api::RobotStatusMapCommand_Request* request, - api::RobotStatusMapCommand_Feedback* response) override; - - /** - * @brief 上传地图(命令码 4010)。 - * @param context gRPC 上下文(未使用)。 - * @param request 请求消息,包含设备 ID 和完整的地图 JSON 字符串。 - * @param response 响应消息,返回上传结果(ret_code)。 - * @return 始终返回 grpc::Status::OK,错误信息通过 response.header 返回。 - */ - grpc::Status RobotConfigUploadMap(grpc::ServerContext* context, - const api::RobotConfigUploadMapCommand_Request* request, - api::RobotConfigUploadMapCommand_Feedback* response) override; - - // ===================== 控制权管理接口 ===================== - - /** - * @brief 抢占机器人控制权(命令码 4005)。 - * @param context gRPC 上下文(未使用)。 - * @param request 请求消息,包含设备 ID 和抢占者昵称。 - * @param response 响应消息,返回抢占结果(ret_code)。 - * @return 始终返回 grpc::Status::OK,错误信息通过 response.header 返回。 - */ - grpc::Status RobotConfigLock(grpc::ServerContext* context, - const api::RobotConfigLockCommand_Request* request, - api::RobotConfigLockCommand_Feedback* response) override; - - /** - * @brief 查询当前控制权持有者(命令码 1060)。 - * @param context gRPC 上下文(未使用)。 - * @param request 请求消息,包含设备 ID。 - * @param response 响应消息,返回是否被锁定、持有者 IP/端口/昵称等信息。 - * @return 始终返回 grpc::Status::OK,错误信息通过 response.header 返回。 - */ - grpc::Status GetCurrentLockStatus(grpc::ServerContext* context, - const api::RobotStatusCurrentLockCommand_Request* request, - api::RobotStatusCurrentLockCommand_Feedback* response) override; - - // ===================== 运动控制接口 ===================== - - /** - * @brief 下发开环速度运动指令(命令码 2010)。 - * @param context gRPC 上下文(未使用)。 - * @param request 请求消息,包含设备 ID 和速度参数(vx, vy, w, steer, duration 等)。 - * @param response 响应消息,返回指令下发结果(ret_code)。 - * @note 此指令会强制取消当前自动导航任务,多舵轮设备仅 vx/vy/w 生效。 - * @return 始终返回 grpc::Status::OK,错误信息通过 response.header 返回。 - */ - grpc::Status RobotMotionControl(grpc::ServerContext* context, - const api::RobotMotionControlCommand_Request* request, - api::RobotMotionControlCommand_Feedback* response) override; - - /** - * @brief 切换载入地图(命令码 2022)。 - * @param context gRPC 上下文(未使用)。 - * @param request 请求消息,包含设备 ID 和目标地图名称。 - * @param response 响应消息,返回切换结果(ret_code)。 - * @return 始终返回 grpc::Status::OK,错误信息通过 response.header 返回。 - */ - grpc::Status RobotLoadMap(grpc::ServerContext* context, - const api::RobotLoadMapCommand_Request* request, - api::RobotLoadMapCommand_Feedback* response) override; - - /** - * @brief 停止开环运动(命令码 2000)。 - * @param context gRPC 上下文(未使用)。 - * @param request 请求消息,包含设备 ID(无业务数据)。 - * @param response 响应消息,返回停止结果(ret_code)。 - * @return 始终返回 grpc::Status::OK,错误信息通过 response.header 返回。 - */ - grpc::Status RobotControlStop(grpc::ServerContext* context, - const api::RobotControlStopCommand::Request* request, - api::RobotControlStopCommand::Feedback* response) override; - - // ===================== 导航任务接口 ===================== - - /** - * @brief 查询地图载入状态(命令码 1022)。 - * @param context gRPC 上下文(未使用)。 - * @param request 请求消息,包含设备 ID。 - * @param response 响应消息,返回 loadmap_status(0=失败, 1=成功, 2=载入中)。 - * @return 始终返回 grpc::Status::OK,错误信息通过 response.header 返回。 - */ - grpc::Status QueryLoadMapStatus(grpc::ServerContext* context, - const api::RobotQueryLoadMapStatusCommand_Request* request, - api::RobotQueryLoadMapStatusCommand_Feedback* response) override; - - /** - * @brief 查询当前地图所有站点信息(命令码 1301)。 - * @param context gRPC 上下文(未使用)。 - * @param request 请求消息,包含设备 ID。 - * @param response 响应消息,返回站点列表(ID、坐标、类型等)。 - * @return 始终返回 grpc::Status::OK,错误信息通过 response.header 返回。 - */ - grpc::Status QueryStationList(grpc::ServerContext* context, - const api::QueryStationListCommand_Request* request, - api::QueryStationListCommand_Feedback* response) override; - - /** - * @brief 指定路径导航(命令码 3066)。 - * @param context gRPC 上下文(未使用)。 - * @param request 请求消息,包含设备 ID 和多段导航任务列表(move_task_list)。 - * @param response 响应消息,返回下发结果(ret_code=0 仅表示接收成功,不表示执行完成)。 - * @attention 每个任务必须包含 task_id, source_id, id,且相邻站点间必须有直接路径。 - * 任务会排队执行,适合多车调度场景。 - * @return 始终返回 grpc::Status::OK,错误信息通过 response.header 返回。 - */ - grpc::Status RobotGoTargetList(grpc::ServerContext* context, - const api::RobotGoTargetListCommand_Request* request, - api::RobotGoTargetListCommand_Feedback* response) override; - - /** - * @brief 查询当前实时导航状态(命令码 1020)。 - * @param context gRPC 上下文(未使用)。 - * @param request 请求消息,包含设备 ID 和 simple 标志。 - * @param response 响应消息,返回任务状态、任务类型、目标、路径等信息。 - * @return 始终返回 grpc::Status::OK,错误信息通过 response.header 返回。 - */ - grpc::Status RobotStatusTaskCurrent(grpc::ServerContext* context, - const api::RobotStatusTaskCurrentCommand_Request* request, - api::RobotStatusTaskCurrentCommand_Feedback* response) override; - - /** - * @brief 批量查询任务状态(命令码 1110)。 - * @param context gRPC 上下文(未使用)。 - * @param request 请求消息,包含设备 ID 和 task_ids 列表(为空则查询所有未完成+最近完成)。 - * @param response 响应消息,返回任务状态包(进度、距离、各任务状态等)。 - * @return 始终返回 grpc::Status::OK,错误信息通过 response.header 返回。 - */ - grpc::Status RobotStatusTaskPackage(grpc::ServerContext* context, - const api::RobotStatusTaskPackageCommand_Request* request, - api::RobotStatusTaskPackageCommand_Feedback* response) override; - - /** - * @brief 单点站点自动规划导航(命令码 3051)。 - * @param context gRPC 上下文(未使用)。 - * @param request 请求消息,包含设备 ID 和完整的导航参数(支持自由导航、动作、PGV 等)。 - * @param response 响应消息,返回下发结果(ret_code=0 仅表示接收成功)。 - * @warning 严禁用于多车调度场景,仅限单车测试;新任务会取消当前任务(不排队)。 - * @return 始终返回 grpc::Status::OK,错误信息通过 response.header 返回。 - */ - grpc::Status RobotGoTarget(grpc::ServerContext* context, - const api::RobotGoTargetCommand_Request* request, - api::RobotGoTargetCommand_Feedback* response) override; - - /** - * @brief 暂停当前导航任务(命令码 3001)。 - * @param context gRPC 上下文(未使用)。 - * @param request 请求消息,包含设备 ID(无业务数据)。 - * @param response 响应消息,返回暂停结果(ret_code)。 - * @return 始终返回 grpc::Status::OK,错误信息通过 response.header 返回。 - */ - grpc::Status RobotTaskPause(grpc::ServerContext* context, - const api::RobotTaskPauseCommand::Request* request, - api::RobotTaskPauseCommand::Feedback* response) override; - - /** - * @brief 继续当前导航任务(命令码 3002)。 - * @param context gRPC 上下文(未使用)。 - * @param request 请求消息,包含设备 ID(无业务数据)。 - * @param response 响应消息,返回继续结果(ret_code)。 - * @return 始终返回 grpc::Status::OK,错误信息通过 response.header 返回。 - */ - grpc::Status RobotTaskResume(grpc::ServerContext* context, - const api::RobotTaskResumeCommand::Request* request, - api::RobotTaskResumeCommand::Feedback* response) override; - - /** - * @brief 取消当前导航任务(命令码 3003)。 - * @param context gRPC 上下文(未使用)。 - * @param request 请求消息,包含设备 ID(无业务数据)。 - * @param response 响应消息,返回取消结果(ret_code)。 - * @return 始终返回 grpc::Status::OK,错误信息通过 response.header 返回。 - */ - grpc::Status RobotTaskCancel(grpc::ServerContext* context, - const api::RobotTaskCancelCommand::Request* request, - api::RobotTaskCancelCommand::Feedback* response) override; - -private: - device::DeviceManager& dmgr_; ///< 设备管理器引用,用于根据 device_id 获取 AGV 设备实例。 -}; - -} // namespace cmvr::service - -#endif // GRPC_AGV_SERVICE_H \ No newline at end of file diff --git a/cmvr-es/service/grpc/include/server_runner.h b/cmvr-es/service/grpc/include/server_runner.h index 7ad0706e..be8eadf8 100644 --- a/cmvr-es/service/grpc/include/server_runner.h +++ b/cmvr-es/service/grpc/include/server_runner.h @@ -11,7 +11,6 @@ #include #include #include "rapidxml/xml_parser.h" -#include "cmvr/config/server_config/server_config.pb.h" namespace cmvr::service { @@ -42,7 +41,6 @@ namespace cmvr::service { std::thread worker_; XmlNode cfg_; - cmvr::config::ServerConfig servercfg_; std::unique_ptr server_; std::string address_; diff --git a/cmvr-es/service/grpc/src/grpc_agv_service.cpp b/cmvr-es/service/grpc/src/grpc_agv_service.cpp deleted file mode 100644 index a48e03b2..00000000 --- a/cmvr-es/service/grpc/src/grpc_agv_service.cpp +++ /dev/null @@ -1,1208 +0,0 @@ -// -// Created by xtkuang on 2025/6/1. -// -/** - * @file grpc_agv_service.cpp - * @brief gRPC AGV 服务实现。 - * 每个 RPC 方法从请求头中提取 device_id,通过 DeviceManager 获取对应的 AGV 设备, - * 调用设备层的具体方法,并将结果转换为 Protobuf 响应。 - * 统一处理异常,设置响应头中的 success/error_message 和时间戳。 - * @author xtkuang - * @date 2025-06-01 - */ - -#include "./service/grpc/include/grpc_agv_service.h" - -#include - -#include "devices/agv/agv_src1100/include/agv_src1100.h" - -#include "devices/agv/abstract_agv.h" - -namespace cmvr::service { - - /** - * @brief 构造函数,获取 DeviceManager 单例。 - */ - gRPCAGVServiceImpl::gRPCAGVServiceImpl() : dmgr_(device::DeviceManager::getInstance()) { - } - - // ===================== GetStatusInfo ===================== - /** - * @brief 获取 AGV 基本信息。 - * @details 从请求中提取 device_id,调用设备层的 getStatusInfo,填充响应。 - * @param context 未使用。 - * @param request 包含设备 ID。 - * @param response 返回状态信息和响应头。 - * @return 始终返回 OK,错误通过 response.header 传递。 - */ - grpc::Status gRPCAGVServiceImpl::GetStatusInfo( - grpc::ServerContext* context, - const api::GetAgvStatusInfoCommand_Request* request, - api::GetAgvStatusInfoCommand_Feedback* response) - { - try { - std::string dev_id = request->header().device_id(); - LOG(INFO) << "[gRPCAGVServiceImpl] (GetStatusInfo): id=" << dev_id; - const auto agv_device = dmgr_.getDevice(dev_id); - - device::AbstractAgv::AgvStatusInfo device_status; - agv_device->getStatusInfo(device_status); - - // 映射到 Protobuf 消息 - auto* proto_status = response->mutable_status(); - proto_status->set_id(device_status.id); - proto_status->set_vehicle_id(device_status.vehicle_id); - proto_status->set_version(device_status.version); - proto_status->set_model(device_status.model); - proto_status->set_dsp_version(device_status.dsp_version); - proto_status->set_current_ip(device_status.current_ip); - proto_status->set_mac(device_status.mac); - proto_status->set_rssi(device_status.rssi); - proto_status->set_ret_code(device_status.ret_code); - proto_status->set_err_msg(device_status.err_msg); - - auto* header = response->mutable_header(); - header->set_success(true); - header->set_error_message(""); - setCurrentTimestamp(header->mutable_timestamp()); - - LOG(INFO) << "GetStatusInfo device_id:" << dev_id << " vehicle_id: " << device_status.vehicle_id; - return grpc::Status::OK; - } - catch (std::exception &e) - { - auto* header = response->mutable_header(); - header->set_success(false); - header->set_error_message(e.what()); - setCurrentTimestamp(header->mutable_timestamp()); - return grpc::Status::OK; - } - } - - // ===================== GetBatteryStatus ===================== - /** - * @brief 查询电池状态。 - * @details 支持 simple 参数,调用设备层 getBatteryStatus。 - * @param context 未使用。 - * @param request 包含设备 ID 和 simple 标志。 - * @param response 返回电池信息。 - */ - grpc::Status gRPCAGVServiceImpl::GetBatteryStatus( - grpc::ServerContext* context, - const api::RobotStatusBatteryCommand_Request* request, - api::RobotStatusBatteryCommand_Feedback* response) - { - try { - std::string dev_id = request->header().device_id(); - LOG(INFO) << "[gRPCAGVServiceImpl] (GetBatteryStatus): id=" << dev_id; - const auto agv_device = dmgr_.getDevice(dev_id); - - bool simple = false; - if (request->has_data()) - { - simple = request->data().simple(); - } - - device::AbstractAgv::BatteryStatus bat_info; - agv_device->getBatteryStatus(bat_info, simple); - - auto* proto_bat = response->mutable_status(); - proto_bat->set_battery_level(bat_info.battery_level); - proto_bat->set_battery_temp(bat_info.battery_temp); - proto_bat->set_charging(bat_info.charging); - proto_bat->set_voltage(bat_info.voltage); - proto_bat->set_current(bat_info.current); - proto_bat->set_max_charge_voltage(bat_info.max_charge_voltage); - proto_bat->set_max_charge_current(bat_info.max_charge_current); - proto_bat->set_manual_charge(bat_info.manual_charge); - proto_bat->set_auto_charge(bat_info.auto_charge); - proto_bat->set_battery_cycle(bat_info.battery_cycle); - proto_bat->set_battery_user_data(bat_info.battery_user_data); - proto_bat->set_extra(bat_info.extra); - proto_bat->set_ret_code(bat_info.ret_code); - proto_bat->set_create_on(bat_info.create_on); - proto_bat->set_err_msg(bat_info.err_msg); - - auto* header = response->mutable_header(); - header->set_success(true); - header->set_error_message(""); - setCurrentTimestamp(header->mutable_timestamp()); - - LOG(INFO) << "GetBatteryStatus device_id:" << dev_id << " level: " << bat_info.battery_level; - return grpc::Status::OK; - } - catch (std::exception &e) - { - auto* header = response->mutable_header(); - header->set_success(false); - header->set_error_message(e.what()); - setCurrentTimestamp(header->mutable_timestamp()); - return grpc::Status::OK; - } - } - - // ===================== GetRobotLocation ===================== - /** - * @brief 查询机器人当前位置。 - * @param context 未使用。 - * @param request 包含设备 ID。 - * @param response 返回坐标、朝向、置信度等。 - */ - grpc::Status gRPCAGVServiceImpl::GetRobotLocation( - grpc::ServerContext* context, - const api::RobotStatusLocCommand_Request* request, - api::RobotStatusLocCommand_Feedback* response) - { - try { - std::string dev_id = request->header().device_id(); - LOG(INFO) << "[gRPCAGVServiceImpl] (GetRobotLocation): id=" << dev_id; - const auto agv_device = dmgr_.getDevice(dev_id); - - device::AbstractAgv::RobotLocation loc_info; - agv_device->getRobotLocation(loc_info); - - auto* proto_loc = response->mutable_status(); - proto_loc->set_x(loc_info.x); - proto_loc->set_y(loc_info.y); - proto_loc->set_angle(loc_info.angle); - proto_loc->set_confidence(loc_info.confidence); - proto_loc->set_current_station(loc_info.current_station); - proto_loc->set_last_station(loc_info.last_station); - proto_loc->set_loc_method(loc_info.loc_method); - proto_loc->set_ret_code(loc_info.ret_code); - proto_loc->set_create_on(loc_info.create_on); - proto_loc->set_err_msg(loc_info.err_msg); - - auto* header = response->mutable_header(); - header->set_success(true); - header->set_error_message(""); - setCurrentTimestamp(header->mutable_timestamp()); - - LOG(INFO) << "GetRobotLocation device_id:" << dev_id << " x: " << loc_info.x << ", y: " << loc_info.y; - return grpc::Status::OK; - } - catch (std::exception &e) - { - auto* header = response->mutable_header(); - header->set_success(false); - header->set_error_message(e.what()); - setCurrentTimestamp(header->mutable_timestamp()); - return grpc::Status::OK; - } - } - - // ===================== RobotConfigDownloadMap ===================== - /** - * @brief 下载指定地图。 - * @param context 未使用。 - * @param request 包含设备 ID 和地图名称。 - * @param response 返回地图 JSON 内容或错误码。 - */ - grpc::Status gRPCAGVServiceImpl::RobotConfigDownloadMap( - grpc::ServerContext* context, - const api::RobotConfigDownloadMapCommand_Request* request, - api::RobotConfigDownloadMapCommand_Feedback* response) - { - try { - std::string dev_id = request->header().device_id(); - LOG(INFO) << "[gRPCAGVServiceImpl] (RobotConfigDownloadMap): id=" << dev_id; - const auto agv_device = dmgr_.getDevice(dev_id); - - std::string map_name = request->data().map_name(); - device::AbstractAgv::DownloadMapResult down_info; - agv_device->downloadMap(down_info, map_name); - - auto* proto_res = response->mutable_status(); - proto_res->set_ret_code(down_info.ret_code); - proto_res->set_create_on(down_info.create_on); - proto_res->set_err_msg(down_info.err_msg); - proto_res->set_map_content(down_info.map_content); - - auto* header = response->mutable_header(); - header->set_success(true); - header->set_error_message(""); - setCurrentTimestamp(header->mutable_timestamp()); - - LOG(INFO) << "DownloadMap device_id:" << dev_id << " map: " << map_name << ", ret_code: " << down_info.ret_code; - return grpc::Status::OK; - } - catch (std::exception &e) - { - auto* header = response->mutable_header(); - header->set_success(false); - header->set_error_message(e.what()); - setCurrentTimestamp(header->mutable_timestamp()); - return grpc::Status::OK; - } - } - - // ===================== GetMapStatus ===================== - /** - * @brief 查询地图状态。 - * @param context 未使用。 - * @param request 包含设备 ID。 - * @param response 返回当前地图、地图列表及文件详情。 - */ - grpc::Status gRPCAGVServiceImpl::GetMapStatus( - grpc::ServerContext* context, - const api::RobotStatusMapCommand_Request* request, - api::RobotStatusMapCommand_Feedback* response) - { - try { - std::string dev_id = request->header().device_id(); - LOG(INFO) << "[gRPCAGVServiceImpl] (GetMapStatus): id=" << dev_id; - const auto agv_device = dmgr_.getDevice(dev_id); - - device::AbstractAgv::MapStatus map_info; - agv_device->getMapStatus(map_info); - - auto* proto_map = response->mutable_status(); - proto_map->set_current_map(map_info.current_map); - proto_map->set_current_map_md5(map_info.current_map_md5); - proto_map->set_ret_code(map_info.ret_code); - proto_map->set_create_on(map_info.create_on); - proto_map->set_err_msg(map_info.err_msg); - - for (auto& name : map_info.maps) - { - proto_map->add_maps(name); - } - - for (auto& file : map_info.map_files_info) - { - auto* proto_file = proto_map->add_map_files_info(); - proto_file->set_name(file.name); - proto_file->set_modified(file.modified); - proto_file->set_size(file.size); - } - - auto* header = response->mutable_header(); - header->set_success(true); - header->set_error_message(""); - setCurrentTimestamp(header->mutable_timestamp()); - - LOG(INFO) << "GetMapStatus device_id:" << dev_id << " current map: " << map_info.current_map; - return grpc::Status::OK; - } - catch (std::exception &e) - { - auto* header = response->mutable_header(); - header->set_success(false); - header->set_error_message(e.what()); - setCurrentTimestamp(header->mutable_timestamp()); - return grpc::Status::OK; - } - } - - // ===================== RobotConfigUploadMap ===================== - /** - * @brief 上传地图。 - * @param context 未使用。 - * @param request 包含设备 ID 和地图 JSON 字符串。 - * @param response 返回上传结果。 - */ - grpc::Status gRPCAGVServiceImpl::RobotConfigUploadMap( - grpc::ServerContext* context, - const api::RobotConfigUploadMapCommand_Request* request, - api::RobotConfigUploadMapCommand_Feedback* response) - { - try { - std::string dev_id = request->header().device_id(); - LOG(INFO) << "[gRPCAGVServiceImpl] (RobotConfigUploadMap): id=" << dev_id; - const auto agv_device = dmgr_.getDevice(dev_id); - - std::string map_json = request->data().map_content(); - device::AbstractAgv::UploadMapResult upload_info; - agv_device->uploadMap(upload_info, map_json); - - auto* proto_res = response->mutable_status(); - proto_res->set_ret_code(upload_info.ret_code); - proto_res->set_create_on(upload_info.create_on); - proto_res->set_err_msg(upload_info.err_msg); - - auto* header = response->mutable_header(); - header->set_success(true); - header->set_error_message(""); - setCurrentTimestamp(header->mutable_timestamp()); - - LOG(INFO) << "UploadMap device_id:" << dev_id << " ret_code: " << upload_info.ret_code; - return grpc::Status::OK; - } - catch (std::exception &e) - { - auto* header = response->mutable_header(); - header->set_success(false); - header->set_error_message(e.what()); - setCurrentTimestamp(header->mutable_timestamp()); - return grpc::Status::OK; - } - } - - // ===================== RobotConfigLock ===================== - /** - * @brief 抢占机器人控制权。 - * @param context 未使用。 - * @param request 包含设备 ID 和抢占者昵称。 - * @param response 返回抢占结果。 - */ - grpc::Status gRPCAGVServiceImpl::RobotConfigLock( - grpc::ServerContext* context, - const api::RobotConfigLockCommand_Request* request, - api::RobotConfigLockCommand_Feedback* response) - { - try { - std::string dev_id = request->header().device_id(); - LOG(INFO) << "[gRPCAGVServiceImpl] (RobotConfigLock): id=" << dev_id; - - const auto agv_device = dmgr_.getDevice(dev_id); - - std::string nick_name = request->data().nick_name(); - device::AbstractAgv::LockResult lock_res; - agv_device->lockRobotControl(lock_res, nick_name); - - auto* proto_status = response->mutable_status(); - proto_status->set_ret_code(lock_res.ret_code); - proto_status->set_create_on(lock_res.create_on); - proto_status->set_err_msg(lock_res.err_msg); - - auto* header = response->mutable_header(); - header->set_success(true); - header->set_error_message(""); - setCurrentTimestamp(header->mutable_timestamp()); - - LOG(INFO) << "Lock robot control, device_id:" << dev_id << ", nick:" << nick_name << ", ret_code:" << lock_res.ret_code; - return grpc::Status::OK; - } - catch (std::exception &e) - { - auto* header = response->mutable_header(); - header->set_success(false); - header->set_error_message(e.what()); - setCurrentTimestamp(header->mutable_timestamp()); - return grpc::Status::OK; - } - } - - // ===================== GetCurrentLockStatus ===================== - /** - * @brief 查询当前控制权持有者。 - * @param context 未使用。 - * @param request 包含设备 ID。 - * @param response 返回锁定状态、持有者信息等。 - */ - grpc::Status gRPCAGVServiceImpl::GetCurrentLockStatus( - grpc::ServerContext* context, - const api::RobotStatusCurrentLockCommand_Request* request, - api::RobotStatusCurrentLockCommand_Feedback* response) - { - try { - std::string dev_id = request->header().device_id(); - LOG(INFO) << "[gRPCAGVServiceImpl] (GetCurrentLockStatus): id=" << dev_id; - - const auto agv_dev = dmgr_.getDevice(dev_id); - - device::AbstractAgv::CurrentLockStatus lock_info; - agv_dev->getCurrentLockStatus(lock_info); - - auto* proto_status = response->mutable_status(); - proto_status->set_locked(lock_info.locked); - proto_status->set_ip(lock_info.ip); - proto_status->set_port(lock_info.port); - proto_status->set_type(lock_info.type); - proto_status->set_nick_name(lock_info.nick_name); - proto_status->set_time_t(lock_info.time_t); - proto_status->set_desc(lock_info.desc); - proto_status->set_ret_code(lock_info.ret_code); - proto_status->set_create_on(lock_info.create_on); - proto_status->set_err_msg(lock_info.err_msg); - - response->mutable_header()->set_success(true); - setCurrentTimestamp(response->mutable_header()->mutable_timestamp()); - - LOG(INFO) << "Query current lock status, device_id:" << dev_id - << ", locked:" << lock_info.locked << ", nick:" << lock_info.nick_name; - return grpc::Status::OK; - } - catch (std::exception &e) - { - response->mutable_header()->set_success(false); - response->mutable_header()->set_error_message(e.what()); - setCurrentTimestamp(response->mutable_header()->mutable_timestamp()); - return grpc::Status::OK; - } - } - - // ===================== RobotMotionControl ===================== - /** - * @brief 开环速度运动控制。 - * @param context 未使用。 - * @param request 包含设备 ID 和速度参数。 - * @param response 返回指令下发结果。 - */ - grpc::Status gRPCAGVServiceImpl::RobotMotionControl( - grpc::ServerContext* context, - const api::RobotMotionControlCommand_Request* request, - api::RobotMotionControlCommand_Feedback* response) - { - try { - std::string dev_id = request->header().device_id(); - LOG(INFO) << "[gRPCAGVServiceImpl] (RobotMotionControl): target device_id=" << dev_id; - const auto agv_dev = dmgr_.getDevice(dev_id); - - device::AbstractAgv::MotionCtrlReq req; - auto& input_data = request->data(); - req.vx = input_data.vx(); - req.vy = input_data.vy(); - req.w = input_data.w(); - req.steer = input_data.steer(); - req.real_steer = input_data.real_steer(); - req.duration = input_data.duration(); - - device::AbstractAgv::MotionCtrlRes res; - agv_dev->robotMotionControl(res, req); - - auto* resp_status = response->mutable_status(); - resp_status->set_ret_code(res.ret_code); - resp_status->set_create_on(res.create_on); - resp_status->set_err_msg(res.err_msg); - - auto* header = response->mutable_header(); - header->set_success(true); - header->set_error_message(""); - setCurrentTimestamp(header->mutable_timestamp()); - - LOG(INFO) << "RobotMotionControl finish, dev_id:" << dev_id - << " vx=" << req.vx << " vy=" << req.vy << " w=" << req.w - << " ret_code=" << res.ret_code; - return grpc::Status::OK; - } - catch (std::exception& e) - { - auto* header = response->mutable_header(); - header->set_success(false); - header->set_error_message(e.what()); - setCurrentTimestamp(header->mutable_timestamp()); - return grpc::Status::OK; - } - } - - // ===================== RobotLoadMap ===================== - /** - * @brief 切换载入地图。 - * @param context 未使用。 - * @param request 包含设备 ID 和目标地图名称。 - * @param response 返回切换结果。 - */ - grpc::Status gRPCAGVServiceImpl::RobotLoadMap( - grpc::ServerContext* context, - const api::RobotLoadMapCommand_Request* request, - api::RobotLoadMapCommand_Feedback* response) - { - try { - std::string dev_id = request->header().device_id(); - LOG(INFO) << "[gRPCAGVServiceImpl] (RobotLoadMap): target device_id=" << dev_id; - const auto agv_dev = dmgr_.getDevice(dev_id); - - device::AbstractAgv::LoadMapReq req; - auto& input_data = request->data(); - req.map_name = input_data.map_name(); - - device::AbstractAgv::LoadMapRes res; - agv_dev->robotLoadMap(res, req); - - auto* resp_status = response->mutable_status(); - resp_status->set_ret_code(res.ret_code); - resp_status->set_create_on(res.create_on); - resp_status->set_err_msg(res.err_msg); - - auto* header = response->mutable_header(); - header->set_success(true); - header->set_error_message(""); - setCurrentTimestamp(header->mutable_timestamp()); - - LOG(INFO) << "RobotLoadMap finish, dev_id:" << dev_id - << " map_name=" << req.map_name - << " ret_code=" << res.ret_code; - return grpc::Status::OK; - } - catch (std::exception& e) - { - auto* header = response->mutable_header(); - header->set_success(false); - header->set_error_message(e.what()); - setCurrentTimestamp(header->mutable_timestamp()); - return grpc::Status::OK; - } - } - - // ===================== QueryLoadMapStatus ===================== - /** - * @brief 查询地图载入状态。 - * @param context 未使用。 - * @param request 包含设备 ID。 - * @param response 返回 loadmap_status。 - */ - grpc::Status gRPCAGVServiceImpl::QueryLoadMapStatus( - grpc::ServerContext* context, - const api::RobotQueryLoadMapStatusCommand_Request* request, - api::RobotQueryLoadMapStatusCommand_Feedback* response) - { - try { - std::string dev_id = request->header().device_id(); - LOG(INFO) << "[gRPCAGVServiceImpl] (QueryLoadMapStatus): device_id=" << dev_id; - const auto agv_dev = dmgr_.getDevice(dev_id); - - device::AbstractAgv::QueryLoadMapStatusRes res; - agv_dev->queryLoadMapStatus(res); - - auto* resp_status = response->mutable_status(); - resp_status->set_loadmap_status(res.loadmap_status); - resp_status->set_ret_code(res.ret_code); - resp_status->set_create_on(res.create_on); - resp_status->set_err_msg(res.err_msg); - - auto* header = response->mutable_header(); - header->set_success(true); - header->set_error_message(""); - setCurrentTimestamp(header->mutable_timestamp()); - - LOG(INFO) << "QueryLoadMapStatus finish dev_id:" << dev_id - << " loadmap_status=" << res.loadmap_status - << " ret_code=" << res.ret_code; - return grpc::Status::OK; - } - catch (std::exception& e) - { - auto* header = response->mutable_header(); - header->set_success(false); - header->set_error_message(e.what()); - setCurrentTimestamp(header->mutable_timestamp()); - return grpc::Status::OK; - } - } - - // ===================== QueryStationList ===================== - /** - * @brief 查询当前地图所有站点信息。 - * @param context 未使用。 - * @param request 包含设备 ID。 - * @param response 返回站点列表。 - */ - grpc::Status gRPCAGVServiceImpl::QueryStationList( - grpc::ServerContext* context, - const api::QueryStationListCommand_Request* request, - api::QueryStationListCommand_Feedback* response) - { - try { - std::string dev_id = request->header().device_id(); - LOG(INFO) << "[gRPCAGVServiceImpl] (QueryStationList): device_id=" << dev_id; - const auto agv_dev = dmgr_.getDevice(dev_id); - - device::AbstractAgv::QueryStationRes res; - agv_dev->queryStationList(res); - - auto* resp_status = response->mutable_status(); - resp_status->set_ret_code(res.ret_code); - resp_status->set_create_on(res.create_on); - resp_status->set_err_msg(res.err_msg); - - for (auto& st : res.stations) - { - auto* pb_st = resp_status->add_stations(); - pb_st->set_id(st.id); - pb_st->set_type(st.type); - pb_st->set_x(st.x); - pb_st->set_y(st.y); - pb_st->set_r(st.r); - pb_st->set_desc(st.desc); - pb_st->set_executor(st.executor); - pb_st->set_prepoint(st.prepoint); - pb_st->set_recfile(st.recfile); - pb_st->set_spin(st.spin); - pb_st->set_use_down_pgv(st.use_down_pgv); - } - - auto* header = response->mutable_header(); - header->set_success(true); - header->set_error_message(""); - setCurrentTimestamp(header->mutable_timestamp()); - - LOG(INFO) << "QueryStationList finish dev_id:" << dev_id - << " station_count=" << res.stations.size() - << " ret_code=" << res.ret_code; - return grpc::Status::OK; - } - catch (std::exception& e) - { - auto* header = response->mutable_header(); - header->set_success(false); - header->set_error_message(e.what()); - setCurrentTimestamp(header->mutable_timestamp()); - return grpc::Status::OK; - } - } - - // ===================== RobotGoTargetList ===================== - /** - * @brief 指定路径导航(多点)。 - * @param context 未使用。 - * @param request 包含设备 ID 和 move_task_list。 - * @param response 返回下发结果(仅表示指令被接收)。 - */ - grpc::Status gRPCAGVServiceImpl::RobotGoTargetList( - grpc::ServerContext* context, - const api::RobotGoTargetListCommand_Request* request, - api::RobotGoTargetListCommand_Feedback* response) - { - try { - std::string dev_id = request->header().device_id(); - LOG(INFO) << "[gRPCAGVServiceImpl] (RobotGoTargetList): target device_id=" << dev_id; - const auto agv_dev = dmgr_.getDevice(dev_id); - - device::AbstractAgv::GoTargetListReq req; - const auto& pb_list = request->data().move_task_list(); - for (const auto& pb_item : pb_list) - { - device::AbstractAgv::MoveTaskItem task; - task.task_id = pb_item.task_id(); - task.source_id = pb_item.source_id(); - task.id = pb_item.id(); - task.operation = pb_item.operation(); - task.jack_height = pb_item.jack_height(); - req.move_task_list.push_back(task); - } - - device::AbstractAgv::GoTargetListRes res; - agv_dev->robotGoTargetList(res, req); - - auto* resp_status = response->mutable_status(); - resp_status->set_ret_code(res.ret_code); - resp_status->set_create_on(res.create_on); - resp_status->set_err_msg(res.err_msg); - - auto* header = response->mutable_header(); - header->set_success(true); - header->set_error_message(""); - setCurrentTimestamp(header->mutable_timestamp()); - - LOG(INFO) << "RobotGoTargetList finish dev_id:" << dev_id - << " task_count=" << req.move_task_list.size() - << " ret_code=" << res.ret_code; - return grpc::Status::OK; - } - catch (std::exception& e) - { - auto* header = response->mutable_header(); - header->set_success(false); - header->set_error_message(e.what()); - setCurrentTimestamp(header->mutable_timestamp()); - return grpc::Status::OK; - } - } - - // ===================== RobotStatusTaskCurrent ===================== - /** - * @brief 查询当前实时导航状态。 - * @param context 未使用。 - * @param request 包含设备 ID 和 simple 标志。 - * @param response 返回任务状态、类型、目标、路径等。 - */ - grpc::Status gRPCAGVServiceImpl::RobotStatusTaskCurrent( - grpc::ServerContext* context, - const api::RobotStatusTaskCurrentCommand_Request* request, - api::RobotStatusTaskCurrentCommand_Feedback* response) - { - try { - std::string dev_id = request->header().device_id(); - LOG(INFO) << "[gRPCAGVServiceImpl] (RobotStatusTaskCurrent): id=" << dev_id; - const auto agv_dev = dmgr_.getDevice(dev_id); - - device::AbstractAgv::RobotStatusTaskCurrentReq req; - if (request->has_data()) - { - req.simple = request->data().simple(); - } - - device::AbstractAgv::RobotStatusTaskCurrentRes res; - agv_dev->robotStatusTaskCurrent(res, req); - - auto* out_data = response->mutable_data(); - out_data->set_ret_code(res.ret_code); - out_data->set_create_on(res.create_on); - out_data->set_err_msg(res.err_msg); - - out_data->set_task_status(res.task_status); - out_data->set_task_type(res.task_type); - out_data->set_target_id(res.target_id); - out_data->set_move_status_info(res.move_status_info); - - for (double p : res.target_point) - out_data->add_target_point(p); - for (auto& s : res.finished_path) - out_data->add_finished_path(s); - for (auto& s : res.unfinished_path) - out_data->add_unfinished_path(s); - - for (auto& c : res.containers) - { - auto* pc = out_data->add_containers(); - pc->set_container_name(c.container_name); - pc->set_desc(c.desc); - pc->set_goods_id(c.goods_id); - pc->set_has_goods(c.has_goods); - } - - auto* header = response->mutable_header(); - header->set_success(true); - header->set_error_message(""); - setCurrentTimestamp(header->mutable_timestamp()); - - LOG(INFO) << "RobotStatusTaskCurrent dev_id:" << dev_id << " task_status:" << res.task_status; - return grpc::Status::OK; - } - catch (std::exception &e) - { - auto* header = response->mutable_header(); - header->set_success(false); - header->set_error_message(e.what()); - setCurrentTimestamp(header->mutable_timestamp()); - return grpc::Status::OK; - } - } - - // ===================== RobotStatusTaskPackage ===================== - /** - * @brief 批量查询任务状态。 - * @param context 未使用。 - * @param request 包含设备 ID 和 task_ids 列表。 - * @param response 返回任务状态包(进度、距离、每个任务状态等)。 - */ - grpc::Status gRPCAGVServiceImpl::RobotStatusTaskPackage( - grpc::ServerContext* context, - const api::RobotStatusTaskPackageCommand_Request* request, - api::RobotStatusTaskPackageCommand_Feedback* response) - { - try { - std::string dev_id = request->header().device_id(); - LOG(INFO) << "[gRPCAGVServiceImpl] (RobotStatusTaskPackage): id=" << dev_id; - const auto agv_dev = dmgr_.getDevice(dev_id); - - device::AbstractAgv::QueryTaskStatusPackageReq req; - const auto& pb_ids = request->data().task_ids(); - for (auto& tid : pb_ids) - { - req.task_ids.push_back(tid); - } - - device::AbstractAgv::QueryTaskStatusPackageRes res; - agv_dev->robotStatusTaskPackage(res, req); - - auto* out_data = response->mutable_data(); - out_data->set_ret_code(res.ret_code); - out_data->set_create_on(res.create_on); - out_data->set_err_msg(res.err_msg); - - auto* pkg_pb = out_data->mutable_task_status_package(); - auto& pkg_data = res.task_status_package; - pkg_pb->set_closest_target(pkg_data.closest_target); - pkg_pb->set_source_name(pkg_data.source_name); - pkg_pb->set_target_name(pkg_data.target_name); - pkg_pb->set_percentage(pkg_data.percentage); - pkg_pb->set_distance(pkg_data.distance); - pkg_pb->set_info(pkg_data.info); - - for (auto& st_item : pkg_data.task_status_list) - { - auto* st_pb = pkg_pb->add_task_status_list(); - st_pb->set_task_id(st_item.task_id); - st_pb->set_status(st_item.status); - st_pb->set_type(st_item.type); - } - - auto* header = response->mutable_header(); - header->set_success(true); - header->set_error_message(""); - setCurrentTimestamp(header->mutable_timestamp()); - - LOG(INFO) << "RobotStatusTaskPackage dev_id:" << dev_id << " query task count:" << req.task_ids.size() << " ret_code:" << res.ret_code; - return grpc::Status::OK; - } - catch (std::exception &e) - { - auto* header = response->mutable_header(); - header->set_success(false); - header->set_error_message(e.what()); - setCurrentTimestamp(header->mutable_timestamp()); - return grpc::Status::OK; - } - } - - // ===================== RobotGoTarget ===================== - /** - * @brief 单点站点自动规划导航。 - * @param context 未使用。 - * @param request 包含设备 ID 和完整的导航参数(支持自由导航、动作、PGV、脚本等)。 - * @param response 返回下发结果。 - * @warning 严禁用于多车调度场景,仅限单车测试。 - */ - grpc::Status gRPCAGVServiceImpl::RobotGoTarget( - grpc::ServerContext* context, - const api::RobotGoTargetCommand_Request* request, - api::RobotGoTargetCommand_Feedback* response) - { - try - { - std::string dev_id = request->header().device_id(); - LOG(INFO) << "[RobotGoTarget] device_id:" << dev_id; - auto agv_dev = dmgr_.getDevice(dev_id); - if (!agv_dev) - { - auto* header = response->mutable_header(); - header->set_success(false); - header->set_error_message("device not found"); - setCurrentTimestamp(header->mutable_timestamp()); - return grpc::Status::OK; - } - - device::AbstractAgv::RobotGoTargetReq req; - auto& pb_data = request->data(); - - // -------- 必填字段 -------- - req.source_id = pb_data.source_id(); - req.id = pb_data.id(); - if (pb_data.has_task_id()) req.task_id = pb_data.task_id(); - - // -------- 运动参数 -------- - if (pb_data.has_angle()) req.angle = pb_data.angle(); - if (pb_data.has_method()) req.method = pb_data.method(); - if (pb_data.has_max_speed()) req.max_speed = pb_data.max_speed(); - if (pb_data.has_max_wspeed()) req.max_wspeed = pb_data.max_wspeed(); - if (pb_data.has_max_acc()) req.max_acc = pb_data.max_acc(); - if (pb_data.has_max_wacc()) req.max_wacc = pb_data.max_wacc(); - if (pb_data.has_duration()) req.duration = pb_data.duration(); - if (pb_data.has_orientation()) req.orientation = pb_data.orientation(); - if (pb_data.has_spin()) req.spin = pb_data.spin(); - if (pb_data.has_delay()) req.delay = pb_data.delay(); - if (pb_data.has_start_rot_dir()) req.start_rot_dir = pb_data.start_rot_dir(); - if (pb_data.has_end_rot_dir()) req.end_rot_dir = pb_data.end_rot_dir(); - if (pb_data.has_reach_dist()) req.reach_dist = pb_data.reach_dist(); - if (pb_data.has_reach_angle()) req.reach_angle = pb_data.reach_angle(); - if (pb_data.has_skill_name()) req.skill_name = pb_data.skill_name(); - - // -------- PGV 二次定位 -------- - if (pb_data.has_pgv()) - { - auto& pgv_pb = pb_data.pgv(); - req.pgv.use_pgv = pgv_pb.use_pgv(); - req.pgv.use_down_pgv = pgv_pb.use_down_pgv(); - if (pgv_pb.has_pgv_adjust_dist()) req.pgv.pgv_adjust_dist = pgv_pb.pgv_adjust_dist(); - if (pgv_pb.has_pgv_adjust_cx()) req.pgv.pgv_adjust_cx = pgv_pb.pgv_adjust_cx(); - if (pgv_pb.has_pgv_adjust_cy()) req.pgv.pgv_adjust_cy = pgv_pb.pgv_adjust_cy(); - if (pgv_pb.has_pgv_x_adjust()) req.pgv.pgv_x_adjust = pgv_pb.pgv_x_adjust(); - } - - // -------- 设备动作 -------- - if (pb_data.has_operation()) req.operation = pb_data.operation(); - if (pb_data.has_jack_height()) req.jack_height = pb_data.jack_height(); - if (pb_data.has_start_height()) req.start_height = pb_data.start_height(); - if (pb_data.has_end_height()) req.end_height = pb_data.end_height(); - if (pb_data.has_fork_mid_height()) req.fork_mid_height = pb_data.fork_mid_height(); - if (pb_data.has_fork_dist()) req.fork_dist = pb_data.fork_dist(); - if (pb_data.has_direction()) req.direction = pb_data.direction(); - if (pb_data.has_recognize()) req.recognize = pb_data.recognize(); - if (pb_data.has_recfile()) req.recfile = pb_data.recfile(); - - // -------- 音频 -------- - if (pb_data.has_sounds_args()) - { - auto& s_pb = pb_data.sounds_args(); - if (s_pb.has_name()) req.sounds_args.name = s_pb.name(); - if (s_pb.has_loop()) req.sounds_args.loop = s_pb.loop(); - if (s_pb.has_stop()) req.sounds_args.stop = s_pb.stop(); - } - - // -------- WaitDI -------- - if (pb_data.has_wait_di_args()) - { - auto& di_pb = pb_data.wait_di_args(); - req.wait_di_args.timeout = di_pb.timeout(); - for (auto& item : di_pb.di()) - { - device::AbstractAgv::DIItem di; - di.id = item.id(); - di.status = item.status(); - req.wait_di_args.DI.push_back(di); - } - } - - // -------- SetDO -------- - if (pb_data.has_set_do_args()) - { - auto& do_pb = pb_data.set_do_args(); - for (auto& item : do_pb.do_list()) - { - device::AbstractAgv::DOItem d; - d.id = item.id(); - d.status = item.status(); - req.set_do_args.DO.push_back(d); - } - } - - // -------- 脚本 -------- - if (pb_data.has_script_name()) req.script_name = pb_data.script_name(); - if (pb_data.has_script_stage()) req.script_stage = pb_data.script_stage(); - if (pb_data.has_script_args()) - { - auto& s_arg_pb = pb_data.script_args(); - for (auto& kv : s_arg_pb.str_kv()) req.script_args.str_kv[kv.first] = kv.second; - for (auto& kv : s_arg_pb.num_kv()) req.script_args.num_kv[kv.first] = kv.second; - for (auto& d : s_arg_pb.do_list()) - { - device::AbstractAgv::DOItem di; - di.id = d.id(); - di.status = d.status(); - req.script_args.do_list.push_back(di); - } - for (auto& di : s_arg_pb.di_list()) - { - device::AbstractAgv::DIItem d; - d.id = di.id(); - d.status = di.status(); - req.script_args.di_list.push_back(d); - } - } - - // -------- 原地旋转 -------- - if (pb_data.has_move_angle()) req.move_angle = pb_data.move_angle(); - if (pb_data.has_speed_w()) req.speed_w = pb_data.speed_w(); - if (pb_data.has_loc_mode()) req.loc_mode = pb_data.loc_mode(); - - // -------- 自由导航 -------- - if (pb_data.has_freego()) - { - auto& fg_pb = pb_data.freego(); - req.freeGo.x = fg_pb.x(); - req.freeGo.y = fg_pb.y(); - req.freeGo.theta = fg_pb.theta(); - } - - // 调用设备层 - device::AbstractAgv::RobotGoTargetRes res; - agv_dev->robotGoTarget(res, req); - - auto* out_data = response->mutable_data(); - out_data->set_ret_code(res.ret_code); - out_data->set_create_on(res.create_on); - out_data->set_err_msg(res.err_msg); - - auto* header = response->mutable_header(); - header->set_success(true); - header->set_error_message(""); - setCurrentTimestamp(header->mutable_timestamp()); - - LOG(INFO) << "[RobotGoTarget] finish ret_code=" << res.ret_code; - return grpc::Status::OK; - } - catch (std::exception& e) - { - auto* header = response->mutable_header(); - header->set_success(false); - header->set_error_message(std::string("exception:") + e.what()); - setCurrentTimestamp(header->mutable_timestamp()); - return grpc::Status::OK; - } - } - - // ===================== RobotControlStop ===================== - /** - * @brief 停止开环运动。 - * @param context 未使用。 - * @param request 包含设备 ID(无业务数据)。 - * @param response 返回停止结果。 - */ - grpc::Status gRPCAGVServiceImpl::RobotControlStop( - grpc::ServerContext* context, - const api::RobotControlStopCommand::Request* request, - api::RobotControlStopCommand::Feedback* response) { - - const auto& header = request->header(); - std::string device_id = header.device_id(); - auto* fb_header = response->mutable_header(); - - if (device_id.empty()) { - fb_header->set_error_message("Missing device_id"); - return grpc::Status::OK; - } - - auto agv = dmgr_.getDevice(device_id); - if (!agv) { - fb_header->set_error_message("Device not found or not an AGV"); - return grpc::Status::OK; - } - - cmvr::device::AbstractAgv::RobotControlStopRes res; - agv->robotControlStop(res); - - auto* status = response->mutable_status(); - status->set_ret_code(res.ret_code); - status->set_create_on(res.create_on); - status->set_err_msg(res.err_msg); - - if (res.ret_code == 0) { - fb_header->set_error_message(""); - } else { - fb_header->set_error_message(res.err_msg.empty() ? "Stop motion failed" : res.err_msg); - } - - return grpc::Status::OK; - } - - // ===================== RobotTaskPause ===================== - /** - * @brief 暂停当前导航任务。 - * @param context 未使用。 - * @param request 包含设备 ID。 - * @param response 返回暂停结果。 - */ - grpc::Status gRPCAGVServiceImpl::RobotTaskPause( - grpc::ServerContext* context, - const api::RobotTaskPauseCommand::Request* request, - api::RobotTaskPauseCommand::Feedback* response) { - - const auto& header = request->header(); - std::string device_id = header.device_id(); - auto* fb_header = response->mutable_header(); - - if (device_id.empty()) { - fb_header->set_error_message("Missing device_id"); - return grpc::Status::OK; - } - - auto agv = dmgr_.getDevice(device_id); - if (!agv) { - fb_header->set_error_message("Device not found or not an AGV"); - return grpc::Status::OK; - } - - cmvr::device::AbstractAgv::RobotTaskPauseRes res; - try { - agv->robotTaskPause(res); - } catch (const std::exception& e) { - fb_header->set_error_message(std::string("Exception: ") + e.what()); - return grpc::Status::OK; - } - - auto* status = response->mutable_status(); - status->set_ret_code(res.ret_code); - status->set_create_on(res.create_on); - status->set_err_msg(res.err_msg); - - if (res.ret_code != 0) { - fb_header->set_error_message(res.err_msg.empty() ? "Pause task failed" : res.err_msg); - } else { - fb_header->set_error_message(""); - } - - return grpc::Status::OK; - } - - // ===================== RobotTaskResume ===================== - /** - * @brief 继续当前导航任务。 - * @param context 未使用。 - * @param request 包含设备 ID。 - * @param response 返回继续结果。 - */ - grpc::Status gRPCAGVServiceImpl::RobotTaskResume( - grpc::ServerContext* context, - const api::RobotTaskResumeCommand::Request* request, - api::RobotTaskResumeCommand::Feedback* response) { - - const auto& header = request->header(); - std::string device_id = header.device_id(); - auto* fb_header = response->mutable_header(); - - if (device_id.empty()) { - fb_header->set_error_message("Missing device_id"); - return grpc::Status::OK; - } - - auto agv = dmgr_.getDevice(device_id); - if (!agv) { - fb_header->set_error_message("Device not found or not an AGV"); - return grpc::Status::OK; - } - - cmvr::device::AbstractAgv::RobotTaskResumeRes res; - try { - agv->robotTaskResume(res); - } catch (const std::exception& e) { - fb_header->set_error_message(std::string("Exception: ") + e.what()); - return grpc::Status::OK; - } - - auto* status = response->mutable_status(); - status->set_ret_code(res.ret_code); - status->set_create_on(res.create_on); - status->set_err_msg(res.err_msg); - - if (res.ret_code != 0) { - fb_header->set_error_message(res.err_msg.empty() ? "Resume task failed" : res.err_msg); - } else { - fb_header->set_error_message(""); - } - - return grpc::Status::OK; - } - - // ===================== RobotTaskCancel ===================== - /** - * @brief 取消当前导航任务。 - * @param context 未使用。 - * @param request 包含设备 ID。 - * @param response 返回取消结果。 - */ - grpc::Status gRPCAGVServiceImpl::RobotTaskCancel( - grpc::ServerContext* context, - const api::RobotTaskCancelCommand::Request* request, - api::RobotTaskCancelCommand::Feedback* response) { - - const auto& header = request->header(); - std::string device_id = header.device_id(); - auto* fb_header = response->mutable_header(); - - if (device_id.empty()) { - fb_header->set_error_message("Missing device_id"); - return grpc::Status::OK; - } - - auto agv = dmgr_.getDevice(device_id); - if (!agv) { - fb_header->set_error_message("Device not found or not an AGV"); - return grpc::Status::OK; - } - - cmvr::device::AbstractAgv::RobotTaskCancelRes res; - try { - agv->robotTaskCancel(res); - } catch (const std::exception& e) { - fb_header->set_error_message(std::string("Exception: ") + e.what()); - return grpc::Status::OK; - } - - auto* status = response->mutable_status(); - status->set_ret_code(res.ret_code); - status->set_create_on(res.create_on); - status->set_err_msg(res.err_msg); - - if (res.ret_code != 0) { - fb_header->set_error_message(res.err_msg.empty() ? "Cancel task failed" : res.err_msg); - } else { - fb_header->set_error_message(""); - } - - return grpc::Status::OK; - } - -} // namespace cmvr::service \ No newline at end of file diff --git a/cmvr-es/service/grpc/src/grpc_humanoid_robot_service.cpp b/cmvr-es/service/grpc/src/grpc_humanoid_robot_service.cpp index 0c4e8469..487fe705 100644 --- a/cmvr-es/service/grpc/src/grpc_humanoid_robot_service.cpp +++ b/cmvr-es/service/grpc/src/grpc_humanoid_robot_service.cpp @@ -17,7 +17,7 @@ using google::protobuf::util::TimeUtil; gRPCHumanoidRobotServiceImpl::gRPCHumanoidRobotServiceImpl():dmgr_(DeviceManager::getInstance()) { - pinocchio_ik_ = std::make_shared("/home/cmvr/Projects/cmvr-es/model/xiaoyan_description/dual_arm.urdf" + pinocchio_ik_ = std::make_shared("/home/linbo/cmvr/cmvr-es/model/xiaoyan_description/dual_arm.urdf" ,"PELVIS_S" ,"R_WRIST_R_S"); pinocchio_ik_->init(); diff --git a/cmvr-es/service/grpc/src/server_runner.cpp b/cmvr-es/service/grpc/src/server_runner.cpp index c2817179..dc689aff 100644 --- a/cmvr-es/service/grpc/src/server_runner.cpp +++ b/cmvr-es/service/grpc/src/server_runner.cpp @@ -22,8 +22,6 @@ #include "service/grpc/include/grpc_hlc_service.h" #include "json/json.h" #include -#include "service/grpc/include/grpc_agv_service.h" -#include "common/utils/config_helper/include/config_helper.h" using namespace cmvr::service; ServerRunner::ServerRunner() = default; @@ -99,7 +97,9 @@ void ServerRunner::threadMain() { using namespace cmvr::monitor; try { - + if (!cfg_.hasChild("DeviceManager")) { + throw std::runtime_error("Device Manager node not found"); + } static std::once_flag reflection_once; std::call_once(reflection_once, []() { @@ -109,8 +109,13 @@ void ServerRunner::threadMain() { auto dmgr_cfg = cfg_.getChild("DeviceManager"); DeviceManager::getInstance(dmgr_cfg); - cmvr::ConfigHelper::getServerConfig(servercfg_); - std::string port = servercfg_.grpc_server().port(); + if (cfg_.hasChild("MonitorManager")) { + auto mmgr_cfg = cfg_.getChild("MonitorManager"); + MonitorManager::getInstance(mmgr_cfg); + } + + auto grpc_cfg = cfg_.getChild("gRPCServer"); + std::string port = grpc_cfg.getAttrDefault("port", "50051"); std::string local_address = "0.0.0.0:" + port; auto camera_service = std::make_unique(); @@ -121,7 +126,6 @@ void ServerRunner::threadMain() { auto biohand_service = std::make_unique(); auto humanoid_robot_service = std::make_unique(); auto hlc_service = std::make_unique(); - auto agv_service = std::make_unique(); grpc::ServerBuilder builder; builder.AddListeningPort(local_address, grpc::InsecureServerCredentials()); @@ -133,7 +137,6 @@ void ServerRunner::threadMain() { builder.RegisterService(biohand_service.get()); builder.RegisterService(humanoid_robot_service.get()); builder.RegisterService(hlc_service.get()); - builder.RegisterService(agv_service.get()); auto local_server = builder.BuildAndStart(); if (!local_server) { diff --git a/dependency/x86/third_party/soem/include/soem/ec_base.h b/dependency/x86/third_party/soem/include/soem/ec_base.h deleted file mode 100644 index 3eafa121..00000000 --- a/dependency/x86/third_party/soem/include/soem/ec_base.h +++ /dev/null @@ -1,42 +0,0 @@ -/* - * This software is dual-licensed under GPLv3 and a commercial - * license. See the file LICENSE.md distributed with this software for - * full license information. - */ - -/** \file - * \brief - * Headerfile for ec_base.c - */ - -#ifndef _ec_base_ -#define _ec_base_ - -#ifdef __cplusplus -extern "C" { -#endif - -int ecx_setupdatagram(ecx_portt *port, void *frame, uint8 com, uint8 idx, uint16 ADP, uint16 ADO, uint16 length, void *data); -uint16 ecx_adddatagram(ecx_portt *port, void *frame, uint8 com, uint8 idx, boolean more, uint16 ADP, uint16 ADO, uint16 length, void *data); -int ecx_BWR(ecx_portt *port, uint16 ADP, uint16 ADO, uint16 length, void *data, int timeout); -int ecx_BRD(ecx_portt *port, uint16 ADP, uint16 ADO, uint16 length, void *data, int timeout); -int ecx_APRD(ecx_portt *port, uint16 ADP, uint16 ADO, uint16 length, void *data, int timeout); -int ecx_ARMW(ecx_portt *port, uint16 ADP, uint16 ADO, uint16 length, void *data, int timeout); -int ecx_FRMW(ecx_portt *port, uint16 ADP, uint16 ADO, uint16 length, void *data, int timeout); -uint16 ecx_APRDw(ecx_portt *port, uint16 ADP, uint16 ADO, int timeout); -int ecx_FPRD(ecx_portt *port, uint16 ADP, uint16 ADO, uint16 length, void *data, int timeout); -uint16 ecx_FPRDw(ecx_portt *port, uint16 ADP, uint16 ADO, int timeout); -int ecx_APWRw(ecx_portt *port, uint16 ADP, uint16 ADO, uint16 data, int timeout); -int ecx_APWR(ecx_portt *port, uint16 ADP, uint16 ADO, uint16 length, void *data, int timeout); -int ecx_FPWRw(ecx_portt *port, uint16 ADP, uint16 ADO, uint16 data, int timeout); -int ecx_FPWR(ecx_portt *port, uint16 ADP, uint16 ADO, uint16 length, void *data, int timeout); -int ecx_LRW(ecx_portt *port, uint32 LogAdr, uint16 length, void *data, int timeout); -int ecx_LRD(ecx_portt *port, uint32 LogAdr, uint16 length, void *data, int timeout); -int ecx_LWR(ecx_portt *port, uint32 LogAdr, uint16 length, void *data, int timeout); -int ecx_LRWDC(ecx_portt *port, uint32 LogAdr, uint16 length, void *data, uint16 DCrs, int64 *DCtime, int timeout); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/dependency/x86/third_party/soem/include/soem/ec_coe.h b/dependency/x86/third_party/soem/include/soem/ec_coe.h deleted file mode 100644 index 45c889e5..00000000 --- a/dependency/x86/third_party/soem/include/soem/ec_coe.h +++ /dev/null @@ -1,73 +0,0 @@ -/* - * This software is dual-licensed under GPLv3 and a commercial - * license. See the file LICENSE.md distributed with this software for - * full license information. - */ - -/** \file - * \brief - * Headerfile for ec_coe.c - */ - -#ifndef _ec_coe_ -#define _ec_coe_ - -#ifdef __cplusplus -extern "C" { -#endif - -/* Storage for object description list */ -typedef struct -{ - /** slave number */ - uint16 Slave; - /** number of entries in list */ - uint16 Entries; - /** array of indexes */ - uint16 Index[EC_MAXODLIST]; - /** array of datatypes, see EtherCAT specification */ - uint16 DataType[EC_MAXODLIST]; - /** array of object codes, see EtherCAT specification */ - uint8 ObjectCode[EC_MAXODLIST]; - /** number of subindexes for each index */ - uint8 MaxSub[EC_MAXODLIST]; - /** textual description of each index */ - char Name[EC_MAXODLIST][EC_MAXNAME + 1]; -} ec_ODlistt; - -/* storage for object list entry information */ -typedef struct -{ - /** number of entries in list */ - uint16 Entries; - /** array of value infos, see EtherCAT specification */ - uint8 ValueInfo[EC_MAXOELIST]; - /** array of value infos, see EtherCAT specification */ - uint16 DataType[EC_MAXOELIST]; - /** array of bit lengths, see EtherCAT specification */ - uint16 BitLength[EC_MAXOELIST]; - /** array of object access bits, see EtherCAT specification */ - uint16 ObjAccess[EC_MAXOELIST]; - /** textual description of each index */ - char Name[EC_MAXOELIST][EC_MAXNAME + 1]; -} ec_OElistt; - -void ecx_SDOerror(ecx_contextt *context, uint16 Slave, uint16 Index, uint8 SubIdx, int32 AbortCode); -int ecx_SDOread(ecx_contextt *context, uint16 slave, uint16 index, uint8 subindex, - boolean CA, int *psize, void *p, int timeout); -int ecx_SDOwrite(ecx_contextt *context, uint16 Slave, uint16 Index, uint8 SubIndex, - boolean CA, int psize, const void *p, int Timeout); -int ecx_RxPDO(ecx_contextt *context, uint16 Slave, uint16 RxPDOnumber, int psize, const void *p); -int ecx_TxPDO(ecx_contextt *context, uint16 slave, uint16 TxPDOnumber, int *psize, void *p, int timeout); -int ecx_readPDOmap(ecx_contextt *context, uint16 Slave, uint32 *Osize, uint32 *Isize); -int ecx_readPDOmapCA(ecx_contextt *context, uint16 Slave, int Thread_n, uint32 *Osize, uint32 *Isize); -int ecx_readODlist(ecx_contextt *context, uint16 Slave, ec_ODlistt *pODlist); -int ecx_readODdescription(ecx_contextt *context, uint16 Item, ec_ODlistt *pODlist); -int ecx_readOEsingle(ecx_contextt *context, uint16 Item, uint8 SubI, ec_ODlistt *pODlist, ec_OElistt *pOElist); -int ecx_readOE(ecx_contextt *context, uint16 Item, ec_ODlistt *pODlist, ec_OElistt *pOElist); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/dependency/x86/third_party/soem/include/soem/ec_config.h b/dependency/x86/third_party/soem/include/soem/ec_config.h deleted file mode 100644 index c0291177..00000000 --- a/dependency/x86/third_party/soem/include/soem/ec_config.h +++ /dev/null @@ -1,31 +0,0 @@ -/* - * This software is dual-licensed under GPLv3 and a commercial - * license. See the file LICENSE.md distributed with this software for - * full license information. - */ - -/** \file - * \brief - * Headerfile for ec_config.c - */ - -#ifndef _ec_config_ -#define _ec_config_ - -#ifdef __cplusplus -extern "C" { -#endif - -#define EC_NODEOFFSET 0x1000 -#define EC_TEMPNODE 0xffff - -int ecx_config_init(ecx_contextt *context); -int ecx_config_map_group(ecx_contextt *context, void *pIOmap, uint8 group); -int ecx_recover_slave(ecx_contextt *context, uint16 slave, int timeout); -int ecx_reconfig_slave(ecx_contextt *context, uint16 slave, int timeout); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/dependency/x86/third_party/soem/include/soem/ec_dc.h b/dependency/x86/third_party/soem/include/soem/ec_dc.h deleted file mode 100644 index d0472dd3..00000000 --- a/dependency/x86/third_party/soem/include/soem/ec_dc.h +++ /dev/null @@ -1,27 +0,0 @@ -/* - * This software is dual-licensed under GPLv3 and a commercial - * license. See the file LICENSE.md distributed with this software for - * full license information. - */ - -/** \file - * \brief - * Headerfile for ec_dc.c - */ - -#ifndef _EC_ECATDC_H -#define _EC_ECATDC_H - -#ifdef __cplusplus -extern "C" { -#endif - -boolean ecx_configdc(ecx_contextt *context); -void ecx_dcsync0(ecx_contextt *context, uint16 slave, boolean act, uint32 CyclTime, int32 CyclShift); -void ecx_dcsync01(ecx_contextt *context, uint16 slave, boolean act, uint32 CyclTime0, uint32 CyclTime1, int32 CyclShift); - -#ifdef __cplusplus -} -#endif - -#endif /* _EC_ECATDC_H */ diff --git a/dependency/x86/third_party/soem/include/soem/ec_eoe.h b/dependency/x86/third_party/soem/include/soem/ec_eoe.h deleted file mode 100644 index 27ca216e..00000000 --- a/dependency/x86/third_party/soem/include/soem/ec_eoe.h +++ /dev/null @@ -1,213 +0,0 @@ -/* - * This software is dual-licensed under GPLv3 and a commercial - * license. See the file LICENSE.md distributed with this software for - * full license information. - */ - -/** \file - * \brief - * Headerfile for ec_eoe.c - */ - -#ifndef _ec_eoe_ -#define _ec_eoe_ - -#ifdef __cplusplus -extern "C" { -#endif - -#include "soem/soem.h" - -/* use maximum size for EOE mailbox data - mbxheader and 2x frameinfo */ -#define EC_MAXEOEDATA (EC_MAXMBX - (sizeof(ec_mbxheadert) + \ - sizeof(uint16_t) + \ - sizeof(uint16_t))) - -/** DNS length according to ETG 1000.6 */ -#define EOE_DNS_NAME_LENGTH 32 -/** Ethernet address length not including VLAN */ -#define EOE_ETHADDR_LENGTH 6 -/** IPv4 address length */ -#define EOE_IP4_LENGTH sizeof(uint32_t) - -#define EOE_MAKEU32(a, b, c, d) (((uint32_t)((a) & 0xff) << 24) | \ - ((uint32_t)((b) & 0xff) << 16) | \ - ((uint32_t)((c) & 0xff) << 8) | \ - (uint32_t)((d) & 0xff)) - -#if !defined(EC_BIG_ENDIAN) && defined(EC_LITTLE_ENDIAN) - -#define EOE_HTONS(x) ((((x) & 0x00ffUL) << 8) | (((x) & 0xff00UL) >> 8)) -#define EOE_NTOHS(x) EOE_HTONS(x) -#define EOE_HTONL(x) ((((x) & 0x000000ffUL) << 24) | \ - (((x) & 0x0000ff00UL) << 8) | \ - (((x) & 0x00ff0000UL) >> 8) | \ - (((x) & 0xff000000UL) >> 24)) -#define EOE_NTOHL(x) EOE_HTONL(x) -#else -#define EOE_HTONS(x) (x) -#define EOE_NTOHS(x) (x) -#define EOE_HTONL(x) (x) -#define EOE_NTOHL(x) (x) -#endif /* !defined(EC_BIG_ENDIAN) && defined(EC_LITTLE_ENDIAN) */ - -/** Get one byte from the 4-byte address */ -#define eoe_ip4_addr1(ipaddr) (((const uint8_t *)(&(ipaddr)->addr))[0]) -#define eoe_ip4_addr2(ipaddr) (((const uint8_t *)(&(ipaddr)->addr))[1]) -#define eoe_ip4_addr3(ipaddr) (((const uint8_t *)(&(ipaddr)->addr))[2]) -#define eoe_ip4_addr4(ipaddr) (((const uint8_t *)(&(ipaddr)->addr))[3]) - -/** Set an IP address given by the four byte-parts */ -#define EOE_IP4_ADDR_TO_U32(ipaddr, a, b, c, d) \ - (ipaddr)->addr = EOE_HTONL(EOE_MAKEU32(a, b, c, d)) - -/** Header frame info 1 */ -#define EOE_HDR_FRAME_TYPE_OFFSET 0 -#define EOE_HDR_FRAME_TYPE (0xF << 0) -#define EOE_HDR_FRAME_TYPE_SET(x) (((x) & 0xF) << 0) -#define EOE_HDR_FRAME_TYPE_GET(x) (((x) >> 0) & 0xF) -#define EOE_HDR_FRAME_PORT_OFFSET 4 -#define EOE_HDR_FRAME_PORT (0xF << 4) -#define EOE_HDR_FRAME_PORT_SET(x) ((uint16)(((x) & 0xF) << 4)) -#define EOE_HDR_FRAME_PORT_GET(x) (((x) >> 4) & 0xF) -#define EOE_HDR_LAST_FRAGMENT_OFFSET 8 -#define EOE_HDR_LAST_FRAGMENT (0x1 << 8) -#define EOE_HDR_LAST_FRAGMENT_SET(x) (((x) & 0x1) << 8) -#define EOE_HDR_LAST_FRAGMENT_GET(x) (((x) >> 8) & 0x1) -#define EOE_HDR_TIME_APPEND_OFFSET 9 -#define EOE_HDR_TIME_APPEND (0x1 << 9) -#define EOE_HDR_TIME_APPEND_SET(x) (((x) & 0x1) << 9) -#define EOE_HDR_TIME_APPEND_GET(x) (((x) >> 9) & 0x1) -#define EOE_HDR_TIME_REQUEST_OFFSET 10 -#define EOE_HDR_TIME_REQUEST (0x1 << 10) -#define EOE_HDR_TIME_REQUEST_SET(x) (((x) & 0x1) << 10) -#define EOE_HDR_TIME_REQUEST_GET(x) (((x) >> 10) & 0x1) - -/** Header frame info 2 */ -#define EOE_HDR_FRAG_NO_OFFSET 0 -#define EOE_HDR_FRAG_NO (0x3F << 0) -#define EOE_HDR_FRAG_NO_SET(x) ((uint16)(((x) & 0x3F) << 0)) -#define EOE_HDR_FRAG_NO_GET(x) (((x) >> 0) & 0x3F) -#define EOE_HDR_FRAME_OFFSET_OFFSET 6 -#define EOE_HDR_FRAME_OFFSET (0x3F << 6) -#define EOE_HDR_FRAME_OFFSET_SET(x) ((uint16)(((x) & 0x3F) << 6)) -#define EOE_HDR_FRAME_OFFSET_GET(x) (((x) >> 6) & 0x3F) -#define EOE_HDR_FRAME_NO_OFFSET 12 -#define EOE_HDR_FRAME_NO (0xF << 12) -#define EOE_HDR_FRAME_NO_SET(x) ((uint16)(((x) & 0xF) << 12)) -#define EOE_HDR_FRAME_NO_GET(x) (((x) >> 12) & 0xF) - -/** EOE param */ -#define EOE_PARAM_OFFSET 4 -#define EOE_PARAM_MAC_INCLUDE (0x1 << 0) -#define EOE_PARAM_IP_INCLUDE (0x1 << 1) -#define EOE_PARAM_SUBNET_IP_INCLUDE (0x1 << 2) -#define EOE_PARAM_DEFAULT_GATEWAY_INCLUDE (0x1 << 3) -#define EOE_PARAM_DNS_IP_INCLUDE (0x1 << 4) -#define EOE_PARAM_DNS_NAME_INCLUDE (0x1 << 5) - -/** EoE frame types */ -#define EOE_FRAG_DATA 0 -#define EOE_INIT_RESP_TIMESTAMP 1 -#define EOE_INIT_REQ 2 /* Spec SET IP REQ */ -#define EOE_INIT_RESP 3 /* Spec SET IP RESP */ -#define EOE_SET_ADDR_FILTER_REQ 4 -#define EOE_SET_ADDR_FILTER_RESP 5 -#define EOE_GET_IP_PARAM_REQ 6 -#define EOE_GET_IP_PARAM_RESP 7 -#define EOE_GET_ADDR_FILTER_REQ 8 -#define EOE_GET_ADDR_FILTER_RESP 9 - -/** EoE parameter result codes */ -#define EOE_RESULT_SUCCESS 0x0000 -#define EOE_RESULT_UNSPECIFIED_ERROR 0x0001 -#define EOE_RESULT_UNSUPPORTED_FRAME_TYPE 0x0002 -#define EOE_RESULT_NO_IP_SUPPORT 0x0201 -#define EOE_RESULT_NO_DHCP_SUPPORT 0x0202 -#define EOE_RESULT_NO_FILTER_SUPPORT 0x0401 - -/** EOE ip4 address in network order */ -typedef struct eoe_ip4_addr -{ - uint32_t addr; -} eoe_ip4_addr_t; - -/** EOE ethernet address */ -OSAL_PACKED_BEGIN -typedef struct OSAL_PACKED eoe_ethaddr -{ - uint8_t addr[EOE_ETHADDR_LENGTH]; -} eoe_ethaddr_t; -OSAL_PACKED_END - -/** EoE IP request structure, storage only, no need to pack */ -typedef struct eoe_param -{ - uint8_t mac_set : 1; - uint8_t ip_set : 1; - uint8_t subnet_set : 1; - uint8_t default_gateway_set : 1; - uint8_t dns_ip_set : 1; - uint8_t dns_name_set : 1; - eoe_ethaddr_t mac; - eoe_ip4_addr_t ip; - eoe_ip4_addr_t subnet; - eoe_ip4_addr_t default_gateway; - eoe_ip4_addr_t dns_ip; - char dns_name[EOE_DNS_NAME_LENGTH]; -} eoe_param_t; - -/** EOE structure. - * Used to interpret EoE mailbox packets. - */ -OSAL_PACKED_BEGIN -typedef struct OSAL_PACKED -{ - ec_mbxheadert mbxheader; - uint16_t frameinfo1; - union - { - uint16_t frameinfo2; - uint16_t result; - }; - uint8 data[EC_MAXEOEDATA]; -} ec_EOEt; -OSAL_PACKED_END - -int ecx_EOEdefinehook(ecx_contextt *context, void *hook); -int ecx_EOEsetIp(ecx_contextt *context, - uint16 slave, - uint8 port, - eoe_param_t *ipparam, - int timeout); -int ecx_EOEgetIp(ecx_contextt *context, - uint16 slave, - uint8 port, - eoe_param_t *ipparam, - int timeout); -int ecx_EOEsend(ecx_contextt *context, - uint16 slave, - uint8 port, - int psize, - void *p, - int timeout); -int ecx_EOErecv(ecx_contextt *context, - uint16 slave, - uint8 port, - int *psize, - void *p, - int timeout); -int ecx_EOEreadfragment( - ec_mbxbuft *MbxIn, - uint8 *rxfragmentno, - uint16 *rxframesize, - uint16 *rxframeoffset, - uint16 *rxframeno, - int *psize, - void *p); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/dependency/x86/third_party/soem/include/soem/ec_foe.h b/dependency/x86/third_party/soem/include/soem/ec_foe.h deleted file mode 100644 index 6348b08a..00000000 --- a/dependency/x86/third_party/soem/include/soem/ec_foe.h +++ /dev/null @@ -1,27 +0,0 @@ -/* - * This software is dual-licensed under GPLv3 and a commercial - * license. See the file LICENSE.md distributed with this software for - * full license information. - */ - -/** \file - * \brief - * Headerfile for ec_foe.c - */ - -#ifndef _ec_foe_ -#define _ec_foe_ - -#ifdef __cplusplus -extern "C" { -#endif - -int ecx_FOEdefinehook(ecx_contextt *context, void *hook); -int ecx_FOEread(ecx_contextt *context, uint16 slave, char *filename, uint32 password, int *psize, void *p, int timeout); -int ecx_FOEwrite(ecx_contextt *context, uint16 slave, char *filename, uint32 password, int psize, void *p, int timeout); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/dependency/x86/third_party/soem/include/soem/ec_main.h b/dependency/x86/third_party/soem/include/soem/ec_main.h deleted file mode 100644 index 70189802..00000000 --- a/dependency/x86/third_party/soem/include/soem/ec_main.h +++ /dev/null @@ -1,624 +0,0 @@ -/* - * This software is dual-licensed under GPLv3 and a commercial - * license. See the file LICENSE.md distributed with this software for - * full license information. - */ - -/** \file - * \brief - * Headerfile for ec_main.c - */ - -#ifndef _ec_main_ -#define _ec_main_ - -#ifdef __cplusplus -extern "C" { -#endif - -#include "soem/ec_options.h" - -typedef struct ec_adapter ec_adaptert; -struct ec_adapter -{ - char name[EC_MAXLEN_ADAPTERNAME]; - char desc[EC_MAXLEN_ADAPTERNAME]; - ec_adaptert *next; -}; - -/** record for FMMU */ -OSAL_PACKED_BEGIN -typedef struct OSAL_PACKED ec_fmmu -{ - uint32 LogStart; - uint16 LogLength; - uint8 LogStartbit; - uint8 LogEndbit; - uint16 PhysStart; - uint8 PhysStartBit; - uint8 FMMUtype; - uint8 FMMUactive; - uint8 unused1; - uint16 unused2; -} ec_fmmut; -OSAL_PACKED_END - -/** record for sync manager */ -OSAL_PACKED_BEGIN -typedef struct OSAL_PACKED ec_sm -{ - uint16 StartAddr; - uint16 SMlength; - uint32 SMflags; -} ec_smt; -OSAL_PACKED_END - -OSAL_PACKED_BEGIN -typedef struct OSAL_PACKED ec_state_status -{ - uint16 State; - uint16 Unused; - uint16 ALstatuscode; -} ec_state_status; -OSAL_PACKED_END - -/** mailbox buffer array */ -typedef uint8 ec_mbxbuft[EC_MAXMBX + 1]; - -#define EC_MBXINENABLE (uint8 *)1 - -typedef struct -{ - int listhead, listtail, listcount; - int mbxemptylist[EC_MBXPOOLSIZE]; - osal_mutext *mbxmutex; - ec_mbxbuft mbx[EC_MBXPOOLSIZE]; -} ec_mbxpoolt; - -#define EC_MBXQUEUESTATE_NONE 0 -#define EC_MBXQUEUESTATE_REQ 1 -#define EC_MBXQUEUESTATE_FAIL 2 -#define EC_MBXQUEUESTATE_DONE 3 - -typedef struct -{ - int listhead, listtail, listcount; - ec_mbxbuft *mbx[EC_MBXPOOLSIZE]; - int mbxstate[EC_MBXPOOLSIZE]; - int mbxremove[EC_MBXPOOLSIZE]; - int mbxticket[EC_MBXPOOLSIZE]; - uint16 mbxslave[EC_MBXPOOLSIZE]; - osal_mutext *mbxmutex; -} ec_mbxqueuet; - -#define ECT_MBXPROT_AOE 0x0001 -#define ECT_MBXPROT_EOE 0x0002 -#define ECT_MBXPROT_COE 0x0004 -#define ECT_MBXPROT_FOE 0x0008 -#define ECT_MBXPROT_SOE 0x0010 -#define ECT_MBXPROT_VOE 0x0020 - -#define ECT_COEDET_SDO 0x01 -#define ECT_COEDET_SDOINFO 0x02 -#define ECT_COEDET_PDOASSIGN 0x04 -#define ECT_COEDET_PDOCONFIG 0x08 -#define ECT_COEDET_UPLOAD 0x10 -#define ECT_COEDET_SDOCA 0x20 - -#define EC_SMENABLEMASK 0xfffeffff - -typedef struct ecx_context ecx_contextt; - -#define ECT_MBXH_NONE 0 -#define ECT_MBXH_CYCLIC 1 -#define ECT_MBXH_LOST 2 - -/** Slave state - * All slave information is put in this structure. Needed for most - * user interaction with slaves. - */ -typedef struct ec_slave -{ - /** state of slave */ - uint16 state; - /** AL status code */ - uint16 ALstatuscode; - /** Configured address */ - uint16 configadr; - /** Alias address */ - uint16 aliasadr; - /** Manufacturer from EEprom */ - uint32 eep_man; - /** ID from EEprom */ - uint32 eep_id; - /** revision from EEprom */ - uint32 eep_rev; - /** serial number from EEprom */ - uint32 eep_ser; - /** Interface type */ - uint16 Itype; - /** Device type */ - uint16 Dtype; - /** output bits */ - uint16 Obits; - /** output bytes, if Obits < 8 then Obytes = 0 */ - uint32 Obytes; - /** output pointer in IOmap buffer */ - uint8 *outputs; - /** output offset in IOmap buffer */ - uint32 Ooffset; - /** startbit in first output byte */ - uint8 Ostartbit; - /** input bits */ - uint16 Ibits; - /** input bytes, if Ibits < 8 then Ibytes = 0 */ - uint32 Ibytes; - /** input pointer in IOmap buffer */ - uint8 *inputs; - /** input offset in IOmap buffer */ - uint32 Ioffset; - /** startbit in first input byte */ - uint8 Istartbit; - /** SM structure */ - ec_smt SM[EC_MAXSM]; - /** SM type 0=unused 1=MbxWr 2=MbxRd 3=Outputs 4=Inputs */ - uint8 SMtype[EC_MAXSM]; - /** FMMU structure */ - ec_fmmut FMMU[EC_MAXFMMU]; - /** FMMU0 function 0=unused 1=outputs 2=inputs 3=SM status*/ - uint8 FMMU0func; - /** FMMU1 function */ - uint8 FMMU1func; - /** FMMU2 function */ - uint8 FMMU2func; - /** FMMU3 function */ - uint8 FMMU3func; - /** length of write mailbox in bytes, if no mailbox then 0 */ - uint16 mbx_l; - /** mailbox write offset */ - uint16 mbx_wo; - /** length of read mailbox in bytes */ - uint16 mbx_rl; - /** mailbox read offset */ - uint16 mbx_ro; - /** mailbox supported protocols */ - uint16 mbx_proto; - /** Counter value of mailbox link layer protocol 1..7 */ - uint8 mbx_cnt; - /** has DC capability */ - boolean hasdc; - /** Physical type; Ebus, EtherNet combinations */ - uint8 ptype; - /** topology: 1 to 3 links */ - uint8 topology; - /** active ports bitmap : ....3210 , set if respective port is active **/ - uint8 activeports; - /** consumed ports bitmap : ....3210, used for internal delay measurement **/ - uint8 consumedports; - /** slave number for parent, 0=master */ - uint16 parent; - /** port number on parent this slave is connected to **/ - uint8 parentport; - /** port number on this slave the parent is connected to **/ - uint8 entryport; - /** DC receivetimes on port A */ - int32 DCrtA; - /** DC receivetimes on port B */ - int32 DCrtB; - /** DC receivetimes on port C */ - int32 DCrtC; - /** DC receivetimes on port D */ - int32 DCrtD; - /** propagation delay */ - int32 pdelay; - /** next DC slave */ - uint16 DCnext; - /** previous DC slave */ - uint16 DCprevious; - /** DC cycle time in ns */ - int32 DCcycle; - /** DC shift from clock modulus boundary */ - int32 DCshift; - /** DC sync activation, 0=off, 1=on */ - uint8 DCactive; - /** link to SII config */ - uint16 SIIindex; - /** 1 = 8 bytes per read, 0 = 4 bytes per read */ - uint8 eep_8byte; - /** 0 = eeprom to master , 1 = eeprom to PDI */ - uint8 eep_pdi; - /** CoE details */ - uint8 CoEdetails; - /** FoE details */ - uint8 FoEdetails; - /** EoE details */ - uint8 EoEdetails; - /** SoE details */ - uint8 SoEdetails; - /** E-bus current */ - int16 Ebuscurrent; - /** if >0 block use of LRW in processdata */ - uint8 blockLRW; - /** group */ - uint8 group; - /** first unused FMMU */ - uint8 FMMUunused; - /** Boolean for tracking whether the slave is (not) responding, not used/set by the SOEM library */ - boolean islost; - /** registered configuration function PO->SO */ - int (*PO2SOconfig)(ecx_contextt *context, uint16 slave); - /** mailbox handler state, 0 = no handler, 1 = cyclic task mbx handler, 2 = slave lost */ - int mbxhandlerstate; - /** mailbox handler robust mailbox protocol state */ - int mbxrmpstate; - /** mailbox handler RMP extended mbx in state */ - uint16 mbxinstateex; - /** pointer to CoE mailbox in buffer */ - uint8 *coembxin; - /** CoE mailbox in flag, true = mailbox full */ - boolean coembxinfull; - /** CoE mailbox in overrun counter */ - int coembxoverrun; - /** pointer to SoE mailbox in buffer */ - uint8 *soembxin; - /** SoE mailbox in flag, true = mailbox full */ - boolean soembxinfull; - /** SoE mailbox in overrun counter */ - int soembxoverrun; - /** pointer to FoE mailbox in buffer */ - uint8 *foembxin; - /** FoE mailbox in flag, true = mailbox full */ - boolean foembxinfull; - /** FoE mailbox in overrun counter */ - int foembxoverrun; - /** pointer to EoE mailbox in buffer */ - uint8 *eoembxin; - /** EoE mailbox in flag, true = mailbox full */ - boolean eoembxinfull; - /** EoE mailbox in overrun counter */ - int eoembxoverrun; - /** pointer to VoE mailbox in buffer */ - uint8 *voembxin; - /** VoE mailbox in flag, true = mailbox full */ - boolean voembxinfull; - /** VoE mailbox in overrun counter */ - int voembxoverrun; - /** pointer to AoE mailbox in buffer */ - uint8 *aoembxin; - /** AoE mailbox in flag, true = mailbox full */ - boolean aoembxinfull; - /** AoE mailbox in overrun counter */ - int aoembxoverrun; - /** pointer to out mailbox status register buffer */ - uint8 *mbxstatus; - /** readable name */ - char name[EC_MAXNAME + 1]; -} ec_slavet; - -/** for list of ethercat slave groups */ -typedef struct ec_group -{ - /** logical start address for this group */ - uint32 logstartaddr; - /** output bytes, if Obits < 8 then Obytes = 0 */ - uint32 Obytes; - /** output pointer in IOmap buffer */ - uint8 *outputs; - /** input bytes, if Ibits < 8 then Ibytes = 0 */ - uint32 Ibytes; - /** input pointer in IOmap buffer */ - uint8 *inputs; - /** has DC capability */ - boolean hasdc; - /** next DC slave */ - uint16 DCnext; - /** E-bus current */ - int16 Ebuscurrent; - /** if >0 block use of LRW in processdata */ - uint8 blockLRW; - /** IO segments used */ - uint16 nsegments; - /** 1st input segment */ - uint16 Isegment; - /** Offset in input segment */ - uint16 Ioffset; - /** Expected workcounter outputs */ - uint16 outputsWKC; - /** Expected workcounter inputs */ - uint16 inputsWKC; - /** check slave states */ - boolean docheckstate; - /** IO segmentation list. Datagrams must not break SM in two. */ - uint32 IOsegment[EC_MAXIOSEGMENTS]; - /** pointer to out mailbox status register buffer */ - uint8 *mbxstatus; - /** mailbox status register buffer length */ - int32 mbxstatuslength; - /** mailbox status lookup table */ - uint16 mbxstatuslookup[EC_MAXSLAVE]; - /** mailbox last handled in mxbhandler */ - uint16 lastmbxpos; - /** mailbox transmit queue struct */ - ec_mbxqueuet mbxtxqueue; -} ec_groupt; - -#define ECT_ESMTRANS_IP 0x0001 -#define ECT_ESMTRANS_PS 0x0002 -#define ECT_ESMTRANS_PI 0x0004 -#define ECT_ESMTRANS_SP 0x0008 -#define ECT_ESMTRANS_SO 0x0010 -#define ECT_ESMTRANS_SI 0x0020 -#define ECT_ESMTRANS_OS 0x0040 -#define ECT_ESMTRANS_OP 0x0080 -#define ECT_ESMTRANS_OI 0x0100 -#define ECT_ESMTRANS_IB 0x0200 -#define ECT_ESMTRANS_BI 0x0400 -#define ECT_ESMTRANS_II 0x0800 -#define ECT_ESMTRANS_PP 0x1000 -#define ECT_ESMTRANS_SS 0x2000 - -/** ENI CoE command structure */ -typedef struct ec_enicoecmd -{ - /** transition(s) during which command should be sent */ - uint16 Transition; - /** complete access flag */ - boolean CA; - /** ccs (1 = read, 2 = write) */ - uint8 Ccs; - /** object index */ - uint16 Index; - /** object subindex */ - uint8 SubIdx; - /** timeout in us */ - int Timeout; - /** size in bytes of parameter buffer */ - int DataSize; - /** pointer to parameter buffer */ - void *Data; -} ec_enicoecmdt; - -/** ENI slave structure */ -typedef struct ec_enislave -{ - uint16 Slave; - uint32 VendorId; - uint32 ProductCode; - uint32 RevisionNo; - ec_enicoecmdt *CoECmds; - int CoECmdCount; -} ec_enislavet; - -/** ENI structure */ -typedef struct ec_eni -{ - ec_enislavet *slave; - int slavecount; -} ec_enit; - -/** SII FMMU structure */ -typedef struct ec_eepromFMMU -{ - uint16 Startpos; - uint8 nFMMU; - uint8 FMMU0; - uint8 FMMU1; - uint8 FMMU2; - uint8 FMMU3; -} ec_eepromFMMUt; - -/** SII SM structure */ -typedef struct ec_eepromSM -{ - uint16 Startpos; - uint8 nSM; - uint16 PhStart; - uint16 Plength; - uint8 Creg; - uint8 Sreg; /* don't care */ - uint8 Activate; - uint8 PDIctrl; /* don't care */ -} ec_eepromSMt; - -/** record to store rxPDO and txPDO table from eeprom */ -typedef struct ec_eepromPDO -{ - uint16 Startpos; - uint16 Length; - uint16 nPDO; - uint16 Index[EC_MAXEEPDO]; - uint16 SyncM[EC_MAXEEPDO]; - uint16 BitSize[EC_MAXEEPDO]; - uint16 SMbitsize[EC_MAXSM]; -} ec_eepromPDOt; - -/** standard ethercat mailbox header */ -OSAL_PACKED_BEGIN -typedef struct OSAL_PACKED ec_mbxheader -{ - uint16 length; - uint16 address; - uint8 priority; - uint8 mbxtype; -} ec_mbxheadert; -OSAL_PACKED_END - -/** ALstatus and ALstatus code */ -OSAL_PACKED_BEGIN -typedef struct OSAL_PACKED ec_alstatus -{ - uint16 alstatus; - uint16 unused; - uint16 alstatuscode; -} ec_alstatust; -OSAL_PACKED_END - -/** stack structure to store segmented LRD/LWR/LRW constructs */ -typedef struct ec_idxstack -{ - uint8 pushed; - uint8 pulled; - uint8 idx[EC_MAXBUF]; - void *data[EC_MAXBUF]; - uint16 length[EC_MAXBUF]; - uint16 dcoffset[EC_MAXBUF]; - uint8 type[EC_MAXBUF]; -} ec_idxstackT; - -/** ringbuf for error storage */ -typedef struct ec_ering -{ - int16 head; - int16 tail; - ec_errort Error[EC_MAXELIST + 1]; -} ec_eringt; - -/** SyncManager Communication Type structure for CA */ -OSAL_PACKED_BEGIN -typedef struct OSAL_PACKED ec_SMcommtype -{ - uint8 n; - uint8 nu1; - uint8 SMtype[EC_MAXSM]; -} ec_SMcommtypet; -OSAL_PACKED_END - -/** SDO assign structure for CA */ -OSAL_PACKED_BEGIN -typedef struct OSAL_PACKED ec_PDOassign -{ - uint8 n; - uint8 nu1; - uint16 index[256]; -} ec_PDOassignt; -OSAL_PACKED_END - -/** SDO description structure for CA */ -OSAL_PACKED_BEGIN -typedef struct OSAL_PACKED ec_PDOdesc -{ - uint8 n; - uint8 nu1; - uint32 PDO[256]; -} ec_PDOdesct; -OSAL_PACKED_END - -/** Context structure, referenced by all ecx functions*/ -struct ecx_context -{ - /** @publicsection */ - /* Network state */ - - /** port, may include red_port */ - ecx_portt port; - /** list of detected slaves */ - ec_slavet slavelist[EC_MAXSLAVE]; - /** number of slaves found in configuration */ - int slavecount; - /** list of groups */ - ec_groupt grouplist[EC_MAXGROUP]; - /** ecaterror state */ - boolean ecaterror; - /** last DC time from slaves */ - int64 DCtime; - - /** @privatesection */ - /* Internal state */ - - /** internal, eeprom cache buffer */ - uint8 esibuf[EC_MAXEEPBUF]; - /** internal, eeprom cache map */ - uint32 esimap[EC_MAXEEPBITMAP]; - /** internal, current slave for eeprom cache */ - uint16 esislave; - /** internal, error list */ - ec_eringt elist; - /** internal, processdata stack buffer info */ - ec_idxstackT idxstack; - /** internal, SM buffer */ - ec_SMcommtypet SMcommtype[EC_MAX_MAPT]; - /** internal, PDO assign list */ - ec_PDOassignt PDOassign[EC_MAX_MAPT]; - /** internal, PDO description list */ - ec_PDOdesct PDOdesc[EC_MAX_MAPT]; - /** internal, SM list from eeprom */ - ec_eepromSMt eepSM; - /** internal, FMMU list from eeprom */ - ec_eepromFMMUt eepFMMU; - /** internal, mailbox pool */ - ec_mbxpoolt mbxpool; - - /** @publicsection */ - /* Configurable settings */ - - /** network information hook */ - ec_enit *ENI; - /** registered FoE hook */ - int (*FOEhook)(uint16 slave, int packetnumber, int datasize); - /** registered EoE hook */ - int (*EOEhook)(ecx_contextt *context, uint16 slave, void *eoembx); - /** flag to control legacy automatic state change or manual state change */ - int manualstatechange; - /** opaque pointer to application userdata, never used by SOEM. */ - void *userdata; - /** In overlapped mode, inputs will replace outputs in the incoming - * frame. Use this mode for TI ESC:s. Processdata is always aligned - * on a byte boundary. */ - boolean overlappedMode; - /** Do not map each slave on a byte boundary. May result in smaller - * frame sizes. Has no effect in overlapped mode. */ - boolean packedMode; -}; - -ec_adaptert *ec_find_adapters(void); -void ec_free_adapters(ec_adaptert *adapter); -uint8 ec_nextmbxcnt(uint8 cnt); -void ec_clearmbx(ec_mbxbuft *Mbx); -void ecx_pusherror(ecx_contextt *context, const ec_errort *Ec); -boolean ecx_poperror(ecx_contextt *context, ec_errort *Ec); -boolean ecx_iserror(ecx_contextt *context); -void ecx_packeterror(ecx_contextt *context, uint16 Slave, uint16 Index, uint8 SubIdx, uint16 ErrorCode); -int ecx_init(ecx_contextt *context, const char *ifname); -int ecx_init_redundant(ecx_contextt *context, ecx_redportt *redport, const char *ifname, char *if2name); -void ecx_close(ecx_contextt *context); -uint8 ecx_siigetbyte(ecx_contextt *context, uint16 slave, uint16 address); -int16 ecx_siifind(ecx_contextt *context, uint16 slave, uint16 cat); -void ecx_siistring(ecx_contextt *context, char *str, uint16 slave, uint16 Sn); -uint16 ecx_siiFMMU(ecx_contextt *context, uint16 slave, ec_eepromFMMUt *FMMU); -uint16 ecx_siiSM(ecx_contextt *context, uint16 slave, ec_eepromSMt *SM); -uint16 ecx_siiSMnext(ecx_contextt *context, uint16 slave, ec_eepromSMt *SM, uint16 n); -uint32 ecx_siiPDO(ecx_contextt *context, uint16 slave, ec_eepromPDOt *PDO, uint8 t); -int ecx_readstate(ecx_contextt *context); -int ecx_writestate(ecx_contextt *context, uint16 slave); -uint16 ecx_statecheck(ecx_contextt *context, uint16 slave, uint16 reqstate, int timeout); -int ecx_mbxhandler(ecx_contextt *context, uint8 group, int limit); -int ecx_mbxempty(ecx_contextt *context, uint16 slave, int timeout); -int ecx_mbxsend(ecx_contextt *context, uint16 slave, ec_mbxbuft *mbx, int timeout); -int ecx_mbxreceive(ecx_contextt *context, uint16 slave, ec_mbxbuft **mbx, int timeout); -int ecx_mbxENIinitcmds(ecx_contextt *context, uint16 slave, uint16_t transition); -void ecx_esidump(ecx_contextt *context, uint16 slave, uint8 *esibuf); -uint32 ecx_readeeprom(ecx_contextt *context, uint16 slave, uint16 eeproma, int timeout); -int ecx_writeeeprom(ecx_contextt *context, uint16 slave, uint16 eeproma, uint16 data, int timeout); -int ecx_eeprom2master(ecx_contextt *context, uint16 slave); -int ecx_eeprom2pdi(ecx_contextt *context, uint16 slave); -uint64 ecx_readeepromAP(ecx_contextt *context, uint16 aiadr, uint16 eeproma, int timeout); -int ecx_writeeepromAP(ecx_contextt *context, uint16 aiadr, uint16 eeproma, uint16 data, int timeout); -uint64 ecx_readeepromFP(ecx_contextt *context, uint16 configadr, uint16 eeproma, int timeout); -int ecx_writeeepromFP(ecx_contextt *context, uint16 configadr, uint16 eeproma, uint16 data, int timeout); -void ecx_readeeprom1(ecx_contextt *context, uint16 slave, uint16 eeproma); -uint32 ecx_readeeprom2(ecx_contextt *context, uint16 slave, int timeout); -int ecx_receive_processdata_group(ecx_contextt *context, uint8 group, int timeout); -int ecx_send_processdata(ecx_contextt *context); -int ecx_receive_processdata(ecx_contextt *context, int timeout); -int ecx_send_processdata_group(ecx_contextt *context, uint8 group); -ec_mbxbuft *ecx_getmbx(ecx_contextt *context); -int ecx_dropmbx(ecx_contextt *context, ec_mbxbuft *mbx); -int ecx_initmbxpool(ecx_contextt *context); -int ecx_initmbxqueue(ecx_contextt *context, uint8 group); -int ecx_slavembxcyclic(ecx_contextt *context, uint16 slave); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/dependency/x86/third_party/soem/include/soem/ec_options.h b/dependency/x86/third_party/soem/include/soem/ec_options.h deleted file mode 100644 index a6b3d7f9..00000000 --- a/dependency/x86/third_party/soem/include/soem/ec_options.h +++ /dev/null @@ -1,128 +0,0 @@ -/* - * This software is dual-licensed under GPLv3 and a commercial - * license. See the file LICENSE.md distributed with this software for - * full license information. - */ - -/** \file - * \brief - * Build options for SOEM - */ - -#ifndef _ec_options_ -#define _ec_options_ - -#ifdef __cplusplus -extern "C" { -#endif - -/* Max sizes */ - -/** standard frame buffer size in bytes */ -#define EC_BUFSIZE (EC_MAXECATFRAME) - -/** number of frame buffers per channel (tx, rx1 rx2) */ -#define EC_MAXBUF (16) - -/** size of EEPROM bitmap cache */ -#define EC_MAXEEPBITMAP (128) - -/** size of EEPROM cache buffer */ -#define EC_MAXEEPBUF (EC_MAXEEPBITMAP << 5) - -/** default group size in 2^x */ -#define EC_LOGGROUPOFFSET (16) - -/** max. entries in EtherCAT error list */ -#define EC_MAXELIST (64) - -/** max. length of readable name in slavelist and Object Description List */ -#define EC_MAXNAME (40) - -/** max. number of slaves in array */ -#define EC_MAXSLAVE (200) - -/** max. number of groups */ -#define EC_MAXGROUP (2) - -/** max. number of IO segments per group */ -#define EC_MAXIOSEGMENTS (64) - -/** max. mailbox size */ -#define EC_MAXMBX (1486) - -/** number of mailboxes in pool */ -#define EC_MBXPOOLSIZE (32) - -/** max. eeprom PDO entries */ -#define EC_MAXEEPDO (0x200) - -/** max. SM used */ -#define EC_MAXSM (8) - -/** max. FMMU used */ -#define EC_MAXFMMU (4) - -/** max. adapter name length */ -#define EC_MAXLEN_ADAPTERNAME (128) - -/** define maximum number of concurrent threads in mapping */ -#define EC_MAX_MAPT (1) - -/** max entries in Object Description list */ -#define EC_MAXODLIST (1024) - -/** max entries in Object Entry list */ -#define EC_MAXOELIST (256) - -/** max. length of readable SoE name */ -#define EC_SOE_MAXNAME (60) - -/** max. number of SoE mappings */ -#define EC_SOE_MAXMAPPING (64) - -/* Timeouts and retries */ - -/** timeout value in us for tx frame to return to rx */ -#define EC_TIMEOUTRET (2000) - -/** timeout value in us for safe data transfer, max. triple retry */ -#define EC_TIMEOUTRET3 (EC_TIMEOUTRET * 3) - -/** timeout value in us for return "safe" variant (f.e. wireless) */ -#define EC_TIMEOUTSAFE (20000) - -/** timeout value in us for EEPROM access */ -#define EC_TIMEOUTEEP (20000) - -/** timeout value in us for tx mailbox cycle */ -#define EC_TIMEOUTTXM (20000) - -/** timeout value in us for rx mailbox cycle */ -#define EC_TIMEOUTRXM (700000) - -/** timeout value in us for check statechange */ -#define EC_TIMEOUTSTATE (2000000) - -/** default number of retries if wkc <= 0 */ -#define EC_DEFAULTRETRIES (3) - -/* MAC addresses */ - -/** Primary source MAC address used for EtherCAT. - * - * This address is not the MAC address used from the NIC. EtherCAT - * does not care about MAC addressing, but it is used here to - * differentiate the route the packet traverses through the EtherCAT - * segment. This is needed to find out the packet flow in redundant - * configurations. */ -#define EC_PRIMARY_MAC_ARRAY {0x0101, 0x0101, 0x0101} - -/** Secondary source MAC address used for EtherCAT. */ -#define EC_SECONDARY_MAC_ARRAY {0x0404, 0x0404, 0x0404} - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/dependency/x86/third_party/soem/include/soem/ec_print.h b/dependency/x86/third_party/soem/include/soem/ec_print.h deleted file mode 100644 index 920f3dac..00000000 --- a/dependency/x86/third_party/soem/include/soem/ec_print.h +++ /dev/null @@ -1,30 +0,0 @@ -/* - * This software is dual-licensed under GPLv3 and a commercial - * license. See the file LICENSE.md distributed with this software for - * full license information. - */ - -/** \file - * \brief - * Headerfile for ec_print.c - */ - -#ifndef _ec_print_ -#define _ec_print_ - -#ifdef __cplusplus -extern "C" { -#endif - -const char *ec_sdoerror2string(uint32 sdoerrorcode); -char *ec_ALstatuscode2string(uint16 ALstatuscode); -char *ec_soeerror2string(uint16 errorcode); -char *ec_mbxerror2string(uint16 errorcode); -char *ecx_err2string(const ec_errort Ec); -char *ecx_elist2string(ecx_contextt *context); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/dependency/x86/third_party/soem/include/soem/ec_soe.h b/dependency/x86/third_party/soem/include/soem/ec_soe.h deleted file mode 100644 index e6dbf3ee..00000000 --- a/dependency/x86/third_party/soem/include/soem/ec_soe.h +++ /dev/null @@ -1,121 +0,0 @@ -/* - * This software is dual-licensed under GPLv3 and a commercial - * license. See the file LICENSE.md distributed with this software for - * full license information. - */ - -/** \file - * \brief - * Headerfile for ec_soe.c - */ - -#ifndef _ec_soe_ -#define _ec_soe_ - -#ifdef __cplusplus -extern "C" { -#endif - -#define EC_SOE_DATASTATE_B 0x01 -#define EC_SOE_NAME_B 0x02 -#define EC_SOE_ATTRIBUTE_B 0x04 -#define EC_SOE_UNIT_B 0x08 -#define EC_SOE_MIN_B 0x10 -#define EC_SOE_MAX_B 0x20 -#define EC_SOE_VALUE_B 0x40 -#define EC_SOE_DEFAULT_B 0x80 - -#define EC_IDN_MDTCONFIG 24 -#define EC_IDN_ATCONFIG 16 - -/** SoE name structure */ -OSAL_PACKED_BEGIN -typedef struct OSAL_PACKED -{ - /** current length in bytes of list */ - uint16 currentlength; - /** maximum length in bytes of list */ - uint16 maxlength; - char name[EC_SOE_MAXNAME]; -} ec_SoEnamet; -OSAL_PACKED_END - -/** SoE list structure */ -OSAL_PACKED_BEGIN -typedef struct OSAL_PACKED -{ - /** current length in bytes of list */ - uint16 currentlength; - /** maximum length in bytes of list */ - uint16 maxlength; - union - { - uint8 byte[8]; - uint16 word[4]; - uint32 dword[2]; - uint64 lword[1]; - }; -} ec_SoElistt; -OSAL_PACKED_END - -/** SoE IDN mapping structure */ -OSAL_PACKED_BEGIN -typedef struct OSAL_PACKED -{ - /** current length in bytes of list */ - uint16 currentlength; - /** maximum length in bytes of list */ - uint16 maxlength; - uint16 idn[EC_SOE_MAXMAPPING]; -} ec_SoEmappingt; -OSAL_PACKED_END - -#define EC_SOE_LENGTH_1 0x00 -#define EC_SOE_LENGTH_2 0x01 -#define EC_SOE_LENGTH_4 0x02 -#define EC_SOE_LENGTH_8 0x03 -#define EC_SOE_TYPE_BINARY 0x00 -#define EC_SOE_TYPE_UINT 0x01 -#define EC_SOE_TYPE_INT 0x02 -#define EC_SOE_TYPE_HEX 0x03 -#define EC_SOE_TYPE_STRING 0x04 -#define EC_SOE_TYPE_IDN 0x05 -#define EC_SOE_TYPE_FLOAT 0x06 -#define EC_SOE_TYPE_PARAMETER 0x07 - -/** SoE attribute structure */ -OSAL_PACKED_BEGIN -typedef struct OSAL_PACKED -{ - /** evaluation factor for display purposes */ - uint32 evafactor : 16; - /** length of IDN element(s) */ - uint32 length : 2; - /** IDN is list */ - uint32 list : 1; - /** IDN is command */ - uint32 command : 1; - /** datatype */ - uint32 datatype : 3; - uint32 reserved1 : 1; - /** decimals to display if float datatype */ - uint32 decimals : 4; - /** write protected in pre-op */ - uint32 wppreop : 1; - /** write protected in safe-op */ - uint32 wpsafeop : 1; - /** write protected in op */ - uint32 wpop : 1; - uint32 reserved2 : 1; -} ec_SoEattributet; -OSAL_PACKED_END - -int ecx_SoEread(ecx_contextt *context, uint16 slave, uint8 driveNo, uint8 elementflags, uint16 idn, int *psize, void *p, int timeout); -int ecx_SoEwrite(ecx_contextt *context, uint16 slave, uint8 driveNo, uint8 elementflags, uint16 idn, int psize, void *p, int timeout); -int ecx_readIDNmap(ecx_contextt *context, uint16 slave, uint32 *Osize, uint32 *Isize); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/dependency/x86/third_party/soem/include/soem/ec_type.h b/dependency/x86/third_party/soem/include/soem/ec_type.h deleted file mode 100644 index faca1707..00000000 --- a/dependency/x86/third_party/soem/include/soem/ec_type.h +++ /dev/null @@ -1,551 +0,0 @@ -/* - * This software is dual-licensed under GPLv3 and a commercial - * license. See the file LICENSE.md distributed with this software for - * full license information. - */ - -/** \file - * \brief - * General typedefs and defines for EtherCAT. - * - * Defines that could need optimisation for specific applications - * are the EC_TIMEOUTxxx. Assumptions for the standard settings are a - * standard linux PC or laptop and a wired connection to maximal 100 slaves. - * For use with wireless connections or lots of slaves the timeouts need - * increasing. For fast systems running Xenomai and RT-net or alike the - * timeouts need to be shorter. - */ - -#ifndef _EC_TYPE_H -#define _EC_TYPE_H - -#ifdef __cplusplus -extern "C" { -#endif - -#include "osal.h" -#include "soem/ec_options.h" - -/* Endianness */ - -/** Define little endian target by default if no endian is set */ -#if !defined(EC_LITTLE_ENDIAN) && !defined(EC_BIG_ENDIAN) -#define EC_LITTLE_ENDIAN -#endif - -/* Error codes */ - -/** return value no frame returned */ -#define EC_NOFRAME -1 -/** return value unknown frame received */ -#define EC_OTHERFRAME -2 -/** return value general error */ -#define EC_ERROR -3 -/** return value too many slaves */ -#define EC_SLAVECOUNTEXCEEDED -4 -/** return value request timeout */ -#define EC_TIMEOUT -5 - -/* Constants */ - -/** maximum EtherCAT frame length in bytes */ -#define EC_MAXECATFRAME 1518 -/** size of DC datagram used in first LRW frame */ -#define EC_FIRSTDCDATAGRAM 20 -/** datagram type EtherCAT */ -#define EC_ECATTYPE 0x1000 -/** maximum EtherCAT LRW frame length in bytes */ -/* MTU - Ethernet header - length - datagram header - WCK - FCS */ -#define EC_MAXLRWDATA (EC_MAXECATFRAME - 14 - 2 - 10 - 2 - 4) - -/** definition for frame buffers */ -typedef uint8 ec_bufT[EC_BUFSIZE]; - -/** ethernet header definition */ -OSAL_PACKED_BEGIN -typedef struct OSAL_PACKED -{ - /** destination MAC */ - uint16 da0, da1, da2; - /** source MAC */ - uint16 sa0, sa1, sa2; - /** ethernet type */ - uint16 etype; -} ec_etherheadert; -OSAL_PACKED_END - -/** ethernet header size */ -#define ETH_HEADERSIZE sizeof(ec_etherheadert) - -/** EtherCAT datagram header definition */ -OSAL_PACKED_BEGIN -typedef struct OSAL_PACKED -{ - /** length of EtherCAT datagram */ - uint16 elength; - /** EtherCAT command, see ec_cmdtype */ - uint8 command; - /** index, used in SOEM for Tx to Rx recombination */ - uint8 index; - /** ADP */ - uint16 ADP; - /** ADO */ - uint16 ADO; - /** length of data portion in datagram */ - uint16 dlength; - /** interrupt, currently unused */ - uint16 irpt; -} ec_comt; -OSAL_PACKED_END - -/** EtherCAT header size */ -#define EC_HEADERSIZE sizeof(ec_comt) -/** size of ec_comt.elength item in EtherCAT header */ -#define EC_ELENGTHSIZE sizeof(uint16) -/** offset position of command in EtherCAT header */ -#define EC_CMDOFFSET EC_ELENGTHSIZE -/** size of workcounter item in EtherCAT datagram */ -#define EC_WKCSIZE sizeof(uint16) -/** definition of datagram follows bit in ec_comt.dlength */ -#define EC_DATAGRAMFOLLOWS (1 << 15) - -/** Possible error codes returned. */ -typedef enum -{ - /** No error */ - EC_ERR_OK = 0, - /** Library already initialized. */ - EC_ERR_ALREADY_INITIALIZED, - /** Library not initialized. */ - EC_ERR_NOT_INITIALIZED, - /** Timeout occurred during execution of the function. */ - EC_ERR_TIMEOUT, - /** No slaves were found. */ - EC_ERR_NO_SLAVES, - /** Function failed. */ - EC_ERR_NOK -} ec_err; - -/** Possible EtherCAT slave states */ -typedef enum -{ - /** No valid state. */ - EC_STATE_NONE = 0x00, - /** Init state*/ - EC_STATE_INIT = 0x01, - /** Pre-operational. */ - EC_STATE_PRE_OP = 0x02, - /** Boot state*/ - EC_STATE_BOOT = 0x03, - /** Safe-operational. */ - EC_STATE_SAFE_OP = 0x04, - /** Operational */ - EC_STATE_OPERATIONAL = 0x08, - /** Error or ACK error */ - EC_STATE_ACK = 0x10, - EC_STATE_ERROR = 0x10 -} ec_state; - -/** Possible buffer states */ -typedef enum -{ - /** Empty */ - EC_BUF_EMPTY = 0x00, - /** Allocated, but not filled */ - EC_BUF_ALLOC = 0x01, - /** Transmitted */ - EC_BUF_TX = 0x02, - /** Received, but not consumed */ - EC_BUF_RCVD = 0x03, - /** Cycle completed */ - EC_BUF_COMPLETE = 0x04 -} ec_bufstate; - -/** Ethercat data types */ -typedef enum -{ - ECT_BOOLEAN = 0x0001, - ECT_INTEGER8 = 0x0002, - ECT_INTEGER16 = 0x0003, - ECT_INTEGER32 = 0x0004, - ECT_UNSIGNED8 = 0x0005, - ECT_UNSIGNED16 = 0x0006, - ECT_UNSIGNED32 = 0x0007, - ECT_REAL32 = 0x0008, - ECT_VISIBLE_STRING = 0x0009, - ECT_OCTET_STRING = 0x000A, - ECT_UNICODE_STRING = 0x000B, - ECT_TIME_OF_DAY = 0x000C, - ECT_TIME_DIFFERENCE = 0x000D, - ECT_DOMAIN = 0x000F, - ECT_INTEGER24 = 0x0010, - ECT_REAL64 = 0x0011, - ECT_INTEGER64 = 0x0015, - ECT_UNSIGNED24 = 0x0016, - ECT_UNSIGNED64 = 0x001B, - ECT_BIT1 = 0x0030, - ECT_BIT2 = 0x0031, - ECT_BIT3 = 0x0032, - ECT_BIT4 = 0x0033, - ECT_BIT5 = 0x0034, - ECT_BIT6 = 0x0035, - ECT_BIT7 = 0x0036, - ECT_BIT8 = 0x0037 -} ec_datatype; - -/** Ethercat command types */ -typedef enum -{ - /** No operation */ - EC_CMD_NOP = 0x00, - /** Auto Increment Read */ - EC_CMD_APRD, - /** Auto Increment Write */ - EC_CMD_APWR, - /** Auto Increment Read Write */ - EC_CMD_APRW, - /** Configured Address Read */ - EC_CMD_FPRD, - /** Configured Address Write */ - EC_CMD_FPWR, - /** Configured Address Read Write */ - EC_CMD_FPRW, - /** Broadcast Read */ - EC_CMD_BRD, - /** Broadcast Write */ - EC_CMD_BWR, - /** Broadcast Read Write */ - EC_CMD_BRW, - /** Logical Memory Read */ - EC_CMD_LRD, - /** Logical Memory Write */ - EC_CMD_LWR, - /** Logical Memory Read Write */ - EC_CMD_LRW, - /** Auto Increment Read Multiple Write */ - EC_CMD_ARMW, - /** Configured Read Multiple Write */ - EC_CMD_FRMW - /** Reserved */ -} ec_cmdtype; - -/** Ethercat EEprom command types */ -typedef enum -{ - /** No operation */ - EC_ECMD_NOP = 0x0000, - /** Read */ - EC_ECMD_READ = 0x0100, - /** Write */ - EC_ECMD_WRITE = 0x0201, - /** Reload */ - EC_ECMD_RELOAD = 0x0300 -} ec_ecmdtype; - -/** EEprom state machine read size */ -#define EC_ESTAT_R64 0x0040 -/** EEprom state machine busy flag */ -#define EC_ESTAT_BUSY 0x8000 -/** EEprom state machine error flag mask */ -#define EC_ESTAT_EMASK 0x7800 -/** EEprom state machine error acknowledge */ -#define EC_ESTAT_NACK 0x2000 - -/* Ethercat SSI (Slave Information Interface) */ - -/** Start address SII sections in Eeprom */ -#define ECT_SII_START 0x0040 - -enum -{ - /** SII category strings */ - ECT_SII_STRING = 10, - /** SII category general */ - ECT_SII_GENERAL = 30, - /** SII category FMMU */ - ECT_SII_FMMU = 40, - /** SII category SM */ - ECT_SII_SM = 41, - /** SII category PDO */ - ECT_SII_PDO = 50 -}; - -/** Item offsets in SII general section */ -enum -{ - ECT_SII_MANUF = 0x0008, - ECT_SII_ID = 0x000a, - ECT_SII_REV = 0x000c, - ECT_SII_SER = 0x000e, - ECT_SII_BOOTRXMBX = 0x0014, - ECT_SII_BOOTTXMBX = 0x0016, - ECT_SII_MBXSIZE = 0x0019, - ECT_SII_TXMBXADR = 0x001a, - ECT_SII_RXMBXADR = 0x0018, - ECT_SII_MBXPROTO = 0x001c -}; - -/** Mailbox types definitions */ -enum -{ - /** Error mailbox type */ - ECT_MBXT_ERR = 0x00, - /** ADS over EtherCAT mailbox type */ - ECT_MBXT_AOE, - /** Ethernet over EtherCAT mailbox type */ - ECT_MBXT_EOE, - /** CANopen over EtherCAT mailbox type */ - ECT_MBXT_COE, - /** File over EtherCAT mailbox type */ - ECT_MBXT_FOE, - /** Servo over EtherCAT mailbox type */ - ECT_MBXT_SOE, - /** Vendor over EtherCAT mailbox type */ - ECT_MBXT_VOE = 0x0f -}; - -/** CoE mailbox types */ -enum -{ - ECT_COES_EMERGENCY = 0x01, - ECT_COES_SDOREQ, - ECT_COES_SDORES, - ECT_COES_TXPDO, - ECT_COES_RXPDO, - ECT_COES_TXPDO_RR, - ECT_COES_RXPDO_RR, - ECT_COES_SDOINFO -}; - -/** CoE SDO commands */ -enum -{ - ECT_SDO_DOWN_INIT = 0x21, - ECT_SDO_DOWN_EXP = 0x23, - ECT_SDO_DOWN_INIT_CA = 0x31, - ECT_SDO_UP_REQ = 0x40, - ECT_SDO_UP_REQ_CA = 0x50, - ECT_SDO_SEG_UP_REQ = 0x60, - ECT_SDO_ABORT = 0x80 -}; - -/** CoE Object Description commands */ -enum -{ - ECT_GET_ODLIST_REQ = 0x01, - ECT_GET_ODLIST_RES = 0x02, - ECT_GET_OD_REQ = 0x03, - ECT_GET_OD_RES = 0x04, - ECT_GET_OE_REQ = 0x05, - ECT_GET_OE_RES = 0x06, - ECT_SDOINFO_ERROR = 0x07 -}; - -/** FoE opcodes */ -enum -{ - ECT_FOE_READ = 0x01, - ECT_FOE_WRITE, - ECT_FOE_DATA, - ECT_FOE_ACK, - ECT_FOE_ERROR, - ECT_FOE_BUSY -}; - -/** SoE opcodes */ -enum -{ - ECT_SOE_READREQ = 0x01, - ECT_SOE_READRES, - ECT_SOE_WRITEREQ, - ECT_SOE_WRITERES, - ECT_SOE_NOTIFICATION, - ECT_SOE_EMERGENCY -}; - -/** Ethercat registers */ -enum -{ - ECT_REG_TYPE = 0x0000, - ECT_REG_PORTDES = 0x0007, - ECT_REG_ESCSUP = 0x0008, - ECT_REG_STADR = 0x0010, - ECT_REG_ALIAS = 0x0012, - ECT_REG_DLCTL = 0x0100, - ECT_REG_DLPORT = 0x0101, - ECT_REG_DLALIAS = 0x0103, - ECT_REG_DLSTAT = 0x0110, - ECT_REG_ALCTL = 0x0120, - ECT_REG_ALSTAT = 0x0130, - ECT_REG_ALSTATCODE = 0x0134, - ECT_REG_PDICTL = 0x0140, - ECT_REG_IRQMASK = 0x0200, - ECT_REG_RXERR = 0x0300, - ECT_REG_FRXERR = 0x0308, - ECT_REG_EPUECNT = 0x030C, - ECT_REG_PECNT = 0x030D, - ECT_REG_PECODE = 0x030E, - ECT_REG_LLCNT = 0x0310, - ECT_REG_WDCNT = 0x0442, - ECT_REG_EEPCFG = 0x0500, - ECT_REG_EEPCTL = 0x0502, - ECT_REG_EEPSTAT = 0x0502, - ECT_REG_EEPADR = 0x0504, - ECT_REG_EEPDAT = 0x0508, - ECT_REG_FMMU0 = 0x0600, - ECT_REG_FMMU1 = ECT_REG_FMMU0 + 0x10, - ECT_REG_FMMU2 = ECT_REG_FMMU1 + 0x10, - ECT_REG_FMMU3 = ECT_REG_FMMU2 + 0x10, - ECT_REG_SM0 = 0x0800, - ECT_REG_SM1 = ECT_REG_SM0 + 0x08, - ECT_REG_SM2 = ECT_REG_SM1 + 0x08, - ECT_REG_SM3 = ECT_REG_SM2 + 0x08, - ECT_REG_SM0STAT = ECT_REG_SM0 + 0x05, - ECT_REG_SM1STAT = ECT_REG_SM1 + 0x05, - ECT_REG_SM1ACT = ECT_REG_SM1 + 0x06, - ECT_REG_SM1CONTR = ECT_REG_SM1 + 0x07, - ECT_REG_DCTIME0 = 0x0900, - ECT_REG_DCTIME1 = 0x0904, - ECT_REG_DCTIME2 = 0x0908, - ECT_REG_DCTIME3 = 0x090C, - ECT_REG_DCSYSTIME = 0x0910, - ECT_REG_DCSOF = 0x0918, - ECT_REG_DCSYSOFFSET = 0x0920, - ECT_REG_DCSYSDELAY = 0x0928, - ECT_REG_DCSYSDIFF = 0x092C, - ECT_REG_DCSPEEDCNT = 0x0930, - ECT_REG_DCTIMEFILT = 0x0934, - ECT_REG_DCCUC = 0x0980, - ECT_REG_DCSYNCACT = 0x0981, - ECT_REG_DCSTART0 = 0x0990, - ECT_REG_DCCYCLE0 = 0x09A0, - ECT_REG_DCCYCLE1 = 0x09A4 -}; - -/** standard SDO Sync Manager Communication Type */ -#define ECT_SDO_SMCOMMTYPE 0x1c00 -/** standard SDO PDO assignment */ -#define ECT_SDO_PDOASSIGN 0x1c10 -/** standard SDO RxPDO assignment */ -#define ECT_SDO_RXPDOASSIGN 0x1c12 -/** standard SDO TxPDO assignment */ -#define ECT_SDO_TXPDOASSIGN 0x1c13 - -/** Ethercat packet type */ -#define ETH_P_ECAT 0x88A4 - -/** Error types */ -typedef enum -{ - EC_ERR_TYPE_SDO_ERROR = 0, - EC_ERR_TYPE_EMERGENCY = 1, - EC_ERR_TYPE_PACKET_ERROR = 3, - EC_ERR_TYPE_SDOINFO_ERROR = 4, - EC_ERR_TYPE_FOE_ERROR = 5, - EC_ERR_TYPE_FOE_BUF2SMALL = 6, - EC_ERR_TYPE_FOE_PACKETNUMBER = 7, - EC_ERR_TYPE_SOE_ERROR = 8, - EC_ERR_TYPE_MBX_ERROR = 9, - EC_ERR_TYPE_FOE_FILE_NOTFOUND = 10, - EC_ERR_TYPE_EOE_INVALID_RX_DATA = 11 -} ec_err_type; - -/** Struct to retrieve errors. */ -typedef struct -{ - /** Time at which the error was generated. */ - ec_timet Time; - /** Signal bit, error set but not read */ - boolean Signal; - /** Slave number that generated the error */ - uint16 Slave; - /** CoE SDO index that generated the error */ - uint16 Index; - /** CoE SDO subindex that generated the error */ - uint8 SubIdx; - /** Type of error */ - ec_err_type Etype; - union - { - /** General abortcode */ - int32 AbortCode; - /** Specific error for Emergency mailbox */ - struct - { - uint16 ErrorCode; - uint8 ErrorReg; - uint8 b1; - uint16 w1; - uint16 w2; - }; - }; -} ec_errort; - -/** Helper macros */ - -/** Set the count value in the Mailbox header */ -#define MBX_HDR_SET_CNT(cnt) ((uint8)((cnt) << 4)) - -/** Macro to make a word from 2 bytes */ -#define MK_WORD(msb, lsb) ((((uint16)(msb)) << 8) | (lsb)) -/** Macro to get hi byte of a word */ -#define HI_BYTE(w) ((w) >> 8) -/** Macro to get low byte of a word */ -#define LO_BYTE(w) ((w) & 0x00ff) -/** Macro to swap hi and low byte of a word */ -#define SWAP(w) ((((w) & 0xff00) >> 8) | (((w) & 0x00ff) << 8)) -/** Macro to get hi word of a dword */ -#define LO_WORD(l) ((l) & 0xffff) -/** Macro to get hi word of a dword */ -#define HI_WORD(l) ((l) >> 16) - -#define get_unaligned(ptr) \ - ({ __typeof__(*(ptr)) __tmp; memcpy(&__tmp, (ptr), sizeof(*(ptr))); __tmp; }) - -#define put_unaligned32(val, ptr) \ - (memcpy((ptr), &(val), 4)) - -#define put_unaligned64(val, ptr) \ - (memcpy((ptr), &(val), 8)) - -#if !defined(EC_BIG_ENDIAN) && defined(EC_LITTLE_ENDIAN) - -#define htoes(A) (A) -#define htoel(A) (A) -#define htoell(A) (A) -#define etohs(A) (A) -#define etohl(A) (A) -#define etohll(A) (A) - -#elif !defined(EC_LITTLE_ENDIAN) && defined(EC_BIG_ENDIAN) - -#define htoes(A) ((((uint16)(A) & 0xff00) >> 8) | \ - (((uint16)(A) & 0x00ff) << 8)) -#define htoel(A) ((((uint32)(A) & 0xff000000) >> 24) | \ - (((uint32)(A) & 0x00ff0000) >> 8) | \ - (((uint32)(A) & 0x0000ff00) << 8) | \ - (((uint32)(A) & 0x000000ff) << 24)) -#define htoell(A) ((((uint64)(A) & (uint64)0xff00000000000000ULL) >> 56) | \ - (((uint64)(A) & (uint64)0x00ff000000000000ULL) >> 40) | \ - (((uint64)(A) & (uint64)0x0000ff0000000000ULL) >> 24) | \ - (((uint64)(A) & (uint64)0x000000ff00000000ULL) >> 8) | \ - (((uint64)(A) & (uint64)0x00000000ff000000ULL) << 8) | \ - (((uint64)(A) & (uint64)0x0000000000ff0000ULL) << 24) | \ - (((uint64)(A) & (uint64)0x000000000000ff00ULL) << 40) | \ - (((uint64)(A) & (uint64)0x00000000000000ffULL) << 56)) - -#define etohs htoes -#define etohl htoel -#define etohll htoell - -#else - -#error "Must define one of EC_BIG_ENDIAN or EC_LITTLE_ENDIAN" - -#endif - -#ifdef __cplusplus -} -#endif - -#endif /* _EC_TYPE_H */ diff --git a/dependency/x86/third_party/soem/include/soem/nicdrv.h b/dependency/x86/third_party/soem/include/soem/nicdrv.h deleted file mode 100644 index 010d8efc..00000000 --- a/dependency/x86/third_party/soem/include/soem/nicdrv.h +++ /dev/null @@ -1,108 +0,0 @@ -/* - * This software is dual-licensed under GPLv3 and a commercial - * license. See the file LICENSE.md distributed with this software for - * full license information. - */ - -/** \file - * \brief - * Headerfile for nicdrv.c - */ - -#ifndef _nicdrvh_ -#define _nicdrvh_ - -#ifdef __cplusplus -extern "C" { -#endif - -#include - -/** pointer structure to Tx and Rx stacks */ -typedef struct -{ - /** socket connection used */ - int *sock; - /** tx buffer */ - ec_bufT (*txbuf)[EC_MAXBUF]; - /** tx buffer lengths */ - int (*txbuflength)[EC_MAXBUF]; - /** temporary receive buffer */ - ec_bufT *tempbuf; - /** rx buffers */ - ec_bufT (*rxbuf)[EC_MAXBUF]; - /** rx buffer status fields */ - int (*rxbufstat)[EC_MAXBUF]; - /** received MAC source address (middle word) */ - int (*rxsa)[EC_MAXBUF]; - /** number of received frames */ - uint64 rxcnt; -} ec_stackT; - -/** pointer structure to buffers for redundant port */ -typedef struct -{ - ec_stackT stack; - int sockhandle; - /** rx buffers */ - ec_bufT rxbuf[EC_MAXBUF]; - /** rx buffer status */ - int rxbufstat[EC_MAXBUF]; - /** rx MAC source address */ - int rxsa[EC_MAXBUF]; - /** temporary rx buffer */ - ec_bufT tempinbuf; -} ecx_redportt; - -/** pointer structure to buffers, vars and mutexes for port instantiation */ -typedef struct -{ - ec_stackT stack; - int sockhandle; - /** rx buffers */ - ec_bufT rxbuf[EC_MAXBUF]; - /** rx buffer status */ - int rxbufstat[EC_MAXBUF]; - /** rx MAC source address */ - int rxsa[EC_MAXBUF]; - /** temporary rx buffer */ - ec_bufT tempinbuf; - /** temporary rx buffer status */ - int tempinbufs; - /** transmit buffers */ - ec_bufT txbuf[EC_MAXBUF]; - /** transmit buffer lengths */ - int txbuflength[EC_MAXBUF]; - /** temporary tx buffer */ - ec_bufT txbuf2; - /** temporary tx buffer length */ - int txbuflength2; - /** last used frame index */ - uint8 lastidx; - /** current redundancy state */ - int redstate; - /** pointer to redundancy port and buffers */ - ecx_redportt *redport; - pthread_mutex_t getindex_mutex; - pthread_mutex_t tx_mutex; - pthread_mutex_t rx_mutex; -} ecx_portt; - -extern const uint16 priMAC[3]; -extern const uint16 secMAC[3]; - -void ec_setupheader(void *p); -int ecx_setupnic(ecx_portt *port, const char *ifname, int secondary); -int ecx_closenic(ecx_portt *port); -void ecx_setbufstat(ecx_portt *port, uint8 idx, int bufstat); -uint8 ecx_getindex(ecx_portt *port); -int ecx_outframe(ecx_portt *port, uint8 idx, int sock); -int ecx_outframe_red(ecx_portt *port, uint8 idx); -int ecx_waitinframe(ecx_portt *port, uint8 idx, int timeout); -int ecx_srconfirm(ecx_portt *port, uint8 idx, int timeout); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/dependency/x86/third_party/soem/include/soem/osal.h b/dependency/x86/third_party/soem/include/soem/osal.h deleted file mode 100644 index f88bce85..00000000 --- a/dependency/x86/third_party/soem/include/soem/osal.h +++ /dev/null @@ -1,222 +0,0 @@ -/* - * This software is dual-licensed under GPLv3 and a commercial - * license. See the file LICENSE.md distributed with this software for - * full license information. - */ - -#ifndef _osal_ -#define _osal_ - -#ifdef __cplusplus -extern "C" { -#endif - -#include "osal_defs.h" -#include -#include - -/* General types */ -#ifndef TRUE -#define TRUE 1 -#endif -#ifndef FALSE -#define FALSE 0 -#endif -typedef uint8_t boolean; -typedef int8_t int8; -typedef int16_t int16; -typedef int32_t int32; -typedef uint8_t uint8; -typedef uint16_t uint16; -typedef uint32_t uint32; -typedef int64_t int64; -typedef uint64_t uint64; -typedef float float32; -typedef double float64; - -typedef struct osal_timer -{ - ec_timet stop_time; -} osal_timert; - -/** - * @brief Returns monotonic time from some unspecified moment in the - * past. - * - * This time must be strictly increasing. It is used for time - * intervals measurement. - * - * @param ts Pointer to an ec_timet structure where the time will be - * stored. - */ -void osal_get_monotonic_time(ec_timet *ts); - -/** - * @brief Returns the current time. - * - * This time is used to set the initial EtherCAT network DC time and - * for logging purposes. - * - * @return ec_timet containing the current time. - */ -ec_timet osal_current_time(void); - -/** - * @brief Calculates the difference between two timestamps. - * - * @param start Pointer to the start timestamp. - * @param end Pointer to the end timestamp. - * @param diff Pointer to an ec_timet structure where the difference - * will be stored. - */ -void osal_time_diff(ec_timet *start, ec_timet *end, ec_timet *diff); - -/** - * @brief Starts the timer with a specified timeout. - * - * @param self Pointer to the timer object. - * @param timeout_usec Timeout in microseconds. - */ -void osal_timer_start(osal_timert *self, uint32 timeout_usec); - -/** - * @brief Checks if the timer has expired. - * - * @param self Pointer to the timer object. - * @return True if the timer is expired, false otherwise. - */ -boolean osal_timer_is_expired(osal_timert *self); - -/** - * @brief Sleeps for a specified duration in microseconds. - * - * @param usec Duration in microseconds. - * @return 0 on success, -1 on failure. - */ -int osal_usleep(uint32 usec); - -/** - * @brief Sleeps until the specified monotonic time. - * - * @param ts Pointer to an ec_timet structure representing the - * absolute time to sleep until. - * @return 0 on success, -1 on failure. - */ -int osal_monotonic_sleep(ec_timet *ts); - -/** - * @brief Allocates memory of the specified size. - * - * @param size Size in bytes to allocate. - * @return Pointer to the allocated memory or NULL on failure. - */ -void *osal_malloc(size_t size); - -/** - * @brief Frees the allocated memory. - * - * @param ptr Pointer to the memory to free. - */ -void osal_free(void *ptr); - -/** - * @brief Creates a new thread. - * - * @param thandle Pointer to the thread handle which will store the - * thread ID. - * @param stacksize Size of the stack for the new thread. - * @param func Pointer to the function to execute in the new thread. - * @param param Pointer to parameters to pass to the thread function. - * @return 1 on success, 0 on failure. - */ -int osal_thread_create(void *thandle, int stacksize, void *func, void *param); - -/** - * @brief Creates a new real-time thread. - * - * @param thandle Pointer to the thread handle which will store the - * thread ID. - * @param stacksize Size of the stack for the new thread. - * @param func Pointer to the function to execute in the new thread. - * @param param Pointer to parameters to pass to the thread function. - * @return 1 on success, 0 on failure. - */ -int osal_thread_create_rt(void *thandle, int stacksize, void *func, void *param); - -/** - * @brief Creates a mutex. - * - * @return Pointer to the created mutex or NULL on failure. - */ -void *osal_mutex_create(void); - -/** - * @brief Destroys a mutex. - * - * @param mutex Pointer to the mutex to destroy. - */ -void osal_mutex_destroy(void *mutex); - -/** - * @brief Locks the mutex. - * - * @param mutex Pointer to the mutex to lock. - */ -void osal_mutex_lock(void *mutex); - -/** - * @brief Unlocks the mutex. - * - * @param mutex Pointer to the mutex to unlock. - */ -void osal_mutex_unlock(void *mutex); - -#ifndef osal_timespec_from_usec -#define osal_timespec_from_usec(usec, result) \ - do \ - { \ - (result)->tv_sec = usec / 1000000; \ - (result)->tv_nsec = (usec % 1000000) * 1000; \ - } while (0) -#endif - -#ifndef osal_timespeccmp -#define osal_timespeccmp(a, b, CMP) \ - (((a)->tv_sec == (b)->tv_sec) \ - ? ((a)->tv_nsec CMP(b)->tv_nsec) \ - : ((a)->tv_sec CMP(b)->tv_sec)) -#endif - -#ifndef osal_timespecadd -#define osal_timespecadd(a, b, result) \ - do \ - { \ - (result)->tv_sec = (a)->tv_sec + (b)->tv_sec; \ - (result)->tv_nsec = (a)->tv_nsec + (b)->tv_nsec; \ - if ((result)->tv_nsec >= 1000000000) \ - { \ - ++(result)->tv_sec; \ - (result)->tv_nsec -= 1000000000; \ - } \ - } while (0) -#endif - -#ifndef osal_timespecsub -#define osal_timespecsub(a, b, result) \ - do \ - { \ - (result)->tv_sec = (a)->tv_sec - (b)->tv_sec; \ - (result)->tv_nsec = (a)->tv_nsec - (b)->tv_nsec; \ - if ((result)->tv_nsec < 0) \ - { \ - --(result)->tv_sec; \ - (result)->tv_nsec += 1000000000; \ - } \ - } while (0) -#endif - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/dependency/x86/third_party/soem/include/soem/osal_defs.h b/dependency/x86/third_party/soem/include/soem/osal_defs.h deleted file mode 100644 index 3d6ad511..00000000 --- a/dependency/x86/third_party/soem/include/soem/osal_defs.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - * This software is dual-licensed under GPLv3 and a commercial - * license. See the file LICENSE.md distributed with this software for - * full license information. - */ - -#ifndef _osal_defs_ -#define _osal_defs_ - -#ifdef __cplusplus -extern "C" { -#endif - -#include -#include - -// define if debug printf is needed -#ifdef EC_DEBUG -#include -#define EC_PRINT printf -#else -#define EC_PRINT(...) \ - do \ - { \ - } while (0) -#endif - -#ifndef OSAL_PACKED -#define OSAL_PACKED_BEGIN -#define OSAL_PACKED __attribute__((__packed__)) -#define OSAL_PACKED_END -#endif - -#define ec_timet struct timespec - -#define OSAL_THREAD_HANDLE pthread_t * -#define OSAL_THREAD_FUNC void -#define OSAL_THREAD_FUNC_RT void - -#define osal_mutext pthread_mutex_t - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/dependency/x86/third_party/soem/include/soem/soem.h b/dependency/x86/third_party/soem/include/soem/soem.h deleted file mode 100644 index e4d43a8e..00000000 --- a/dependency/x86/third_party/soem/include/soem/soem.h +++ /dev/null @@ -1,28 +0,0 @@ -/* - * This software is dual-licensed under GPLv3 and a commercial - * license. See the file LICENSE.md distributed with this software for - * full license information. - */ - -/** \file - * \brief - * SOEM main include file - */ - -#ifndef _SOEM_H -#define _SOEM_H - -#include "soem/ec_options.h" -#include "soem/ec_type.h" -#include "nicdrv.h" -#include "soem/ec_base.h" -#include "soem/ec_main.h" -#include "soem/ec_dc.h" -#include "soem/ec_coe.h" -#include "soem/ec_foe.h" -#include "soem/ec_soe.h" -#include "soem/ec_eoe.h" -#include "soem/ec_config.h" -#include "soem/ec_print.h" - -#endif /* _SOEM_H */ diff --git a/dependency/x86/third_party/soem/lib/libsoem.a b/dependency/x86/third_party/soem/lib/libsoem.a deleted file mode 100644 index 511036788210ffebe1d4cbac55e6e92e078f23dd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 691060 zcmd>{34B!5760FxC7G;~gb>1_1Q;c#ECFQEs7x{m6CEHxz{IK{WFb)?m_^Ydiy^`o zqiAi_iq)#E+S(RdC4jb~2#Bb4qlzo;L`2cnec}H*_ntd*=O(n;<@5RfKmYgnWZt{q zd+xdCo_p?n_q{iHGbF#Nsk;8u#L+pi+ZZh=kM+--oUxN9u*keb%d&2=tfc4vJAaQ) zvaBEY=jc1gI?|uB$z!QMSKpA;>)-P?ST6o^UohVK|K%_KV5#+e|2z*pYUzIo^EX-l z_TL|UR)YSwcaoLx{r_sKmsU2{wzMv(scNZO&{Va^B2raT6O%4DbC%!*vjSr8FI5M# zrkyEBu{xlvizHG)#S!E$WkqzLq!<-PP-vFa8RAMpECC)U6sl(jX4Ez{H8!b4Q*Bj^ zI=-l>p`}(NW-ToZ%n(Oqkt31>t!i#=Sg=^+#gIb3&K6ZIF_K#TjKIQ%=9WnQj6h9o zb9Gb0l9qU%|7Kx%6 zMK!f`4U22*8ynA81Tj(~YeMz{J<(jdSo$6rO|{h*suI-Nx~8fHi)t53yNi411tBR> z0>`T7Dp7e2Vh}#6Z6Y63)-~1E>R@T*lGf(>Xb3E6ToO$*G)Iq@R8^l3hel$hdSPR; z>YAp;CF+>GjN>JZjSEFy#OtD?nrdrW7uQsAgIM;0*4kF769qAHV$wB@ zi)&**?b0O;O|ht{v8Ad-L!!f=tNP;Vg$=S#kRmIinrKW_E^1xa(jd$vH#UK{FeYZD!1@XF?zMls^XMw}2qJY`n;Lo%YO8*6GcF&!(> z=Em9{+88Mj3O^d+nkI!Jb2J1Z*%7;zR@Rb0Bm<|u*^Na)muMgoF$bH1K#UCgBZqRM)-zZsc0T-?%F&xf(oxybD~p^KRr&Vzg9f~1}Xs~q2A zfg1!!0t&7{dePd7J-vt`pFtGTv% zn!nIm($qjZSTnz>xpq_|FY1r9mY*fq9|1Cux%{qz`8+{?=dS4?i(ycE>{Bm|*ztYv-nQwkM}q*4pXm z0dLo*b>ZUFx%xn(>e>&s3<-8FAF%SP#EX(P{lsNiLGSHPH{}OAT6~j2 z;ZOEcJ>IT^Mc$2X7wia(8(`&aZQDMG4hLF=;d_3y^ij+D(K-IJ{pa}4-B#xv9Iz}D z%lk|eJh0t6c#5>|%z$^}m;SjW;qUx&{AF`y!@OYI2ie6P_dG-@U~zffs_9wQ$|7&q zJDo+zb#`&;>79XNzgt`CSQ~W0;-IS|c5ap!L~=U z{hdYMsOI@Aw~6*aSKeoawgVgLMtKLX7QG_+<4kUAQTR#fl;`J!Kb;-k?d^J*S{(`p z*pyHQj?m;MLgCNhp)b4}w{G!3OR!<!QYa2XlXvgb!2D2>YgmADt6^ayFS3Zt;1-Wxiyg z$zQf59>#>ihhf_x*cNPfk<5ZYf$+zrFf8O7gwKF48}<#3weK?&6I#xy@Q1gzZ(W|| z?b=h)*)q5^6n>8lCz-rwmDSmS@P5teSbu&`duibOzhrO9|6}&1{@-CQg{nWi%iHy= zx9hW-V2A6NTo@Llah10<)EV%_c~SUFoN9J@H}1z&?ti#DZA-Ff3eV{QUuL*F6#l3bbNNx) zT>eS;Lr~#Gn#y-zD$fX3Z&zKqa+$k0xn-yjikZs~#mwbXaMpk+H~}0LIRPAaJi0#c z|2y{6ebC4`=|8YP@jth}B>Y!-KHW~|`+v*{hQFO1eomh7<+8(nW>NSJbikv0GM!Cb zz+S}ZG#LJ9OA@uG^FFuceVpmB*pTj*hcRb*oHO+^z2;Ax-2G+K5C+l`#6m7k?#aP$ zvM)gMWX!CYowrqV@Zs@385H4L3w@KVq|AS?HFVZDRpU~-+mBQ&_Q#Qx}vj=-q_fA}e~I;|w^@*P+wM86(pS#Hbr zz(DH+`Z$U{^sSD1k2kLr^g3w$WO-e3=aD-*j-WVp9@}pe2{>p{VU#=QL>!NFxf^ir zMM@o)l^@AIq5su5_Ik7uBL@}&jq>E&Mmd^#*E9Vn$15uyfz_vExn#}&k>inxK_VwvCUQhh znoJB$I0t1iWn!2}WXXgt^AK{ft?nLrwsz4Z3)?ZW=4+tXVyfH2V`CpIUbn#GaP%<=rk{jCH7B$ z0$PQN#G|z1qcdKN@WB~xM~-vSw?>YKWE{e=*Co_>JyESmr+RB~p6u_%M+^RjQah|+ z$w{-GM@sl5X-?8W9CPB#L=tt#oC=X}`uu@X>qjKy!r=_-96F4XB9)@B?3JV{kw}z{ zNSg1Y8jqnaygtEcSt7*cr;K8UAWre=l7c%H|4Tt3?iQbM;xk@+dcY zvmCOxQ|vr)mn?TFa+fN1X>wQKk_Fsi=ZM+4WXdCVR-6z?le-+b8zXm?N7g5IX>yk> zcW${$k-Jp6^U9q?-a%)}ok#AHF~Y=x6LG@YiO zJb2Jbu8JFjoJ{dSG?N@_r8@zK(@~w9Kv^#`B71&36>3n~RvMN0&K8B}khZz7k2ZV# zomf4HY4baK0M)2AX2XX=m)e+35Yr|@W`Wz35cbSw_9AyK)u*I*%RqpV{&j%V8Wwmr*nDGtjMuDNH$b&kZthX>epMLZ>3Q;vx$e&}e+D(ywVsSySC z7!*vn<}TI}EqWqsx-h0FRXEdijdh(XDwPST8+%IGW30idaUYOR(#IhHL$0o*fejDHI9&Z4n7d zE~a;yU|es#^FV~7chZ(yp2GtYKXe~`Av*Ym-8S{r$>eJ4w(_`T7I&0unWgs^YZA_d?F3;6)0RDccB{MJV{4Y(d4{LS0zf!>~(UcX}(P6M+W)wSVu>L zWnMZvdU-Fwvb>jIS>8*qEbk>)mfO7t3sv~<1xtNw8=}FoD}&om-%GI6_Yy4iy#z~r zFTql8_Y9VLBUorfcV28@hyXf2Hh>~QLBVno4DKyh+9Ui(!Gclr{etCgUDXc^mN~XZ zA2)L&qw@Mb?C3f6Q9VZG9Q!!Elo*wB?4IlEIreG#Xgm5M>r7iLk{#F7MZ82ch4J@2 zBc)DGE3=IC9tMnM<;RB2U6Iz*_vO~)_YyYwy@XAEFJY74OW5T15;plp*w9k%)YuRa zbIfV6q11D&Qfxnx`JbNiRz%h+JmqMU7RBdFsDa{hg~n+z^TA`1BR&(P7G9`~k1Q|~ zuGtj57|_;2k-nw5EU0VxCksuQ7uEH~7(Qv_9w|I0WO0Y)_7a}Cy@Y3OFX5TnBRr4m z8J>Ubk8fO{w*NwSdXE&I@!8xOZ!h8L?Ik?Dy@aQ?m+>@XYvE;o0A= z>i546&x=t*Z{fKu!h3|LxYQKaKi?}n-_tdH|L{amTh`xWZcap}8;TiRUO^ycMUzUa zvcJbHXn2`oc7*rn$Gjibj~D5heqcX#9ibm9@r+Ik zn68)`Jz_j|9iuO0#CYt=(~&HAN>6l#esv+G=IF~EG59vebeb5VTVlGl=Xl&6zZs9W z{wEU&FM9UlEiwHVS@Y1UY77h|H=37-KaOy?x7SO=N#y14bBTD1uIl@* z`o(#xK4#JXu5>T_n}~&<@NfMvea$b?Xg%Chs~ZYu!o(e`$pQ=s7Q^Co4@Q8RSD2_oCJO#7#;1Lsc!MQpT zAyaUk9!>>IWdM0ZXasr&P*F?(^^BI8|E*|wdgy;MT5vB|Z2wlY(Cl=iSfQ_@()RDi ziicuFR$)ybRrXOSb`t^ucPl6{f_h{kO(XVE1@tWf=F_R)_;^0aTj|U2nL~$@j-#@Z zsU$_XkaA46DXos_1gwrX;MfvH29O9n7L!>6>^mZ21MG`5E{Ysa)_@J*e(K8}7}1sGP&T+5j{2xkD78wV#1u}^kSPAXBk?x20Cy`qnedmM2ol%I5R z5@k=qv)TA84dCIqPz0VJ%V78@jiLzGmn75-NoGB$I2mFdv*MKTy}(o@sVnsQtfZt< zlbpCmoi@rot$(Bu!~2)?Nw+iNr(|E!pBj+vwUUN-N^q1R4CAI)Rv%TDt~S$ck!atj zO5~a64{4PF*s;Z2O?rml4L)U zO+!W5AqP=|r8X7J8R8r-${s(;I<0?x|HY}~!<3Ur z{DgG-nAAQ=&cOo*lZVm}&m%b;@Pv32IvmX!-A^@3bix=kAt?#%fe+v&GCz-N6%jEU zbr2lOK@xH0IMI8eP-J|Jk74lli6TTfv>X5vM#16I-NXCmi0UTlGME{TrQsTF98y={ z&X$|MC4{8dTVq-7`@_(3v`&C8!e*1A zX&;fTJR{1^M5dtZ8C7T&ct@sP>FSU=XDM&UD$9|*CKC->BeU6AuDkj?+6S$Dv`;wG zK37@sX`e3%e%Yrz6IQgNgk8#csBUZeyXk1cyXmiIK;P>bZ)eEPe>>wF07I?}a&Kk8 zthb~bI$p?C3OT83CS<-LC6OknT4o{V)ePA1sw_Q=OUoWqQst1^kYS(e+5iinCK3p) z_tHBu(2$OdmowmPQE@anA>Ku~T^Vms;WtnPQbOol(TC9gcE-Vs#K_a>TU-Y-zRbYd z?Mqao;i&W{86JMDeT(Z8k}VUm5eP|HgK7clSef?gH2Cw^X}6@qpSPsnkuLptNBV;R zQGW`#GRWPY4u9S*<={^t$Nm&DGa>UJDbf8Ya%Lgt=5+Y;W?35k6s46l(w1^a9ZG{s z4oNjpf8Lq4I}JYCopw_?{3(iR{ydcSIpuzy_RDk>{w1nFN(e>#c}M#1(ly7qewV&E z9sb;mrfOLE^N#dCrbk@o`XkAf3E2oFe`+moJ?;I67ry+5_bab(qKq;@%M?|^Nzj>! zs)^=l?;)=}%XJ8f97~n+q4$8-E_WRu^%bf})Gr%SUrt4}FQ-1_Mav)ZKBCtyGRu%D zsH$DinTo2~1)ZfRk71Xf<$^kKZNgSbTmk%NF9PS!vet^oxc{y94_e3SymeU$P^idMVGEJLQCs&+wVDynK1be5vZae|f$ zsyj~L3gCk&aNI$upB?AAGUc}^aL#X2K2BlhV2r+;@;brSQx2rSmj_Tn`Eq6Q#$>c$ zWAfjUHPZzxQ&gEQ=uAb~beaK@f0qo`{f<;ssO;zj{&eD(iKybs#Lts3@IO!bB1x-B zWMY;{6jaqDDCQ|im6HUWrKnLsIrzs(Xx+!Mf@mj+Vim~Vo&W{BajCFE|j$SgnlUD*W{;PC+r*&Qdh$DZ$IZ@Abe__sR;Qo)Xy=$Zq$*740%R>Zx|m4IcR82G5@; z`%lPLp4t+>CmuO_;_vrFJr(v`>4|viN-8@OWp9iy<*Bge8c)P2u4_oJObA9Gc`A&O zQBUo2hvVR>aNL@Bcxp}j+IY=VBC`ycf+|l5I#W^QDM4o`8ugUm<>0I0;i*-!f~coN zb_KHc#K9GNWKB^|?TOnT2Y>93Yl}yA8%irrT@m*y%K24XdwkSWd*fb-i+Ji4DmxQp zyCY0_YH!>daS^Au-XOs;AsB(=sl6x}_0$H}UKc#I*Y$!Mo_fLkqFeKn$SgyqpvqH% z&Qw%+O3+z~Mm;5XIruYfcEtBlG*Gj*GsNXUGT=Iu03veLvE1S zWjyT12FA0_j!3ZQz-Hnn_Ppxx4 z;Do0haNgp=dG!|8Z7$7IBC`ycf+|l5I#W^QDM4o`%ARrwS}v%5UKO|k_+}Scd$X)n z`R76B%TBoBW#=zl(erAD>uPe^)vlXdaQICq!NEoTbKPoR?LaH8cIde?#2A+^pW2wc&HeEofyW1|C4F9gG0j1Lz< zT#E>~xjbZKlaY0&BkltF0^u#9zmr7MuCoQ$fO7P0OOc?CSCZ8GF_Pp?CCL>)welPO z^#*?1$bO?#wEak_0zagNNCV+DZWkX(w~|_i%u@4*N=F4SCEP+ycJ8T-H2{GAWO-a|HyEX1U1B4gWx{oaHcTgj~6j(?<>D zDp(FfM#xntISd)`Jy?p@tO_wu&@g8+1`IVV4k11a$J982mXo~z;UoKirhC`PRgP;s zq9FDnD1Z>R!cRuro_w~7#k1#mn$tL7=T)g7M4_M9N`yQY6(YYVF%CU4(RFgb*@96J zn1}wHSLJNRv1s`m(Q-gmjBaMN2ZiT4I)Qd=^>_{Nuxdah(V@&0)vZ{y(VzrF? zCH+(uHCp_uFey;~wJ|ofyE?qveYYE@+|6!UitLTUI;A^~)^A}C7I!y#Xwkbl0W0EN33Ms4D+$*r zdy_2JUFmnF<9@}1>6UAM#{LZ43i>9)a^0SFXBKWEK9FU-i^r^F7Ckz`f17-rnnF7Y z6#U^}u=W@h(AXUIV22ma7=$!`2&ae-5tGrRaU{Z9%(e2|;I^#oyI)+UduYA8e=*pFneJ4fyaJdiImO+V_gd>jgWUsQp)1bQAMLw- zg=k=6oHNyS$2*(y(a!fQ(a@V#l*j!7<~Y4|W@CTBtvz7b?zvDl@dEeU)7*a9;v!LP zJu=9Q%c-sIi6w6PjGXdOqo~cDE9!b{^cLCZH!V+wD0xPoz9^gGc6o#c^5OS$pztjB zptw)mu6*~Pa<{9+J?Iy1*JXfkn;UE9#FY>^^(?CNo>8Ml#W_>n)|GiKb0@ALC2^jN zU$_TBk18Agf;({}a!#Snj+-W0ya(T&clL!@==Qs8+nt#YoalUUdHH3sw|{Hfk84$} zjQmLxTp|C=$szR6)+ZhI0u_xDUhPTZMp*J$w>S82pT9KPuXj5l{rbYCk$!dP{ra!WsyL!qh~4wu_C!SP*%S%AMXn49Tm>esg2*Xa zn|6M)FG4*&bTTQ2y#J%k~-94y|h7ZT|E^YX@ zkF^{w`*ixqcW2P91%)nukcP@SjLK0MP_sqYzrHNm6TfhxC(6YLdvlrS2~2^1aMBzY z?dSit0oyeLHbo71EMmZ}SOehci_xR(26X6S+EjE;)E}`ODjJQ6v>d0Ub!d&}fj*eR z<(x%xQ4Z!J1pZ}lo-ytOv^VJtcfw)vvxuxjPn$cbRwOaYVa}416nj}Z7_(s9kHuW% zxs3elTq-?z^|E|SRhiC@ou|X;>x9#<6m6#3>u4Uw_CoBc7SfxOTk#H)3F8H)!$~;ON>)rS^cR*zNKsK~VewRyt**D4 zs4nq3M)5XH^_pqgLtuVu172ynxN=eLBD~sBh>15#FKk@AAV++(E~Ym(*Vg3FOH_&q zD+{LuD#dH1RrRHTlESi(#R|n6R?+CDY9&nXvR1NzkUv!7pKUcX;dRWy3uB{sg=Hnv zipCh}JR@z&k3%DrYxv#T>IIb%W8wE`d#D-QqU!ppCQbe9irImZ;$q?m%G+)haztul+`7@LNUcWvW^vz@&h`j0((%m`Sg~ zjXIT5d7`FqQBM*iKPs*ARiw=k6$s54a0>@4d0D<0T*XqU)lBawZCct~rIjhHC=A7P zrvUVt+)BJ!xaHy{wW?9{zHt>HrA37^0#PRiLKqcgGKj@nM`OaV1usEvSWNvbgUMQm zcNEjNDDkbRMR*~y?30)xm6hkyTZ1brMZA(Njf*($#rx2?lV%mp4Tz4@-^{LTlrdZ> z-epTKA8)QlsN}>{qeP2l7tWp|UuYZ~PyP@M8e-%SG$A^AXHUzc!7QRV@|ILe@vswt zK$D581O*CbOfQ>-R%zpzB1}b1DZ)sT7Cz;?0%WOIymBB`RyJO=7_Wk5Ulh)mRvIcQ zi+0NFY0-c}-J0fcR-{X;snh4=6y{CL$;%rxe$<$pk&9Yd7uTM$pca}Ns*$AMZa8J> zqzNM@jL#XlAZO%^u{k5_N=pjySw*i+uW!QpeOsGHwl2O1JvOqX3S*)fnFxdG^GD*H z%Jkm$#-@?|uJDe{>{5O>NU+ zyzf}NR=Kh=au%pt*oar~E^2J5wJ=rD$cT<6Eiy22D=>D8XBUZCfnJlW!oV*_RwQc0 zWJvn+CO}5S7)?G(9NxfWC+p{cr@1cFxWHf21dGvd(f8_PqtE)Y%8O=2M|iPJQgh`G z72q`0*a}@xLeq%ygE*tmFC_3tMf}s`C#G4RA$dj;rsM7B=J^!!c~Qt;p_+r*(`BT@#VM}$`FY#R-|fS4IDDTq2rdow0EnvF%Lkk5DmM}2&rqBBb5{7vDk zgvn!kJbi|ohxFMNzwZG11BH0&KY4;y6_u5XTk%5w=6;qWQn9qO{1OmvrN8(w6XmIJwBT*K1{G zmlhQX<44ywE~*_}z39Rw`7yh;rL?AT^z0c$(?-*5SY!P&39M5_)BH=XoUZ<-lHW_0 zHM+IAX>`Nl>V>T}wWBe)RZbW`a$&>b)}Mc5TH)G%2QiI>E zARu0GKC&6hsaj0bk?f{c{IG^tOhAakJ(^pCfE$Uw#ZnceCt-}shkT{O;F7leD z3vrMyPH-r8s%1?rEIcVEcg}pw9j$zp8JX8Al6lL=jvX~-{HSpwID_tSi5t9W*walg zoSksz+}cWa#nioAeBV)Q_9e3{duf_IFgf0{4!7m*5$u&j5v{MC-))ahpmR*-M2iZ93oQ*#`_#8i(zN%*>k^Zr|Yq53u* zp+1Tg`WF-0{u{AE9l$`%V{Mmk(j;H5|2k>7$uw* zUc6&$q&Y*gU(D@b_{U56x+g>4cLaVsIO(~S<;5J2K&ucDPXP4_Zgg?Ig$K2j&*e&S;Kq^$Cl#jm>*IxhcsM@9}60bNyBVUF4`Ad5c_* z5RCpVib%!&+{*Xf2Db@@#ir%|l*e5i5i8pXfSIK`>r$vhsYUc6Z; z!slvS*MF79b^Yr#uIs;F#ftcUhgk7uGf2y#`Su)Yh17QS&i%U4&(WV zvhhDvk$x}GIQ?(0ijxa9uG{&0jq7$ksBzuSJsQ{Td{g7Po#S{OBs*zcr|g-faox_f z8rSXoi^g?3pU}8&=PMf5?HR)JDcM8+6Rhkh*SKEqRT|gpU9WMy-g`8z*ZZ-?2Wj;V zq(ueUU=*E@jE6J(EG?{JOl^-kBgUN610l{V6& z*NeABi_L8BiyGJKeOKdpy`O1ZuQy1Av6=eUXk6F7MdS4U%_<)DXk4%ND~;>*PNYU+ zGut~~<9eL4`-P9GjM~)ySW`7ZhR0O~3lOKK59M^0!8s{kn1BSGHh8KQS;w&e>EVzR zBv0d1@2`3MOfvZUJfEIs@VU&34ZfQBG=ty7=d)P`KaQ+&`*hGVb^54ZfZIu*Kkw%y$}m5c_wJ!PUK(0|x&@ z^_Ri7vwuD}csi%QHMm-*%Abgk=U7tx2>T(E+o${!=6VMhd==ZDWANuWevUQxYSw>( z!An{HScBij?V4opE7+fEJ+JIMll2D-dGR$+*mSzVZ)N#XgI~b$qs}Kv|1g$2+mL^N z<>wn*#bv$0FXi?&8a$iRR~UR0k9T!{LD};q`(Mp-ihs-H*Bg3vu${LV{8>)lW$-a< z=N}CId-lJIBUNuQ+w*5b{uE9>YVa)f|89d{&vq)ilzw$zVXq;-kK;|v-%4ICUM%Z9 zLtg&ok!7jtG9{nJ?fTS^-^X#NuGf_OD=hEib|^kdl{ffMZdba&*R!9q4c^S{8f@_A zSpHap)31HdHqzkqvp=+rH~9HHFPv=f70e3^-pRbg;Lmcsr3Sx^<<+=U{!!PH=Nj^_ zak|#v@@JPUYoWp4=XA5dd|~i-9!K(*#G>b^4_Ll0$DPtoFKeQ0sKN2#MzM`H_$S=1 ze1orH`4WSx`$TgM{xREAZE#j&{l?&@^7#FW!3)`+yA58#{6&K+JKr(5vhx#zD?1Mx z{4@4@3j0I(b2G=u5Q8f_M;Tn%d9uNkok4>uJLec&**V|f%FafES98CsaiHv1@enrT zRXqI6;6u2*w;TKr`)h;2Te#jo8@!&=j~l#}`~3xjtMUJ~!Mj=hV}q-8v;5J%=(tGc ze(W1BBTxB(eq@fe2?n3S@^cLSGfu1POQq*Uj`LQ|C$)ss{rf8oJwDcdy~fFvnfRma zCWF(xY1(epIJw{*ZvSsJPI^uyVQhae_;`t}`!r5^syY3T#!1f-mfvRZh0Gt*IO+L_ z`Ewd4J(n|o!QhuMSL+4k&);zWzGuka#PSCWuI>vQ(m1JD$Nu?R<7ChMtmm-7f5+U# z^(%W$=6NR5;FoZ}tLs#fC;fX_k57{)zK8jEgCAl($>8rZKSSemSi<$rW3J+D4ByXc z(B#PlSF)a^8Yg?yeTQWRkK=LCZt$a+tLq-s-u}$5HRLBU|CzzZF~5m9C8~_aKfroUHTY}H3k?1e^H~P}JM*~) zKgj$?%qdCnc^CV!+2F4*f6(CXGv8sx zQsd+|b$=tz;7b1ljgx8>SLz!Xq@VOm=6Qy`vqGEVdCX7OIO$PwHAmy5=Ny)=FgX1v zBW>quob;%;YS1|8xrpT#8N88slg3GpimOXCPI_);`Bes2_pPom_=7C}Q-g10{wssO z#Qbi9Kg;}&%vJx^b38nt$ke=Hh2Z|4>e9rQsd=Ijgy`*%YSR|Rm>gy07==e#!HIENzdEu`C|Vr{nFrfGr!m1+nC>P@Q0XhH~3!Wy9}=GzdWUJa)CNu?A18g`4Q`R)!+x1 zzo~K3qs|uxHBNfm^nvYTgVPTc)Akv2)i0m&b;WqE%vb(z;r=>}xso5l@^cM&<^L*; zlPNy7r`F&{GjBDxpZUcGKZSYN;Ab-LH27@h*J_-aq~@`kHBO?7SpJs=Z(#mwjqCk* zuf|EVjpgq*_zLD*G_LpKc8!zh&slz#!LMV!*Wh>;huB^<_@9^`H28DOKQ{PZnSaJy z_50b}@2T`925gF7%e=zi>iYjugKuE@o0+S4yMgPy%aB)JE_le0SMm0^#!1Dytmi3% ztM3)PX7F!V{!N2_!Tb}0XYh6UX9iDY{+xz_kCOo! zClgO)`J)X!iTN<*s=tn5|5s`9q<=ck4+{->_5IT}Ltgp+Dvgsp>bdQ;20xSSxy|60 zF#ol|FJ!*a;Oaif7K2~O@(*d8+NH+(;~FPB)%Q}KGWcz*=Q)k*{rH;3Nza2U|E9q= zF@I0vdOv=mankb~%YSC@Cz;3b7k6m9ko+O$i3b0G`2d5vx!;dA_+geG#$3gT^5@A0 ze}v^PH~7=ce`D}{%pYg2#=Cl#&mKcweGmErLte$(=NczJC_P^rd?2?siN4f`P5JFa z=3axV@39Ov_!%re%-~a*=W1N<-*FlzJ5?M`H268JhknX{Hs$|`?6)fnK9~6onjWh6 zJt~Opc0*o$-+YT9ul%`P<7DS$Y|k!(FJr#f;I}Y;)!^#B@Iiy$&+;D|{9fjtX`I@n z{ATkFXVs6pS>A2%oy-$8PI^AVA8ma#PVIe*<@+1_Rpv))T<_lz8Yevt%EUI(;NP&G zQw%3Pi8*H;1ijj%Us3JE;0(+0!^Mm=X^nN{h@IZRo~yZ#Nem1{x*ZF z`?G5d-o*0j4E_t|*BM+rH@}g&vVSkzzs=y6Fn`(Le`J1)_|2f`INHj*#NcY3(7;^H zOJjI^wHWg1|2#Sjc{PrHrg5rLJs0@7!GFzu_>IBUeY4*i{6Uugi^2cK{1Jmc%Y3KC z390LaXEjcCe#r8FGx&by`*f7UqZQT_Xv#z{{n>v_W9YnZ=c@Ozl=Gx)EWA2Rq3=KnDG zBh0^LuHr;}?;(!9#EgyNgofNa{L$7|<7EHKtf#-h_b?x7a3?9mcC5j_W%zL2iIN7Pj(L#-rohw;> ziNTjKZ`C;IQR8TZ#!1f&EZ=7EYniXrIO$R2=vs}Fo-UUEnZfU6{wst3jrrXMf13Fw zgCAo4pus<2zMZ-1_XLiUr!;vQM|1E;+p8KU``vsW{0)O2X8rFQTs^1x(BQ*a&*ui8 z&HQVF&tz`l7j$V;_V;J|k2CldzVBCH@K2auX>g16Y%sX`Uft8oshQNjlXx7xWXSL1 z`|}?g@@gD?t#R^a6}RiK!GFX&Rs2w3^txj?^9+Mu!hDFquVa3U!PhZAPUHGG8n1D( z^EWI%$>6_ceu~ERaWqxqq~{TqKf~Y;GB4G*K90`PIO%zV<9G%Qn9IEFgKhxxC9G#9o+J2>RvVSz|x!d3)nE%n>XENVt@R`gX zGWey;w;BA$%pYT}>~G+4@rl6`nJ4lWrIdUD^D=|0?@uo^_=PNg19LTw__n`wn<20M z-+Hqlug1|fjgvn+xLrF9-p>4Q2EU8>O9sD#dAGs;!u+7Yw=n-iNM2LCbh zH3q+vd56K(^R0EvmHnI9{w)Sq|7Y^7!LMccLG(fdY)a3q%%>Q92lIK%Rh)c9PQrGc zA+O$xu*#5EUzHiJLK^7k5iKlA$y{xj%WD~4So#sZw($~ZXb=p z*wj1_V4i01bC_ou{7mNknXC9w|38*z@J~np+cJZTFZ+PsYH;;`Tw9r|czcBB^T!SO zTS+;#Hw<|dZwE9^epAn7KQeeTx9cAoC;#(p1nWDElb+jI-Z2;nY|5WEF;6%6Uzztc z_%`M~gJ*lC-duxwn2*%Bh+7_y6E#kDHnRN524BGZbc6q#`E-M;=aKU@F6!lV=@Nr~ zB+a*$8~jb4Pu6Oj?0k~zz0%-+W`2#vsopo~1KUj+Cp|%aj&+N{r!c=m#lM5&{83Gwy2OV++Md!l>A!;YJZtdFnZIoCTbRFY@SB+LH~1Fj z9~k`i%s(}FUw(e`rNPznJVy?-B6_}9!tG7U5m>}OwRgNsS_3pr?JZ{gM;m+!^Wg?> zW`4ZEFJM02;LDj$GWas)euJ-NUS#mgnNMe~`d790%p>qchMpT)&jkj*p7{!c-_5+u z;CC`#%Us#Bg8hHD!3Q({;}Q5{hJN)t@(F`);(GTOTs@E6Yw*Wd{!N3w!2DfZ@eref|SM$I4HD?Hs9*Vd zspkLb8YlT7{M>Gq!TU3>GWhAtYYl!X^A>}zWWLnkOPQ}R_+89941O!~pBwyX=D#rb zW6W>YIQc={@4DCEYMy_{;A)=VrE&7x7p(tDgCAo4tj4KcHP64Ianf@PKX2P-@S~V_ zGq)j2r<{}VN82ZwJk3{p`C@&eanfJRden<1RQwb$PZ&ztqH$HhJjLL1m}eQhiTMD7 z*D)VvaP_=%xWOM_`7z8@d)GkK}Wd85I{FmE>a3Cx!>`R6Nx-_CZ@*r$!+#3_#C z2QLRB_W@KEhntY6M^GWoIF|RMhxlSnA7}8ZI6cbX_i}oy!C&I^1cNI-pJZ_LewKj2 z)%#XT3_gbf7~6EYi?pkf`D}x$|643K_$4fVj=|fQ&o}su%oiB^F6Ijj-o^X^gKuYk zp~0VL{$qo`%zUN6)q7al4X)n1cQtb=&O<_w9OlGyKF{E{a{3;FuVZ_@F}S+#TFUuK z|30qd4uh-bhF=(5J(mx0Ii*K^|DoRC>ib3N|4x*=`W~11e^bTh@c_!@!K1kPp63*U ztM3~tdzHNU9=Lknh2mn7OTOXfLW*zYd9U2y>i^*GGPwGGSZh@C#f$L!3fQ+a$$h;# zYN}mWH448@(qfGg@8KUczq#2OC1_3U{MH5d@s+wp2gSU~_DgoG`4hiF>_(;>wd(`3=14b>zk3fvVtGw|e5O_a7r}3us zQTTa?CACd07qh9g^_B9McQ~T{vt15~Q%I?KQfZsQ2kQS66<717O4GWEx?+j~ykA-? z4^;fn^#s+!-74M*ND?%!64&Qu;_7)99qSv1e*~R?BhvZrmZy2w)Je)!{fCE9D!Q4n zdC2Om{V$#`%Pg5H_tbt`Z<_5d05f$`iC>MTPdpDa8fMCtLdIlNZpJwJw5+RmJtI>+ zWvQEbEB`9nJw_Gey(v2nytnoT|0D~j^3;AhZ<_6I0W)<{$&?^{M&HXOQ$1zF$mp&7 z9b8_$5AI0iS0kf0`b@-5pNv1#{-1(T{i>Zf6x&tQG1wMRqqR-hs`j@cueb7_ zR>(qMDnZtvl4`%<2<1Qgku0FrAzVD-D^-3omsj~}Kj>_A6uVdD6y1dbTI$lgt#3J8 zK1YW*R{m4?4IGe8v-}iQKMfjfGt0k%yx#h6?ftUQ1~!=buMhrIdH&i%uXR(&DOL1| ze&z9(g=1R#ndPZ_jUftx_Z!rRou&TYU8Z`*D&2nRt^A}eS>k{y$a`*1gyMH4svB!Z zHIB}a+Zf){P9Dq83FF6$oJr#+jGrWq<|VEu3Kcu-s74!acZjAURakcgqI8PDQ!sz` zv;OWq`YZpLb>5G*`sez?pZVwb!+ZQ?{_qoXg5hsx2g2KeZ68dk33j-S$wjGPIN;08 z+v*+R@|A?&3+@c~a!8kVM8KD}pN>M|?}Fiv0&8o0Sv%Tu@DG@FFzoUL!(~2CT_D`z z%fRPCUnUBQ&I_OI8zjmFJD|8Gc)YBp)f;SkbkdK`@t^HK$A7NBa+{Fx4h~psFKnIT zFAI3P-klQ&ze|?F%<^EzYTr5-f!fkSoeOTq;wDExk)<38UFlo9gftPS}*dAkB= z$fOX`xzwaFF%1mnZKak?K>Sr}zpSsmcyE zY<(K}I|^NvcjJTZK~J!f&7?2<$ir{C+8%MPT;`i(wG8TL@#So$ewniJJ1hLTcj}j$ zY08C5BZ$OulXh{l%pHg|$f5AxqdE4tS9?*y7f$$evH+lzKP~i7&^tA7osN-zkn6|C9cVm9Ke{FuB$%mTW7Yf7K-EO>1 zJmRmS4u5jQO4ueG+x_BqhY$NZDw0Y6mCz3>?UK$5(%SYXdb=K*yq5ye+j$fwOmEkV z!H#9VG3Y+oCo~Yy2RT6}EC?eE5K}S?FxW!j{Uza71L3brDEzn=DJ1Cb|VfSh=gIVqs*5reY&=d=)?P5B52ySI(HU`pjlhs-el!acooD^cui{C zI&W=(hobLPFd1@d3zCGr;cck?s$Zb5dd8QCps!?Qu_H?KBo$r0fC7P{!S88%)4TF3 zr*}1-jzvg_h{#{rvP1;~g<{0VE#DqK?Cp9c6#fX_g#XYkVG*1+Ms$^Es<&&G?J2SM ztZea3O6z_M{Ss9zE%R@pxjUlvD5#a48dF;wFnVz&V(opz8tiN@M;zR>&)Z&#J+cze z!3k~M_?2Pr>Ka1f*F)h0b!}h9w5%GSeq!NGNt;l*+0SEn z+lSD=*ePBn;@?s1g51W3B5_K+bsbJY36a}(P3CAU2`47r|Q`^zo;C@|A-(%P6Aw>HxbVpInwzu0t0M13gy zi8jHAnbLdJP8ilRnmeu(=a9&p7#q=Clm@#f>fKoE7So%zeI}T@QICoqL7C-ok26(dekU)G2Zjg_&~)gcws#+< znPl)33#ZWWs2k_C*QsBu?(;;373e5V#%zV)ip(raFe$8y4TK4D>QI3&TqIQ>^hb)$ z25tKj&_ClW^mmST^?1iQ)jv!*~_3 z5UO70%b~{EMUy}Bc9u}5qk-cHSvX~8Bcz-~Bfdq`$tQ!e5CnLtSZpt5}viQq*{_x)JW}*^fLNX_BAR+bKp2 z$mR1We~Ncwf!n+BbY5?a3wSp!^trI$NcOG#66=i>TJUrS!jDUJk$&5R+1A@t5E;L% z4}~9V+qW-2r*&i9I-FP1($GEGn9$b2x7}BeG}bw7-^Q&tV-sCgZKi9EY1QANpR%pM zi2d+EYEj<7&VXgZ4W8~_qFA7#0LzG8DnA+OL}!W8(9*pC;^urm4XdiTXb{%#0W9Ln z#Ns`?J8u`}{9K$FlY^aDT*b`nSmw|SmIm8+l}pp4KfFD5iO7qLlJKJMW!=+2zaIie13xz$E|Y$RuCVNef*% zzkTOl+fEp5Sm;Z}ngr8)oqyFkhvA}t&yBdP4R|w{3BXk2`$uX|P6%n~IC&raI zej0K5YCGaexELdmOo1!wR=suD7G{-HKjGg|gfX^in>CL;>)%-ie~I{x zJwOX||JuuWDu*wG zIW(Ulii*@obG2SgNnDdG^I_@iz*^ZqdGNA)S{7lI^mIvQtDHOh;lT&4!IX#V!=jF& zh6x9h{%Xa%k< zV>%pHh!hl4==zCndf=k=Y~QKV!e3&jdFto=KG6zx27JZhdazVf8KLld!nDJg_yY#IJC)?&GMp|exjSrw-ohDhuEn`pOc8+{6$)Ksas z`A%4n>+OOW5&yL9qj_^(0K*`+yA8*p5o-L4bxle5aLg=Pl=t?WVD;NyZU_qQXD=k7W=ZhAKsS`w6}*y_d(J3C7t(;K6{?Sn$|gJ zbP&@b=D9sVgh%$Y@aqTqMJKqRaKNkH%YH&`@^)Q!4pmgw{%p%T!fJRwZ zhLlQE5h%Feq)X8-#+z^#<^mybRt1eg54v4kv$WhuvkMj~wzq35PBjx^BorzcC^X`aQkPtLj}XrIxKYIPq%tEZWZegv?Ej8C-b(G7pW~;L&W@(@A0lV zgt~*(+x&ll47wU~EM1F@6VnCXJ_?0txo|vww)Bgl@aL4{IWU9f$JH2UkD-_Yxi~Y9 z^RN64m!iecNIA4J7CqBh`t4@AtX;e2vE5iKe`R|w|09x!|4GYyW4)KVQ9ya2DEu9K z6@CpBj>AaWX=CVN;ug+8hyPW$#y>~&!&wz@$OCefxbi!Pw|yb{FyG-_P1kj>pglxw zgKIpb%_+X&zhmW2*x(csFYM}Gj3SZ#l=klsc5^A0uzbf#vfPECK|L!j@=x%0{v-|E zBrKsBXEX)S`IyU23RZ`FCj>jMOkdm|-{&ZefB1d6$2QK1JAhC2^RAZqaT>Sdr32Qe z^?Q&`aoxjcZV>%Lf$Tx6|AxP()~+7A9Y#>Q3Pn{|0h|=tg6|L75aCFyYH5T zSECKwb}GMPwS&r92X+a)6l$LCxJW(1J;Eh8I^GtYCcL$F4SB2kIY?4}tA1{Mwfk2< z>&U(|JFa^?5^`ij$2!T&@uAIR;(qS5@S}6WpJK5jrZKUqF~>%*`VraDJ4HJ}ovxK? zxSY{BNCXS5W93v4>dYJrKMXF#ToR&_o`1(>bSy_uDNTw2VO_yFh&QSUg#qAsvj-Cn_!0S-fm-)#RD)Jns{)2a%C8-*u6#$!ZL7DoPGFVRsyki{Lp6oQfydVQH<8!mii9c_Uh!}C0Ct}a zUB#V)(gJJO3u}s2zufY$zq9b$eEXuS41)t}%Y38B+8nYrJFxa_z9S?|&L)#{+P5MB zG4JYW(2P-lB*ZWl>cZE(x&}u1SAOkm8Q`~FzV727LVgirMdhpLNL}oNZh*ap44y?xn>|ModtZW6d8Pi}h(;_;d%YpWmlNW-eMDz2M08hquzA)Jr4W zY+EI`emACbQRoM9-B00IJse@(eep}3ndNVEk zVr~#cTgxi1gq9#y-nn$!lka-A?OP#ke6B=Ze!O?}Bx;fv9I&232L?xNquCiA%i{UU zbQA&si&Z|KdfVwcGZ;Jdf$&!CR1}>vM0269b>o3YY5u@m-oBJVqsuN@eb~F2eru;~ zzXw;-{)ayr?6-INpFtk(iFmtkAK@wci&HEtA-q>7Q$45~w~0#~;iv7dR(&NiwxLl0 z`wO{V2v2ojoq4!r6sZtsdS~(l!Or}th!wd$$4y^Bvb+-7%&{L`@6oT}O z1$Vd4_7xxzg!y!9B}Ca{V%*$)9n^XUuOxB*L)0Aqh@G2g-#t={o&fnKF2*^^+wCCMc;39FJC^>VlxSi-}q=3}|qJWkw98lYh`Kjv^?Vx*!0TXGWK^ zb;fut2|q7x1z=@VkEQ-`=J@n>?TC!YQ0MppxdaJs3*gehAHK@vp{WaJG8f$@pgnyP zA&650{hlY($Zg>L+O(g@+#E zw!%&a>bG_jxNw7EnYjFo-X9inFcS)+_lCtt$I#wc5NEA)T6mB_Q&2%7JXP$%L9(bt z-Wj?;MZWet4Cf>MR(B$5q!2 zqW;2hnm9(E1v@hz!s_S+>M2|}2RoN%gzPukzVdk6XhQnz5tp|ecV8@A7Np^%;SH=! z`0NqP1W!|KotZa@WqsyzCHC_LJ67k2{_dWLAyBstw>i}q6VGO7@L_7g;G;l~1hI&H z5yWEL(SzB5KKO)-yI;cC33W`*2!#`HUD59gTz#Z}5$Y`Ql(fC^MQFq>`|;q2zqY;k z1sQ%BJ!8Q!PEFy*+aC5z?-<%I?(BTb)6FQdl`zE|B{hRI+fkZ9d zu4Vncs{{M4dk8flIu}OQPjn|uobcq0G`bg5j*004x@kB>-U{;2sgItd#f&N38KAb& zGZhSv8ebfFfQCo(l$U+v8$%c?7#8&WUY+2?Jt4e@HgY>Co|Y3wychgA}*1D=BWKCi{0x^~MX=$lE?*WMsz-+ny)3Xq#jq+?2t^XU~CA zNn?T@nA5{N7w$yBLK?H;^rmjlh_W#i(y2thOHZ$!$7!tGykTVJ$F<&!aY4^(J$cW1 zH+~)JoSxOTKdsK+=CtXyMDP(YW@xO;cm(!&{cFok#;v63X(gSRKduX3LI3@RWCk7L zk_m^IbcpY)s8K_|dlmFP^mNGHD~1f+1QkQ(RUFWe!O}K8ur^%`nRmcQ7&9b>oNH&$ zNtZRN9pcBcx(6ZR#CeX~5r@Ik^E{OUukc7I!=)Ozje=+j^x~vd<{OWt-voe3IEB!O zE1!hr{gLiJi2*b>D-=%mho7C%ad!F_)5G`(byi>uy@fHf*M7WY#4a_4X2Tq944u(2 zDE*A^pgv*@(fBUvNT(<8=|vr(^lxzE6XPfcSOiP>*&=UODE&LcIX=5RqGf#jk%!gV zU*)|NaZ_5}O$&zi%+7lmV+o5z8`I1^W9fV%&)eabriH%ZSz8Yr8hK%v}0Cq|A(WUj^WM3=h(-};m#j?2oAWS`LgP8?^+-(Qm1BL@~n zvTq$g*^@k@>9SO2ZyzE`Xy&+{=|?4o;m=BQCwVdvrgU9nxzm!j&|W6`_I(n^9#ke8 zB*o*#r4=gixD$ubu@sM+AQB!cZ6KJaI&Pwqjzz)T_$<%ks$hI}JT+1ljPIXD&2x!L z;|GWwk4y{_Imt4SBXZ;y?&60gG@wkT%o!#USu%m;4XVkuCg*wz6TZOVK&r`8G^i|c zT8504xlC1l3@JOYl+ zI3sdAIOEL7aZdWU$nlVjY8+>|L|s{)XqCyQdOt-@5~-tKXdgt4Kj^l6)FDIdd>}d} z@TV<~ew_Wr_ z_LHd#l3%b>u0r|zQ>p**a(CDX$*4xww98KUfP`dfx1D?;B-3Q-Njr(G$dIYO%2cLI zJ!2=5O<6MaoSjBa$v%#nFo;|BrcEY{p}J0)l9H8hH4ab&Bc;DM=ET6%-y%bf+mxdc zw;~~jYRcf0?MP(HGDCz|j!X;_V!1MLj7Z4;dq^3c^jFYHRvJwcB0NdE7@J-dq-pf- z>?9JP%4D9EwgJa!-6$-^PD-xxD1f7Hs7*OO5A(6CYGmr;fSefRAr(_(&X{CU8W1yg z%2*F6^Hs{!c#*2%)P!WJ(!j1S96+PjX$QE!kB7` zxEg7~RH5U#TA3;lN^aoP8KT_MM`hkLQEn5bW{7fk$=^OpnJK#b9!|{`c^f!2Cxgu2 z#HqQN)Sp{9b*4z|=Tx~!rR}!T$gk4b70y#|JPhu%(ozQxqTk{Yo=6>%)I|p}F;s-3 zobFSH31i}9j!z`wW#SkiCIc$_kSc9UJ>FRe zbA)Z4)c-@$-|{Rm8RT^8Y^1dCt9aPoh8H_x=6n^MRS?JI{H}bDnn2 znL9UkP{-7eR%bA?iEcxfpwC+@D?F67c16H2=Pd}}frclx?vM5ymt%3k5&5iVbzXfi zJc)jxNwq(gO{%wrOgc>w)R`GWVJQ5^)0x*QpGsI9OGTA zWNOHrKNtbE3QE6e4yo1owLp08Odv{~d46@~`PG@H)ZtE#OQsgd97f^!tQwvPvpq_f z+A7onbuXaqEley&v#mlM&!adLuo6pw(m$f%`8AMH?wM5%6JI}jcLPgt$#wi5_{i+8t2KE>aY_5wV{1H%Fq({X`t9Nx@+P}A`N zE{yc@FyBuMBs>D+^wxBnKten457Kl>Ah9>mvxPq`z`75bD|EX+*rDDvof&9z8TkX!lAgU4aT(uHvrG62E)zr5 zovT>Da7Aomwvmdsh6PMe#7@3*tRk*s0n_@;oo?+)d>Fk9;!^As^xR<59 zr-=JG^xs#+-}uf4ig<{L|0v=SCO%Ze-w5jw<4r1X`th zrika5_(~DaGx41wUS#4YRXtv2!jj7JDtF`x^-rdPLj8+5Lqh$Vsa8V0#cJqk^)|O_ zEBD^zd#OUb&s3UF|6xk@YvGTWYA5%OFqI+HQKmA5`ftJpjHSI$pYy$rLVd|pC!xM( zsYzB+E2@*?+@z>3igSyivK8l6MRil0mny1<;=EE(z3lj8Snyig;`MbRP3@G1Jmtix ziIOr=)3hx4Leq}be+#S`gn%&r#|!r4^_YH=iHY}xS-8usd#g2ZxVKHyQBU&29bP{> znSOnPR+Lsa$L2aA)F4H<>fT_bw%WRfhpKyOkscnd?qw*>k!tW+iW;q!>|GU=XLH!8 zAL0p*Qw#W>>fQv!*;`Q)71dW!g^KF0s7Z>-QPi=D8l*opL4h$QNYmB^O@Fe3hpw%vv%d=xe|5Am@<$E(G_nVSBG7JJlL3MAXiY9TyYDLio z2M#>dRx_d~a+-YsbYn%b*4oMRXJ!=NdQn7CXT~^sgQ7(KMn#GIO?C)P%j9$Ho@ciuLKHY(24N+i5ovmXeU5G#+G;1$+Ck}eag4T? zC~K%EP~l5s>~@)=#BP@>%2QIVP*hM9z0!VE_tt8golGki#dnQ%QZ**X@D9~VdfOS} zmunRzez{I@irubPRJ@Y8OHm1mxcHz|SHC3yq5hq7$E0Gpw)J)SuykLg`aWxv0@Uor!B`78M zRaGm5dQDNH-aqXyF@~%}icDI@vd{@)fY7!g!`%0R`{?CKef6s*9yNa z!&kk%WApMSAQIlSTcZ)H6$pH2cibW(K8_J_Bu2z1b|SH%OwdTL#3d{BtB5>m&yI+6 zzpy+0OGJJZBl2qznL4X*rWO9iP9!#|JV#8C-`Y0-N~97`TLtd!A66n&B-#ovZ7avn zR)F_ezrr)Lp{2DwGpw@`$w0KWXBq-YcUs$PnmVSXy3ol7kUBi|(0V;gqYcwU#Q#~z zD**CcTD?anrZB~Wm3$t+&hCA!Q`_RN&7&puLhaaKLS!UF={~4iUv>E2i z$Bn@qlkpi7KNq$;Cq=KjgemwGo)so{tHEZ2TMgE>I=2egX>|a2BzC*DRBVLEV7ser zj-APCKDEuU+w+}x)usdAN$mSRRB5HmWOsF_lfgOk_vMuWgM5nR`VH>-34x0Vh{~!ABoT45mZf@_r+Ros zqbiM-^7$Qd< zZ$hAL&kLN6|BC}_@&D{VJ^r5)Sc(5j0_(t*Ou3m%8Nq}6+>IIJSZwX-u(vP6-*RO| zKXm(^qRR4J7>fYkPF-)mtIDD6w;+ z4XO8qk%rOH6p$~HsdRHXUK*Q@xuU(j{nl(r$4lKl&C>Bwca%TeIc+a>r}|Tm1N`z9 z((LjU((H=ZG(-Aa*+P8jE%et~H%>nbCqu4*BruKpiW>X4hXyUYJmO1G0ZM3(0g#gf$vwluQq`fKxL4OW|(Qhiik; z0Bn{{X^6CD8lXvw2{j9NT} zA0N|UZ$E_91t&$i+qGHpq`QNgB~Q9r)GT>2T1cLZ*yQ09$TU(Q(?|iD)Y}^gw%Y0t zn_x(}4zcOgstSnu1X&HsYE@qgDAlh#XL$!VqZ!uG<;RTD(&DMDQ*0h`+s?6kElgIY zx+#x$`@PI^|JNp~J=D+tMux={x^mp@vP?D1CSa` zQ(E!;#+-X7!Wd0aDia^JfK!>E)ieoS`+IZlNMYh7fy}wI-fUscofTn?_D?0^j0jF8 z!Z(1H3dF6=nW;duTqPckFt=1C9&ln-yfbC7Zg0N>>nE-f4>-+Nybn0dSG*56k9nX!1puv~WEX)KVLC>wZQBb-$EZ6? z`rtQ}nzvGGK&O7|$S5lbS07)K(379jDW_9pLV!9dZv;&0@w)9c!1 z4d3aD0BGJYw3b?Qga2dw#4bv9Li`>GIRnRTTb=FgB#h0R?DTCmTPHiC{ga#U6;Yz- zgRwSFH$NW-eTvh3rrp5=lU^j_jG{kq|>x-9lPTh^@@Y;oXhMp0V%V0tm8J%2Ka+3Drv0GbAgz88RMRD zun4(FJz=5QZ?@O;-P%iCR7ZZ`Qo{c)phqn+4cGim!{~;Q6U%J6uX$6wyQU>_i*4Td ztu{)@NTU{_VYPaWrcI`vW@6Oa-y0~=usYDGSv0H;4DdUHqhWPmObgg_dS_4{Iknd6 zz{1BgiL;z6Pl*j^=9{8w zSWMd~fpmWl9Qad>z@OSe;7^MU{1$?|BH*5o`+ptefzuJ(#k`!H9m2D*tnQ(#o<4_B zjGPVe(Jv+u>buBr5XM{6~Qg9ZUP_x=N-yYWjHr2nN&%v7x zC3bsyp<5r^MTyoR((9rxLgKODGLj07e2Av>-Zo3a1P`W{%DjL>(R)>DQ~+g+JxmGs-3I`pgy|1F z0GJ(NjrK!|o)N*L^8!yTZc_-;LL}>&W$WE7H2jT+56?tcWBdL-g16XrWHO;CY1r^) z290m}Ec(1ZL=XS%mLK^~kF<^UPZQKB5nS~jy-S|i7`Yq8$urvo|6R|#7-<|G+HY9r zNQ5=o4|ygNS!INftW@SG-^AEwW^?Ws`|x5l=ED%O!218WRWt-t;k^k}ZxW@Y-567H z)fZL(Z>P`J_(RpkF8+$Epn?A4s-UsWp(*}ydko60y)x5ZZn@#guKq>rv8c8y`8#$f zwv$=GmBY8-j9QyT&h`uGhyS=Vvue6c*ZxK=R^nUPJ;sgx!@8Q%In?oi4QBM)|3c*cHLggM$1ndHp~9udOpaC%k|<)opv#}EuJ z|DmB%+vEOE+75~KOJ-{@+D0wHHGdjqrF~qIO}yxei2hXC>H`xySCdds#7 zjIxQ=*EEIc>%+LVxJi3rAqnbMd;4+F`no-K9Qyh&aBCL*N5^b+LT}J&vK7dn7tI1m zmJ=SGI3{sa;^dT(c#GaS);_~tmtv>D0G^eb5@@%7B8n`2_@4PZ%@>Smh%Hx~M;zJq z7-o@Xp`k5+@#(%k@ulD1AUmS#MBARs9k)WOkSQuD+nSs*5Nu%&7kI~#703>&{zgKB zWV{EO9in?}j9pJH(m_jt~qG8CY5;J2z#HePE(DuU$#Dy}+BF zNXAI-ZY3s;OLW3`5mlK84=r-+@tKi9_sX1{(#CEZHzDKHOlpzI1`*RDsyWH45D&s`R3+Mhhv*j1lj1?BT2AWD9()n^lhZVH_Jas6v#4 zjE{*}g!BY4eWXc#j>U1wfN~LDn&R)PBLXIg1tOyS5q^v` z_dE317z`37i%7);V2UUb0j5S`l!TWgE|4IdmV*#i#_N@t#qOKVF~rJ{sb<7#=htnf zpOZ#1i;R+&SskVno*gBq1DN8bEP6{Ho{Jprp~4MCctX6PXkU@N$lX_TSCPBRp0`sN z)a_#2{&SIiqWfocyEyjtY~0>cWFP15QMYHu-kyWocNW>D?w#uPoSpQr-?lCH`@#)H zfk8WQe_N3~*WFfhZIM%N@1(y-X1OmFZsVKV(8`{Lw;-84jL-V`$=;u=uH82Ej*esN zxTE7Rj-%u5%7-R*<=>yr<8bfKf2Pnrj``fJ;~pM|=Oho0d!`UKWfVYfABWeUZda%% z5A^PF_8j+ag+k}%2YAqD3HrP!-@%&Ay-4+1vJ<^NJ&~cOCw@5*eF6G%;y()*`e(ro z>K@RJ!kY>ix~Y&gD%mM7d%8QvUy%=s-JgGVAx;Nz+b)e!z$Fw_&_J%iS>k;CT3W>x3I8fVy$Q z-zR|j`-E4BdS$};d{CS757Ox%#tHEL{L2eKy-)z{P87`{_Ar~{tF>YTT&SVrLJjyU ztO}|B!{h#%kCo@DdMfmKqoZ1r9_K z9qvctE*ww8xp4dr8qSXKTPMJFTPIwRPbis^&SXoRRg-^=e}h=x82|BjckT8o9iI}+ zV7c2T?wuH4YlQ}N4-M+M)7?AqWu?WJ1yJToc+z)sN8zqQpt}n1_M-<2?<4d+^c&?h zIzl&)y8xs6rU0jJAlGR~rvTYg2zn1BGJUY{VWJ;~ETSV)xP7ZeE7Yg+X~>HW;hhlm zc0nw%15#oUvPBF+W?YXDLca0U3kCLEedXZ!6RsJ*kw$-G!3zlJZwN#qupWnnA+ByK za6-Lz>T9w!trv0Qk^&cR_3lKF-iBBOrVjVeh<}bm>i%=&)nkylSC82>My2kqF=vjG z)QxanH4%ch{IZ@&MeC_dp(60(c-q4D{nz8S?s z|7O(BqY2+N=IB^D(^$y!bhrmbJwwdTjCx~~yUxrq_k%HCkBO`0k0f`xUyr#y4}NMI zd;3_NZXf&l*vPHd$x5@8qO&7o(pG|)@5eX|ZFjoglL&m|IrcAOarz5HF#P)1_lSNE zQkec??6*XJ3q^^J2xLJjDNd&;27~z?o(`kF2a#SHYZ`mySQ8`v8yF4$5ja{U)FXLK zWE}-IAy!oK5Ui5tk-Y!p>ExknV&>#|B=5~Ul{{|(_9xG^gMJupB#3{aGdTPe#Z!$?ild|1b;^|aB!7& zzZtYIm*Qhz?)|w4iTiV}f@t@u!H0$r`pu9hMkxFEJ5$#ZfE#l!riK^iUXknIg=I9| zH5l}+!A}pyO#I2>6rG+La%c$ielz6!p}5P+;w~v$qLeKWWu?Md z>T7J4`{3ZahXg}s)?3JzZ-#6hs;|!T9mW@T4>LzXySdDT+TZ`K0WkhuxCgGtavI$q>etkt3Sj@g3_uC~ixPu*iN%~i)+C-A z`Xj$?=)ZH2d`*UNy0N`KLVA1uI|k6CrW#|N$&D8&Cd+-R|9Ac2tNjC>9DvhXIbY@Q z@|suV`v<%~01J$_a^A|})B1sdkz5<~BI`*o1P1SP-yg6$2XpW4oIN?TWOwhP7IWS8 z1Fs%LO9sWRauyWW&p|KlIXSy?0zCv#rNfjQA?(?F1$2JRndAMfrTc;7(! zvXuMtzzYX~oAq`tL{soQL>xoV=;C?|i-ge-=*(R%R9zx$cawq(F)|E^yL@mU z$Oi}hjJ|G{z9gE(P?3{+N0042;oI#!_w|Ht_w_u|Q~CBt&l`IIxUtvfzNo#M`<~lZ z;pg^!mwmCS#uX{kJ)=BOrNnWp1k<91Y-rqTdfw6# zX1%@l!QPy6T0WiK?|Sa-Mb6&a>$6@6*Ux%g+8g7!wD;$IKz-ilTpAsZ^+YvDHA%Sl z^!cd|$?S)5M_t7dx4?vC{v#wA=e?}v9;GG$U-Uh@A61#N(b#F)vo4~KyT0oWU1_50 z`cgM^`%<@8x`BG7+wLAQMrE#IaP8~?9e4KN+mQr=kej-~tT%Pt*Nv>ET1WS*>$~3E z6-K?8x|$6k+#04rO`(X!KlgHUObWh23e3gny>6TUN4tI59rTyozwOR@Sc*($MwGqM zZG8_E6Ao6}{cHEb-ATgX?nk<3)LM;JN31;6rZyqqcJ4U1M>izYk?x;%Z{Nb*q3S+K z|EAl%?zVd138Bw*hs~eszP?Aox;Gw-=g$u)b=)7jUeyh5;IepsS11oFvqHKcfM{An)q(Yc>|Y>QG$8;JUgCMHudZO9w2An2npiX7i*kZ64_I zZWhe)Zq~bicfiltKV+joC}X1MN3%Z8B4d1<^+gsO_C?k=SSoU+-ZvB~Sce>AIA12^1G$jTt#&XxUf2lpJ^iuncoiP55 zozCy1taN^-^_?S2EyYy~uFaib%gwmww^IARv`1w9r9GqiS;PAFx3)*v-3pX)lzUWi zN+5^8oBiMd0Wod_mg{n&{t`6CTq_Ww%$ zq4r<5r#oM_KbIt*+u{8V(CYmTA9N&?w8L4A@}3UQ63eq4{@Fq8z?Giv-cFBFkB@eG zvJ-r9ga)KbiF;S4`#MFo1@{r-Y%u=4Q=EFSQLp8}_h2XIjL=T^LESM?N#RAkG4kDh+=%^o`B^nY!(Ai!1s@2`1~KqFD9eI1v}jrlRrwvtM&`l(VpU?fPk~xHBrDfwcF8-@u>JebmjiF)xlO2Lap9yg+Xl%-x3COOZcfU zsHehj5cLMBSqe-2k__sXpsr8folb$gJN;!M&u(`$nRaKp8`=?iL%Ul*wlEI& zNUJN7@F2+y__tn*6uz?J2Pgx2HUi z0!1HyBWd}^uHm>N#O?aWrPfoM^b*I9)C>PnFe%c+RJR3mnqy%glUvbT8Jq3%A`Lhq2ZsC4kYo@ z9rv@=8_880leZ?jb;x%2xztZm0eq7BRVo7rTMpXWQ(on+A4vaudWc?tuqX7|>HZzw za6mDSHh&;G$eUaLtu-?0Z!{W;ivw*yk%>W(U85-k{HK(QD0wbQy@rCJ#T-(fhI2=b zBc)Y$>r!WAoxj1UPSdV++!0?;S9FImgn?I6eoCR040TC2s86D=NxeChw=+829^e6D ze?!J4P91S%UC>yn*{p~AX41CSWD56hC?9U-&aG%ou`tWwaGi(r;=YtMp=DMJ zkh3=4SC{XwcLg9|SKvJtb@@H_s0+``+lljIiP5^uT*ct}z(rmD0QbP9*OYhI@8jK5 z_kH`ZMEZ)3_DbaJ-eG@>cemYd?W-Jq0(_P8po8|<20iH9=Q4EE-4o=C{lQ-n_~M$x zTOke~*W8o%5MQu7$b!Bl(1cZx!miK-jPhcT^G`z4eI)P~7eRK^{l&v+fAGy9PEWPP2n7u4DA)h?(#F3dg)|9wK> zC+>Gdedituf_go8Fb=1KaqrPs9uEC0gnR!A{Tu@IbLhNyQ0K+p6A$X1_y^*t!w2HO zAo2@3VY428SpuP#CG1WBx;tSnlY0{m6L~n{oueY+-*dXrXl&JWiDgFgc8e$;GS;M6f*W^-q>i7dC(QIV_x+V^!^2WqCD(~IE z&P*F4L3Ar+_iSX@?TNk;&l4%#B6av?9DP#JO;>DgHIHvZww9!+Yt2A$o^g$h+I)?D zldWp=P4*rDY9wGNc4zFNJGveNnk|rCBmgNy-Yt9%$Z@FJ2{Ez?#;J;rIHYQrgo9h$ZTVYsyR+V&L5>KP}J4wlpCi6^*cM($m zmFj+#`faMa2~(ft{+zKP)7_l8B@;W9{h73zyr+|UAGVbA^!n1_?)Kp~52wo)hqmJdLU-tCv%7o5UVtBr z`Ctq_5c__N`x8FW;t%Th(=Yl&>6twD5I!}c%SZCum+_g9`=7ke^DOuI{Fn3bY4G-m z?(O(UnLjS)kG%0PZ{p7r)rYW`qCmqxTy*ijKN8~vO$6*Nf!6#QAhlAdrz#$iJU#FiXQG&K#m{c=(vN(z ziLdxS3qSO#{*oL^8gB<}H{c~Kc5Tm|8QS5w!7oz**v?AZb5gy8pxv+)_uQZp&x}E5 zl?^)V#Fuz(sTW@2wOinYYr)XuB?XCbsXnKq$kj%#4d0pS*}(;&DOp>CPQM*M5}c%i z=&61RU1iZ#-i8QRLRTdW($yDql>_miS5xF*?Wp4vQqxOX;3bR!IaYdI;Kfm|O~J|q zXyD}rQ@su&z1)+%P6xm>joY}vCsMot!4j_@MiU&HLQR4zJvSr8>lt(!LVMA5%2uzZ zvjTtn%SmkT5>E0uI@Nhz+p(a@1i_hJ3c6|Q)RDq%rg&*iT^_E&d0xsq)`o7o5K20_ z!Hueq>0Tnpgc7f<6~N!jF7e_vvf7=93{CLb(-|mEf#5v+RtwIlUNVz5-wgyqDW29!mT2mW{vV*sW4(3!&!C;&Z*XF@GX)L?lBK!rkptBh*Qi9F}>ue0+OKWfN zatH`b^x70dy$Niz0VK)!Z0$jBB3)hN<&#ry_fmuDUUDrihNH*K9B+7ums#m02M2l` z0HovvH*@H`wHC3HhA2TdJ8TXc=bg2A2SX*m`Z!o}d~ZSx(6L@Gmeb_r@p&KXNs@Xqy)o76!WWGx z)MIdymjx0X^b&V<+z5?Q58&p-q}qb{xEjcfg40>oeha;>#|52^2UEzPd*HVo&db&g z-0cY+dyIh+CwQHld-%rC5=v(**w^O4ENH%E0>^~rdnsc}sOiZi==(aa$0+n&D(nS3 z9;aK0SE7Pu{!U0d3%12yn^`>t*UE*xH$y^23YjOk%v%g&^thOGb}nC=w?S$4F$az9 zT%rRBT`G*YrjY(!gB(YFg7z2C0SYQD)ZqWVmRQoAa{?v74K#e`t1~DhDO~tY(7~^~ z1$W>^rwu_T?L8=w;3QHpYNTS6NX4izkFq~;FI`e@Cr-(uORhmpss#NXR3M6yP#)rb z6W5r~I7A)(D5aOR67)jO&$!?d2-$XC%6u>FSg%hxZZwkhyqr8QeFB^4rZc8sU^&h! z0m|;F-arIsT0Jfocq74>GS(Y6-|L-6EmKld9ltg*^3YziMNN1sfMN;mxL{}jmrm#u z290W)CNY_?5jmLp8SHg2E?^v_3z~qDs#y{g20FzXMC|*#B08Pv74Gx~zVEeiN<$4^ zFRnf#r(l#^o1Lt!sF7tkUXs%ZWi9AbGRA`Vh-fZH6?`*QaMJUqwL8!*)yp^*DlOEblEE}VNNZ;qNp{Y)A4h$@!Aqf1 zsyyS|w5~GNImyA-y$sl)-6}6*9(*`8)oW*?%sE}qnKOB-*Um{VL4C{StS^HjsYs>- z_i=|Q!OJ#MwM<9UWuq1#>R{C1#T?3QyzUDCEksne2J9qEL3EPUoS1^5-VaFs zfnGmuoQjUYK;wg>hpB~#v+H!q&}`=|r@>1uZiwjq+Q(S}u!B~B9Nc0LOzvp#27uk=g(hY9m`4_7lHW|)Z_7f`m z?mLaL&vhPmdAf2AoJO-%Pv=pWN@ODf83w5E`sI14W4(SAz!m_b?(Sm`-+h|j!&po_ ziRNm7MgFyeDWMHha&j<(wGOo<3pnvq+*eU?kEi0kDsKl}()w~0R&{bkork7UZP)tB z+VYRu0!ljZkm9T=LFqwW(5kZ&<@iF}O7n8(c^zuK+_PxqhqWhEPV;)+g3Cs%LZRk3 zuit1MG8tZ#HeAS>aaxOO#?;5)h>N96Z6qTgU{%fLX$8xwbgZ<3_KD7^mCL<0A3)%Q z<^_kAj8iEyuj-)AAOAF>XDomJf7X~&8gZAFn_Xhwx_)L{i%68 zM<)lJb36n%JI=|VB{TxMP4Ep~?_nqL9=Xa%+Y6(WfGNHj7NAKMi?jqM0h7fr5H(C} zh`{8&mC#iMEqf}>3(;D8qZiLzEz_Z}Pk?JZ3If|1bP%INg)BQ*;-xzEC~~yL$VGMr zg81`iTnYl{?l!MA0uWzap?MDzq1XC2TrLx{hOXsFAVKYPg4goyCJCHWr&HH+(O1(X z!iJ(Y2|NSr;1};vvL-xEMFmSq41&-pkh+Q4k|82J2mu7pY?T}gErmo}rJ%y3uO>#CM)|^_5AusqihT3-r>H;Uf6JH5>6%}|D@0()yQzbZ$HL7LYM7C=0TVY@+z^nZHY*1^o1#Jpc~1sF`VZHL;&Y@kqtN3w0N zZ?M4YviYFbH`LGTa=+JiUY@q|nuE&DHk%#F1TPVL@(vQ2Lc2w6&%U?fQZG9~E5)9h zXvConydiRep;Rvu!W=ZC0CwuUbUIDrD0YIQ6>59AYeVADL# zi#txOv7o}o&>yRe`_{e3_RI`!_J#!U)2rxzR-QM+8AMy&S?|$RNhOTn)a|7Jlwg5T zuor{Sjuye8C^wL-uT!Rjux7`%&otnIvM zsCXF^^IKIZ3Eqb&ra_HEuM7Pd)N z0ZzvatcC5Q9t=9kA=>95I%s2{9$iqr)nm~Wojkf@@JOCS=Tt%)VbU}&@fch#rR8dx z*S)_NnhcWab*k6B+)JPCb*J1~2-ajTVG}M@dJQ4pRjOcm9H=v@QpM6Ve=L0z+MAP; zL*Z1oLrqP6*D3a~UyBo@R}Lp-tXcaA9XNwgM{d<7lbowHwB^>LA}u z4!-K8q%mkaZQ-N@s(fiU!80hccpO7lbyK#|C8a-}*1%hlZ5)+rTInRfZ)qcOITe>_ zl-^CsoGmN>c$XWjLtzf!l+T@fbRDaQq@Da*p1ywkf1kq@E7Nwnz49ehb?hGnyYL)>aQ&ELQeb+BtD*b zxG3t8#o%5Zd4r7Ko*p`g>69`UL5imY#K#*$r-<0i~sSmzn=!suotMpRP4=Vj8uRmX4f}wMz*PY4Zyv}r{D0O&7 zJvkNnCoc@%1{0;(UI%)7;jF+MHU_KX)I6Fx@p!K-wxe%g7Iah9Ubz&@nN%!g5UpA2 zvDHbm8F8JSQ@nPgXzA*l&`|OULBzq{#?q8rL&-q-EKfSsGIya`<@PKgBkm5_%8Iu0 z_NqJ>E9h(hg!HTAReZyHY?-_~EKQS~jjK0!?P!%nU5XA&Eu@ulnsa?<2POsQmsKT; zNYXEHA*5Q8^W7?_;U5jfiafy0vMGp2)izLd;cuwe(^?bWeP7Q zX+g_bwZRKD{q7!PBS#UE@CjP{J^lKx`OCg{XD1d%O0m`!~Ln&?F~uYV$oY$SWoM#i+=>y1Y(Pd+ZV zC5TE#6}*LIHum*Xrl{4mQ%bY?^c1W$+hD&NzX4nMRaE#c<0ptoCD%l!|BNaL)QFJFcS@fX+TY=6TUookGCH?}n zFSvJ|-4LqG3-oSVIfZ1|&f}u&ftCDF5`p$|d}sstRBAUZ$r3_s=g$Ol9T#%iveiWv zWhtm{3Bi;_BhWX`t3@u}47CR25JLs0o#lzmH*@s#NJrTMjxr<) zo`jFVAQBqbou4*R9m#dh!E+kmNl-O}*@pwINdoYFq4&x8Hng;z4;rHZcm?c&&v3G* zu*{Dd^CBU4CGH^P$Wcxk6k3EF%39FbiSSK!-o{P@#MNd1l9iyAQE-8@an4Rqas53j zj<73Pz@3(sM8Z;6VSFHkaqtHE_scR~8i_{chENuTW=USj4vx*e(&0;uB{`LZ;4_AV zS41$y9RTcq(}uz(RO(8-c&eGGe(?{{Imb(z1tl;yW2T@Fa-9F*&tuR81-tVZTca$x zP}3I0+KCXFQkgR)2MaFSt|(hwTD+Qm53Q)6xODo&lH$_*nMGDbMQv$)MV(c@eDTVP z=ttf4<@I%xMtN+wk(O6-XmYz~oT~0`S!{UabW&VuOfao!&s;@S%`f+pw&z zVsUvXsQ{j$g3_Wnb4rUB%ql9KTR3y_j7c*Im^sIqzIgTA#w8H4y0omOx}joq1737s z(Ij|jT}@+cgVnHlNn_>mit42e%Xqj2GmGYy7MIZIOG~TK2B-4H4U75aF-66tGmA>+ z6wQt$=UNr@Rggp4)m5yt8Y))QR#g)KIg9J-tCm(cz`> zIM!r!yJ1ajg@wOsj5cMbXcJXf)0eEC*+>f2tg411YH_ooe0oiJh4P6tv8rL39!b`F6 zejEnMMb%YhH5Fwml(#85top_!W%&DGwT;V{SCm`03$NF#L|={77ReeMwe=jbb?KZr z7D*q%(o0LpO(Q7)5OXUQS6dYpv7*r`;t{cWWi^e^ zqG)E(G6a`ZUjt*3WQ5W3n(C!lxFqA2*40?EmT@p!`Acf*8VbmyEJj`VA`G%hC(k%8 zf7;~2(m5rih55z#DugVFp=Ah7Ycd>H*RZ6D%(I%Ja#dXwoR5GaTh?1JF>Ayz$f*>` zwBwLz6fYI!S!K%>*Rj52i6u2P$ewE0k|SBEH>YB$FO*g^BF0PW*|f2lK+-vOp|n4C ziJWV#SiG9u3sX>#HrC5f8ms+^l$O>brjf9Vs~ZVkQrEJ8- zR92cP?supbo3Qht*Ee; z@OV|2?y3h{! zvtn_rHLI?syir;}Qt7H1>ZhWvs(NX(Al1uIwK3>Z)oRs%liNKsHq@T~UEtojbj(X2l9Fj$|TEHF68rY_22h5T3DsA-RIIG3hn6#HxZCK= zLDh_cD5@@vuDq_M7Nh45>gq~)7_%hRR45J~icoA=UCN29N>n5Vsa8Q|bUv9(b1tCO zrJQu^6qNSHipkZL%JaG`H=y$Q3vhAWVq`Ykm&TaOHFxgx%H@lf)>{Rz@M28d$W4+@ zK|v8am-3d&l~uQzN;`F|Ymr(F6fJ42FKeu;BTr$Rs1;NrD6}wr&Lv^m=A1%BC9|w) zHA`i}QbQxxH=qNWIf39*Orq*4Rv=!j+VYC!i`PK2vSn6XMLCO)$W(sBO*O;AkQf0? zpWr^Fw31d8Iu7VgU750PWFCmojnbP-2u-=iasi+oYcV%iGa*&gcGi=$uonAcooqV9 zDz9l=vK+OMXYg9KrAqaB%-d`u9q}rC5r7zJY29)@Ew>tJswmI3Dwo%wgZc&(SG3ow zrdfrP%NoEN6CU}~NEtYRrZ0-W!kL(DR#ugva_U8snj^3dV)28mNmX^UkSm;tKxCgT zt!RKAkr6RQ@sD(Zx8~B=ts;(H8s{L69bG-CQ?54+JViqer3qPPxMRPz!eLc;6^^Gf(A7DoE1{Qdeu@ab6x0p*ry;Uv6 zqTZ^kTMS;rUL;X4OjW$7dZ@Jlf{9CSVF4}Ns%mJVT3U5K>f ziA_xmdIF+!OtB0a;XCPRlGE znrg`vt&*`uL;%V%Sm)l%qIyh^$kVyJY%MLNdWYIEqo!dR7MT{hqjg6i2RsF`b~|k| zpjK3Qn!UI{Aof|gnYB6()jWC3jColFxg)Z2b908~49Xg?qM@<6V(iiitmLc8KrXH; zTQ+v}$l(Kq56v2|G;6@j!C3<;XH71^|BLI_RF^HQtEonK4rr`ig&BK5!(x<|dfbGs z%T5_UiGt-qP2GU{1~8(%+$h6hd6~YA$qF~hDt$D9J384TO~pWZ_Qn_&FQ@HGRW&Wy z=1(m^^yrzI-AT)_s`6--V}(prb260^waKCj6gevHh=lrORdDfScna%aM6jwvl*nok zgzZ)ZEMHBDS5uFTk<2OV8nqne8O2(R?5nlvm!S;sO1@4{oKzDmwfLhNQ6F7ZPDH4v z4FG&xTUS+sW%(M+<5VVT*@~gmmRI@rgoB}R*&1wIt%jAQs4Zo*4dr+(Ev0FX7Aa~0 zrK&m?C8}JMP=3?WI%?A7X+@P`xL|SpVl0FU z3wTRPc|j9Cx`^f^Ky<8@t12{kJu!E>tTAv?Ei8Bh!j+Io6ECOlu0X2BSr)8f-A;DXC^wv;?{VDeCvNu47^()x?ytfBK-HKXOsc;Ki ztU`>@(3!*|9sr;QEG@-Uz-pj%J<4ljV<-!E6`7Gm5+>D#nx&NAg$1%3!15ZS)3g2D zqGL*Bg znM~EXp2AQ(!0Lv&Dm-YI92qWGuIMyUO#2Bb>?*g>CoKZ%R@bYC9Xw|vY%MNlJ{J{J zE>J3{+HGOUz=165R;>T()^Hxws!bq04S;*Cfy-)ER17Rzv9eD6H@Bi;R(Z`pRORUd zslJ#0E(exu1EB;KaXDqb%lL;xSpysE>jqX;mo0BBuNa83TRME`faO)yjjIPNt!^C1 zxyLNatCkF0T2|JaB{x?${{ylHk{jxOS8b|wzssPNrD^?l8Efj7ts2NHT~iL9-vtg0x_S!S_rIM+9nmseEsMIAPhTySZnK`gwl9#D@b5*3(xXfRPet|}m9 zjTyaLj))zAf7Q&(6om|tG+LaX7O~?Bo0amql?qz1xQcj0=VewN&;F1!&a#dvC>Wj9 zd)^W}*lCoxbUKw~noosh#6&KoyZ$BIh4Bw~VV-Nj5DV%&!U{W~aLZ>L4b%9$H?tQ*j7#?(1urNGw zW85*}+Q7<$@W}k|p!{&}iQ%k?;f#slexgNxrFt1l!e8noMOYR%E_A?ofjg08 z6_KoCNLFEZE8ao2Ar0~kX@&IHKuO*Kk;lI$^ZW9SA$jec7UfMLc{J7#f8zsxXqL__ zuZZ-U6d5ZzIh$qCS{}an^*dw5NuzwP)cx5%l5~^_-L#m;#1EFwPdnWkOTKd^8N4D8N(!pv!Xn z8^qsuk>4YX>JzmS>oZyTalIWV2ybC|Q~3;F2QJ9NB^p@NqKYsj_jj%I$3IcY{T&+{ zH&MS#4nzDzh$TZz!g(SA6Cl2MRKfzcgJY_2=7STz%8cz^Ny|&7;(%i;$&HRbnx|2FlbP|gmKNQ|c}lr(lE^D3p)Bth8t>#5<99-ROTMC( zzj;u4xYp;tw#3l<&!Q?WC;B&bVwyR>@dsy}n7=$6ZQ@vDwdRLowQ3FmaV-3C;}#Cb zYTXP?0Mb@{1C8(P5f1uE4XWqnfnOJG5g+jzIQfR;-ywqZkHpa5@oY(c*>B+0zk#3n z8~B#rz%Ts`{Mz5ZN$-~Q+z*__bx8h;p7^2ZeK91B-D!%bZK9;!$iwOK)f=u=>HOCR9L^K{v7Nq6e=A|ljtf4VB>PXOpD_IY$Ogr8+c zxae7yj|HUf{_j3~*R3KCNuq#dO;zUR^%aHGPrs^TqYtMlpz+;4oI*|GS@0hnR3<6Z zG~UmLQ=Dsjybq@k)Odvtrx4WmTRuF+hkxh8X$`3PdrAD8`V{zZKmW}>oI+g7nJx1Y z$)Q@N@$-Fnx(|QAhx^C-i4XUWD~|-@pp$=GB|hA*XPpoC+xc!E?$`fWAMV$ae<4Ns z_49X=dPB|p<6Y;&{c`sF2L7-Q_s2hx_B?QXlTOXFUH-Vsm@; z^x=Mc7W#0%JwNf`e)%&)Fboc|kKYbUe7N6_r}=PyzgPKif4~3o;eNjy_2GUwmGP_& z>*M#=dp_K6&+mM=-=0&cF%BA+f4rN0xPQD?`f&ev@Alz-Jzw_W{_%E}d5rq?k9V67 zr=~jZZuH^)xC+Wrh2;3G9rAM#n+p^2KOZmh;r@8o>BIf;e9(vco|E>MRNT5oZ`d%{nq<%zunI9;eNZ_ z<-`4U`>zl8+bz@yir^r9{QdUu;eI{KeYoFlYkat0&X&$Bp5^=P_Nfo|+vjB2H<!e)~M)!~OP&&x-2lw@+6e?(cV;5BJ;WOdsyI&m}(GFXxGDiw`rd zhRZ&QaKC-7@Zls<$Du4Acuhxb^g(Uupjt~j#|c1Vr-N`>GKg4S^Z0SPrh|V14`*7A z%Tk(Pt1thrHbP@cYCe+YS6t3KkruBI*LiaK7 zF+vYFaQ)nSvVmVC`pq+N{hVTnfj=SpiW&p|ga(4+bOXOu=yMEwfyCS8241AZSi1~7 zUF?5{f%g=<-EZI};@_7HytUAW4cwD>`^3PLB%bvjb<+NNN%E_m#GS_fBXT+#cxTbG zw}Eev@eVccDg z7a8~f(SMtPZllpVe!28K_9R2x>j+1eMKW6a1AoSA){);FlZtPZH06G4Ov2{h)!* z6+7tXjoNNs$#~x|_?HU*#|FNa8shlQz_SHUlDO0IuNV9DGVs?#|Dgu1>rTFb>-EhH z1OHCq=Xe8OFL}4jzaQ(*|pEB?-WL$?0e45BVYT&PkoMw1HV@CXs>}kDst{I@Xcb+#|&IQAAiZf-_`3A1X)o9=gWn5<( z_;X4fORulBo;^gL9R~kyvGZO7m&Z!hK?DE0==r39pDgq8>jvII^!(VszZO6KVBq@w zfCSM?>;J3J?G3z#$nR_5xia3-27b2qYl?wiC2|%T_%z`^*}&^0uTM4b-ICYm8u-6O z{?!IPRN~=Y179Nce9FM}{_0-_-dXxRV&KW*uOAJ3i0Dth$4&>`CX?MP+{l798RR`+|cg{R+-zj$3Y~b>7fOWBf zKP~#~H1M`E-n|BXmgsqpfiDs}JZ9jFMV}W9{9?fm8+d22+ff5Am+}5!;HOEyaZ-1+ zJwKOmr5gAIX`gN26D8h88TkDoXOe-(iJo%}yqCn;Nd|sI<`Ml|MC+L;{yo*;*Uvl7 zHt==Q@1+L*h}4to41Bfl-)7)PrQdrE{A|(xaRbj6zrSqYw}^e-G4Ov&UjNs?6NLXK z1OL1DH(ugS+vh6LC*8nLl5ur2@ZqB8AOjyS>#?y0K0)-DV&K}}^9_8O@Ru9-9Kru& z;8#i=UT5IjM9&Kh{3{vnHUkd}|1Ac-NbI)Xz+V#k|J}fsikue>{8aJF+Xnu<$p5c_ zUn%zg*}yZz4vErE`*E-In`z)nP#fv!W#A*kZu)tZmUDvSZ-K#|Dg3hy{A8g^4ZNp} zcZGpZ6#O&;uM)qUW8jry=PL}nK=irMz+V&mP6OBb6a6{69`9_))8`ES$Hfl+Ht=my z2aXu{3sQfyKec>U_~OJr8lNQdb((>fihsKq_$8ueu7U3r`;0g6=cN8mH}G7s^YI4$ zrSMl7_?t4`RR->goXrM4SmO2)1HWDTew~3&Q{iddZs426ZrL(EZMSY>hXDqDtniO9 z@Xa!=$p)_HQ@xMT^7VXLVespDu-?G+Jb0#o>$-iBf$O?-je+ZY++*N6AMY{n`^EnM zFz~GsS1%j5o|pCWPCedlgzs~MKTYiUtAXDud66Xk)N(2X?_l8CzkLk+J6R8mFz^); z4@CxEDC3=L;G>0qgMq&*_=N^OK;mbcf$RP4qXw>@%jxIqdc69%++l-%q2$Y_20lyb z(oY6{z1TTX=1(pEQK={G4EzJhlkNttpDPYF@MFa<6AXO6=sCl{drQBI47^10`eXxN zA#&Cl_#*Mwxd#5N#OD2L}Eh$(OGT z{A97eLobWq(0&Pt{;dtXPU>1m10N&y>1*Iq#LklpJXzwS*ueF;mKeC+Pu3dv28sXM z4LnWc-)rDc%DA2|aQ*#(R}Eahhw;9F|4qi_(#tnKyXof|$p&5|c{IYnBl|c5&z1a| zW8hbboKp>aki^MV2L3n6_ZtmduSY*I@CLmOl=uB~Jm@<4pn>bU|BQj_`v1Ct|0H^T zXy9jwU%oN$$CY_4R~{5-eOifqrWyDWk+ayqwVu@meo*Xrnt^LQ&oS_OB;Kwta6Mn% zXy97U*A0B4Q@_B_VGua)|_+`vB;zicq@y{e9a>wd41b!jgjPN^tm*y?BC5R-q3kIV)s6Fw=*VoLRy@*TAO;UgE<^&SV)tnGYv9 zHNwBtz^epb?!!q=qu6eE;e*L{XTi#DL3fHCbT{xH6m0eJ;ePuJ_2DEZRq}eIfhP+-)`ycE9dF0_aFWwk_@^0o zPr+yVaFV0rZIKTrIWvU+Bm>VAyuyc*oS&!>j#?j1kp3K@!N9K~5pBa_2Ja7en0OH179Ta@Aly&C$etw;Uwn* z;eXV?&k_6u1HVb|R}6f+;Q#XB)bB*G+Ytl5O2+$*4<~)z5IH{>xc*!zLEfv^dC^+x zZP>sQ1#c%fbw{E*X5&8`**<-W;88n}K> zYo>u85;+S5*ZQv){mXp(q<=Os;LzV+(Dvyj^WZ8UKaDF}@bv~hK=4fl-cRrg419#( zO$I(p@T(2HNbns7o-g=r1J~c%++*PSy{kKXxZe&B3vSx&X@fsk?Dj7oPWD+M`n+Y} zCkg(cfmaLuiGeQ{{A&Z(?`eE*;H!lHSHZRZ&t#K#5xx66L3ojYZxs9l13ypj zMg!j|_yq?355ccC@T4rI2Yw=$53Ns6!T)XG69rF@@1bb^&4L#g_!WX5Z{VZ5T6|Os zuIJN6S?8=V_zy`P*=q3Xb=o!`PJX{k?7!2%FA)3&A5Qg6&%d|(aFX*k;lI3hZAeP z@c-AqmkR!?f&W!-N8(7=r5glqW8l9Co@U_R2;Sa@lb(8B>*2$RRlj%9$H22CPI7!W z$|Sj@DmJN$LC@L|C8|R?_ue@_(1Zf!N*UmdL6jQhm$^+ zh@343ezxFE2L8I>ml^nzg6}Z!ZnBTN-oQHuzE^Oqzs~2s`uIuoC-_IlV?LbpZxA_8 z8hD-HFB-UhPw!O&KVA6WH}G==|H#1g_nW>j@XLh%8w0;Y@E?7+e_XmQns#WHMWa^F z6^Z{_rYx&xmb%b+q2JHzYv9*Nze5askKiK={1(9{82ACf3l01(!KVnW^*>+od8vUf zl=}BLn|Z@bqkp53Oe_$(I}h?<4pL2EI)2%?5sk z;MW-V7h;D!g6nywi>z<|YVhm7-=gPj&9CR3*L-p){`Kd-Zy5NK;+MC5xWDdw;=@Uf zesA(K1OGzgeC@+Yj-Ge)7ZPh~sHnQt}Ti5lUE_u&NTc_-C}lRiU*Kf}NW3*O0x z6Qt*z-aee<94q|&4LncqTpv!5uIKp%uIu?UA5QZ1`_lTkjvntyk$;lGuixV=H}KuU zUuWP0@{^G-FNbAWU zIPUl1WVfG0&O-+Nh2W3*aFTP0;LrPTlG9J*zii<8dkXIxc!}_TWZ=buf9k`j-%q69 z?|nG+yHfanHgNquWw0BVaM<`4z`qye`A)Ln)ST-1|3}-Gz(-YN{odQ%Nt$fkgndsS zK*DAsi0q^xk&YxNtH>hA5+~!vXh3F$C6a`2CpHkpeZh6cUEByDVnA73P()N1R1h2` zqKF`fpyvJ0sj5zOCCtqCzW44Ay6*YcsZ*y;ojSGL+nr_jPgk;rll;e}{8R&fRN_4i z{CSCI8n}91Jix#e{}2OL{38r}y_7%Fz^f!a&cL@ye4>HBDe6*i>kVAJ|8TR!mHscPeWii_RX#6SZ{T-G{6hnOOyV^LzDeSCXJHUk z??H(t8hB&*+@imM=SzIFffq`Ak%2!a@n;PDJ&C_*;O|R2d?<19H;T8{$awpi!M{sB zXFX}~t98-u8Xg5XlVpFnx(GWce;y<87!9X-)qXHZ!%0rDnSroaFpnuHQy!ILY}!@{cia^}Vne z2JVs1DGChycPZzu8cy|oMvjN8Qp2gp&z1Om1K%w1#|*rKj00;WuEyPyq%5w@2EY3K$-M@@8h3{^oJ91M z&x^k?@QxDyPQ%G=YTW&-;Uwoa$$!qkmrC490~0Q#zZx%b8cuT5d(}Pze?rP>s^MPn ztMSr7!%5CNlE0IIZ;*Iz18*$%cNZJD`kjY74fjI68ZS3#IMu7Z4>HfdlcoF$4JSFu zpYPIek~3TK-)G=gOMJD4lN{yGPir{IDVF@t8+eh#UzWJ?+jjDOsUrq{fqWk*B^^R> zDgN~mA8Fv5W&FQV;;P@(`>C@GeoMZW^;d&m`R!5-r*^$2_5YiJtM|EAXt)>r%5U$} zaFX+-l=F~*@0a*v8cuT5eDS=7lN|ohDI_l$_*p5ZO2bKx`aahd4JSDrWPICZ;H@P7 zu7;DG%VZq;M8iqWFvPB3+Y!)4csU3RvJ!n)c8u(aFR1v@~0X2#S-tS;Uq_m zi+&nTawbXsK?XiS;sFgOIci+wX*kLGljOhLz^|3~cnv2xYFx}PaOLOMYdFcTlyd%L z;Eu%SX*kKRh{gx5N)0DDuS)(q4g5uk->2auNBRE~8cuR{NdBh`{4I$;ui+#|{m$>( z8cuT5_sDh{_&zD;kcN|-X)>OiFmN?qe$#N0e^$z|dw{@2{z>?E5|5U+8ZYYmQ$r10 zjk~`X_yE;j1Ak89hYehPpW#P|tMNj*(eoaD@u{Cy34s>BCsILY~!#4pisl2a`4OAUO1#4pouk~5|eK5$La zaFTPEc2aCPo| zP{T=|0xAD116SYQ_{qR4CI1-%4@vwt4JUoJ(*VKc?Ilt*exH*3@dmDb*R`3%)wq5| zu9rG!{1hS=3bED&8cyv}-|OgY;BUx!2O0SH63;U5ZzP^;;Em*ZIN!iyBtAysN`LkJ z**_cj_yo~_M-BXs65n9pH%WZAfmcZUTLWJ&@t+O+kiP%dx4F2jkfyZZvRPn30+CsxgL>t*JZ4A7L z#FI6g{8p`7x@$Pe2}u4641A!(`)D}HiI2wzuAv%Ea;}p6!wr0r#Df}6a#UO$tKlSP zp5(v6z~@MOwt?R*@j?S%A@LhDoa$Ba@Xs1f^{$uv3k|$V;voayDe+1J-z@RFG@R-^ z?#2hMM>L%3{Z;ZmVc=&Z{k zv!$HR4SbNqzc=t}B>tm;&y@ICiK}t?lJqN2CMv^4{!j7ndLnQ&)o{}1HYum2fiIJI zih=(_;++lrF^Tsw@C_2b(7@M9{9=hK{i~$@R~dM$#FrU(2Z=vy;9VuY&cFvse3yYQ zl=$Zc{*J`G7qa=n=NKvuq)1%FnLO#AJq&*J_hp6~{3X*kL0A^DdZ zc$&l?H1J^(f5gCtNc`^_PW7sI_@ahWy?>PauNwF`iSN*GlB2$FyVt-~+&iq{B!91z z^NoS4=NjK>ILTM*z@Igo9f<1 z4_w!3IMo}H{5KkSxx|Y!oaCr^VX=mjoHr$ZiGe>S@nsrLa@=ygbccqMoDBKA>}~^Z zD)GM?`12Bf+Q9!V@s}m8;-~suyw^2;ivJ_ay~Th&n5m(1OHLtUl{lg z692})f0Ou813xG6QxaGDtKVO%)%Z#OF{C^$UmuYw|BsRHT{JOp^}VdN2HsfmCmXm= z;^_vSBJrLE-bUgVN?hqbmO3BTl?Gl)3geoo$)R>lWfbqvYB=?a`X1LU2ClyMRc7F0 zq&|*;kCOOui7P!Fsn1geoUYF$HSnk8erlD(NewoE zTo6yVRp(kCX*kKLlyW{baP|Gb&orFm zsPBV+t>Gl+c`4^x1AkiLKWI3~`Hl+V`bEPD`atskX5eo~JiZ?n3(wWn8cy;@NjYN-e3-Nl&$|`_{nKy6z_pC;e^t{_`0F|3S)+=?^Ac6gP-}wB(O7@DURCYdGnn)^)8l zoaC&M{Ot{Vxx_nZILT4#v7Q=Ea<)kROap&K;sYeE<~8;E-q{*IO@YnvpRTbQ&i0Yt z2e`t(&q?{y4P1R+WtM?=l5(y$@JxyS$-pm=_+KTi^e>V7)3Wp6j~H?WOF2&%_&|w2 zZ{Q;&{*r;`Nc=SeA1m=U41Bc2-;%h}f2Gv_Uk1KIK1cpRlSBQI!o7m$s~S%IqP}nA z9v~Q$f2!~2#2dK!zK!3&Z<2bpGVo%Fw=?iX67MK+)vn!A|KSGy7g82iz9xs-)q_#2 zA2pm<|0d;MZQ$zrKYujvyCnY&27bH5Z!z!}B>q9)7if|0?@suYs$->v=%ps$WFw zE$g(wuk^Hr2o4qBVv_~!F>sf}8)-Pro2p$+G@MMiw&rC>F+Vmi+k! zzChyYJWsW^OyXA<{P#$Fih(~Y@v9B|DTx;t_-his*1$iM_#6X2DDk-l{)5E-V&K0? ze6fMY%XnUD;7ug%7kM4^&qf1R z&$HrXyOkXE{C%QXy6Ppj9>f4;ux>KRCjZW8Gq)ZI<9ue-We(d_v}RFOy`Hr!H6VP>IWuH?MS5h)7j@R48>5bw!o} zWP1EfadRY2q|J~}Ju-}2Ed^{*{3KkD!IbrrQ7>V1N%s^4K6~Il$^5@9Pw~~12`F6u zL^*#DMwiJ;kE0uE|K1On-SW!7MeV2ch1vdLz)YD`GH(Wb;`isMkan58mm|-_i2bT- zM4_jXMXEkcUUDN7BVO+^Q6fVm;ZKuyI&vCn|G}YxQ)j_rIx$HC zGEw!b^kLu)mG87p6v&qK%i`g-s`6{jQ~sI_qJ&y+oUiZZRzpXgT^Sm^wa)7f98CM2<$V=Rd&%S;7Fxs#Gg?+`{^w`BYd6&5CI*B$mqjgkW z#Tn|4@c_zVI17L0x|Nmhyp!j9 zgj1B7=3D#fg!k}m@o}jahepR_Eh|dx>#KTo5VALa>&|oXQu`Hvt&gvi){~cpn(m}( z@|`!G_cQnBrN5JZ?JomzGQZEs{3dOt4awXZms=V#Z3IhE< zyrmfgZ&!i?p{9)?dBJlPcTPl|feLroZeQ7E)B!Da65?Cy3M^at5pF}-8MT3Bu0b*L zynzz89VqVR4fv|uCDnF8pgS{@>K}?iNxrIT8|DctyK4`Sk}Y;%SyJ&?t!zP{WD_x4 zzOwE343@k;ovG3uu)OTDVi?N~1!A1{H=l72ggD=dHNV%^1`D%O`%w@3)}sHimc0@~ zeHtuzD>!*-U`k+W;Htp1Hv>3fg!QZKfa4Bqb5Yy$Hu0`=Z>?<*ut zc&xk%Fy&QHrZiKq0t~*gJ+MZgf{hcs^13)*LVRm*4`6_l)_(PkAuav$H?Wjx&SQu`KN=3Bct7>Ykd4GO0Jh}-4d zf5A|xml{v)&aTLdNysUCyQpKyHap9=b{jJ7x=h$3r>uJJVIH?Miu%c+j*$+RxdHVG zZ#m-jmC|4OhOWD5%x3P#s11fZA5-OO*kTm3c|1$R0B)(;UpIa||NZ#&{=XQ%W_|yI z@jC~Nm*ck@jnsYp|M&RahWg-`^Qqf?rQK;@iq81Y;}?#x?6&_$!#wuR<852oIyoY z%S-l2Q)0iYTzCk9<3$=A&D7$DV=Wagi353Q>r}3U^@27C+RM1?r9*owl zhmK>XAJ|qxQ;roVv227vcLE}41!Bi127=eM!W z$)J5Uct3(V4l?1tq`Rw@eB+ zzn0Y&4bLz9p2X$Z-xoN%j)6i?Q2AhmC^jqDUC8m3Z3AQQ*0-oGt>;#jz3nSKj*1ZQ zj;=vJR)p3;d7#Vx3AEy+ERq%~^JB5YeQ>lTTqzq^6gQ;*2fpPmg0W41)&Uoj35-fb1ONX)(V76t#dh4k5xrJMW+#>}J;{^`m zHCxdmvYuS$>&)u)OQ40XYJZ;dbFgqfmC18XWIMl~3_b%T?W5ai*8o*UVAB18-6s4az)v!JZHsCh|s3|1ipPU%Oa^S06^q^c(f7YUY}_0HYRrW3O3BsrUD z-27)b?m9U&v1kD+qUIwtp4p)=52sbEpbn&2(5HD(HkJn!@N3L)S-e}J9-e^)2R7ov z3cOCAd7+|I|IzNEe+%Cvdtj|P{yppniaG>J-m-IiRo`U~e2yyctw^Hm1*tv^pwH*- zT3=k+-r7Jrq-^JyY9j*w@%Gp`6#47r2=VaKsELptgb}@>VC}A;$iq}jiy)ekn^GFI zkDM-U4Q9{WVR?dORA@nz6*yf^C{{exs2-~*f97u4o9My<=MM6W0;hyLqoyw}W#GSg zp}f?lr;A6SWO7l5Jm>Aq-TBUOp+cS$pDKezd@9xyjq{uz^PG3IhEXk8cQr-ks+D=m;z_lARee$i zzAEf|p|W${%&+I}BGW#3uC`W|3x=jdVOjN>4BA!C&@yXoDv&bVVEud@N=x2?l8sRz zfzmId=N5iOJsfz$?*(z~TLWONywEFK?*vOhFXdm5M1WP4RZ(#-Zk!znv_S-XH^Fmb zoE@EOYk#kGN+_32cov3|t5`@z2opmmP_o?{EP2zLTe8g?xV1J5d2=7hE!-o9>bJhC zCDcO&C1+yh#f$OeETQ|;NF;|TndJG-vuY65j}0(G8>Y)oFLl+C!oixzOnniu~QKXt|i+LtrG z>MvjE3>2)nStu>`hwPW|H|2@%a0&9Y(6E@zN8Fg-%E|KNfuUf`<{Cm)tft1t{3c^V zL0$K)Jn3tHcKn9oKicSr-!e~cFDIv>D;RpCA4;+KVCeN7fSn_mr-LQmoY`Pu;03pr zRV&u*Wi11uRQj3FvSi$6R-aCyAAT1sWnuyre1;2FY~lC zG5XmWyTOelh$TtMcj&?JM2PJXf^z7()M(85sZrbTUud6sdL5lmQUkYRVJz+(dX4 z)~?}uuhaxzcnI|n!LD$Du7mrk2*-d@se7Gei>&A7Xq zKBmKu8F|JPmb%Fm*4iD)6ZpH`l4&K&N53&i0_=5wSAY*(*3QPp|i)_y#&M zQVr5GGdL4x`3+?zb0*5GOd%7kfLQX54fIF9ky)p$E}Qh;;1?Wh(ZG80<$UK%=4rGd zFSVEG`V7G$gbT(Ebacv$T0=MV5sWB1F144GNc(e5A@bG;g#xdUJJu6L4hcSpnsPo- zbowquxf-ra!{Vfs<)Xd^X-C?-)!*uR&5qJ8!)=5kP^am zf@Wb1=2du7g=K|ony9cOJ}x?hPM{%=IdSTh+6x8-t#^e>7zb(%QE}@uTsEsHW?M-f zrg7-=`v$Mjy=1G+q5>6VWF6G=TcH1T-|}M!wV0QeKZe1|PjlH`w4QU0ZJ^N)-Eb(! zWr5IPDv*Fv9h|wtel%{#zGqB+0oEk_%QgC>Ne~KDo4hZb
@#Bt`JVn(TT0@sQ_Mu0e}3YA@jzh@%G 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
{3lORijLp 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�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%0DBnA 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%0A_)^2C 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%0Dits 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 diff --git a/protos/cmvr/api/agv_command.proto b/protos/cmvr/api/agv_command.proto deleted file mode 100644 index 8cfe89a1..00000000 --- a/protos/cmvr/api/agv_command.proto +++ /dev/null @@ -1,906 +0,0 @@ -/** - * @file agv_command.proto - * @brief AGV 控制相关的 Protobuf 消息定义。 - * 涵盖仙工智能(Seer)SRC 系列控制器的主要 API 命令, - * 包括状态查询、电池、位置、地图管理、控制权、运动控制、导航任务等。 - * @note 所有命令均遵循仙工协议帧格式,通过 gRPC 服务(AgvService)对外暴露。 - */ - -syntax = "proto3"; - -import "cmvr/api/common.proto"; - -package cmvr.api; - -// ============================================================================ -// 1. 基本信息查询(命令码 1000, 0x03E8) -// ============================================================================ - -/** - * @brief AGV 基本信息响应结构。 - * @note 对应仙工 API 1000(0x03E8)的响应体,包含设备标识、软硬件版本、网络状态等。 - */ -message AgvStatusInfo { - optional string id = 1; ///< AGV 设备唯一 ID - optional string vehicle_id = 2; ///< 车辆编号(如 "agv_001") - optional string version = 3; ///< 软件版本号 - optional string model = 4; ///< 车型型号(如 "SRC-1100") - optional string dsp_version = 5; ///< DSP 固件版本 - optional string current_ip = 6; ///< 当前 IP 地址 - optional string mac = 7; ///< MAC 地址 - optional int32 rssi = 8; ///< Wi-Fi 信号强度(0~100) - optional int32 ret_code = 9; ///< 返回码,0 表示成功 - optional string err_msg = 10; ///< 错误信息(失败时填充) -} - -/** - * @brief 获取 AGV 状态命令(请求/反馈)。 - * @note 请求携带设备 ID(header.device_id),反馈返回 AgvStatusInfo。 - */ -message GetAgvStatusInfoCommand { - message Request { - CommandHeader.Request header = 1; ///< 公共请求头,包含 device_id - } - - message Feedback { - CommandHeader.Feedback header = 1; ///< 公共响应头(success/error_message/timestamp) - AgvStatusInfo status = 2; ///< AGV 状态信息 - } -} - - -// ============================================================================ -// 2. 电池状态查询(命令码 1007, 0x03EF) -// ============================================================================ - -/** - * @brief 电池状态响应结构。 - * @note 对应仙工 API 1007(0x03EF),支持简单/全量两种模式。 - */ -message AgvBatteryStatus { - optional double battery_level = 1; ///< 电池电量(0~1,对应 0%~100%) - optional double battery_temp = 2; ///< 电池温度(摄氏度) - optional bool charging = 3; ///< 是否正在充电 - optional double voltage = 4; ///< 当前电压(V) - optional double current = 5; ///< 当前电流(A) - optional double max_charge_voltage = 6; ///< 最大充电电压(-1 表示不支持) - optional double max_charge_current = 7; ///< 最大充电电流(-1 表示不支持) - optional bool manual_charge = 8; ///< 是否连接手动充电器(仅 SRC-2000) - optional bool auto_charge = 9; ///< 是否连接自动充电桩(仅 SRC-2000) - optional int32 battery_cycle = 10; ///< 电池循环次数(从 BMS 读取,可能不准) - optional string battery_user_data = 11; ///< 电池用户自定义数据 - optional string extra = 12; ///< 扩展字段(预留) - optional int32 ret_code = 13; ///< 返回码,0 表示成功 - optional string create_on = 14; ///< 数据创建时间戳(ISO 8601) - optional string err_msg = 15; ///< 错误信息 -} - -/** - * @brief 电池状态请求数据。 - */ -message RobotStatusBatteryRequestData { - optional bool simple = 1; ///< true=仅关键数据,false=全量数据(默认 false) -} - -/** - * @brief 电池状态查询命令。 - */ -message RobotStatusBatteryCommand { - message Request { - CommandHeader.Request header = 1; ///< 公共请求头 - RobotStatusBatteryRequestData data = 2; ///< 请求参数 - } - message Feedback { - CommandHeader.Feedback header = 1; ///< 公共响应头 - AgvBatteryStatus status = 2; ///< 电池状态信息 - } -} - - -// ============================================================================ -// 3. 机器人位置查询(命令码 1004, 0x03EC) -// ============================================================================ - -/** - * @brief 机器人位置响应结构。 - * @note 对应仙工 API 1004(0x03EC),返回世界坐标系下的位姿。 - */ -message AgvRobotLocation { - optional double x = 1; ///< X 坐标(米) - optional double y = 2; ///< Y 坐标(米) - optional double angle = 3; ///< 朝向角(弧度),逆时针为正 - optional double confidence = 4; ///< 定位置信度(0~1) - optional string current_station = 5; ///< 当前所在站点 ID(若在站点上) - optional string last_station = 6; ///< 上一个经过的站点 ID - optional int32 loc_method = 7; ///< 定位方式(0=自然轮廓, 1=反光柱, 2=二维码, 3=里程计...) - optional int32 ret_code = 8; ///< 返回码,0 表示成功 - optional string create_on = 9; ///< 时间戳 - optional string err_msg = 10; ///< 错误信息 -} - -/** - * @brief 位置查询命令(无请求数据)。 - */ -message RobotStatusLocCommand { - message Request { - CommandHeader.Request header = 1; ///< 公共请求头 - } - message Feedback { - CommandHeader.Feedback header = 1; ///< 公共响应头 - AgvRobotLocation status = 2; ///< 位置信息 - } -} - - -// ============================================================================ -// 4. 地图下载(命令码 4011, 0x0FAB) -// ============================================================================ - -/** - * @brief 地图下载请求数据。 - */ -message RobotConfigDownloadMapRequestData { - optional string map_name = 1; ///< 要下载的地图名称(必填) -} - -/** - * @brief 地图下载响应结果。 - */ -message AgvDownloadMapResult { - optional int32 ret_code = 1; ///< 返回码,0 表示成功 - optional string create_on = 2; ///< 时间戳 - optional string err_msg = 3; ///< 错误信息 - optional string map_content = 4; ///< 地图 JSON 文件内容(成功时填充) -} - -/** - * @brief 地图下载命令。 - */ -message RobotConfigDownloadMapCommand { - message Request { - CommandHeader.Request header = 1; - RobotConfigDownloadMapRequestData data = 2; - } - message Feedback { - CommandHeader.Feedback header = 1; - AgvDownloadMapResult status = 2; - } -} - - -// ============================================================================ -// 5. 地图状态查询(命令码 1300, 0x0514) -// ============================================================================ - -/** - * @brief 单个地图文件信息。 - */ -message MapFileInfo { - optional string name = 1; ///< 文件名 - optional string modified = 2; ///< 最后修改时间 - optional int64 size = 3; ///< 文件大小(字节) -} - -/** - * @brief 地图状态响应结构。 - * @note 对应仙工 API 1300(0x0514),返回当前地图及所有地图列表。 - */ -message AgvMapStatus { - optional string current_map = 1; ///< 当前加载的地图名称 - optional string current_map_md5 = 2; ///< 当前地图的 MD5 校验值 - repeated string maps = 3; ///< 所有地图名称列表 - repeated MapFileInfo map_files_info = 4; ///< 每个地图文件的详细信息 - optional int32 ret_code = 5; ///< 返回码,0 表示成功 - optional string create_on = 6; ///< 时间戳 - optional string err_msg = 7; ///< 错误信息 -} - -/** - * @brief 地图状态查询命令(无请求数据)。 - */ -message RobotStatusMapCommand { - message Request { - CommandHeader.Request header = 1; - } - message Feedback { - CommandHeader.Feedback header = 1; - AgvMapStatus status = 2; - } -} - - -// ============================================================================ -// 6. 上传地图(命令码 4010, 0x0FAA) -// ============================================================================ - -/** - * @brief 上传地图请求数据。 - */ -message RobotConfigUploadMapRequestData { - optional string map_content = 1; ///< 完整的地图 JSON 字符串(必填) -} - -/** - * @brief 上传地图响应结果。 - */ -message AgvUploadMapResult { - optional int32 ret_code = 1; ///< 返回码,0 表示成功 - optional string create_on = 2; ///< 时间戳 - optional string err_msg = 3; ///< 错误信息 -} - -/** - * @brief 上传地图命令。 - */ -message RobotConfigUploadMapCommand { - message Request { - CommandHeader.Request header = 1; - RobotConfigUploadMapRequestData data = 2; - } - message Feedback { - CommandHeader.Feedback header = 1; - AgvUploadMapResult status = 2; - } -} - - -// ============================================================================ -// 7. 抢占控制权(命令码 4005, 0x0FA5) -// ============================================================================ - -/** - * @brief 抢占控制权请求数据。 - */ -message RobotConfigLockRequestData { - optional string nick_name = 1; ///< 抢占者昵称/标识(必填) -} - -/** - * @brief 抢占控制权响应结果。 - */ -message AgvLockResult { - optional int32 ret_code = 1; ///< 返回码,0 表示成功 - optional string create_on = 2; ///< 时间戳 - optional string err_msg = 3; ///< 错误信息 -} - -/** - * @brief 抢占控制权命令。 - */ -message RobotConfigLockCommand { - message Request { - CommandHeader.Request header = 1; - RobotConfigLockRequestData data = 2; - } - message Feedback { - CommandHeader.Feedback header = 1; - AgvLockResult status = 2; - } -} - - -// ============================================================================ -// 8. 查询当前控制权所有者(命令码 1060, 0x0424) -// ============================================================================ - -/** - * @brief 当前控制权所有者响应结构。 - * @note 对应仙工 API 1060(0x0424)。 - */ -message AgvCurrentLockStatus { - optional bool locked = 1; ///< 是否被锁定(有持有者) - optional string ip = 2; ///< 持有者 IP 地址 - optional int32 port = 3; ///< 持有者端口 - optional uint32 type = 4; ///< 持有者类型(0=default, 2=roboshop, 0xDD=srd) - optional string nick_name = 5; ///< 持有者昵称 - optional int64 time_t = 6; ///< 锁定时间戳(Unix 秒) - optional string desc = 7; ///< 描述信息 - optional int32 ret_code = 8; ///< 返回码,0 表示成功 - optional string create_on = 9; ///< 时间戳 - optional string err_msg = 10; ///< 错误信息 -} - -/** - * @brief 查询控制权命令(无请求数据)。 - */ -message RobotStatusCurrentLockCommand { - message Request { - CommandHeader.Request header = 1; - } - message Feedback { - CommandHeader.Feedback header = 1; - AgvCurrentLockStatus status = 2; - } -} - - -// ============================================================================ -// 9. 开环速度运动控制(命令码 2010, 0x07DA) -// ============================================================================ - -/** - * @brief 开环速度运动控制请求数据。 - * @note 命令码 2010(0x07DA)。下发后会强制取消当前自动导航任务。 - * 多舵轮设备仅 vx/vy/w 生效;steer/real_steer 仅单舵轮设备有效。 - * duration = -1 表示持续运行直到下一次指令。 - */ -message RobotMotionControlRequestData { - optional double vx = 1; ///< X 方向线速度(m/s) - optional double vy = 2; ///< Y 方向线速度(m/s) - optional double w = 3; ///< 角速度(rad/s),逆时针为正 - optional double steer = 4; ///< 舵轮转向角(单舵轮,单位 rad) - optional double real_steer = 5; ///< 实际舵轮角度(单舵轮,优先级高于 steer) - optional int64 duration = 6; ///< 持续时间(ms),-1 表示一直运行 -} - -/** - * @brief 开环运动控制响应结果。 - */ -message RobotMotionControlResult { - optional int32 ret_code = 1; ///< 返回码,0 表示成功 - optional string create_on = 2; ///< 时间戳 - optional string err_msg = 3; ///< 错误信息 -} - -/** - * @brief 开环运动控制命令。 - */ -message RobotMotionControlCommand { - message Request { - CommandHeader.Request header = 1; - RobotMotionControlRequestData data = 2; - } - message Feedback { - CommandHeader.Feedback header = 1; - RobotMotionControlResult status = 2; - } -} - - -// ============================================================================ -// 10. 切换载入地图(命令码 2022, 0x07E6) -// ============================================================================ - -/** - * @brief 切换地图请求数据。 - * @note 命令码 2022(0x07E6)。目标地图必须已存在于机器人中。 - */ -message RobotLoadMapRequestData { - optional string map_name = 1; ///< 目标地图名称(必填,仅字母数字-_) -} - -/** - * @brief 切换地图响应结果。 - */ -message RobotLoadMapResult { - optional int32 ret_code = 1; ///< 返回码,0 表示成功 - optional string create_on = 2; ///< 时间戳 - optional string err_msg = 3; ///< 错误信息 -} - -/** - * @brief 切换地图命令。 - */ -message RobotLoadMapCommand { - message Request { - CommandHeader.Request header = 1; - RobotLoadMapRequestData data = 2; - } - message Feedback { - CommandHeader.Feedback header = 1; - RobotLoadMapResult status = 2; - } -} - - -// ============================================================================ -// 11. 查询地图载入状态(命令码 1022, 0x03FE) -// ============================================================================ - -/** - * @brief 地图载入状态响应。 - * @note 命令码 1022(0x03FE)。loadmap_status: 0=失败, 1=成功, 2=载入中。 - * 载入中(2)时禁止执行重定位操作。 - */ -message RobotQueryLoadMapStatusResult { - optional int32 loadmap_status = 1; ///< 0=失败, 1=成功, 2=载入中 - optional int32 ret_code = 2; ///< 返回码,0 表示成功 - optional string create_on = 3; ///< 时间戳 - optional string err_msg = 4; ///< 错误信息 -} - -/** - * @brief 查询地图载入状态命令(无请求数据)。 - */ -message RobotQueryLoadMapStatusCommand { - message Request { - CommandHeader.Request header = 1; - } - message Feedback { - CommandHeader.Feedback header = 1; - RobotQueryLoadMapStatusResult status = 2; - } -} - - -// ============================================================================ -// 12. 查询站点列表(命令码 1301, 0x0515) -// ============================================================================ - -/** - * @brief 单个站点信息。 - */ -message StationItem { - optional string id = 1; ///< 站点唯一 ID - optional string type = 2; ///< 站点类型(如 "LocationMark", "ChargePoint") - optional double x = 3; ///< X 坐标(米) - optional double y = 4; ///< Y 坐标(米) - optional double r = 5; ///< 朝向角(弧度) - optional string desc = 6; ///< 描述信息 - optional string executor = 7; ///< 执行器信息 - optional string prepoint = 8; ///< 前置点 ID - optional string recfile = 9; ///< 识别文件路径 - optional bool spin = 10; ///< 是否允许自旋 - optional bool use_down_pgv = 11; ///< 是否使用下视 PGV -} - -/** - * @brief 站点列表响应结果。 - */ -message QueryStationListResult { - repeated StationItem stations = 1; ///< 站点列表 - optional int32 ret_code = 2; ///< 返回码,0 表示成功 - optional string create_on = 3; ///< 时间戳 - optional string err_msg = 4; ///< 错误信息 -} - -/** - * @brief 查询站点列表命令(无请求数据)。 - */ -message QueryStationListCommand { - message Request { - CommandHeader.Request header = 1; - } - message Feedback { - CommandHeader.Feedback header = 1; - QueryStationListResult status = 2; - } -} - - -// ============================================================================ -// 13. 指定路径导航(命令码 3066, 0x0BFA) -// ============================================================================ - -/** - * @brief 单段导航任务单元。 - * @note 用于 3066 命令的 move_task_list 数组。 - * source_id 和 id 之间必须有直接路径,不可跳点。 - */ -message MoveTaskItem { - optional string task_id = 1; ///< 任务唯一 ID(必填) - optional string source_id = 2; ///< 起始站点 ID(必填) - optional string id = 3; ///< 目标站点 ID(必填) - optional string operation = 4; ///< 到达后执行的动作(如 "JackLoad") - optional double jack_height = 5; ///< 顶升高度(米) -} - -/** - * @brief 指定路径导航请求数据。 - */ -message RobotGoTargetListRequestData { - repeated MoveTaskItem move_task_list = 1; ///< 导航任务序列(顺序执行) -} - -/** - * @brief 指定路径导航响应结果。 - * @note ret_code=0 仅表示指令被接收,不表示执行完成。 - */ -message RobotGoTargetListResult { - optional int32 ret_code = 1; ///< 返回码,0 表示成功接收 - optional string create_on = 2; ///< 时间戳 - optional string err_msg = 3; ///< 错误信息 -} - -/** - * @brief 指定路径导航命令。 - */ -message RobotGoTargetListCommand { - message Request { - CommandHeader.Request header = 1; - RobotGoTargetListRequestData data = 2; - } - message Feedback { - CommandHeader.Feedback header = 1; - RobotGoTargetListResult status = 2; - } -} - - -// ============================================================================ -// 14. 查询当前实时导航状态(命令码 1020, 0x03FC) -// ============================================================================ - -/** - * @brief 1020 命令请求数据。 - */ -message RobotStatusTaskReqData { - optional bool simple = 1; ///< true=仅返回 task_status,false=全量 -} - -/** - * @brief 导航任务中的料箱信息。 - */ -message NavContainerItem { - optional string container_name = 1; ///< 料箱名称 - optional string desc = 2; ///< 描述 - optional string goods_id = 3; ///< 绑定的货物 ID - optional bool has_goods = 4; ///< 是否已绑定货物 -} - -/** - * @brief 1020 命令响应数据。 - */ -message RobotStatusTaskResData { - optional int32 task_status = 1; ///< 0=NONE, 1=WAITING, 2=RUNNING, 3=SUSPENDED, 4=COMPLETED, 5=FAILED, 6=CANCELED - optional int32 task_type = 2; ///< 0=无, 1=自由导航到坐标, 2=自由导航到站点, 3=路径导航到站点, 7=平动转动 - optional string target_id = 3; ///< 目标站点 ID(task_type 为 2/3 时) - repeated double target_point = 4; ///< 目标坐标 [x, y, r](task_type 为 1 时) - repeated string finished_path = 5; ///< 已经过的站点列表 - repeated string unfinished_path = 6; ///< 尚未经过的站点列表 - optional string move_status_info = 7; ///< 附加信息(如错误描述) - repeated NavContainerItem containers = 8; ///< 料箱信息 - optional int32 ret_code = 9; ///< 返回码,0 表示成功 - optional string create_on = 10; ///< 时间戳 - optional string err_msg = 11; ///< 错误信息 -} - -/** - * @brief 查询当前导航状态命令。 - */ -message RobotStatusTaskCurrentCommand { - message Request { - CommandHeader.Request header = 1; - RobotStatusTaskReqData data = 2; - } - message Feedback { - CommandHeader.Feedback header = 1; - RobotStatusTaskResData data = 2; - } -} - - -// ============================================================================ -// 15. 批量查询任务状态(命令码 1110, 0x0456) -// ============================================================================ - -/** - * @brief 1110 命令请求数据。 - */ -message QueryTaskStatusPackageReqData { - repeated string task_ids = 1; ///< 要查询的任务 ID 列表;为空则查所有未完成 + 最近一条完成 -} - -/** - * @brief 单条任务状态项。 - */ -message SingleTaskStatusItem { - optional string task_id = 1; ///< 任务 ID - optional int32 status = 2; ///< 状态码(同 task_status) - optional int32 type = 3; ///< 任务类型(同 task_type) -} - -/** - * @brief 任务状态包(包含最近完成和所有未完成的任务)。 - */ -message TaskStatusPackage { - optional string closest_target = 1; ///< 最近站点 ID - optional string source_name = 2; ///< 起点站点名称 - optional string target_name = 3; ///< 终点站点名称 - optional double percentage = 4; ///< 任务进度百分比(0~100) - optional double distance = 5; ///< 剩余距离(米) - optional string info = 6; ///< 附加信息 - repeated SingleTaskStatusItem task_status_list = 7; ///< 所有任务状态列表 -} - -/** - * @brief 1110 命令响应数据。 - */ -message QueryTaskStatusPackageResData { - optional TaskStatusPackage task_status_package = 1; ///< 任务状态包 - optional int32 ret_code = 2; ///< 返回码,0 表示成功 - optional string create_on = 3; ///< 时间戳 - optional string err_msg = 4; ///< 错误信息 -} - -/** - * @brief 批量查询任务状态命令。 - */ -message RobotStatusTaskPackageCommand { - message Request { - CommandHeader.Request header = 1; - QueryTaskStatusPackageReqData data = 2; - } - message Feedback { - CommandHeader.Feedback header = 1; - QueryTaskStatusPackageResData data = 2; - } -} - - -// ============================================================================ -// 16. 单点站点自动规划导航(命令码 3051, 0x0BEB) -// ============================================================================ - -/** - * @brief DI(数字输入)点位。 - */ -message DIItem { - int32 id = 1; ///< DI 编号 - bool status = 2; ///< 状态(true=高电平, false=低电平) -} - -/** - * @brief DO(数字输出)点位。 - */ -message DOItem { - int32 id = 1; ///< DO 编号 - bool status = 2; ///< 状态(true=高电平, false=低电平) -} - -/** - * @brief 音频播放参数。 - */ -message SoundArgs { - optional string name = 1; ///< 音频文件名 - optional int32 loop = 2; ///< 循环次数(0=播放一次, 1=循环) - optional int32 stop = 3; ///< 1=停止播放 -} - -/** - * @brief WaitDI 动作参数。 - */ -message WaitDIArgs { - repeated DIItem DI = 1; ///< 需要等待的 DI 条件列表 - optional double timeout = 2; ///< 超时时间(秒),0 表示无限等待 -} - -/** - * @brief SetDO 动作参数。 - */ -message SetDOArgs { - repeated DOItem do_list = 1; ///< 要设置的 DO 列表 -} - -/** - * @brief PGV(二维码)二次定位参数。 - */ -message PgvParam { - optional bool use_pgv = 1; ///< 是否使用上视 PGV - optional bool use_down_pgv = 2; ///< 是否使用下视 PGV - optional double pgv_adjust_dist = 3; ///< 最大调整半径(米) - optional double pgv_adjust_cx = 4; ///< 二维码坐标系 X 偏移(米) - optional double pgv_adjust_cy = 5; ///< 二维码坐标系 Y 偏移(米) - optional double pgv_x_adjust = 6; ///< 仅调整 X 方向误差(米) -} - -/** - * @brief 自由导航目标点(坐标 + 朝向)。 - */ -message FreeGoPoint { - double x = 1; ///< X 坐标(米) - double y = 2; ///< Y 坐标(米) - double theta = 3; ///< 朝向角(弧度) -} - -/** - * @brief 脚本自定义参数。 - */ -message ScriptArgs { - map str_kv = 1; ///< 字符串键值对 - map num_kv = 2; ///< 数字键值对 - repeated DOItem do_list = 3; ///< DO 列表 - repeated DIItem di_list = 4; ///< DI 列表 -} - -/** - * @brief 3051 命令请求数据(单点站点自动规划导航)。 - * @warning 严禁用于多车调度场景,仅限单车测试/验证。 - * 下发此任务会取消当前正在执行的任务(不排队)。 - * 成功下发后会自动清除指定的 warning/error 报错码。 - * 自由导航(freego)仅支持双轮差速底盘。 - */ -message RobotGoTargetReqData { - // -------- 必填字段 -------- - string source_id = 1; ///< 起始站点 ID("SELF_POSITION" 表示当前位置) - string id = 2; ///< 目标站点 ID("SELF_POSITION" 表示原地执行 operation) - optional string task_id = 3; ///< 任务 ID(建议提供) - - // -------- 运动通用参数 -------- - optional double angle = 4; ///< 目标点朝向角(弧度) - optional string method = 5; ///< "forward"(正走)或 "backward"(倒走) - optional double max_speed = 6; ///< 最大线速度(m/s),0 使用默认 - optional double max_wspeed = 7; ///< 最大角速度(rad/s) - optional double max_acc = 8; ///< 最大加速度(m/s²) - optional double max_wacc = 9; ///< 最大角加速度(rad/s²) - optional int64 duration = 10; ///< 到达后等待时间(毫秒) - optional int32 orientation = 11; ///< 全向车保持的角度(暂不使用) - optional bool spin = 12; ///< 是否随动(托盘旋转) - optional int64 delay = 13; ///< 延迟结束导航状态(毫秒),缺省 0 - optional int32 start_rot_dir = 14; ///< 起步旋转方向:-1=顺时针, 0=就近, 1=逆时针 - optional int32 end_rot_dir = 15; ///< 到点旋转方向:-1=顺时针, 0=就近, 1=逆时针 - optional double reach_dist = 16; ///< 到点位置精度(米) - optional double reach_angle = 17; ///< 到点角度精度(弧度) - optional string skill_name = 18; ///< 技能名称:"Action" 或 "GotoSpecifiedPose" - - // -------- PGV -------- - optional PgvParam pgv = 19; ///< 二次定位参数 - - // -------- 设备动作 -------- - optional string operation = 20; ///< 操作类型(JackLoad/ForkUnload/RollerLoad/HookLoad/WaitDI/SetDO/sound/Script 等) - optional double jack_height = 21; ///< 顶升高度(米) - optional double start_height = 22; ///< 货叉起步前举升高度(米) - optional double end_height = 23; ///< 货叉到点后举升高度(米) - optional double fork_mid_height = 24; ///< 货叉行走过程中举升高度(米) - optional double fork_dist = 25; ///< 货叉前移距离(米) - optional string direction = 26; ///< 方向:"left"/"right"/"front"/"back" - optional bool recognize = 27; ///< 是否启用视觉识别 - optional string recfile = 28; ///< 识别文件路径(如 "shelf/s0002.shelf") - - // -------- 音频 -------- - optional SoundArgs sounds_args = 29; ///< 音频参数 - - // -------- WaitDI / SetDO -------- - optional WaitDIArgs wait_di_args = 30; ///< WaitDI 参数 - optional SetDOArgs set_do_args = 31; ///< SetDO 参数 - - // -------- 脚本 -------- - optional string script_name = 32; ///< 脚本文件名 - optional ScriptArgs script_args = 33; ///< 脚本参数 - optional int32 script_stage = 34; ///< 执行阶段:0=导航前, 1=导航中, 2=导航后, 3=脚本控制导航 - - // -------- 原地旋转(GoByOdometer) -------- - optional double move_angle = 35; ///< 旋转角度(弧度) - optional double speed_w = 36; ///< 旋转角速度(rad/s) - optional int32 loc_mode = 37; ///< 定位模式:1=激光定位, 0=里程定位 - - // -------- 自由导航(仅双轮差速) -------- - optional FreeGoPoint freego = 38; ///< 自由导航目标(若提供则忽略 id 字段) -} - -/** - * @brief 3051 命令响应数据。 - */ -message RobotGoTargetResData { - optional int32 ret_code = 1; ///< 返回码,0 表示成功接收 - optional string create_on = 2; ///< 时间戳 - optional string err_msg = 3; ///< 错误信息 -} - -/** - * @brief 单点导航命令。 - */ -message RobotGoTargetCommand { - message Request { - CommandHeader.Request header = 1; - RobotGoTargetReqData data = 2; - } - message Feedback { - CommandHeader.Feedback header = 1; - RobotGoTargetResData data = 2; - } -} - - -// ============================================================================ -// 17. 停止开环运动(命令码 2000, 0x07D0) -// ============================================================================ - -/** - * @brief 停止开环运动请求数据(无业务字段)。 - * @note 命令码 2000(0x07D0)。下发零速度指令,终止当前持续运动。 - */ -message RobotControlStopRequestData -{ -} - -/** - * @brief 停止开环运动响应结果。 - */ -message RobotControlStopResult -{ - optional int32 ret_code = 1; ///< 返回码,0 表示成功 - optional string create_on = 2; ///< 时间戳 - optional string err_msg = 3; ///< 错误信息 -} - -/** - * @brief 停止开环运动命令。 - */ -message RobotControlStopCommand -{ - message Request - { - CommandHeader.Request header = 1; - RobotControlStopRequestData data = 2; - } - message Feedback - { - CommandHeader.Feedback header = 1; - RobotControlStopResult status = 2; - } -} - - -// ============================================================================ -// 18. 暂停/继续/取消导航任务(命令码 3001/3002/3003) -// ============================================================================ - -/** - * @brief 暂停当前导航任务(命令码 3001, 0x0BB9)。 - * @note 请求无业务数据。 - */ -message RobotTaskPauseCommand { - message Request { - CommandHeader.Request header = 1; - // 无 data 字段 - } - message Feedback { - CommandHeader.Feedback header = 1; - /** - * @brief 暂停结果。 - */ - message Result { - optional int32 ret_code = 1; ///< 返回码,0 表示成功 - optional string create_on = 2; ///< 时间戳 - optional string err_msg = 3; ///< 错误信息 - } - Result status = 2; - } -} - -/** - * @brief 继续当前导航任务(命令码 3002, 0x0BBA)。 - * @note 请求无业务数据。 - */ -message RobotTaskResumeCommand { - message Request { - CommandHeader.Request header = 1; - // 无 data 字段 - } - message Feedback { - CommandHeader.Feedback header = 1; - /** - * @brief 继续结果。 - */ - message Result { - optional int32 ret_code = 1; ///< 返回码,0 表示成功 - optional string create_on = 2; ///< 时间戳 - optional string err_msg = 3; ///< 错误信息 - } - Result status = 2; - } -} - -/** - * @brief 取消当前导航任务(命令码 3003, 0x0BBB)。 - * @note 请求无业务数据。 - */ -message RobotTaskCancelCommand { - message Request { - CommandHeader.Request header = 1; - // 无 data 字段 - } - message Feedback { - CommandHeader.Feedback header = 1; - /** - * @brief 取消结果。 - */ - message Result { - optional int32 ret_code = 1; ///< 返回码,0 表示成功 - optional string create_on = 2; ///< 时间戳 - optional string err_msg = 3; ///< 错误信息 - } - Result status = 2; - } -} \ No newline at end of file diff --git a/protos/cmvr/api/agv_service.proto b/protos/cmvr/api/agv_service.proto deleted file mode 100644 index 521ac025..00000000 --- a/protos/cmvr/api/agv_service.proto +++ /dev/null @@ -1,74 +0,0 @@ -/** - * @file agv_service.proto - * @brief 定义AGV服务的gRPC接口,聚合所有AGV相关命令。 - * 服务端实现类为 gRPCAGVServiceImpl。 - */ -syntax = "proto3"; - -import "cmvr/api/agv_command.proto"; - -package cmvr.api; - -service AgvService { - // 基本控制 - rpc GetStatusInfo(GetAgvStatusInfoCommand.Request) returns (GetAgvStatusInfoCommand.Feedback); - - rpc GetBatteryStatus(RobotStatusBatteryCommand.Request) returns (RobotStatusBatteryCommand.Feedback); - - rpc GetRobotLocation(RobotStatusLocCommand.Request) returns (RobotStatusLocCommand.Feedback); - - rpc RobotConfigDownloadMap(RobotConfigDownloadMapCommand.Request) returns (RobotConfigDownloadMapCommand.Feedback); - - - rpc GetMapStatus(RobotStatusMapCommand.Request) returns (RobotStatusMapCommand.Feedback); - - - rpc RobotConfigUploadMap(RobotConfigUploadMapCommand.Request) returns (RobotConfigUploadMapCommand.Feedback); - - // 抢占机器人控制权 - rpc RobotConfigLock(RobotConfigLockCommand.Request) returns (RobotConfigLockCommand.Feedback); - - // 查询当前控制权所有者 - rpc GetCurrentLockStatus(RobotStatusCurrentLockCommand.Request) returns (RobotStatusCurrentLockCommand.Feedback); - - // 开环速度运动控制 - rpc RobotMotionControl(RobotMotionControlCommand.Request) returns (RobotMotionControlCommand.Feedback); - - // 切换载入地图 命令码2022 - rpc RobotLoadMap(RobotLoadMapCommand.Request) returns (RobotLoadMapCommand.Feedback); - - - // 查询地图载入状态 命令码1022 - rpc QueryLoadMapStatus(RobotQueryLoadMapStatusCommand.Request) returns (RobotQueryLoadMapStatusCommand.Feedback); - - - // 查询当前地图站点列表 命令码1301 - rpc QueryStationList(QueryStationListCommand.Request) returns (QueryStationListCommand.Feedback); - - - // 指定路径导航 命令码3066 - rpc RobotGoTargetList(RobotGoTargetListCommand.Request) returns (RobotGoTargetListCommand.Feedback); - - // 查询当前导航状态 1020 robot_status_task_req - // 1020 查询当前实时导航 robot_status_task_req - rpc RobotStatusTaskCurrent(RobotStatusTaskCurrentCommand.Request) returns (RobotStatusTaskCurrentCommand.Feedback); - - // 批量查询任务状态 1110 robot_status_task_status_package_req - rpc RobotStatusTaskPackage(RobotStatusTaskPackageCommand.Request) returns (RobotStatusTaskPackageCommand.Feedback); - - - // 3051 单点站点自动规划导航 robot_task_gotarget_req 0x0BEB - rpc RobotGoTarget(RobotGoTargetCommand.Request) returns (RobotGoTargetCommand.Feedback); - - - - // 0x07D0 停止开环运动 - rpc RobotControlStop(RobotControlStopCommand.Request) returns (RobotControlStopCommand.Feedback); - - // 暂停当前导航任务 命令码 3001 (0x0BB9) - rpc RobotTaskPause(RobotTaskPauseCommand.Request) returns (RobotTaskPauseCommand.Feedback); - // 继续当前导航任务 命令码 3002 (0x0BBA) - rpc RobotTaskResume(RobotTaskResumeCommand.Request) returns (RobotTaskResumeCommand.Feedback); - // 取消当前导航任务 命令码 3003 (0x0BBB) - rpc RobotTaskCancel(RobotTaskCancelCommand.Request) returns (RobotTaskCancelCommand.Feedback); -} \ No newline at end of file diff --git a/protos/cmvr/config/agv_config/agv_config.proto b/protos/cmvr/config/agv_config/agv_config.proto deleted file mode 100644 index cb972f1d..00000000 --- a/protos/cmvr/config/agv_config/agv_config.proto +++ /dev/null @@ -1,20 +0,0 @@ -// camera_config.proto - 所有配置都在这里 -syntax = "proto3"; -package cmvr.config; - - -message AGVsrc1100Config{ - string id = 1; - string ip = 2; - bool enable = 3; - int32 port_status = 4; // 19204 状态查询 - int32 port_control = 5; // 19205 控制API - int32 port_nav = 6; // 19206 导航API - int32 port_config = 7; // 19207 配置API - int32 port_other = 8; // 19210 其他API - int32 port_push = 9; // 19301 推送API -} - -message AGVConfig{ - repeated AGVsrc1100Config src1100_agvs = 1; -} diff --git a/protos/cmvr/config/biohead_config/biohead_config.proto b/protos/cmvr/config/biohead_config/biohead_config.proto deleted file mode 100644 index 80c7d052..00000000 --- a/protos/cmvr/config/biohead_config/biohead_config.proto +++ /dev/null @@ -1,44 +0,0 @@ -// biohead.proto -syntax = "proto3"; - -package cmvr.config; - -// 眉毛配置 -message EyeBrowConfig { - string serial = 1; // 如 "64:0~3" - string offest = 2; // 偏移量,如 "90 90 90 90" - string jLmtUp = 3; // 上限,如 "90 170 155 110" - string jLmtLow = 4; // 下限,如 "20 77 90 20" -} - -// 眼睛配置 -message EyeConfig { - string serial = 1; // 如 "64:4~9" - string offest = 2; // 偏移量,如 "90 90 90 90 90 90" - string jLmtUp = 3; // 上限,如 "90 150 165 90 120 115" - string jLmtLow = 4; // 下限,如 "20 90 90 25 70 75" -} - -// 嘴巴配置 -message MouthConfig { - string serial = 1; // 如 "65:0~9" - string offest = 2; // 偏移量,如 "90 90 90 90 90 90 90 90 90 90" - string jLmtUp = 3; // 上限,如 "150 110 130 140 100 105 110 125 90 95" - string jLmtLow = 4; // 下限,如 "70 30 80 80 65 55 45 80 85 90" -} - -// BioHead配置 -message BioHeadConfig { - string id = 1; // 设备ID,如 "bio_head" - string serial = 2; // 串口路径,如 "/dev/ttyUSB0" - int32 ctrlFreq = 3; // 控制频率,如 50 - - EyeBrowConfig eyebrow = 4; // 眉毛配置 - EyeConfig eye = 5; // 眼睛配置 - MouthConfig mouth = 6; // 嘴巴配置 - bool enable = 7; // 是否启用 -} - -message HeadConfig { - repeated BioHeadConfig bioheads = 3; -} \ No newline at end of file diff --git a/protos/cmvr/config/monitor_config/monitor_config.proto b/protos/cmvr/config/monitor_config/monitor_config.proto deleted file mode 100644 index 368c4660..00000000 --- a/protos/cmvr/config/monitor_config/monitor_config.proto +++ /dev/null @@ -1,22 +0,0 @@ -syntax = "proto3"; - -package cmvr.config; - -// 文件夹监控配置 -message FolderConfig { - string fileDir = 1; // 文件夹路径 - int32 maxVolume = 2; // 最大容量(MB) -} - -// 磁盘监控配置 -message DiskMonitorConfig { - string id = 1; // 监控器ID,如 "file_monitor" - int32 freq = 2; // 监控频率(Hz) - repeated FolderConfig folders = 3; // 要监控的文件夹列表 - bool enable = 4; // 是否启用 -} - -// 监控管理器配置 -message MonitorManagerConfig { - DiskMonitorConfig disk_monitor = 1; // 磁盘监控配置 -} \ No newline at end of file diff --git a/protos/cmvr/config/robot_config/robot_config.proto b/protos/cmvr/config/robot_config/robot_config.proto deleted file mode 100644 index 1838f1eb..00000000 --- a/protos/cmvr/config/robot_config/robot_config.proto +++ /dev/null @@ -1,88 +0,0 @@ -syntax = "proto3"; - -package cmvr.config; - -// 电机配置 -message MotorConfig { - int32 id = 1; // 电机ID - string jointName = 2; // 关节名称 - float limitQLb = 3; // 位置下限 - float limitQUb = 4; // 位置上限 - float limitQd = 5; // 速度限制 -} - -// 通用的CAN设备配置(手臂、头部、腰部都可以复用) -message CanDeviceConfig { - string id = 1; // CAN ID - string devId = 2; // 设备ID - int32 channelId = 3; // 通道ID - bool enable = 4; // 是否启用 - string toolFrame = 5; // 工具坐标系(手臂需要,头/腰可选) - repeated MotorConfig motors = 6; // 电机列表 -} - -// CAN管理器配置 -message CanManagerConfig { - string id = 1; // CAN管理器ID - string devId = 2; // 设备ID - CanDeviceConfig left_arm = 3; // 左臂CAN - CanDeviceConfig right_arm = 4; // 右臂CAN - CanDeviceConfig head = 5; // 头部CAN - CanDeviceConfig waist = 6; // 腰部CAN -} - -// 人形机器人配置 -message HumanRobotConfig { - string id = 1; // 机器人ID,如 "hc01" - int32 dof = 2; // 自由度,如 14 - string urdf = 3; // URDF文件路径 - string baseLink = 4; // 基座link名称,如 "PELVIS_S" - string jointNames = 5; // 关节名称列表,逗号分隔 - string linkNames = 6; // link名称列表,逗号分隔 - int32 bufferSize = 7; // 缓冲区大小,如 50 - bool verbose = 8; // 是否详细输出 - int32 updFreq = 9; // 更新频率,如 500 - CanManagerConfig can_manager = 10; // CAN管理器配置 - bool enable = 11; // 是否启用 -} - -// EtherCAT主站配置 -message EthercatMasterConfig { - string interface = 1; // 网络接口,如 "eth0" - int32 cycle_time_us = 2; // 周期时间(微秒),默认1000 - bool enable = 3; // 是否启用 -} - -// EtherCAT电机驱动配置 -message EthercatMotorConfig { - int32 slave = 1; // 从站ID - string jointName = 2; // 关节名称 - float limitQLb = 3; // 位置下限 - float limitQUb = 4; // 位置上限 - float limitQd = 5; // 速度限制 -} - -// EtherCAT机器人配置 -message EthercatRobotConfig { - string id = 1; // 机器人ID - int32 dof = 2; // 自由度 - string urdf = 3; // URDF文件路径 - string baseLink = 4; // 基座link名称 - string jointNames = 5; // 关节名称列表,逗号分隔 - string linkNames = 6; // link名称列表,逗号分隔 - int32 bufferSize = 7; // 缓冲区大小 - bool verbose = 8; // 是否详细输出 - int32 updFreq = 9; // 更新频率(Hz) - bool enable = 10; // 是否启用 - - EthercatMasterConfig master = 11; // EtherCAT主站配置 - repeated EthercatMotorConfig motors = 12; // 电机配置列表 -} - - - -// 主配置 -message RobotConfig { - repeated HumanRobotConfig humanrobots = 1; - repeated EthercatRobotConfig ethercat_robots = 2; // 新增的EtherCAT机器人 -} \ No newline at end of file diff --git a/protos/cmvr/config/server_config/server_config.proto b/protos/cmvr/config/server_config/server_config.proto deleted file mode 100644 index 911b12ca..00000000 --- a/protos/cmvr/config/server_config/server_config.proto +++ /dev/null @@ -1,10 +0,0 @@ -syntax = "proto3"; -package cmvr.config; - -message GrpcServerConfig{ - string port = 1; -} - -message ServerConfig{ - GrpcServerConfig grpc_server = 1; -} \ No newline at end of file diff --git a/request.txt b/request.txt index 9e9809c7..9a4d60e2 100644 --- a/request.txt +++ b/request.txt @@ -33,6 +33,5 @@ third_party/modbus/3.1.11 third_party/visp/3.7.0 third_party/mainif/0.0.5 third_party/matplotplusplus/1.2.0 -third_party/soem