From 09d322378aab1468287ab7aebd8f3f7173d74bf3 Mon Sep 17 00:00:00 2001 From: gitea Date: Thu, 20 Mar 2025 18:13:49 +0800 Subject: [PATCH] init commit --- .gitignore | 4 + assets/files/projects/NB4h_R580_3G.zip | Bin 0 -> 28708 bytes assets/files/projects/autotest.xml | 842 ++++++ assets/files/projects/configs.xlsx | Bin 0 -> 227620 bytes assets/files/projects/fieldbus_device.json | 75 + assets/files/projects/registers.json | 520 ++++ assets/files/projects/registers.xml | 843 ++++++ .../protocols/hmi/collision.get_params.json | 5 + .../protocols/hmi/collision.set_params.json | 6 + .../protocols/hmi/collision.set_state.json | 7 + .../protocols/hmi/controller.get_params.json | 5 + .../files/protocols/hmi/controller.heart.json | 5 + .../protocols/hmi/controller.reboot.json | 8 + .../protocols/hmi/controller.set_params.json | 8 + .../protocols/hmi/device.get_params.json | 5 + .../protocols/hmi/diagnosis.get_params.json | 8 + .../files/protocols/hmi/diagnosis.open.json | 12 + .../files/protocols/hmi/diagnosis.save.json | 8 + .../protocols/hmi/diagnosis.set_params.json | 10 + .../files/protocols/hmi/drag.get_params.json | 5 + .../files/protocols/hmi/drag.set_params.json | 10 + .../hmi/fieldbus_device.get_params.json | 5 + .../hmi/fieldbus_device.load_cfg.json | 8 + .../hmi/fieldbus_device.set_params.json | 9 + .../protocols/hmi/io_device.load_cfg.json | 5 + .../files/protocols/hmi/jog.get_params.json | 5 + .../files/protocols/hmi/jog.set_params.json | 10 + assets/files/protocols/hmi/jog.start.json | 10 + .../hmi/log_code.data.code_list.json | 8 + assets/files/protocols/hmi/log_code.data.json | 5 + .../protocols/hmi/modbus.get_params.json | 5 + .../protocols/hmi/modbus.get_values.json | 8 + .../files/protocols/hmi/modbus.load_cfg.json | 8 + .../protocols/hmi/modbus.set_params.json | 12 + .../protocols/hmi/move.get_joint_pos.json | 5 + .../protocols/hmi/move.get_monitor_cfg.json | 5 + .../files/protocols/hmi/move.get_params.json | 5 + assets/files/protocols/hmi/move.get_pos.json | 5 + .../hmi/move.get_quickstop_distance.json | 5 + .../protocols/hmi/move.get_quickturn_pos.json | 5 + .../files/protocols/hmi/move.quick_turn.json | 8 + .../protocols/hmi/move.set_monitor_cfg.json | 8 + .../files/protocols/hmi/move.set_params.json | 17 + .../hmi/move.set_quickstop_distance.json | 8 + .../protocols/hmi/move.set_quickturn_pos.json | 15 + assets/files/protocols/hmi/move.stop.json | 8 + .../protocols/hmi/overview.get_autoload.json | 5 + .../protocols/hmi/overview.get_cur_prj.json | 5 + .../files/protocols/hmi/overview.reload.json | 9 + .../protocols/hmi/overview.set_autoload.json | 8 + .../protocols/hmi/register.set_value.json | 11 + .../protocols/hmi/rl_task.pp_to_main.json | 8 + assets/files/protocols/hmi/rl_task.run.json | 8 + .../protocols/hmi/rl_task.set_run_params.json | 9 + assets/files/protocols/hmi/rl_task.stop.json | 8 + .../safety.safety_area.overall_enable.json | 7 + ...safety.safety_area.safety_area_enable.json | 8 + .../hmi/safety.safety_area.set_param.json | 5 + .../hmi/safety.safety_area.signal_enable.json | 7 + .../files/protocols/hmi/safety_area_data.json | 5 + .../protocols/hmi/servo.clear_alarm.json | 5 + .../protocols/hmi/socket.get_params.json | 5 + .../protocols/hmi/socket.set_params.json | 17 + .../protocols/hmi/soft_limit.get_params.json | 5 + .../protocols/hmi/soft_limit.set_params.json | 12 + .../files/protocols/hmi/state.get_state.json | 5 + .../protocols/hmi/state.get_tp_mode.json | 5 + .../protocols/hmi/state.set_tp_mode.json | 8 + .../protocols/hmi/state.switch_auto.json | 5 + .../protocols/hmi/state.switch_manual.json | 5 + .../protocols/hmi/state.switch_motor_off.json | 5 + .../protocols/hmi/state.switch_motor_on.json | 5 + .../hmi/system_io.query_configuration.json | 5 + .../hmi/system_io.query_event_cfg.json | 5 + .../hmi/system_io.update_configuration.json | 9 + assets/files/protocols/hmi/原理/协议原理.xlsx | Bin 0 -> 17658 bytes assets/files/protocols/hmi/原理/解包示例.txt | 56 + assets/files/version/file_version_info.txt | 43 + assets/files/version/release_change.md | 490 ++++ assets/files/version/requirements.txt | 11 + assets/files/version/version | 1 + assets/media/icon.ico | Bin 0 -> 165662 bytes assets/media/updated.png | Bin 0 -> 4124 bytes assets/media/upgrade.png | Bin 0 -> 2827 bytes code/aio.py | 518 ++++ code/analysis/brake.py | 218 ++ code/analysis/current.py | 427 +++ code/analysis/iso.py | 213 ++ code/analysis/wavelogger.py | 160 + code/autotest/do_brake.py | 374 +++ code/autotest/do_current.py | 268 ++ code/common/clibs.py | 62 + code/common/openapi.py | 2577 +++++++++++++++++ code/durable/create_plot.py | 85 + code/durable/factory_test.py | 239 ++ code/test.py | 113 + code/ui/login_window.py | 135 + code/ui/main_window.py | 954 ++++++ code/ui/reset_window.py | 170 ++ readme.md | 129 + ui/login.ui | 249 ++ ui/main.ui | 2004 +++++++++++++ ui/reset.ui | 271 ++ 103 files changed, 12549 insertions(+) create mode 100644 .gitignore create mode 100644 assets/files/projects/NB4h_R580_3G.zip create mode 100644 assets/files/projects/autotest.xml create mode 100644 assets/files/projects/configs.xlsx create mode 100644 assets/files/projects/fieldbus_device.json create mode 100644 assets/files/projects/registers.json create mode 100644 assets/files/projects/registers.xml create mode 100644 assets/files/protocols/hmi/collision.get_params.json create mode 100644 assets/files/protocols/hmi/collision.set_params.json create mode 100644 assets/files/protocols/hmi/collision.set_state.json create mode 100644 assets/files/protocols/hmi/controller.get_params.json create mode 100644 assets/files/protocols/hmi/controller.heart.json create mode 100644 assets/files/protocols/hmi/controller.reboot.json create mode 100644 assets/files/protocols/hmi/controller.set_params.json create mode 100644 assets/files/protocols/hmi/device.get_params.json create mode 100644 assets/files/protocols/hmi/diagnosis.get_params.json create mode 100644 assets/files/protocols/hmi/diagnosis.open.json create mode 100644 assets/files/protocols/hmi/diagnosis.save.json create mode 100644 assets/files/protocols/hmi/diagnosis.set_params.json create mode 100644 assets/files/protocols/hmi/drag.get_params.json create mode 100644 assets/files/protocols/hmi/drag.set_params.json create mode 100644 assets/files/protocols/hmi/fieldbus_device.get_params.json create mode 100644 assets/files/protocols/hmi/fieldbus_device.load_cfg.json create mode 100644 assets/files/protocols/hmi/fieldbus_device.set_params.json create mode 100644 assets/files/protocols/hmi/io_device.load_cfg.json create mode 100644 assets/files/protocols/hmi/jog.get_params.json create mode 100644 assets/files/protocols/hmi/jog.set_params.json create mode 100644 assets/files/protocols/hmi/jog.start.json create mode 100644 assets/files/protocols/hmi/log_code.data.code_list.json create mode 100644 assets/files/protocols/hmi/log_code.data.json create mode 100644 assets/files/protocols/hmi/modbus.get_params.json create mode 100644 assets/files/protocols/hmi/modbus.get_values.json create mode 100644 assets/files/protocols/hmi/modbus.load_cfg.json create mode 100644 assets/files/protocols/hmi/modbus.set_params.json create mode 100644 assets/files/protocols/hmi/move.get_joint_pos.json create mode 100644 assets/files/protocols/hmi/move.get_monitor_cfg.json create mode 100644 assets/files/protocols/hmi/move.get_params.json create mode 100644 assets/files/protocols/hmi/move.get_pos.json create mode 100644 assets/files/protocols/hmi/move.get_quickstop_distance.json create mode 100644 assets/files/protocols/hmi/move.get_quickturn_pos.json create mode 100644 assets/files/protocols/hmi/move.quick_turn.json create mode 100644 assets/files/protocols/hmi/move.set_monitor_cfg.json create mode 100644 assets/files/protocols/hmi/move.set_params.json create mode 100644 assets/files/protocols/hmi/move.set_quickstop_distance.json create mode 100644 assets/files/protocols/hmi/move.set_quickturn_pos.json create mode 100644 assets/files/protocols/hmi/move.stop.json create mode 100644 assets/files/protocols/hmi/overview.get_autoload.json create mode 100644 assets/files/protocols/hmi/overview.get_cur_prj.json create mode 100644 assets/files/protocols/hmi/overview.reload.json create mode 100644 assets/files/protocols/hmi/overview.set_autoload.json create mode 100644 assets/files/protocols/hmi/register.set_value.json create mode 100644 assets/files/protocols/hmi/rl_task.pp_to_main.json create mode 100644 assets/files/protocols/hmi/rl_task.run.json create mode 100644 assets/files/protocols/hmi/rl_task.set_run_params.json create mode 100644 assets/files/protocols/hmi/rl_task.stop.json create mode 100644 assets/files/protocols/hmi/safety.safety_area.overall_enable.json create mode 100644 assets/files/protocols/hmi/safety.safety_area.safety_area_enable.json create mode 100644 assets/files/protocols/hmi/safety.safety_area.set_param.json create mode 100644 assets/files/protocols/hmi/safety.safety_area.signal_enable.json create mode 100644 assets/files/protocols/hmi/safety_area_data.json create mode 100644 assets/files/protocols/hmi/servo.clear_alarm.json create mode 100644 assets/files/protocols/hmi/socket.get_params.json create mode 100644 assets/files/protocols/hmi/socket.set_params.json create mode 100644 assets/files/protocols/hmi/soft_limit.get_params.json create mode 100644 assets/files/protocols/hmi/soft_limit.set_params.json create mode 100644 assets/files/protocols/hmi/state.get_state.json create mode 100644 assets/files/protocols/hmi/state.get_tp_mode.json create mode 100644 assets/files/protocols/hmi/state.set_tp_mode.json create mode 100644 assets/files/protocols/hmi/state.switch_auto.json create mode 100644 assets/files/protocols/hmi/state.switch_manual.json create mode 100644 assets/files/protocols/hmi/state.switch_motor_off.json create mode 100644 assets/files/protocols/hmi/state.switch_motor_on.json create mode 100644 assets/files/protocols/hmi/system_io.query_configuration.json create mode 100644 assets/files/protocols/hmi/system_io.query_event_cfg.json create mode 100644 assets/files/protocols/hmi/system_io.update_configuration.json create mode 100644 assets/files/protocols/hmi/原理/协议原理.xlsx create mode 100644 assets/files/protocols/hmi/原理/解包示例.txt create mode 100644 assets/files/version/file_version_info.txt create mode 100644 assets/files/version/release_change.md create mode 100644 assets/files/version/requirements.txt create mode 100644 assets/files/version/version create mode 100644 assets/media/icon.ico create mode 100644 assets/media/updated.png create mode 100644 assets/media/upgrade.png create mode 100644 code/aio.py create mode 100644 code/analysis/brake.py create mode 100644 code/analysis/current.py create mode 100644 code/analysis/iso.py create mode 100644 code/analysis/wavelogger.py create mode 100644 code/autotest/do_brake.py create mode 100644 code/autotest/do_current.py create mode 100644 code/common/clibs.py create mode 100644 code/common/openapi.py create mode 100644 code/durable/create_plot.py create mode 100644 code/durable/factory_test.py create mode 100644 code/test.py create mode 100644 code/ui/login_window.py create mode 100644 code/ui/main_window.py create mode 100644 code/ui/reset_window.py create mode 100644 readme.md create mode 100644 ui/login.ui create mode 100644 ui/main.ui create mode 100644 ui/reset.ui diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b40a6ad --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +venv/ +.idea/ +**/__pycache__/ +assets/files/examples/ \ No newline at end of file diff --git a/assets/files/projects/NB4h_R580_3G.zip b/assets/files/projects/NB4h_R580_3G.zip new file mode 100644 index 0000000000000000000000000000000000000000..a7ee4ae6f794ef09db6462e638ed4913f5dab661 GIT binary patch literal 28708 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/files/projects/configs.xlsx b/assets/files/projects/configs.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..45205607916dbc90cc432b0e7820c17e562e3086 GIT binary patch literal 227620 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

lq1H21OZFq%c_02W5dyu~^C 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 OP=6kwZ=UQ7LjNB)*48Hg literal 0 HcmV?d00001 diff --git a/assets/files/projects/fieldbus_device.json b/assets/files/projects/fieldbus_device.json new file mode 100644 index 0000000..e7de260 --- /dev/null +++ b/assets/files/projects/fieldbus_device.json @@ -0,0 +1,75 @@ +{ + "device_list": [ + { + "enable": true, + "endian": 1, + "extend_attr": { + "RTU": { + "serial_name": "" + }, + "TCP": { + "ip": "0.0.0.0", + "port": 502 + }, + "coils": { + "max_num": 16, + "start_addr": 0 + }, + "discrete_input": { + "max_num": 16, + "start_addr": 0 + }, + "hold_register": { + "max_num": 2000, + "start_addr": 40000 + }, + "input_register": { + "max_num": 2000, + "start_addr": 4000 + }, + "protol_type": "TCP", + "slaver_id": 1 + }, + "mode": "slaver", + "name": "autotest", + "type": "MODBUS" + } + ], + "support_types": [ + { + "device_type": "MODBUS", + "support_mode": { + "master": 10, + "slaver": 10 + } + }, + { + "device_type": "CCLINK", + "support_mode": { + "master": 0, + "slaver": 1 + } + }, + { + "device_type": "ETHERCAT", + "support_mode": { + "master": 0, + "slaver": 10 + } + }, + { + "device_type": "PROFINET", + "support_mode": { + "master": 0, + "slaver": 1 + } + }, + { + "device_type": "EtherNetIP", + "support_mode": { + "master": 0, + "slaver": 1 + } + } + ] +} diff --git a/assets/files/projects/registers.json b/assets/files/projects/registers.json new file mode 100644 index 0000000..2936126 --- /dev/null +++ b/assets/files/projects/registers.json @@ -0,0 +1,520 @@ +{ + "MODBUS": [ + { + "property": { + "device_name": "autotest", + "endian": 1 + }, + "regs": { + "rd": [ + { + "addr": 40000, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "ctrl_clear_alarm", + "len": 1, + "name": "r_clear_alarm", + "retain": false, + "type": "bool" + }, + { + "addr": 40001, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "ctrl_estop_reset", + "len": 1, + "name": "r_estop_reset", + "retain": false, + "type": "bool" + }, + { + "addr": 40002, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "ctrl_estop_reset_clear_alarm", + "len": 1, + "name": "r_onekey_reset", + "retain": false, + "type": "bool" + }, + { + "addr": 40003, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "ctrl_motor_off", + "len": 1, + "name": "r_motor_off", + "retain": false, + "type": "bool" + }, + { + "addr": 40004, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "ctrl_motor_on", + "len": 1, + "name": "r_motor_on", + "retain": false, + "type": "bool" + }, + { + "addr": 40005, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "ctrl_motoron_pptomain_start", + "len": 1, + "name": "r_onekey_start", + "retain": false, + "type": "bool" + }, + { + "addr": 40006, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "ctrl_motoron_start", + "len": 1, + "name": "r_motoron_start", + "retain": false, + "type": "bool" + }, + { + "addr": 40007, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "ctrl_pause_motoroff", + "len": 1, + "name": "r_pause_motoroff", + "retain": false, + "type": "bool" + }, + { + "addr": 40008, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "ctrl_pptomain", + "len": 1, + "name": "r_pp2main", + "retain": false, + "type": "bool" + }, + { + "addr": 40009, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "ctrl_program_start", + "len": 1, + "name": "r_prog_start", + "retain": false, + "type": "bool" + }, + { + "addr": 40010, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "ctrl_program_stop", + "len": 1, + "name": "r_prog_stop", + "retain": false, + "type": "bool" + }, + { + "addr": 40011, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "ctrl_reduced_mode", + "len": 1, + "name": "r_reduced_mode", + "retain": false, + "type": "bool" + }, + { + "addr": 40012, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "ctrl_soft_estop", + "len": 1, + "name": "r_soft_estop", + "retain": false, + "type": "bool" + }, + { + "addr": 40013, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "ctrl_switch_auto_motoron", + "len": 1, + "name": "r_auto_motoron", + "retain": false, + "type": "bool" + }, + { + "addr": 40014, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "ctrl_switch_operation_auto", + "len": 1, + "name": "r_switch_auto", + "retain": false, + "type": "bool" + }, + { + "addr": 40015, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "ctrl_switch_operation_manu", + "len": 1, + "name": "r_switch_manual", + "retain": false, + "type": "bool" + }, + { + "addr": 40016, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "enable_safe_region01", + "len": 1, + "name": "r_safe_region01", + "retain": false, + "type": "bool" + }, + { + "addr": 40017, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "enable_safe_region02", + "len": 1, + "name": "r_safe_region02", + "retain": false, + "type": "bool" + }, + { + "addr": 40018, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "enable_safe_region03", + "len": 1, + "name": "r_safe_region03", + "retain": false, + "type": "bool" + }, + { + "addr": 40100, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "", + "len": 1, + "name": "act", + "retain": false, + "type": "bool" + } + ], + "rdwr": [ + { + "addr": 40500, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "sta_alarm", + "len": 1, + "name": "w_alarm_state", + "retain": false, + "type": "bool" + }, + { + "addr": 40501, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "sta_collision", + "len": 1, + "name": "w_clsn_alarm_stat", + "retain": false, + "type": "bool" + }, + { + "addr": 40502, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "sta_collision_open", + "len": 1, + "name": "w_clsn_open_stat", + "retain": false, + "type": "bool" + }, + { + "addr": 40503, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "sta_controller_is_running", + "len": 1, + "name": "w_controller_running", + "retain": false, + "type": "bool" + }, + { + "addr": 40504, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "sta_encoder_low_battery", + "len": 1, + "name": "w_encoder_low", + "retain": false, + "type": "bool" + }, + { + "addr": 40505, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "sta_estop", + "len": 1, + "name": "w_estop_stat", + "retain": false, + "type": "bool" + }, + { + "addr": 40506, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "sta_motor", + "len": 1, + "name": "w_motor_stat", + "retain": false, + "type": "bool" + }, + { + "addr": 40507, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "sta_operation_mode", + "len": 1, + "name": "w_operation_mode", + "retain": false, + "type": "bool" + }, + { + "addr": 40508, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "sta_program", + "len": 1, + "name": "w_prog_stat", + "retain": false, + "type": "bool" + }, + { + "addr": 40509, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "sta_program_not_run", + "len": 1, + "name": "w_prog_not_run", + "retain": false, + "type": "bool" + }, + { + "addr": 40510, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "sta_program_reset", + "len": 1, + "name": "w_prog_reset", + "retain": false, + "type": "bool" + }, + { + "addr": 40511, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "sta_reduced_mode", + "len": 1, + "name": "w_reduced_mode_stat", + "retain": false, + "type": "bool" + }, + { + "addr": 40512, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "sta_robot_is_busy", + "len": 1, + "name": "w_robot_is_busy", + "retain": false, + "type": "bool" + }, + { + "addr": 40513, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "sta_robot_moving", + "len": 1, + "name": "w_robot_moving", + "retain": false, + "type": "bool" + }, + { + "addr": 40514, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "sta_safe_door", + "len": 1, + "name": "w_safe_door", + "retain": false, + "type": "bool" + }, + { + "addr": 40515, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "sta_safe_region01", + "len": 1, + "name": "w_safe_region01", + "retain": false, + "type": "bool" + }, + { + "addr": 40516, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "sta_safe_region02", + "len": 1, + "name": "w_safe_region02", + "retain": false, + "type": "bool" + }, + { + "addr": 40517, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "sta_safe_region03", + "len": 1, + "name": "w_safe_region03", + "retain": false, + "type": "bool" + }, + { + "addr": 40518, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "sta_soft_estop", + "len": 1, + "name": "w_soft_estop_stat", + "retain": false, + "type": "bool" + }, + { + "addr": 40600, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "", + "len": 1, + "name": "ready_to_go", + "retain": false, + "type": "bool" + }, + { + "addr": 40601, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "", + "len": 1, + "name": "scenario_time", + "retain": false, + "type": "float" + }, + { + "addr": 40603, + "addr_1st": 0, + "addr_2nd": 0, + "bit_bias": 0, + "byte_bias": 0, + "function": "", + "len": 1, + "name": "capture_start", + "retain": false, + "type": "bool" + } + ] + } + } + ] +} diff --git a/assets/files/projects/registers.xml b/assets/files/projects/registers.xml new file mode 100644 index 0000000..7d71547 --- /dev/null +++ b/assets/files/projects/registers.xml @@ -0,0 +1,843 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/files/protocols/hmi/collision.get_params.json b/assets/files/protocols/hmi/collision.get_params.json new file mode 100644 index 0000000..62b0a45 --- /dev/null +++ b/assets/files/protocols/hmi/collision.get_params.json @@ -0,0 +1,5 @@ +{ + "id": "xxxxxxxxxxx", + "module": "safety", + "command": "collision.get_params" +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/collision.set_params.json b/assets/files/protocols/hmi/collision.set_params.json new file mode 100644 index 0000000..813f7ca --- /dev/null +++ b/assets/files/protocols/hmi/collision.set_params.json @@ -0,0 +1,6 @@ +{ + "id": "xxxxxxxxxxx", + "module": "safety", + "command": "collision.set_params", + "data": null +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/collision.set_state.json b/assets/files/protocols/hmi/collision.set_state.json new file mode 100644 index 0000000..04bfbb5 --- /dev/null +++ b/assets/files/protocols/hmi/collision.set_state.json @@ -0,0 +1,7 @@ +{ + "module": "safety", + "command": "collision.set_state", + "data": { + "collision_state": false + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/controller.get_params.json b/assets/files/protocols/hmi/controller.get_params.json new file mode 100644 index 0000000..92dac53 --- /dev/null +++ b/assets/files/protocols/hmi/controller.get_params.json @@ -0,0 +1,5 @@ +{ + "id":"xxxxxxxxxxx", + "module":"system", + "command":"controller.get_params" +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/controller.heart.json b/assets/files/protocols/hmi/controller.heart.json new file mode 100644 index 0000000..27300d6 --- /dev/null +++ b/assets/files/protocols/hmi/controller.heart.json @@ -0,0 +1,5 @@ +{ + "id": "xxxxxxxxxxx", + "module": "system", + "command": "controller.heart" +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/controller.reboot.json b/assets/files/protocols/hmi/controller.reboot.json new file mode 100644 index 0000000..a0ef6da --- /dev/null +++ b/assets/files/protocols/hmi/controller.reboot.json @@ -0,0 +1,8 @@ +{ + "id": "xxxxxxxxxxx", + "module": "system", + "command": "controller.reboot", + "data": { + "arg": 6 + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/controller.set_params.json b/assets/files/protocols/hmi/controller.set_params.json new file mode 100644 index 0000000..3759298 --- /dev/null +++ b/assets/files/protocols/hmi/controller.set_params.json @@ -0,0 +1,8 @@ +{ + "id": "xxxxxxxxxxx", + "module": "system", + "command": "controller.set_params", + "data": { + "time": "2020-02-28 15:28:30" + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/device.get_params.json b/assets/files/protocols/hmi/device.get_params.json new file mode 100644 index 0000000..62cd79d --- /dev/null +++ b/assets/files/protocols/hmi/device.get_params.json @@ -0,0 +1,5 @@ +{ + "id": "xxxxxxxxxxx", + "module": "system", + "command": "device.get_params" +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/diagnosis.get_params.json b/assets/files/protocols/hmi/diagnosis.get_params.json new file mode 100644 index 0000000..7e57457 --- /dev/null +++ b/assets/files/protocols/hmi/diagnosis.get_params.json @@ -0,0 +1,8 @@ +{ + "id": "xxxxxxxxxxx", + "module": "robot", + "command": "diagnosis.get_params", + "data": { + "version": "1.4.1" + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/diagnosis.open.json b/assets/files/protocols/hmi/diagnosis.open.json new file mode 100644 index 0000000..7fe21c2 --- /dev/null +++ b/assets/files/protocols/hmi/diagnosis.open.json @@ -0,0 +1,12 @@ +{ + "id": "xxxxxxxxxxx", + "module": "robot", + "command": "diagnosis.open", + "data": { + "open": false, + "display_open": false, + "overrun": false, + "turn_area": false, + "delay_motion": false + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/diagnosis.save.json b/assets/files/protocols/hmi/diagnosis.save.json new file mode 100644 index 0000000..6564248 --- /dev/null +++ b/assets/files/protocols/hmi/diagnosis.save.json @@ -0,0 +1,8 @@ +{ + "id": "xxxxxxxxxxx", + "module": "robot", + "command": "diagnosis.save", + "data": { + "save": true + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/diagnosis.set_params.json b/assets/files/protocols/hmi/diagnosis.set_params.json new file mode 100644 index 0000000..0bb13cc --- /dev/null +++ b/assets/files/protocols/hmi/diagnosis.set_params.json @@ -0,0 +1,10 @@ +{ + "id": "xxxxxxxxxxx", + "module": "robot", + "command": "diagnosis.set_params", + "data": { + "display_pdo_params": [], + "frequency": 50, + "version": "1.4.1" + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/drag.get_params.json b/assets/files/protocols/hmi/drag.get_params.json new file mode 100644 index 0000000..9c56d28 --- /dev/null +++ b/assets/files/protocols/hmi/drag.get_params.json @@ -0,0 +1,5 @@ +{ + "id": "xxxxxxxxxxx", + "module": "dynamic", + "command": "drag.get_params" +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/drag.set_params.json b/assets/files/protocols/hmi/drag.set_params.json new file mode 100644 index 0000000..52b0b6f --- /dev/null +++ b/assets/files/protocols/hmi/drag.set_params.json @@ -0,0 +1,10 @@ +{ + "id": "xxxxxxxxxxx", + "module": "dynamic", + "command": "drag.set_params", + "data": { + "enable": true, + "space": 0, + "type": 0 + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/fieldbus_device.get_params.json b/assets/files/protocols/hmi/fieldbus_device.get_params.json new file mode 100644 index 0000000..6eca806 --- /dev/null +++ b/assets/files/protocols/hmi/fieldbus_device.get_params.json @@ -0,0 +1,5 @@ +{ + "id": "xxxxxxxxxxx", + "module": "fieldbus", + "command": "fieldbus_device.get_params" +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/fieldbus_device.load_cfg.json b/assets/files/protocols/hmi/fieldbus_device.load_cfg.json new file mode 100644 index 0000000..79e1731 --- /dev/null +++ b/assets/files/protocols/hmi/fieldbus_device.load_cfg.json @@ -0,0 +1,8 @@ +{ + "id": "xxxxxxxxxxx", + "module": "fieldbus", + "command": "fieldbus_device.load_cfg", + "data": { + "file_name": "fieldbus_device.json" + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/fieldbus_device.set_params.json b/assets/files/protocols/hmi/fieldbus_device.set_params.json new file mode 100644 index 0000000..d6828f6 --- /dev/null +++ b/assets/files/protocols/hmi/fieldbus_device.set_params.json @@ -0,0 +1,9 @@ +{ + "id": "xxxxxxxxxxx", + "module": "fieldbus", + "command": "fieldbus_device.set_params", + "data": { + "device_name": "modbus_1", + "enable": true + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/io_device.load_cfg.json b/assets/files/protocols/hmi/io_device.load_cfg.json new file mode 100644 index 0000000..790f75c --- /dev/null +++ b/assets/files/protocols/hmi/io_device.load_cfg.json @@ -0,0 +1,5 @@ +{ + "id": "xxxxxxxxxxx", + "module": "io", + "command": "io_device.load_cfg" +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/jog.get_params.json b/assets/files/protocols/hmi/jog.get_params.json new file mode 100644 index 0000000..db98a71 --- /dev/null +++ b/assets/files/protocols/hmi/jog.get_params.json @@ -0,0 +1,5 @@ +{ + "id": "xxxxxxxxxxx", + "module": "motion", + "command": "jog.get_params" +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/jog.set_params.json b/assets/files/protocols/hmi/jog.set_params.json new file mode 100644 index 0000000..6c89d14 --- /dev/null +++ b/assets/files/protocols/hmi/jog.set_params.json @@ -0,0 +1,10 @@ +{ + "id": "xxxxxxxxxxx", + "module": "motion", + "command": "jog.set_params", + "data": { + "step": 1000, + "override": 0.2, + "space": 5 + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/jog.start.json b/assets/files/protocols/hmi/jog.start.json new file mode 100644 index 0000000..afe54cc --- /dev/null +++ b/assets/files/protocols/hmi/jog.start.json @@ -0,0 +1,10 @@ +{ + "id": "xxxxxxxxxxx", + "module": "motion", + "command": "jog.start", + "data": { + "index": 0, + "direction": true, + "is_ext": false + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/log_code.data.code_list.json b/assets/files/protocols/hmi/log_code.data.code_list.json new file mode 100644 index 0000000..c6f1ee2 --- /dev/null +++ b/assets/files/protocols/hmi/log_code.data.code_list.json @@ -0,0 +1,8 @@ +{ + "id": "log_code.data.code_list", + "s": { + "log_code.data": { + "code_list": [] + } + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/log_code.data.json b/assets/files/protocols/hmi/log_code.data.json new file mode 100644 index 0000000..021b678 --- /dev/null +++ b/assets/files/protocols/hmi/log_code.data.json @@ -0,0 +1,5 @@ +{ + "g": { + "log_code.data": "null" + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/modbus.get_params.json b/assets/files/protocols/hmi/modbus.get_params.json new file mode 100644 index 0000000..8bdde30 --- /dev/null +++ b/assets/files/protocols/hmi/modbus.get_params.json @@ -0,0 +1,5 @@ +{ + "id": "xxxxxxxxxxx", + "module": "fieldbus", + "command": "modbus.get_params" +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/modbus.get_values.json b/assets/files/protocols/hmi/modbus.get_values.json new file mode 100644 index 0000000..d356362 --- /dev/null +++ b/assets/files/protocols/hmi/modbus.get_values.json @@ -0,0 +1,8 @@ +{ + "id": "xxxxxxxxxxx", + "module": "fieldbus", + "command": "modbus.get_values", + "data": { + "mode": "all" + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/modbus.load_cfg.json b/assets/files/protocols/hmi/modbus.load_cfg.json new file mode 100644 index 0000000..95d5e69 --- /dev/null +++ b/assets/files/protocols/hmi/modbus.load_cfg.json @@ -0,0 +1,8 @@ +{ + "id": "xxxxxxxxxxx", + "module": "fieldbus", + "command": "modbus.load_cfg", + "data": { + "file" : "registers.json" + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/modbus.set_params.json b/assets/files/protocols/hmi/modbus.set_params.json new file mode 100644 index 0000000..46a651e --- /dev/null +++ b/assets/files/protocols/hmi/modbus.set_params.json @@ -0,0 +1,12 @@ +{ + "id": "xxxxxxxxxxx", + "module": "fieldbus", + "command": "modbus.set_params", + "data": { + "enable_slave": true, + "ip": "192.168.0.160", + "port": 502, + "slave_id": 0, + "enable_master": false + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/move.get_joint_pos.json b/assets/files/protocols/hmi/move.get_joint_pos.json new file mode 100644 index 0000000..9754405 --- /dev/null +++ b/assets/files/protocols/hmi/move.get_joint_pos.json @@ -0,0 +1,5 @@ +{ + "id": "xxxxxxxxxxx", + "module": "motion", + "command": "move.get_joint_pos" +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/move.get_monitor_cfg.json b/assets/files/protocols/hmi/move.get_monitor_cfg.json new file mode 100644 index 0000000..75fb033 --- /dev/null +++ b/assets/files/protocols/hmi/move.get_monitor_cfg.json @@ -0,0 +1,5 @@ +{ + "id": "xxxxxxxxxxx", + "module": "motion", + "command": "move.get_monitor_cfg" +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/move.get_params.json b/assets/files/protocols/hmi/move.get_params.json new file mode 100644 index 0000000..bfe06e2 --- /dev/null +++ b/assets/files/protocols/hmi/move.get_params.json @@ -0,0 +1,5 @@ +{ + "id": "xxxxxxxxxxx", + "module": "motion", + "command": "move.get_params" +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/move.get_pos.json b/assets/files/protocols/hmi/move.get_pos.json new file mode 100644 index 0000000..9124631 --- /dev/null +++ b/assets/files/protocols/hmi/move.get_pos.json @@ -0,0 +1,5 @@ +{ + "id": "xxxxxxxxxxx", + "module": "motion", + "command": "move.get_pos" +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/move.get_quickstop_distance.json b/assets/files/protocols/hmi/move.get_quickstop_distance.json new file mode 100644 index 0000000..ac5f0b2 --- /dev/null +++ b/assets/files/protocols/hmi/move.get_quickstop_distance.json @@ -0,0 +1,5 @@ +{ + "id": "xxxxxxxxxxx", + "module": "motion", + "command": "move.get_quickstop_distance" +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/move.get_quickturn_pos.json b/assets/files/protocols/hmi/move.get_quickturn_pos.json new file mode 100644 index 0000000..74ecae0 --- /dev/null +++ b/assets/files/protocols/hmi/move.get_quickturn_pos.json @@ -0,0 +1,5 @@ +{ + "id" : "xxxxxxxxx", + "module": "motion", + "command": "move.get_quickturn_pos" +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/move.quick_turn.json b/assets/files/protocols/hmi/move.quick_turn.json new file mode 100644 index 0000000..3a72afc --- /dev/null +++ b/assets/files/protocols/hmi/move.quick_turn.json @@ -0,0 +1,8 @@ +{ + "id": "xxxxxxxxxxx", + "module": "motion", + "command": "move.quick_turn", + "data": { + "name":"home" + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/move.set_monitor_cfg.json b/assets/files/protocols/hmi/move.set_monitor_cfg.json new file mode 100644 index 0000000..68c3f35 --- /dev/null +++ b/assets/files/protocols/hmi/move.set_monitor_cfg.json @@ -0,0 +1,8 @@ +{ + "id": "xxxxxxxxxxx", + "module": "motion", + "command": "move.set_monitor_cfg", + "data": { + "ref_coordinate": 1 + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/move.set_params.json b/assets/files/protocols/hmi/move.set_params.json new file mode 100644 index 0000000..8788a6c --- /dev/null +++ b/assets/files/protocols/hmi/move.set_params.json @@ -0,0 +1,17 @@ +{ + "id": "xxxxxxxxxxx", + "module": "motion", + "command": "move.set_params", + "data": { + "MOTION": { + "JOINT_MAX_SPEED": [1.0,0.0,0.0,0.0,0.0,0.0], + "JOINT_MAX_ACC": [1.0,0.0,0.0,0.0,0.0,0.0], + "JOINT_MAX_JERK": [1.0,0.0,0.0], + "TCP_MAX_SPEED": 500, + "DEFAULT_ACC_PARAMS": [0.3,1.0], + "VEL_SMOOTH_FACTOR": 3.33, + "ACC_RAMPTIME_JOG": 0.01 + } + } + +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/move.set_quickstop_distance.json b/assets/files/protocols/hmi/move.set_quickstop_distance.json new file mode 100644 index 0000000..000d5f5 --- /dev/null +++ b/assets/files/protocols/hmi/move.set_quickstop_distance.json @@ -0,0 +1,8 @@ +{ + "id": "xxxxxxxxxxx", + "module": "motion", + "command": "move.set_quickstop_distance", + "data":{ + "distance": 2 + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/move.set_quickturn_pos.json b/assets/files/protocols/hmi/move.set_quickturn_pos.json new file mode 100644 index 0000000..164e442 --- /dev/null +++ b/assets/files/protocols/hmi/move.set_quickturn_pos.json @@ -0,0 +1,15 @@ +{ + "id" : "xxxxxxxxx", + "module": "motion", + "command": "move.set_quickturn_pos", + "data": { + "enable_home": false, + "enable_drag": false, + "enable_transport": false, + "joint_home": [0.0,0.0,0.0,0.0,0.0,0.0,0.0], + "joint_drag": [0.0,0.0,0.0,0.0,0.0,0.0,0.0], + "joint_transport": [0.0,0.0,0.0,0.0,0.0,0.0,0.0], + "end_posture": 0, + "home_error_range":[0.0,0.0,0.0,0.0,0.0,0.0,0.0] + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/move.stop.json b/assets/files/protocols/hmi/move.stop.json new file mode 100644 index 0000000..17648f0 --- /dev/null +++ b/assets/files/protocols/hmi/move.stop.json @@ -0,0 +1,8 @@ +{ + "id": "xxxxxxxxxxx", + "module": "motion", + "command": "move.stop", + "data":{ + "stoptype": 0 + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/overview.get_autoload.json b/assets/files/protocols/hmi/overview.get_autoload.json new file mode 100644 index 0000000..49e3b79 --- /dev/null +++ b/assets/files/protocols/hmi/overview.get_autoload.json @@ -0,0 +1,5 @@ +{ + "id": "xxxxxxxxxxx", + "module": "project", + "command": "overview.get_autoload" +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/overview.get_cur_prj.json b/assets/files/protocols/hmi/overview.get_cur_prj.json new file mode 100644 index 0000000..8e50c5a --- /dev/null +++ b/assets/files/protocols/hmi/overview.get_cur_prj.json @@ -0,0 +1,5 @@ +{ + "id": "xxxxxxxxxxx", + "module": "project", + "command": "overview.get_cur_prj" +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/overview.reload.json b/assets/files/protocols/hmi/overview.reload.json new file mode 100644 index 0000000..a2485a2 --- /dev/null +++ b/assets/files/protocols/hmi/overview.reload.json @@ -0,0 +1,9 @@ +{ + "id": "xxxxxxxxxxx", + "module": "project", + "command": "overview.reload", + "data": { + "prj_path": "", + "tasks": [] + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/overview.set_autoload.json b/assets/files/protocols/hmi/overview.set_autoload.json new file mode 100644 index 0000000..791d4fb --- /dev/null +++ b/assets/files/protocols/hmi/overview.set_autoload.json @@ -0,0 +1,8 @@ +{ + "id": "xxxxxxxxxxx", + "module": "project", + "command": "overview.set_autoload", + "data": { + "autoload_prj_path": "" + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/register.set_value.json b/assets/files/protocols/hmi/register.set_value.json new file mode 100644 index 0000000..6b64af3 --- /dev/null +++ b/assets/files/protocols/hmi/register.set_value.json @@ -0,0 +1,11 @@ +{ + "id": "xxxxxxxxxxx", + "module": "fieldbus", + "command": "register.set_value", + "data": { + "name": "", + "type": "bool", + "bias": 0, + "value": 0 + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/rl_task.pp_to_main.json b/assets/files/protocols/hmi/rl_task.pp_to_main.json new file mode 100644 index 0000000..3e2247f --- /dev/null +++ b/assets/files/protocols/hmi/rl_task.pp_to_main.json @@ -0,0 +1,8 @@ +{ + "id": "xxxxxxxxxxx", + "module": "project", + "command": "rl_task.pp_to_main", + "data": { + "tasks": [] + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/rl_task.run.json b/assets/files/protocols/hmi/rl_task.run.json new file mode 100644 index 0000000..52e89a1 --- /dev/null +++ b/assets/files/protocols/hmi/rl_task.run.json @@ -0,0 +1,8 @@ +{ + "id": "xxxxxxxxxxx", + "module": "project", + "command": "rl_task.run", + "data": { + "tasks": [] + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/rl_task.set_run_params.json b/assets/files/protocols/hmi/rl_task.set_run_params.json new file mode 100644 index 0000000..8490383 --- /dev/null +++ b/assets/files/protocols/hmi/rl_task.set_run_params.json @@ -0,0 +1,9 @@ +{ + "id": "xxxxxxxxxxx", + "module": "project", + "command": "rl_task.set_run_params", + "data": { + "loop_mode": true, + "override": 1.0 + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/rl_task.stop.json b/assets/files/protocols/hmi/rl_task.stop.json new file mode 100644 index 0000000..03b0f6b --- /dev/null +++ b/assets/files/protocols/hmi/rl_task.stop.json @@ -0,0 +1,8 @@ +{ + "id": "xxxxxxxxxxx", + "module": "project", + "command": "rl_task.stop", + "data": { + "tasks": [] + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/safety.safety_area.overall_enable.json b/assets/files/protocols/hmi/safety.safety_area.overall_enable.json new file mode 100644 index 0000000..517e64b --- /dev/null +++ b/assets/files/protocols/hmi/safety.safety_area.overall_enable.json @@ -0,0 +1,7 @@ +{ + "c": { + "safety.safety_area.overall_enable": { + "enable": true + } + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/safety.safety_area.safety_area_enable.json b/assets/files/protocols/hmi/safety.safety_area.safety_area_enable.json new file mode 100644 index 0000000..505fe50 --- /dev/null +++ b/assets/files/protocols/hmi/safety.safety_area.safety_area_enable.json @@ -0,0 +1,8 @@ +{ + "c": { + "safety.safety_area.safety_area_enable": { + "id": 0, + "enable": true + } + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/safety.safety_area.set_param.json b/assets/files/protocols/hmi/safety.safety_area.set_param.json new file mode 100644 index 0000000..28b65da --- /dev/null +++ b/assets/files/protocols/hmi/safety.safety_area.set_param.json @@ -0,0 +1,5 @@ +{ + "c": { + "safety.safety_area.set_param": null + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/safety.safety_area.signal_enable.json b/assets/files/protocols/hmi/safety.safety_area.signal_enable.json new file mode 100644 index 0000000..8a1fbac --- /dev/null +++ b/assets/files/protocols/hmi/safety.safety_area.signal_enable.json @@ -0,0 +1,7 @@ +{ + "c": { + "safety.safety_area.signal_enable": { + "signal": true + } + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/safety_area_data.json b/assets/files/protocols/hmi/safety_area_data.json new file mode 100644 index 0000000..e9b982a --- /dev/null +++ b/assets/files/protocols/hmi/safety_area_data.json @@ -0,0 +1,5 @@ +{ + "g": { + "safety_area_data": null + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/servo.clear_alarm.json b/assets/files/protocols/hmi/servo.clear_alarm.json new file mode 100644 index 0000000..bc3b7f8 --- /dev/null +++ b/assets/files/protocols/hmi/servo.clear_alarm.json @@ -0,0 +1,5 @@ +{ + "id": "xxxxxxxxxxx", + "module": "robot", + "command": "servo.clear_alarm" +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/socket.get_params.json b/assets/files/protocols/hmi/socket.get_params.json new file mode 100644 index 0000000..2971e6a --- /dev/null +++ b/assets/files/protocols/hmi/socket.get_params.json @@ -0,0 +1,5 @@ +{ + "id": "xxxxxxxxxxx", + "module": "network", + "command": "socket.get_params" +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/socket.set_params.json b/assets/files/protocols/hmi/socket.set_params.json new file mode 100644 index 0000000..9635cae --- /dev/null +++ b/assets/files/protocols/hmi/socket.set_params.json @@ -0,0 +1,17 @@ +{ + "id": "xxxxxxxxxxx", + "module": "network", + "command": "socket.set_params", + "data": { + "enable": true, + "ip": "", + "name": "c1", + "port": "8080", + "suffix": "\r", + "type": 1, + "reconnect_flag": false, + "auto_connect": true, + "disconnection_triggering_behavior": 0, + "disconnection_detection_time": 10 + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/soft_limit.get_params.json b/assets/files/protocols/hmi/soft_limit.get_params.json new file mode 100644 index 0000000..6f1f222 --- /dev/null +++ b/assets/files/protocols/hmi/soft_limit.get_params.json @@ -0,0 +1,5 @@ +{ + "id": "xxxxxxxxxxx", + "module": "safety", + "command": "soft_limit.get_params" +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/soft_limit.set_params.json b/assets/files/protocols/hmi/soft_limit.set_params.json new file mode 100644 index 0000000..a0a3844 --- /dev/null +++ b/assets/files/protocols/hmi/soft_limit.set_params.json @@ -0,0 +1,12 @@ +{ + "id": "xxxxxxxxxxx", + "module": "safety", + "command": "soft_limit.set_params", + "data": { + "enable": true, + "upper": [0,0,0,0,0,0,0], + "lower": [0,0,0,0,0,0,0], + "reduced_upper": [0,0,0,0,0,0,0], + "reduced_lower": [0,0,0,0,0,0,0] + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/state.get_state.json b/assets/files/protocols/hmi/state.get_state.json new file mode 100644 index 0000000..1a2b981 --- /dev/null +++ b/assets/files/protocols/hmi/state.get_state.json @@ -0,0 +1,5 @@ +{ + "id": "xxxxxxxxxxx", + "module": "system", + "command": "state.get_state" +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/state.get_tp_mode.json b/assets/files/protocols/hmi/state.get_tp_mode.json new file mode 100644 index 0000000..5b59ed0 --- /dev/null +++ b/assets/files/protocols/hmi/state.get_tp_mode.json @@ -0,0 +1,5 @@ +{ + "id": "xxxxxxxxxxx", + "module": "system", + "command": "state.get_tp_mode" +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/state.set_tp_mode.json b/assets/files/protocols/hmi/state.set_tp_mode.json new file mode 100644 index 0000000..608dd02 --- /dev/null +++ b/assets/files/protocols/hmi/state.set_tp_mode.json @@ -0,0 +1,8 @@ +{ + "id": "xxxxxxxxxxx", + "module": "system", + "command": "state.set_tp_mode", + "data": { + "tp_mode": "with" + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/state.switch_auto.json b/assets/files/protocols/hmi/state.switch_auto.json new file mode 100644 index 0000000..0c0a872 --- /dev/null +++ b/assets/files/protocols/hmi/state.switch_auto.json @@ -0,0 +1,5 @@ +{ + "id": "xxxxxxxxxxx", + "module": "system", + "command": "state.switch_auto" +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/state.switch_manual.json b/assets/files/protocols/hmi/state.switch_manual.json new file mode 100644 index 0000000..0451d04 --- /dev/null +++ b/assets/files/protocols/hmi/state.switch_manual.json @@ -0,0 +1,5 @@ +{ + "id": "xxxxxxxxxxx", + "module": "system", + "command": "state.switch_manual" +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/state.switch_motor_off.json b/assets/files/protocols/hmi/state.switch_motor_off.json new file mode 100644 index 0000000..0618931 --- /dev/null +++ b/assets/files/protocols/hmi/state.switch_motor_off.json @@ -0,0 +1,5 @@ +{ + "id": "xxxxxxxxxxx", + "module": "system", + "command": "state.switch_motor_off" +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/state.switch_motor_on.json b/assets/files/protocols/hmi/state.switch_motor_on.json new file mode 100644 index 0000000..0bbcc72 --- /dev/null +++ b/assets/files/protocols/hmi/state.switch_motor_on.json @@ -0,0 +1,5 @@ +{ + "id": "xxxxxxxxxxx", + "module": "system", + "command": "state.switch_motor_on" +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/system_io.query_configuration.json b/assets/files/protocols/hmi/system_io.query_configuration.json new file mode 100644 index 0000000..27d16bd --- /dev/null +++ b/assets/files/protocols/hmi/system_io.query_configuration.json @@ -0,0 +1,5 @@ +{ + "id": "xxxxxxxxxxx", + "module": "io", + "command": "system_io.query_configuration" +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/system_io.query_event_cfg.json b/assets/files/protocols/hmi/system_io.query_event_cfg.json new file mode 100644 index 0000000..5af3217 --- /dev/null +++ b/assets/files/protocols/hmi/system_io.query_event_cfg.json @@ -0,0 +1,5 @@ +{ + "id": "xxxxxxxxxxx", + "module": "io", + "command": "system_io.query_event_cfg" +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/system_io.update_configuration.json b/assets/files/protocols/hmi/system_io.update_configuration.json new file mode 100644 index 0000000..aaeb41f --- /dev/null +++ b/assets/files/protocols/hmi/system_io.update_configuration.json @@ -0,0 +1,9 @@ +{ + "id": "xxxxxxxxxxx", + "module": "io", + "command": "system_io.update_configuration", + "data": { + "input_system_io": {}, + "output_system_io": {} + } +} \ No newline at end of file diff --git a/assets/files/protocols/hmi/原理/协议原理.xlsx b/assets/files/protocols/hmi/原理/协议原理.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..7f1dcbb59d5b76a4e3b2e9a270e969e08c789de5 GIT binary patch literal 17658 zcmb8X1wb50^FEAAkl^kf9D)UR_u%es!QCAaTmvCE!F3_HOR(VX!2*lBehayK?=G+W zzuyeI+thS*cRf}0%X+8ijVv3EFn@V`bv{KD*1If%<5lqsoL+uNro<&Xs@7QyBWoj|3b{*6hH z2I#VGm^Q^RAs>S9l8!Hu_D9l-x&eZEPTo$8p>4;vv6C2v$nRJmDp4rjhX~%+Pp-KR zQLBG*(TCKwNcQ44bD`sHJ7{V1<#qE+$+U2z`+49^1Pm+tSa~13>IOG_)hc6KdL~yo zH6A}E&=e`Jk1E@i3mv#Ym&A-YE*wA$)vcz36L0sEXEUHGlrrM-BT0LpQ30C-flO$C ztIwBUOnyD@W|wm`sJg6XBoprRY3K28FCny|VKxGTMeuWjO^bENu<0o2C-vJQL!mf3 zB9ISrTNaiFg`CG_-y(Z-UFfzQPxIJO4HS*Ns3#sDK&{rNKiYn+ssB$$_27SD6a+cS z8ssR{zdOp<-qG~QQ}5&T6}m_;dRFA_kdY1J2@11HfdUvUa`T zMIU;-2yXluZ4CU?RX7_lU>7{l*g`Pi0=ZBsvaN-v)X@+5>|6Jjyp@mNcTwEFVR@rS zT#gKkH7waB*e>_MUBA6+^mhMQ(yQ#&wC-Ol?C_0&d1d~mIL(**QveU9dyccVP3q}j zLG*#mU*;_6hL(I9*U` zV1yn*KZ*?aq{o!ma?c~*La&DeI@K$WJJZ!lNsaHKJT~DvMPe`SY_9Qv65-9jjSE3@ z(Dwp$R_N}&cJ%lXKuQw^udP+Tzqt+hf|;Un-YN~EhWU*8K#*F(cK~*;6P%@TWxcBw zsmYaI^!sC7dfo9?d>qs7J;PLH&UF$aM{{Gbl%hqt&5ftv3YiGFPSs#<)lEvS21aZ+ z`v!K+2B`0LTylX67|lsSO3C2JBxUOmXtIttOzUt)p)A9eN`0JDJVyl+tk9oq#MSQs zv)t1i<)OPh8kwqz3|-a8*oZTOG}O_-VyR*Dkw#h!gGXtxP(BsZJ2}S+**#)A2gl3j zHs*GxUy0|-z6;#ZAk$Dxn)9Da`ETgke&5yF);A-5LiRrs6C?;(cbjKQ`p*@Re^qd^ zceHl0Fg11loxX&@t5pQB!N3#(z`$_+t?si5%QKB-S}PDpRfo0_@7}?7U$Wb^Z{V7E zC8c%T`?h%-zw?To_Qy^=-ft67(v8bhIu9;g%1tr@7Uo3#^j=2nl;b>KJM`o8*1mGO zxpRCxyL$KTWDB_7d3JoTdwRcdNQ^x?%JFcqx86Cp2i(|Ruvdtgh>3dq)Jp$wKk@i5 z8b+)z`sjVlo!QE`gG;2(&*R7I@P)k-~K8_SU2EcEj-IYCh|S#|`j8wc>Ge z@BT8y69Cee*uK5PLmlI)Am zI~vB5L8DO`wy^E@aC~p?xqf)_A*EeUzr`Hds%l8{9N!A*AS*?e1ZOzHM_4dTr6F*XVLKtx#-92y2FIR!1x^bTj zZ)`nhUTHNELaryN$$TQ@wg-;UpG*NNOW`R?Xl&s_MG&s4D>Opcv)Zttcn2(-2& z7uu&>X6d*2>fD_V-wCG}`Io}3w5dp_RIs3TqN4=nNdOib+hVG|uRO)@d^E+E;)sMBqtJY&deIl9r-9C?j?UB?%<$*DJZa%=G_*^~11sjjqukGt

NU*wx=>8I+)>gVW3?q>|Q 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 - 封包解包顺序:帧长度二字节/包长度四字节/协议二字节/预留二字节,\x04\x00:\x00\x00\tR:\x02:\x00 +> - 帧长度和包长度没有必然关系,单帧的时候是帧长度减去包长度等于6,包长度指的是所有内容的长度 +> - HMI内部每次发送1024个字节,进行分包,有效内容长度规则是:第一帧 frm_value-6(帧大小减去包头长度),第二帧(包含)及之后的帧,帧大小即是数据长度 + + +================================================================================================== +单包多帧示例 +\x04\x00\x00\x00-h\x02\x00 +frm_value = int.from_bytes(b"\x04\x00") = 1024 | 每次发送的帧的报文总长度,包括包头(如果有),但不包括帧头 +pkg_value = int.from_bytes(b"\x00\x00-h") = 11624 | 包长度是不包括所有的帧和包报文头的,单包单帧的情况也是一样的 +pkg_size = 6 字节 +有效数据长度 = 10240 + 402 + 1004 - 20 - 2(首个帧头) = 11624 +有效数据长度+包报文头 = 11624 + 6 = 11630 +有效数据长度+包报文头+帧报文头 = 11624 + 6 + 22 = 11652 +首帧长度 1018=frm_value-pkg_size=1024-6:如下是第一帧和第二帧的拼接 +b'{\n\t"command" : "diagnosis.result",\n\t"data" : \n\t[\n\t\t{\n\t\t\t"channel" : 0,\n\t\t\t"name" : "hw_joint_vel_feedback",\n\t\t\t"value" : \n\t\t\t[\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t"channel" : 1,\n\t\t\t"name" : "hw_joint_vel_feedback",\n\t\t\t"value" : \n\t\t\t[\n\t\t\t\t-0.0,\n\t\t\t\t-0.0,\n\t\t\t\t6.7411265092630715e-06,\n\t\t\t\t0.00049929277011941824,\n\t\t\t\t-0.0,\n\t\t\t\t-0.0,\n\t\t\t\t-0.0027270103772138884,\n\t\t\t\t-0.0,\n\t\t\t\t-0.0,\n\t\t\t\t0.00012044146029883355,\n\t\t\t\t-0.0022272681986605192,\n\t\t\t\t-0.0027270103772138884,\n\t\t\t\t0.0020686270214758614,\n\t\t\t\t-0.0027270103772138884,\n\t\t\t\t0.0020686270214758614,\n\t\t\t\t1.3482253018526145e-06,\n\t\t\t\t-0.002697798' +单帧长度 1024:如下是第二帧和第三帧的拼接 +b'8290070812,\n\t\t\t\t0.00050648330506263212,\n\t\t\t\t-0.0,\n\t\t\t\t0.0020753681479851243,\n\t\t\t\t2.8762139772855775e-05,\n\t\t\t\t-0.0,\n\t\t\t\t-0.0,\n\t\t\t\t-0.0,\n\t\t\t\t-0.0,\n\t\t\t\t3.5952674716069715e-05,\n\t\t\t\t-0.0,\n\t\t\t\t-0.0,\n\t\t\t\t0.0001496530085056402,\n\t\t\t\t0.0001276319952420475,\n\t\t\t\t-0.0027252127434780845,\n\t\t\t\t6.7411265092630715e-06,\n\t\t\t\t-0.0026977988290070812,\n\t\t\t\t8.5387602450665581e-06,\n\t\t\t\t0.0020686270214758614,\n\t\t\t\t0.0020686270214758614,\n\t\t\t\t-0.0,\n\t\t\t\t7.1905349432139438e-06,\n\t\t\t\t1.3482253018526145e-06,\n\t\t\t\t-0.0,\n\t\t\t\t-0.0,\n\t\t\t\t-0.0,\n\t\t\t\t-0.0,\n\t\t\t\t2.8762139772855775e-05,\n\t\t\t\t-0.0,\n\t\t\t\t6.7411265092630715e-06,\n\t\t\t\t-0.0,\n\t\t\t\t0.00049929277011941824,\n\t\t\t\t-0.0,\n\t\t\t\t-0.0\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t"channel" : 2,\n\t\t\t"name" : "hw_joint_vel_feedback",\n\t\t\t"value" : \n\t\t\t[\n\t\t\t\t1.1984224905356572e-06,\n\t\t\t\t0.00010705907582118537,\n\t\t\t\t-0.0,\n\t\t\t\t-0.0,\n\t\t\t\t-0.0,\n\t\t\t\t-0.0,\n\t\t\t\t-0.0,\n\t\t\t\t-0.0,\n\t\t\t\t-0.0,\n\t\t\t\t-0.0,\n\t\t\t\t-0.0,\n\t\t\t\t-0.0,\n\t\t\t\t-0.0,\n\t\t\t\t-0.0,\n\t\t\t\t-0.0,\n\t\t\t\t-0.0,\n\t\t\t\t-0.0024236097500266104,\n\t\t\t\t-0.0,\n\t\t\t\t-0.0,\n\t\t\t\t-0.0,\n\t\t\t\t-0.0,\n\t\t\t\t-0.0024240092241901226,\n\t\t' + +--------------------------------------------------------------------------------------------------- +第一帧有效 - 402 +b'{\n\t"command" : "diagnosis.result",\n\t"data" : \n\t[\n\t\t{\n\t\t\t"channel" : 0,\n\t\t\t"name" : "hw_joint_vel_feedback",\n\t\t\t"value" : \n\t\t\t[\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0.0,\n\t\t\t\t0' +--------------------------------------------------------------------------------------------------- +中间有 10 帧,共计 10240 个字节,也有 10 个 \x04\x00,共计 20 个字节 +--------------------------------------------------------------------------------------------------- +最后一帧有效 - 1004 +b'065717e-17,\n\t\t\t\t-5.5516340429065717e-17,\n\t\t\t\t-5.5516340429065717e-17,\n\t\t\t\t-5.5516340429065717e-17,\n\t\t\t\t-5.5516340429065717e-17,\n\t\t\t\t-5.5516340429065717e-17,\n\t\t\t\t-5.5516340429065717e-17,\n\t\t\t\t-5.5516340429065717e-17,\n\t\t\t\t-5.5516340429065717e-17,\n\t\t\t\t-5.5516340429065717e-17,\n\t\t\t\t-5.5516340429065717e-17,\n\t\t\t\t-5.5516340429065717e-17,\n\t\t\t\t-5.5516340429065717e-17,\n\t\t\t\t-5.5516340429065717e-17,\n\t\t\t\t-5.5516340429065717e-17,\n\t\t\t\t-5.5516340429065717e-17,\n\t\t\t\t-5.5516340429065717e-17,\n\t\t\t\t-5.5516340429065717e-17,\n\t\t\t\t-5.5516340429065717e-17,\n\t\t\t\t-5.5516340429065717e-17,\n\t\t\t\t-5.5516340429065717e-17,\n\t\t\t\t-5.5516340429065717e-17,\n\t\t\t\t-5.55163404\x01n29065717e-17,\n\t\t\t\t-5.5516340429065717e-17,\n\t\t\t\t-5.5516340429065717e-17,\n\t\t\t\t-5.5516340429065717e-17,\n\t\t\t\t-5.5516340429065717e-17,\n\t\t\t\t-5.5516340429065717e-17,\n\t\t\t\t-5.5516340429065717e-17,\n\t\t\t\t-5.5516340429065717e-17,\n\t\t\t\t-5.5516340429065717e-17,\n\t\t\t\t-5.5516340429065717e-17,\n\t\t\t\t-5.5516340429065717e-17,\n\t\t\t\t-5.5516340429065717e-17\n\t\t\t]\n\t\t}\n\t],\n\t"module" : "robot"\n}' +--------------------------------------------------------------------------------------------------- + +================================================================================================== +单包单帧示例 +\x00\\\x00\x00\x00V\x02\x00 +frm_value = int.from_bytes(b"\x00\\") = 92 | 每次发送的帧的报文总长度,包括包头(如果有),但不包括帧头 +pkg_value = int.from_bytes(b"\x00\x00\x00V") = 86 | 包长度是不包括所有的帧和包报文头的,单包单帧的情况也是一样的 +pkg_size = 6 +有效数据长度 = 86 +有效数据长度+包报文头 = 86 + 6 = 92 +有效数据长度+包报文头+帧报文头 = 86 + 6 +2 = 94 +帧长度 86=frm_value-pkg_size=92-6 + +b'\x00\\\x00\x00\x00V\x02\x00{\n\t"data" : \n\t{\n\t\t"name" : "xCore"\n\t},\n\t"id" : "controller.heart-1719734550.9790015"\n}' diff --git a/assets/files/version/file_version_info.txt b/assets/files/version/file_version_info.txt new file mode 100644 index 0000000..aa27a6e --- /dev/null +++ b/assets/files/version/file_version_info.txt @@ -0,0 +1,43 @@ +# UTF-8 +# +# For more details about fixed file info 'ffi' see: +# http://msdn.microsoft.com/en-us/library/ms646997.aspx +VSVersionInfo( + ffi=FixedFileInfo( + # filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4) + # Set not needed items to zero 0. + filevers=(0, 3, 1, 6), + prodvers=(0, 3, 1, 6), + # Contains a bitmask that specifies the valid bits 'flags'r + mask=0x3f, + # Contains a bitmask that specifies the Boolean attributes of the file. + flags=0x0, + # The operating system for which this file was designed. + # 0x4 - NT and there is no need to change it. + OS=0x4, + # The general type of file. + # 0x1 - the file is an application. + fileType=0x1, + # The function of the file. + # 0x0 - the function is not defined for this fileType + subtype=0x0, + # Creation date and time stamp. + date=(0, 0) + ), + kids=[ + StringFileInfo( + [ + StringTable( + '040904b0', + [StringStruct('CompanyName', 'Rokae - https://www.rokae.com/'), + StringStruct('FileDescription', 'All in one automatic toolbox'), + StringStruct('FileVersion', '0.3.1.6 (2025-03-21)'), + StringStruct('InternalName', 'AIO.exe'), + StringStruct('LegalCopyright', '© 2024-2025 Manford Fan'), + StringStruct('OriginalFilename', 'AIO.exe'), + StringStruct('ProductName', 'AIO'), + StringStruct('ProductVersion', '0.3.1.6 (2025-03-21)')]) + ]), + VarFileInfo([VarStruct('Translation', [1033, 1200])]) + ] +) diff --git a/assets/files/version/release_change.md b/assets/files/version/release_change.md new file mode 100644 index 0000000..8d7e904 --- /dev/null +++ b/assets/files/version/release_change.md @@ -0,0 +1,490 @@ +v0.0.1(2024/05/18) +Draft + +v0.0.2(2024/05/20) +1. 功能模块化,为后面其他功能奠定一个基本的框架 +2. 使用了多线程提高效率 +3. 优化了准备工作中的细节 +4. 运行初始化时自动删除 raw_data_dir 中的 .xlsx 文件 +5. 优化了输出格式 +6. 使用 pyinstaller 库进行代码冻结并调试成功 + +v0.0.3(2024/05/21) +1. just_open函数打开失败的信息中,添加文件名 +2. 删除global变量,函数全部通过传参实现 +3. configuration.xlsx配置文件增加AXIS常量,表示那个轴,取值为 j1/j2/j3/j4/j5/j6/j7 +4. [bugfix] 增加get_threshold_step函数,用来获取在计算row_start时合适的阈值和步长,主要是解决了二轴最差工况下,最大速度是个尖端的问题: + a. load100_speed100_reachxxx 二轴 threshold = 50 step = 20 + b. 其他 threshold = 50 step = 100 + 如上是一个比较保守的设定,因为设定的step比较小,找到点之后要往后延200最好 +5. 在find_row_start_dp函数中新增一个参数data_file,方便后续调试 + +v0.0.4(2024/05/22) +1. 重新标定了get_threshold_step函数,让处理更加准确 +2. 新定义了now_doing_msg函数,实时输出处理信息 +3. 修改了find_row_start和find_row_start_dp函数,增加的部分相同,处理数据的时候,先判断是否是空值,或者是0,此时可以加快步进 +4. 修改了just_open函数,不在做重试 + +v0.0.5(2024/05/23) +1. 完善了函数注释 +2. 调整了阈值和步长 +3. 删除了just_open函数,以及对应的win32com库(Thank GOD!终于可以不用这个库了) +4. 重写了获取开始点位的代码,直接使用speed来判断,而不用角度,所以find_row_start_dp以及copy_data_to_excel_file函数也被一并删除 +5. 修改了配置文件configs.xlsx的初始参数顺序及结构,使程序通用性更强 +6. 将initialazation中的预定义变量赋值调整到try...except...之外,更方便排查问题 +7. 修改结束时间的格式,精确到秒 + +v0.0.6(2024/05/23) +1. 为了调整多功能框架,aio.py文件将会作为入口程序存在,不实现具体功能,功能的实现将由具体的功能脚本实现,aio.py只负责条件调用 +2. 新增了自动化处理电流数据(电机/过载)的功能 + +v0.1.0(2024/05/29) +1. 修改为customtkinter图形化界面 +2. 支持工业机器人制动数据处理(理论上支持,测试数据有问题,待验证) +3. 删除configs.xlsx配置表格,直接在界面配置,新增layout.xlsx文件,存储customtkinter布局 +4. 电流尚未支持 + +v0.1.1(2024/05/30) +1. 增加版本检测功能 +2. 修改了无效数据下的动作 +3. textbox只在开始和结束时改变状态,而不是每次写入都更改 +4. 调整了brake的结构 +5. 重新在write2textbox中添加exitcode参数,并补齐相关逻辑和修改brake中的调用方式 +6. 修复参数检查无效的情况 +7. 屏蔽电流相关的功能 + +v0.1.2(2024/06/01) +1. 增加iso数据处理功能 +2. 重新修改了README.md +3. 单独将rokae拉出来,作为一个独立的repo进行维护,与scripts分离 + +v0.1.3(2024/06/01) +1. 完成电流处理的基本功能 +2. 修复了一些已知bugs + +v0.1.4(2024/06/06) +1. AV/RR支持小数 +2. 可处理电机电流单轴以及多轴数据,可根据需要进行参数设定处理不同轴的数据 +3. 界面初始位置修改,以及删除所有entry的长度设定,因为设定无效 +4. 修改了layout.xlsx布局,增加了duration/trqH/STO字段,以及额外的RC行,整体扩展了区域 +5. 更改label/entry/optionmenu等控件的生成方式,使用循环实现,更加简洁和容易维护(暂未实现) +6. 支持工业/协作两条产品线的电机电流数据处理,包括单轴,场景,max/avg计算 + +v0.1.5(2024/06/12) +1. [aio.py] 主界面切换不同功能时保持placehold一致 +2. [brake.py] 由于制动采集模板和内容的更改,适配了新的数据,更新了算法 +3. [aio.py] 新增tabview组件,区分数据处理和自动化测试功能 +4. [aio.py] 重新调整界面配色 +5. [aio.py] 修改了write2textbox函数,定制化显示每一行的颜色,针对每一行可自定义输出内容颜色 +6. [brake.py/iso.py/current.py] 由于第 5 点的更改,同时修改了其他文件相关引用的部分 +7. [aio.py] 更改label/entry/optionmenu等控件的生成方式,使用循环实现,更加简洁和容易维护 +8. [aio.py] 修改customtkinter库中C:\Users\Administrator\AppData\Local\Programs\Python\Python312\Lib\site-packages\customtkinter\windows\widgets\ctk_tabview.py文件,参考https://github.com/TomSchimansky/CustomTkinter/issues/2296,实现修改tabview组件的字体大小 +9. [aio.py] 修改menu_main->menu_main_dp,menu_sub->menu_sub_dp,为后续其他tab功能按钮做扩展,是针对第三点做出的相应调整 +10. [layout.xlsx] 添加了各个功能的流程图 + +v0.1.5.1(2024/06/12) +1. [current.py] 修改cycle功能中,数据清理范围为70000行,并将threshold从2调整为5 +2. [current.py] 修改位置超限提示,使更清楚了解问题原因 +3. [current.py] 修改find_point函数中错误提示,增加定位信息 +4. [README.md] 精简打包命令 +5. [requirements.txt] 新增必要库配置文件 + +v0.1.5.2(2024/06/13) +1. [brake.py/aio.py]: 将sto修改为estop +2. [brake.py] 修改了速度计算逻辑,新版本的vel列数据遵循如下规则,av = vel * 180 / pi,根据av再计算speed +3. [brake.py] 将threshold修改为常量50 +4. [brake.py] 提高了输出提示语的明确性,删除了不必要的省略号 +5. [brake.py] 更正了之前的数据copy错误,重新优化了estop处是否达到指定百分比的判定逻辑 + +v0.1.5.3(2024/06/14) +1. [aio.py] 修改w_param为84,适配14寸电脑屏幕 +2. [brake.py] 将判定合规逻辑修改为角速度超过指定角速度的95% +3. [README.md] 稍作修改,包括打包方式,功能特性等 + +v0.1.6.0(2024/06/15) +1. [aio.py] 新增wavelogger处理界面 +2. [wavelogger.py] 新增精度数据处理模块 + +v0.1.6.1(2024/06/16) +1. [wavelogger.py] bugfix single_file_proc函数中,修改_start起始点的计算逻辑 +2. [wavelogger.py] bugfix find_point函数中,当判断条件为临界值 2.0 的时候,针对forward和backward两种情况,对row_target做与判断逻辑相同的处理,目的是避免形成死循环 + +v0.1.6.2(2024/06/16) +1. [current.py] 修改了max/avg相关功能中对于返回值的处理逻辑,并在输出框以行的形式打印出来 + +v0.1.6.3(2024/06/18) +1. [current.py] 适配电机电流中速度使用hw_joint_vel_feedback的数据,取消对device_servo_vel_feedback的支持,后续所有涉及到速度相关的数据均已前者为准,现已完成对单轴和场景的适配 + +> !!WARNING:目前版本的电机电流程序还支持DriverMaster采集的数据处理,等明确后,将不再支持,也即所有的电机电流数据(工业+协作),都是用诊断曲线来采集 + +v0.1.7.0(2024/06/19)-未发布 +1. [openapi.py] 初步搭建起框架,完成了新老协议的封包/解包/异步采集日志的操作(未充分测试,但基本无问题) +2. [openapi.py] 修改了封包的规则,使之更加明晰,封包操作没有实现分包功能,目前看实际场景用不到 + +v0.1.7.0(2024/06/21)-未发布 +1. [openapi.py] 定义 MAX_FRAME_SIZE 常量(1024),替换socket接收以及响应数据处理相关部分 +2. [openapi.py] 使用 int.to_bytes 和 int.from_bytes 替换 binascii 模块的功能 +3. [aio.py] 修改了Data Process中初始化的动作,使得初始化时的状态统一成程序刚启动时的样子 + +v0.1.7.0(2024/06/23)-未发布 +1. [aio.py] 增加了tabview的点击行为函数,每次点击tab都会初始化 +2. [aio.py] 增加了Automatic Test界面元素,包括如下,并完成了功能框架的搭建 + - 标签:文件/角速度/减速比 + - 按钮:急停及恢复 + - 输入框:文件路径/角速度/减速比 + - OptionMenu:负载 + - 进度条 +3. [openapi.py] 增加心跳检测函数,并开启线程执行;取消在该文件中生成实例 +4. [aio.py] 完成detect_network,并在main函数开启线程 +5. 将templates文件夹移动到assets内 + +v0.1.7.0(2024/06/24)-未发布 +1. [openapi.py] 建联部分做容错逻辑,并将读写文件做自适应处理 +2. [aio.py] 将读写文件做自适应处理,引入openapi模块并生成实例,做心跳检测,将socket超时时间修改为3s + +v0.1.7.0(2024/06/25)-未发布 +1. [aio.py] 取消了在本文件中开启openapi线程的做法,并修改如下: + - 通过包的方式导入其他模块 + - 使用current_path来规避文件路径问题 + - 声名了 self.hr 变量,用来接收openapi的实例化 + - 修改了对于segment button的错误调用 + - 设定progress bar的长度是10 + - 完善了segmented_button_callback函数 + - 在detect_network函数中增加heartbeat初始化 + - tabview_click函数中新增textbox清屏功能,以及实例化openapi,并做检测 +2. [openapi.py] 取消了初始化中无限循环检测,因为阻塞了aio主界面进程!!!socket也无法多次连接!!!浪费了好多时间!!!很生气!!!! + - 通过tabview切换来实现重新连接,并保留了异常处理部分 + - 将所有的 __xxxx 函数都替换成 xxxx 函数,去掉了 __ + - 使用current_path来规避文件路径问题 +3. [do_brake.py] 初步完成了机器状态收集的功能,还需要完善 + - 使用current_path来规避文件路径问题 + - 新增validate_resp函数,校验数据 + - 完善了调用接口 + +> **关于HMI接口** +> - 封包解包顺序:帧长度二字节/包长度四字节/协议二字节/预留二字节,\x04\x00:\x00\x00\tR:\x02:\x00 +> - 帧长度和包长度没有必然关系,单帧的时候是帧长度减去包长度等于6,包长度指的是所有内容的长度 +> - HMI内部每次发送1024个字节,进行分包,内容长度规则是:第一帧1024-6=1018(帧大小减去包头长度),第二帧(包含)及之后的帧,帧长度即是数据长度 + +v0.1.7.0(2024/06/26)-初步可用 +1. [aio.py] 在detect_network函数中需改查询时间间隔是1s,在tabview_click中增加textbox配置normal的语句 +2. [do_brake.py -> btn_functions.py] 新增执行相应函数,并在get_state函数中设置无示教器模式 +3. [openapi.py] 新增sock_conn函数,并做连接时的异常处理,新增类参数w2t +4. [aio.py] 修改customtkinter库中C:\Users\Administrator\AppData\Local\Programs\Python\Python312\Lib\site-packages\customtkinter\windows\widgets\ctk_tabview.py文件,参考https://github.com/TomSchimansky/CustomTkinter/issues/2296,实现修改tabview组件的字体大小,使用原生字体,同时将segmented button字体修改为原生,为了解决segmented button在禁用和启用时,屏幕抖动的问题,并将大小修改为16 +5. [aio.py] 修改了segmented_button_callback的实现逻辑,使代码更简洁 +6. [aio.py] 修改了在tabview_click函数中对于实例化HmiRequest的动作,使每次切换标签都会重新实例化,也就是每次都会重新连接,修复显示不正确的问题 +7. [openapi.py] 新增了socket关闭的函数,并增加msg_id为None的处理逻辑 +8. [btn_functions.py] 完善了状态获取的功能,新增告警获取以及功能切换的逻辑 +9. [aio.py] 修改了版本 +10. [current.py] max/avg功能结束之前会将结果数据追加写入源文件,avg算法更改为average+3×std +11. [wavelogger.py] 算法更改为 average+3×std + +v0.1.7.1.0(2024/06/29) +1. [APIs: aio.py] + - 对于automatic test删除了输入框,使用configs.xlsx配置文件作为参数输入 + - 完善initialization/param_check/func_start_callback函数中对于automatic test的处理 + - 将textbox组件一直设置为normal状态,不再频繁切换disabled + - 将所有的f_h文件对象修改为f_hb,并将connection_state修改为c_state + - 在detect_network函数中,实例化HmiRequest,并在无限循环中检测心跳是否正常,如异常,则销毁hr,重新生成 + - 取消在tabview切换时,检测心跳的逻辑,这样做无法保证实时性 +2. [APIs: openapi.py] + - 将sock_conn函数移出__init__,单独作为连接函数存在 + - 新增全局变量self.t_bool,控制所有的线程中无限循环的启停,也就是可以人为的退出线程 + - 移除close_sock函数 + - heartbeat函数中新增打印所有消息的代码,调试时打开,平常关闭 + - execution函数中,新增对overview.set_autoload和overview.reload的支持 + - execution函数中,对send动作增加异常处理逻辑 +3. [APIs: do_brake.py] + - 新增文件,处理制动测试流程,建立连接,导入project,pp2main,run,采集并处理曲线数据,本地修改RL程序,推送至控制器等 + - 目前完成: + - 文件合规性检查 + - 导入工程并设置为运行工程 +4. [APIs: current.py] 修改scenario/single电机电流最大长度为150s +5. 在本文件中更新关于制动自动化测试的相关内容 +6. [t_change_ui: aio.py/brake.py/current.py] 整体修改了操作界面,删除了大部分的配置输入框,改用 configs.xlsx 配置文件替代,并优化了max/avg功能中写入结果数据的方式 + +v0.1.7.1.1(2024/06/29) +1. [APIs: aio.py] + - 修改detect_network函数中sleep语句放到最后,重新生成HmiRequest实例中增加sleep(4),这个停顿时间一定是比openapi中heartbeat函数的sleep要长1s以上才能正常工作 + - 修改write2textbox函数,新增默认参数tab_name,只有当该值与当前tab一致时,函数才会有输出 + - 第二条改动影响到了automatic_test文件夹下所有的文件 +2. [APIs: openapi.py] + - 规定了所有的网络异常均由heartbeat函数来定义,其他异常不做中断处理 + - execution函数中合并了case条件 + - 增加了N多指令,多为诊断曲线和rl程序相关 + - 日志保留条数修改为20000 +3. [APIs: do_brake.py] + - 实现自动推送工程到xCore并自动运行 + - 初步实现了Modbus发送消息和检测状态 +4. [APIs: do_current.py] + - 将do_brake.py的内容完全拷贝到此文件,待修改 + +v0.1.7.2(2024/06/30) +1. 初步完成NB4h_R580_3BH7.zip工程的设计 +2. 重新研究了解包操作,重新实现了一版 +3. 修改openapi.pi中excution为execution函数 +4. 增加了解包原理性文档 + +v0.1.7.3(2024/07/01) +1. [APIs: openapi.py] + - 继续完善封包解包操作,并优化了所有调试信息,默认打开状态,直到bug数量明显减少 + - 修复了两个bug,删除了一个多余的break,另一个是补齐了self.broke的重置 +2. [APIs: do_current.py] 使用原工程的工程名进行move操作,语义更加明确 + +> 目前看openapi.py封包解包没有任何问题了,但是所有的调试信息都默认打开,以便可以第一时间保留现场 +> 打开诊断,跑了10多分钟,共计解包没有报错,应该是没有问题了 + +v0.1.7.4(2024/07/02) +1. [APIs: openapi.py] + - 增加了modbus的python实现 + - heartbeat函数修改发送间隔为1s + - 清除了绝大部分调试性输出,发现太多的这种输出也会导致心跳丢包...,不清楚这个原理是什么 + - 在get_response函数中的while self.pkg > 0循环中,删除了else语句,因为它永不会被执行到 + - 在get_response函数中,修复一个bug,在flag==0的else语句中,补齐了index==6的情况 +2. [APIs: do_current.py] + - 完成了六个轴的电机电流动作的执行,以及数据采集 + - 完成了对应的RL程序的编写 +3[APIs: aio.py] + - 引入modbus实例化,并以参数的形式,传递给相应的tabview + - 新增pre_warning函数,在做自动化测试之前,确保所有条件皆具备 + +v0.1.7.5(2024/07/03) +1. [APIs: aio.py] + - 增加触发急停和恢复急停功能逻辑 +2. [APIs: do_current.py] + - 重新调整运行顺序,增加数据处理的逻辑(惯量负载逻辑暂不实现,等待软件部解决了修改工程之后不生效的问题再考虑) +3. [APIs: btn_functions.py] + - 增加触发急停和恢复急停的modbus实现,仅适用于自动化测试 + +v0.1.7.6(2024/07/04) +1. [APIs: aio.py] + - Automatic Test逻辑中增加选择current时,需要选负载类型的逻辑 +2. [APIs: do_current.py] + - 单轴/场景电机电流的采集已完成 +3. [APIs: openapi.py] + - 增加了modbus读取浮点数的功能 + - 优化了get_from_id的逻辑 +4. [autotest.xml]: 新增了scenario_time只写寄存器 + +v0.1.8.0(2024/07/04) +1. [APIs: do_current.py]: 完成了堵转电流和惯量负载电机电流的采集和处理,至此,电机电流的自动化工作基本完成 + +v0.1.8.1(2024/07/05) +1. [APIs: do_brake.py]: 完成了制动性能测试框架的搭建,可以顺利执行完整的测试程序,但是未实现急停和数据处理 +2. [APIs: aio.py]: 修改了do_brake主函数的参数 +3. 增加工程文件target.zip + +v0.1.8.2(2024/07/08) +1. [APIs: do_brake.py]: 完成了制动性能测试逻辑,只不过制动信号传递生效延迟不可控,暂时pending +2. [APIs: do_current.py]: 修改曲线数据时序,主要是value data取反即可,解决了波形锯齿明细的问题 +3. [APIs: openapi.py]: modbus新增了触发急停信号的寄存器 stop0_signal,并重写了解除急停,socket新增了register.set_value协议 + +v0.1.9.0(2024/07/10) +1. 完成了制动性能的自动化采集 +2. 完善了modbus浮点数读写相关的功能 +3. 修改了target.zip工程,该工程目前适配电机电流和制动性能 + +v0.1.9.1(2024/07/12) +1. [APIs: do_brake.py] + - 修改正负方向拍急停的逻辑,基本原理为:运行之前发送正负方向信号pon给RL,RL根据信号以及速度正负号运作 + - 由于上述修改,正负方向急停准确率可达100% +2. [APIs: aio.py] + - 修改write2textbox的输出逻辑,实现更加灵活的自定义输出,同时修改相关部分 +3. [APIs: openapi.py] + - modbus类新增指示政府方向急停的信号pon,将modbus类入参中的tab_name删除,并修改tab_name的值为'openapi' + - socket类种修改tab_name的值为'openapi' + +v0.1.9.2(2024/07/13) +1. [APIs: do_brake.py] + - 修改ready_to_go信号的接收逻辑,适配大负载机型 +2. [APIs: do_current.py] + - 修改ready_to_go信号的接收逻辑,适配大负载机型 + - 调整单轴测试时间为35s,适配大负载机型,调整堵转电流持续时间15s,适当减少测试时间 + - 将act信号置为False的动作放在初始化,增加程序健壮性 + - 修改所有输出文件的命名,在扩展名之前加入时间戳 + - 删除多余的时序矫正语句——item['value'].reverse(),使输出的曲线为平滑的自然顺序 +3. [current: current.py] + - 在find_point函数种,当无法找到正确点位时,继续执行,而不是直接终止执行 + - max功能计算逻辑矫正,应该是取绝对值的最大值 + - 整体梳理了trq/trqh的传递路径,现已修正完毕 + - 减速比rr数据源修改为configs.xlsx +4. 在current工程main函数增加 VelSet 100语句 + +v0.1.9.3(2024/07/15) +1. [APIs: openapi.py] + - 修改modbus连接失败报错输出形式,使之只在automatic test页面显示 + - 将该文件移动至toplevel,为后面扩展做准备 + - 修改heartbeat文件路径,使后续打包的时候更方便 +2. [APIs: aio.py] + - 修改heartbeat文件路径,使后续打包的时候更方便 + - 修改write2textbox函数的打印逻辑,先判断网络相关 + +v0.1.9.4(2024/07/15) +1. [profile: aio.py]:完善durable text相关逻辑 +2. [profile: do_brake/do_current/btn_functions.py]:删除validate_resp函数,修改execution函数 +3. [profile: factory_test.py] + - 新增耐久/老化测试程序 + - 实现六轴折线图显示 +4. [profile: openapi.py]:多次合并遗留问题处理 +5. templates文件夹组织架构调整 + +v0.2.0.0(2024/07/17) +1. [profile: aio.py] + - 增加velocity相关逻辑 + - 修改负载信息为曲线信息 +2. [profile: factory_test.py] + - 增加velocity相关逻辑 +3. [profile: current.py] + - 修正减速比获取的规则 +4. [profile: openapi.py] + - HmiRequest模块:日志取消记录move.monitor相关 + - HmiRequest模块:增加了durable_lock变量,控制文件读写互斥 + +v0.2.0.1(2024/07/19) +1. [main: aio.py] + - 修改了x轴显示,使之为时间刻度 + - 修改pre_warning函数,增加了durable test的初始化 +2. [main: factory_test.py] + - 增加了数据计算错误的判断逻辑 + - 增加了历史数据保存的逻辑 + - 增加了文件读写互斥的逻辑 + - 修改功能为输出有效电流和最大电流,并将数据结构简化 + +v0.2.0.2(2024/07/26) +1. [main: current.py] + - 修正堵转电流无法正确写入结果文件的问题 +2. [main: do_brake.py] + - 初始速度采集等待时间设置为可通过configs.xlsx配置文件调整的 + - 初次速度采集停止逻辑修改为tasks.stop指令(未验证) + - 急停信号触发前,pending时间设置为固定值10s + - 实现正负方向速度采集逻辑 + - 工程名变更逻辑实现修改为通配符,方便后续根据机型保存文件 + - 增加超差后写诊断的逻辑,并可以通过configs.xlsx配置文件调整 + - 程序输出中增加时间戳,方便调试定位日志时间 +3. [main: do_current.py] + - 工程名变更逻辑实现修改为通配符,方便后续根据机型保存文件 +4. 为工程文件添加更详细的注释 +5. 补充了do_current/do_brake的流程图 +6. [main: openapi.py] + - 将modbus motor_on/off的实现方法改为高电平脉冲触发 +7. configs.xlsx配置表新增write_diagnosis/get_init_speed两个参数 + +v0.2.0.3(2024/07/27) +1. [APIs: do_brake.py]: 精简程序,解决 OOM 问题 +2. [APIs: do_current.py]: 精简程序,解决 OOM 问题 +3. [APIs: factory_test.py]: 精简程序,解决 OOM 问题 +4. [APIsL openapi.py] + - 心跳修改为 1 s,因为 OOM 问题的解决依赖于长久的打开曲线开关,此时对于 hr.c_msg 的定时清理是个挑战,将心跳缩短,有利于清理日志后,避免丢失心跳 + - 新增 diagnosis.save 命令,但是执行时,有问题,待解决 + +v0.2.0.4(2024/07/30) +1. [APIs: do_brake.py]: 修复制动数据处理过程中,只取曲线的最后 240 个数据 +2. [APIs: aio.py]: 判定版本处,删除 self.destroy(),因为该语句会导致异常发生 + - 心跳修改为 1s,因为 OOM 问题的解决依赖于长久的打开曲线开关,此时对于 hr.c_msg 的定时清理是个挑战,将心跳缩短,有利于清理日志后,避免丢失心跳 + - 新增 diagnosis.save 命令,但是执行时,有问题,待解决 + +v0.2.0.5(2024/07/31) +此版本改动较大,公共部分做了规整,放置到新建文件夹 commons 当中,并所有自定义模块引入 logging 模块,记录重要信息 +1. [t_change_ui: clibs.py] + - 调整代码组织结构,新增模块,将公共函数以及类合并入此 + - 将一些常量放入该模块 + - 引入logging/concurrent_log_handler模块,并作初始化操作,供其他模块使用,按50M切割,最多保留10份 + - prj_to_xcore函数设置工程名部分重写,修复了多个prj工程可能不能执行的问题,并优化输入密码的部分 +2. [t_change_ui: openapi.py] + - 完全重写了 get_from_id 函数,使更精准 + - 在 msg_storage 函数中,增加 logger,保留所有响应消息 + - 删除 heartbeat 函数中的日志保存功能部分 + - 心跳再次修改为 2s... +3. [t_change_ui: aio.py] + - 增加了日志初始化部分 + - detect_network 函数中修改重新实例化HR间隔为 4s,对应心跳 + - create_plot 函数中增加 close('all'),解决循环画图不销毁占用内存的问题 +4. [t_change_ui: do_brake.py] + - 使用一直打开曲线的方法规避解决了 OOM 的问题,同时修改数据处理方式,只取最后 12s + - 优化 ssh 输入密码的部分 +5. [t_change_ui: do_current.py] + - 保持电流,只取最后 15s + - 优化 ssh 输入密码的部分 +6. [t_change_ui: all the part]: 引入 commons 包,并定制了 logging 输出,后续持续优化 +7. [APIs: btn_functions.py]: 重写了告警输出函数,从日志中拿数据 +8. [APIs: aio.py]: 将日志框输出的内容,也保存至日志文件 +9. [APIs: do_brake.py] + - 修改获取初始速度的逻辑,只获取configs文件中配置的时间内的速度 + - 新增 configs 参数 single_brake,可针对特定条件做测试 +10. [APIs: all]: 添加了 logger.setLevel(INFO),只有添加这个,单个模块内才生效 + +v0.2.0.6(2024/08/09) +1. [t_change_ui: all files] + - 修改了 logger 的实现 + - 尤其是 clibs.py,使用日志字典,重写了日志记录的功能 + +v0.2.0.7(2024/08/16) +1. [t_change_ui: clibs.py]:修改了 hmi.log 的日志等级为 WARNING +2. [t_change_ui: openapi.py]:根据第一步的修改,将此模块日志记录等级调整至 warning +3. [current: current.py] + - README新增了整机自动化测试的前置条件,即滑块需要滑动到最右端 + - current修改了文件校验的逻辑 +4. [t_change_ui: aio.py] + - 修改变量命名,widgit -> widget + - 根据第 5 点变动,同步修改代码实现 + - 调整 UI 界面代码顺序,使之符合 layout.xlsx 描述 + - 将版本检查的部分单独封装成一个函数,在 detect_network 线程初始化时调用一次,并且程序启动也不会受到阻塞 +5. [t_change_ui: layout.xlsx]:修改了组件布局方式 + + +> 前两个修改点,修复的是网络提示的颜色不正确问题,因为日志将 textbox 中的内容也作为 debug 信息写入 hmi.log 了 + +v0.2.0.8(2024/08/20) +1. [t_change_ui: clibs.py] + - 从外部拷贝 icon.ico 文件到 templates 目录 + - 在 assets 目录新建 logs 目录,存放日志文件,并增加了相应的逻辑保证正常执行 +2. [t_change_ui: aio.py]:增加 App 窗口图标代码 +3. [t_change_ui: openapi.py]:将重复输出的网络错误提示,从 textbox 中转移到 debug.log 日志文件中 +4. [main: openapi.py]:新增 rl_task.set_run_params 指令支持,可设定速度滑块以及是否重复运行 +5. [main: do_brake/do_current/factory_test.py]:在初始化运动时增加 `clibs.execution('rl_task.set_run_params', hr, w2t, tab_name, loop_mode=True, override=1.0)` + +v0.2.0.9(2024/10/09) +1. [main: do_brake.py] 采集完成后,pending 3s,使速度完全将为 0 + +v0.2.1.0(2024/12/05) +1. [current: do_current.py] 增加了 hw_sensor_trq_feedback 曲线的采集 +2. [current: current.py] 增加了 hw_sensor_trq_feedback 曲线数据的处理,以及修改了之前数据处理的逻辑 +3. [current: clibs.py] 新增可手动修改连接 IP 地址的功能,存储在 assets/templates/ipaddr.txt 中,默认是 192.168.0.160 + +v0.2.1.1(2024/12/16) +1. [main: do_brake.py] 修改了 SSH 的固定 IP 为 clibs 中读取的内容,并删除了每次都 reload 工程的动作,改为只在修改 RL 工程时 reload 一次,旨在减少最近频繁出现的“无法获取overview.reload-xxxxxx”请求的响应,初步判断是 xCore 的问题,非 AIO 问题,已反馈待版本修复 +2. [main: wavelogger.py] 新增异常数据校验功能 + +v0.3.0.0(2025/01/09) +1. 重构了 AIO 工具: + - UI 界面 + - 电机电流数据处理功能 + - xCore 通讯协议实现 +2. 将之前的功能迁移到新代码 + +v0.3.1.0(2025/01/23) +1. 实现了从机型文件读取相关测试参数,并用于自动化测试或者数据处理 +2. 完整实现了所有功能的迁移,并自测验证通过 +3. 重新实现并优化了如下功能: + - 电机电流数据采集,优化代码,提高执行效率,并适配新的报告文件 + - 制动数据采集,比之前采集正确率提高 30%,基本可以做到 100% 的数据准确度 + - 耐久数据采集并记录,优化了执行以及数据展示 + - 基恩士数据采集处理,适配任意编码格式的文件处理 + +v0.3.1.2(2025/02/11) +1. 修改自动测试(制动/转矩/耐久)的read_ready_to_go信号等待时间为15s +2. 废弃write_diagnosis参数 +3. 优化do_brake中,触发超差写诊断等待操作流程 + +v0.3.1.3(2025/02/12) +1. 修改自动测试(制动)的打开关闭曲线逻辑,开始时打开,完整测试结束时关闭 + +v0.3.1.3(2025/02/13) +1. 修改自动测试(电机电流)的数据处理方法,从以前的Multiprocessing->threading,因为Intel CPU遇到Multiprocessing会重新打开一个AIO实例 + +v0.3.1.5(2025/03/10) +1. 因公司网络环境调整,修改服务器校验IP + +v0.3.1.6(2025/03/21) +1. 调整耐久(场景)测试工程,以及相应的数据采集逻辑,将采集规则由固定时间间隔,修改为固定运动动作周期 +> Tips:工程以及寄存器文件均有变动 \ No newline at end of file diff --git a/assets/files/version/requirements.txt b/assets/files/version/requirements.txt new file mode 100644 index 0000000..8660c7f --- /dev/null +++ b/assets/files/version/requirements.txt @@ -0,0 +1,11 @@ +chardet==5.2.0 +customtkinter==5.2.2 +matplotlib==3.10.0 +numpy==2.2.2 +openpyxl==3.1.5 +pandas==2.2.3 +paramiko==3.5.0 +pdfplumber==0.11.5 +Pillow==11.1.0 +pymodbus==3.8.3 +pyinstaller==6.12.0 diff --git a/assets/files/version/version b/assets/files/version/version new file mode 100644 index 0000000..245a459 --- /dev/null +++ b/assets/files/version/version @@ -0,0 +1 @@ +0.3.1.6@03/21/2025 \ No newline at end of file diff --git a/assets/media/icon.ico b/assets/media/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..4c3b21a9a6232171d15cc583e9176124cc45edf0 GIT binary patch literal 165662 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 V1*&8qjW_NCrbd?JDnqZh{{c_sLbLz? literal 0 HcmV?d00001 diff --git a/assets/media/upgrade.png b/assets/media/upgrade.png new file mode 100644 index 0000000000000000000000000000000000000000..8c88c01e28e7054b073f7d64d96d2f97d8fb7913 GIT binary patch literal 2827 zcmd6p`9IX#AIHyU%$RAi458AbiMf_=MXEvciNPr3QcAKWiO8C=WE5G$%*ZawxOTdB z5y`&Jt%w^bl095mMo8aJkH_~v`2Or^o_%nd)nUQVwwz0DPMP^~{C4)(h#brXMJTN5AE1dqn)jve}*2 z>COyg!N#aGv}feum!BmQX$*1y9#*n~U2<7*_1A>W;Y<&nMXHpxV5=SdJuBFG?H>)^-yv(wJ4?z;Zpm=#AjLR)EE2Iz1_2*5;LLDBo z925|iyGpxkPq`xp48r#_Ish%lJUD4nZ}FYAF+8W>d?k9OU5OapM6G zO>bQY7RIR``(-xMi(^nPqOft%Uu2{@Y1(HqVq)XlE%ORS3e`^+1r6*Pk=+@HCZVE4 zTbBLFkZ|xTjv9y3f*n^jjF^Q=Tt&wsw(~ zL?FU6(S7@N3`Djx=;bNDLR6~xTkVlmaeREnxQ1v{()=?2B6}2rlKN~vv;)%wMz}MAYrEW!Y+r>M7#Adb2A_wdg7iGFq^h|8EG1}`gvzRZ-UuIIh+?4G;s)nM>}MVlxW|@YKho(R;N~ zx|$XyW2>|GmSB&K6H5NrEBS)`fg+mNaZQE(s%zORt|Qh@1ApE-utrY5aah{9)mui; zD#*KewXwKXU9E1NW0h>q;Os4ElD)m+rq5XP@%=cXSZ>&Rr;uy(F;gfHVzC+6&i-~OWV8|VPzU+Ri zB%D3yE4jy zp?lYc*92=G6d3)6A1Lwi6+X)QsSQ87;do*8{`{Kfip|nHO^tDBop9Dx?E7uq!3~#@ z;RgRp8ShT7|K3DkRo_KN-1O!ob+FiXIO|@ww_c|{d(L$oaEZIx1_;SsK(haQpensn z0Yyl@KgWisjzaX#RkK69sdN zvH}M?Y!<}w5|VZQ|4@!DF8@tyRm#%x+$sEUwc%TO^=1Eau_&51|N4TQM4)|J@D#9n z*|y>Qb;Rt{POqQT0#r`1-pKu)J0#qYytW`$dU9o>w-5(!eWQrgio+Lf4!JLs}6|0Y`|Au^wC=97(MTF z^^B}Kc0=P`-HqFAKPH`hoU6=QRY~bM*b@j$Yh`F^+Dv~N($al81-(DhoG=)akT`ks z-MQbumr(JMu<%O>c~{>CwwY~nGepdm4Dk|Wz}D0FUV`%P|kBY01M>T zjdB}!riSh>;kkjazeiHHBK4=)ssuP*T<40q2p}BPiVHQx0BV79&YwCEcr(spA$t-0 zPwpmFQW1;o@~Jvwc6LlM!AO*ULH~1g`2kz zTqQRj{~uJ6-5=v2j%|)VxFTpx0IhQhw~L zoKjIAIT}T;d|!9`+GnvEt$(b8`tmpVjJ$#+WYp=cvk6urLnMy`3oE-fA4H;_jIx2< z@|^5@75>lWOGhUZo|QYfq2E7gclLO(YUFk8o@g5|teZYG+P#=MbY5Yn7xgx|!D=7J zM_x{>N;a*M8KiiGYr8)>JtP-m3<@nz$^+7*zo8%o55nP7+C7&5+|5mrz#}aUz8jM8 z4AA43lr=?U0SUE?Q?!AA+^1`PvL*mVe-^sZhytdK9jhjpfR1aGstmw@oPgNbEEobp z;rdSB@kzX2xF`NLs`ya+SLbauo!jU(yuSPPs+)OI<@br6X40qm!RH$0=6r7p(;ikI zIyaiw&#u%g+tl22R5g+>;Z#(d7__zNy>H z&7*BYLMJ4tJ<1_ZL?^ED#8F1eqfl{_o?70Be#LrOx8-(=JDaPw+|=FHPbm48KRVOg z9oe$ly))A%FKy&Co?hSGcP%yJURHxiY>U^%Eu6*o2XY7<58@x4=Nxm|CiB``|xQLhu>@ literal 0 HcmV?d00001 diff --git a/code/aio.py b/code/aio.py new file mode 100644 index 0000000..d585d18 --- /dev/null +++ b/code/aio.py @@ -0,0 +1,518 @@ +import json +import threading +import time + +import ui.login_window as login_window +import ui.reset_window as reset_window +import ui.main_window as main_window +from PySide6 import QtWidgets +from PySide6.QtCore import Qt, QThread, Signal, QObject +import sys +import re +import pymysql +import hashlib +import datetime +import common.clibs as clibs +import common.openapi as openapi +from PySide6.QtWidgets import QMessageBox +from PySide6.QtGui import QColor, QTextCursor, QTextCharFormat, QDoubleValidator +from analysis import brake, current, wavelogger, iso + + +class MultiWindows: + login_window = None + reset_window = None + main_window = None + + +class ConnDB(QObject): + completed = Signal(tuple) + + def __init__(self): + super().__init__() + + def do_conn(self, action): + conn, cursor = None, None + try: + conn = pymysql.connect(host='10.2.20.216', user='root', password='Rokae_123457', port=13306, charset='utf8', connect_timeout=clibs.INTERVAL*10) + cursor = conn.cursor() + except Exception: + ... + finally: + self.completed.emit((conn, cursor)) + + +class RunProg(QObject): + completed = Signal(tuple) + + def __init__(self): + super().__init__() + + def program(self, action): # 0: prog 1: idx + prog, idx, network = action + if idx in range(7): + run = prog.processing + elif idx == -1: + run = prog.net_conn + elif idx == -99: + run = prog + + try: + run() + self.completed.emit((True, prog, "", idx, network)) # 运行是否成功/返回值/报错信息/idx + except Exception as err: + self.completed.emit((False, None, err, idx, network)) # 运行是否成功/返回值/报错信息/idx + + +class ThreadIt(QObject): + completed = Signal(tuple) + + def __init__(self): + super().__init__() + + def run_program(self, action): + try: + res = action[0](*action[1]) + self.completed.emit((True, res, "")) # 运行是否成功/返回值/报错信息 + except Exception as err: + self.completed.emit((False, "", err)) + + +class LoginWindow(login_window.Ui_Form): + action = Signal(int) + + def __init__(self): + super(LoginWindow, self).__init__() + self.setupUi(self) + self.le_username.setFocus() + self.conn, self.cursor = None, None + if not clibs.status["mysql"]: + self.setup_DB() + + def get_user_infos(self, results): + self.conn, self.cursor = results + if self.conn is None and self.cursor is None: + QMessageBox.critical(self, "网络错误", "无法连接至服务器数据库,稍后再试......") + try: + MultiWindows.reset_window.close() + except Exception: + ... + finally: + self.close() + else: + self.cursor.execute("SET autocommit = 1;") + clibs.status["mysql"] = 1 + + def setup_DB(self): + self.t = QThread(self) + self.conn_db = ConnDB() + self.conn_db.moveToThread(self.t) + self.conn_db.completed.connect(self.get_user_infos) + self.action.connect(self.conn_db.do_conn) + self.t.start() + self.action.emit(1) + + def user_login(self): + username = self.le_username.text() + password = self.le_password.text() + md = hashlib.md5(password.encode()) + password = md.hexdigest() + + self.cursor.execute("use user_info;") + self.cursor.execute("select * from UserInfo;") + user_infos = self.cursor.fetchall() + for user_info in user_infos: + if user_info[0] == username and user_info[1] == password and user_info[2] == 0: + MultiWindows.main_window = MainWindow(self.conn, self.cursor, username) + MultiWindows.main_window.show() + self.close() + else: + t = datetime.datetime.now().strftime("%H:%M:%S") + self.label_hint.setText(f"[{t}] 用户名或密码错误,或用户已登录......") + self.label_hint.setStyleSheet("color: red;") + + def reset_password(self): + MultiWindows.reset_window = ResetWindow(self, self.conn, self.cursor) + MultiWindows.reset_window.show() + self.setVisible(False) + + +class ResetWindow(reset_window.Ui_Form): + def __init__(self, login_window, conn, cursor): + super(ResetWindow, self).__init__() + self.setupUi(self) + self.le_username.setFocus() + self.login_window = login_window + self.conn = conn + self.cursor = cursor + + def reset_password(self): + username = self.le_username.text() + old_password = self.le_old_password.text() + md = hashlib.md5(old_password.encode()) + password = md.hexdigest() + new_password_1 = self.le_new_password_1.text() + new_password_2 = self.le_new_password_2.text() + + self.cursor.execute("use user_info;") + self.cursor.execute("select * from UserInfo;") + user_infos = self.cursor.fetchall() + + for user_info in user_infos: + if user_info[0] == username and user_info[1] == password and user_info[2] == 0: + break + else: + t = datetime.datetime.now().strftime("%H:%M:%S") + self.label_hint.setText(f"[{t}] 用户名或密码错误,或用户已登录......") + self.label_hint.setStyleSheet("color: red;") + return + + if new_password_1 != new_password_2 or len(new_password_1) < 8: + t = datetime.datetime.now().strftime("%H:%M:%S") + self.label_hint.setText(f"[{t}] 两次输入的新密码不匹配,或长度小于8位......") + self.label_hint.setStyleSheet("color: red;") + else: + md = hashlib.md5(new_password_1.encode()) + password = md.hexdigest() + self.cursor.execute(f"UPDATE UserInfo SET password = '{password}' WHERE username = '{username}'") + self.close() + + def reset_cancel(self): + self.login_window.setVisible(True) + self.close() + + def closeEvent(self, event): + self.login_window.setVisible(True) + self.close() + + +class MainWindow(main_window.Ui_MainWindow): + action = Signal(tuple) + + def __init__(self, conn, cursor, username): + super(MainWindow, self).__init__() + self.setupUi(self) + self.conn = conn + self.cursor = cursor + self.username = username + self.predoes() + # self.t = threading.Thread(target=self.state_detection) + # self.t.daemon = True + # self.t.start() + + def predoes(self): + # ========================= db int ========================= + t = datetime.datetime.now().strftime("%Y%m%d%H%M%S") + self.cursor.execute(f"UPDATE UserInfo SET online = 1 WHERE username = '{self.username}';") + self.cursor.execute(f"CREATE DATABASE IF NOT EXISTS {self.username};") + self.cursor.execute(f"use {self.username};") + self.cursor.execute(f"CREATE TABLE {t}_log (id INT AUTO_INCREMENT PRIMARY KEY, timestamp TIMESTAMP NOT NULL default CURRENT_TIMESTAMP, level ENUM('DEBUG', 'INFO', 'WARNING', 'ERROR'), module VARCHAR(255) NOT NULL, content TEXT);") + self.cursor.execute(f"INSERT INTO {t}_log (module, level, content) VALUES (%s, %s, %s)", ("aio", "info", "testing")) + self.cursor.execute("SHOW TABLES;") + tables = [x[0] for x in self.cursor.fetchall()] + if len(tables) > clibs.MAX_LOG_NUMBER: + for table in sorted(tables)[:-10]: + self.cursor.execute(f"DROP TABLE {table};") + + # ========================= clibs ========================= + clibs.cursor = self.cursor + clibs.tb_name = f"{t}_log" + + # ========================= clibs ========================= + # validator = QDoubleValidator(bottom=300, decimals=2) + # self.le_durable_interval.setValidator(validator) + + # ========================= styleSheet ========================= + tws = [self.tw_funcs, self.tw_docs] + for tw in tws: + tw.setStyleSheet(""" + QTabBar::tab:selected { + background: #0078D4; + color: white; + border-radius: 4px; + } + QTabBar::tab:!selected { + background: #F0F0F0; + color: #333; + } + QTabWidget::pane { + border: 1px solid #CCCCCC; + } + """) + + # ============================↓↓↓debug↓↓↓============================ + # print(f"self.cb_data_func.currentIndex() = {self.cb_data_func.currentIndex()}") + + def run_program_thread(self, prog, idx, prog_done, network): + self.tw_docs.setCurrentIndex(0) + # self.pte_output.clear() + if idx != -99: + prog.output.connect(self.w2t) + self.t = QThread(self) + self.run = RunProg() + self.run.moveToThread(self.t) + self.run.completed.connect(prog_done) + self.action.connect(self.run.program) + self.t.start() + self.action.emit((prog, idx, network)) + + def w2t(self, msg, color="black"): + self.pte_output.appendHtml(f"{msg}") + cursor = self.pte_output.textCursor() + cursor.movePosition(QTextCursor.End) + self.pte_output.setTextCursor(cursor) + self.pte_output.ensureCursorVisible() + self.update() + + def prog_start(self): + def prog_done(results): + flag, result, error, idx, network = results + clibs.running[idx] = 0 + # if flag is False: + # self.w2t(f"{clibs.functions[idx]}运行失败:{error}", "red") + # elif flag is True: + # ... + + if sum(clibs.running) > 0: + if sum(clibs.running) == 1: + QMessageBox.critical(self, "运行中", f"{clibs.functions[clibs.running.index(1)]}正在执行中,不可同时运行两个处理/测试程序!") + return + else: + self.w2t(f"clibs.running = {clibs.running}", "red") + self.w2t(f"clibs.functions = {clibs.functions}", "red") + QMessageBox.critical(self, "严重错误", "理论上不允许同时运行两个处理程序,需要检查!") + return + + if self.tw_funcs.currentIndex() == 0 and self.cb_data_func.currentIndex() == 0: + self.run_program_thread(brake.BrakeDataProcess(self.le_data_path.text()), 0, prog_done, None) + elif self.tw_funcs.currentIndex() == 0 and self.cb_data_func.currentIndex() == 1: + self.run_program_thread(current.CurrentDataProcess(self.le_data_path.text(), self.cb_data_current.currentText()), 1, prog_done, None) + elif self.tw_funcs.currentIndex() == 0 and self.cb_data_func.currentIndex() == 2: + self.run_program_thread(iso.IsoDataProcess(self.le_data_path.text()), 2, prog_done, None) + elif self.tw_funcs.currentIndex() == 0 and self.cb_data_func.currentIndex() == 3: + self.run_program_thread(wavelogger.WaveloggerDataProcess(self.le_data_path.text()), 3, prog_done, None) + elif self.tw_funcs.currentIndex() == 1 and self.cb_unit_func.currentIndex() == 0: + self.w2t(f"{clibs.functions[4]}功能待开发.....", "red") + elif self.tw_funcs.currentIndex() == 1 and self.cb_unit_func.currentIndex() == 1: + self.w2t(f"{clibs.functions[5]}功能待开发.....", "red") + elif self.tw_funcs.currentIndex() == 2: + self.w2t(f"{clibs.functions[6]}功能待开发.....", "red") + + def prog_stop(self): + QMessageBox.warning(self, "停止运行", "运行过程中不建议停止运行,可能会损坏文件,如果确实需要停止运行,可以直接关闭窗口!") + + def prog_reset(self): + self.pte_output.clear() + + def file_browser(self): + idx_dict = {0: self.le_data_path, 1: self.le_unit_path, 2: self.le_durable_path} + dir_path = QtWidgets.QFileDialog.getExistingDirectory() + tab_index = self.tw_funcs.currentIndex() + if dir_path: + idx_dict[tab_index].setText(dir_path) + + def curve_draw(self): + ... + + def durable_cb_change(self): + ... + + def pre_page(self): + ... + + def realtime_page(self): + ... + + def next_page(self): + ... + + def load_sql(self): + ... + + def search_keyword(self): + ... + + def prog_done_conn(self, results): + flag, result, error, idx, network = results + if flag is False: + self.w2t(f"{network.upper()}连接失败", "red") + elif flag is True: + clibs.status[network] = 1 + if network == "hmi": + self.btn_hmi_conn.setText("断开") + clibs.c_hr = result + elif network == "md": + self.btn_md_conn.setText("断开") + clibs.c_md = result + elif network == "ec": + self.btn_ec_conn.setText("断开") + clibs.c_ec = result + + def prog_done_disconn(self, results): + flag, result, error, idx, network = results + if flag is False: + self.w2t(f"{network.upper()}断开连接失败", "red") + elif flag is True: + clibs.status[network] = 0 + if network == "hmi": + self.btn_hmi_conn.setText("连接") + clibs.c_hr = result + elif network == "md": + self.btn_md_conn.setText("连接") + clibs.c_md = result + elif network == "ec": + self.btn_ec_conn.setText("连接") + clibs.c_ec = result + + def hmi_conn(self): + if self.btn_hmi_conn.text() == "连接": + clibs.ip_addr = self.le_hmi_ip.text().strip() + ip_pattern = re.compile(r"(([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.){3}([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])") + if not ip_pattern.fullmatch(clibs.ip_addr): + self.w2t(f"{clibs.ip_addr} 不是一个有效的 IP 地址", "red") + return + self.run_program_thread(openapi.HmiRequest(clibs.ip_addr, clibs.socket_port, clibs.xService_port), -1, self.prog_done_conn, "hmi") + elif self.btn_hmi_conn.text() == "断开": + self.run_program_thread(clibs.c_hr.close, -99, self.prog_done_disconn, "hmi") + + def md_conn(self): + if clibs.status["hmi"] == 0: + QMessageBox.warning(self, "告警", "打开Modbus连接之前,需要先打开HMI连接!") + return + + if self.btn_md_conn.text() == "连接": + clibs.modbus_port = self.le_md_port.text().strip() + self.run_program_thread(openapi.ModbusRequest(clibs.ip_addr, clibs.modbus_port), -1, self.prog_done_conn, "md") + elif self.btn_md_conn.text() == "断开": + self.run_program_thread(clibs.c_md.close, -99, self.prog_done_disconn, "md") + + def ec_conn(self): + if clibs.status["hmi"] == 0: + QMessageBox.warning(self, "告警", "打开外部通信连接之前,需要先打开HMI连接!") + return + + if self.btn_ec_conn.text() == "连接": + clibs.external_port = self.le_ec_port.text().strip() + self.run_program_thread(openapi.ExternalCommunication(clibs.ip_addr, clibs.external_port), -1, self.prog_done_conn, "ec") + elif self.btn_ec_conn.text() == "断开": + self.run_program_thread(clibs.c_ec.close, -99, self.prog_done_disconn, "ec") + + def hmi_page(self): + self.sw_network.setCurrentIndex(0) + + def md_page(self): + self.sw_network.setCurrentIndex(1) + + def ec_page(self): + self.sw_network.setCurrentIndex(2) + + def hmi_send(self): + def prog_done(results): + ... + + def hmi_send_thread(): + if clibs.status["hmi"] == 0: + QMessageBox.critical(self, "错误", "使用该功能之前,需要先打开HMI连接!") + return + + cmd = self.pte_hmi_send.toPlainText() + req = json.dumps(json.loads(cmd), separators=(",", ":")) + print(f"type of cmd = {type(cmd)}") + print(f"type of req = {type(req)}") + if "id" in req: # 老协议 + print(f"wrong req = {req}") + msg_id = json.loads(req)["id"] + clibs.c_hr.c.send(clibs.c_hr.package(req)) + print(f"msg_id ={msg_id}") + clibs.logger("INFO", "aio", f"hmi: [send] 老协议请求发送成功 {req}") + records = clibs.c_hr.get_from_id(msg_id, "done") + print(f"req = {req}") + print(f"records = {records}") + self.pte_him_recv.clear() + self.pte_him_recv.appendPlainText(records) + else: # 新协议 + clibs.c_hr.c_xs.send(clibs.c_hr.package_xs(json.loads(cmd))) + data = "" + time.sleep(clibs.INTERVAL/5) + _ = clibs.c_hr.c_xs.recv(1024) + while len(_) == 1024: + data += _ + _ = clibs.c_hr.c_xs.recv(1024) + + print(f"data = {data}") + self.pte_him_recv.clear() + self.pte_him_recv.appendPlainText(data.decode()) + self.run_program_thread(hmi_send_thread, -99, prog_done, None) + + def md_send(self): + ... + + def ec_send(self): + ... + + def hmi_cb_change(self): + cmd = self.cb_hmi_cmd.currentText() + self.pte_hmi_send.clear() + self.pte_him_recv.clear() + with open(f"{clibs.PREFIX}/files/protocols/hmi/{cmd}.json", mode="r", encoding="utf-8") as f_hmi: + t = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%f") + hmi_dict = json.load(f_hmi) + if "id" in hmi_dict.keys(): + hmi_dict["id"] = f"{cmd}-{t}" + + self.pte_hmi_send.appendPlainText(json.dumps(hmi_dict, indent=2, separators=(",", ":"))) + + def md_cb_change(self): + ... + + def ec_cb_change(self): + ... + + def check_interval(self): + try: + interval = float(self.le_durable_interval.text()) + interval = 300 if interval < 300 else int(interval) + except Exception: + interval = 300 + self.le_durable_interval.setText(str(interval)) + + def state_detection(self): + while True: + time.sleep(clibs.INTERVAL) + if clibs.status["hmi"] == 0 and self.btn_hmi_conn.text() == "断开": + self.btn_hmi_conn.setText("连接") + elif clibs.status["hmi"] == 1 and self.btn_hmi_conn.text() == "连接": + self.btn_hmi_conn.setText("断开") + + def closeEvent(self, event): + idx = -1 if clibs.running.count(1) == 0 else clibs.running.index(1) + info_text = "当前无程序正在运行,可放心退出!" if idx == -1 else f"当前正在运行{clibs.functions[idx]},确认退出?" + reply = QMessageBox.question(self, "退出", info_text) + if reply == QMessageBox.Yes: + try: + self.cursor.execute(f"use user_info;") + self.cursor.execute(f"UPDATE UserInfo SET online = 0 WHERE username = '{self.username}'") + self.cursor.close() + self.conn.close() + finally: + clibs.lock.release() + + if clibs.status["md"] == 1: + self.run_program_thread(clibs.c_md.close, -99, self.prog_done_disconn, "md") + if clibs.status["ec"] == 1: + self.run_program_thread(clibs.c_ec.close, -99, self.prog_done_disconn, "ec") + if clibs.status["hmi"] == 1: + self.run_program_thread(clibs.c_hr.close, -99, self.prog_done_disconn, "hmi") + + self.close() + event.accept() + else: + event.ignore() + + +if __name__ == '__main__': + app = QtWidgets.QApplication(sys.argv) + window = LoginWindow() + window.show() + sys.exit(app.exec()) + diff --git a/code/analysis/brake.py b/code/analysis/brake.py new file mode 100644 index 0000000..a855df6 --- /dev/null +++ b/code/analysis/brake.py @@ -0,0 +1,218 @@ +import json +import os.path +import time +import pandas +from PySide6.QtCore import Signal, QThread +import openpyxl +import re +from common import clibs + + +class BrakeDataProcess(QThread): + output = Signal(str, str) + + def __init__(self, dir_path, /): + super().__init__() + self.dir_path = dir_path + self.idx = 0 + + def logger(self, level, module, content, color="black", error="", flag="both"): + clibs.logger(level, module, content, color, flag, signal=self.output) + if level.upper() == "ERROR": + raise Exception(f"{error} | {content}") + + def check_files(self, rawdata_dirs, result_files): + msg_wrong = "需要有四个文件和若干个数据文件夹,可参考如下确认:
" + msg_wrong += "- reach33/66/100_XXXXXXX.xlsx
- *.cfg
" + msg_wrong += "- reach33_load33_speed33
- reach33_load33_speed66
...
- reach100_load100_speed66
- reach100_load100_speed100
" + + if len(result_files) != 4 or len(rawdata_dirs) == 0: + self.logger("ERROR", "brake-check_files", msg_wrong, "red", "InitFileError") + + config_file, reach33_file, reach66_file, reach100_file = None, None, None, None + for result_file in result_files: + filename = result_file.split("/")[-1] + if re.match(".*\\.cfg", filename): + config_file = result_file + elif filename.startswith("reach33_") and filename.endswith(".xlsx"): + reach33_file = result_file + elif filename.startswith("reach66_") and filename.endswith(".xlsx"): + reach66_file = result_file + elif filename.startswith("reach100_") and filename.endswith(".xlsx"): + reach100_file = result_file + else: + if not (config_file and reach33_file and reach66_file and reach100_file): + self.logger("ERROR", "brake-check_files", msg_wrong, "red", "InitFileError") + + reach_s = ['reach33', 'reach66', 'reach100'] + load_s = ['load33', 'load66', 'load100'] + speed_s = ['speed33', 'speed66', 'speed100'] + prefix = [] + for rawdata_dir in rawdata_dirs: + components = rawdata_dir.split("/")[-1].split('_') # reach_load_speed + prefix.append(components[0]) + if components[0] not in reach_s or components[1] not in load_s or components[2] not in speed_s: + msg = f"报错信息:数据目录 {rawdata_dir} 命名不合规,请参考如下形式
" + msg += "命名规则:reachAA_loadBB_speedCC,AA/BB/CC 指的是臂展/负载/速度的比例
" + msg += "规则解释:reach66_load100_speed33,表示 66% 臂展,100% 负载以及 33% 速度情况下的测试结果文件夹
" + self.logger("ERROR", "brake-check_files", msg, "red", "WrongDataFolder") + + _, rawdata_files = clibs.traversal_files(rawdata_dir, self.output) + if len(rawdata_files) != 3: + msg = f"数据目录 {rawdata_dir} 下数据文件个数错误,每个数据目录下有且只能有三个以 .data 为后缀的数据文件" + self.logger("ERROR", "brake-check_files", msg, "red", "WrongDataFile") + + for rawdata_file in rawdata_files: + if not rawdata_file.endswith(".data"): + msg = f"数据文件 {rawdata_file} 后缀错误,每个数据目录下有且只能有三个以 .data 为后缀的数据文件" + self.logger("ERROR", "brake-check_files", msg, "red", "WrongDataFile") + + result_files = [] + for _ in [reach33_file, reach66_file, reach100_file]: + if _.split("/")[-1].split("_")[0] in set(prefix): + result_files.append(_) + + self.logger("INFO", "brake-check_files", "数据目录合规性检查结束,未发现问题......", "green") + return config_file, result_files + + def get_configs(self, config_file): + try: + with open(config_file, mode="r", encoding="utf-8") as f_config: + configs = json.load(f_config) + + p_dir = config_file.split('/')[-2] + if not re.match("^[jJ][123]$", p_dir): + self.logger("ERROR", "brake-get_configs-1", "被处理的根文件夹命名必须是 [Jj][123] 的格式", "red", "DirNameError") + + axis = int(p_dir[-1]) # 要处理的轴 + rrs = [abs(_) for _ in configs["TRANSMISSION"]["REDUCTION_RATIO_NUMERATOR"]] # 减速比,rr for reduction ratio + avs = configs["MOTION"]["JOINT_MAX_SPEED"] + rr = rrs[axis-1] + av = avs[axis-1] + return av, rr + except Exception as Err: + self.logger("ERROR", "brake-get_configs-2", f"无法打开 {config_file},或者使用了错误的机型配置文件,需检查
{Err}", "red", "OpenFileError") + + def now_doing_msg(self, docs, flag): + now = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())) + file_type = 'file' if os.path.isfile(docs) else 'dir' + if flag == 'start' and file_type == 'dir': + self.logger("INFO", "brake-now_doing_msg", f"[{now}] 正在处理目录 {docs} 中的数据......") + elif flag == 'start' and file_type == 'file': + self.logger("INFO", "brake-now_doing_msg", f"[{now}] 正在处理文件 {docs} 中的数据......") + elif flag == 'done' and file_type == 'dir': + self.logger("INFO", "brake-now_doing_msg", f"[{now}] 目录 {docs} 数据文件已处理完毕") + elif flag == 'done' and file_type == 'file': + self.logger("INFO", "brake-now_doing_msg", f"[{now}] 文件 {docs} 数据已处理完毕") + + @staticmethod + def data2result(df, ws_result, row_start, row_end): + data = [] + for row in range(row_start, row_end): + data.append(df.iloc[row, 0]) + data.append(df.iloc[row, 1]) + data.append(df.iloc[row, 2]) + + i = 0 + row_max = 1000 if row_end - row_start < 1000 else row_end - row_start + 100 + for row in range(2, row_max): + try: + ws_result.cell(row=row, column=1).value = data[i] + ws_result.cell(row=row, column=2).value = data[i + 1] + ws_result.cell(row=row, column=3).value = data[i + 2] + i += 3 + except Exception: + ws_result.cell(row=row, column=1).value = None + ws_result.cell(row=row, column=2).value = None + ws_result.cell(row=row, column=3).value = None + + def get_row_range(self, data_file, df, conditions, av, rr): + row_start, row_end = 0, 0 + ratio = float(conditions[2].removeprefix('speed')) / 100 + av_max = av * ratio + threshold = 0.95 + + for row in range(df.index[-1] - 1, -1, -10): + if df.iloc[row, 2] != 0: + row_start = row - 20 if row - 20 > 0 else 0 # 急停前找 20 个点 + break + else: + self.logger("ERROR", "brake-get_row_range", f"数据文件 {data_file} 采集的数据中没有 ESTOP 为非 0 的情况,需要确认", "red", "StartNotFoundError") + + for row in range(row_start, df.index[-1] - 1, 10): + speed_row = df.iloc[row, 0] * clibs.RADIAN * rr * 60 / 360 + if abs(speed_row) < 1: + row_end = row + 100 if row + 100 <= df.index[-1] - 1 else df.index[-1] - 1 + break + else: + self.logger("ERROR", "brake-get_row_range", f"数据文件 {data_file} 最后的速度未降为零", "red", "SpeedNotZeroError") + + av_estop = abs(df.iloc[row_start - 20:row_start, 0].abs().mean() * clibs.RADIAN) + if abs(av_estop / av_max) < threshold: + filename = data_file.split("/")[-1] + msg = f"[av_estop: {av_estop:.2f} | shouldbe: {av_max:.2f}] 数据文件 {filename} 触发 ESTOP 时未采集到指定百分比的最大速度,需要检查" + self.logger("WARNING", "brake-get_row_range", msg, "#8A2BE2") + + return row_start, row_end + + @staticmethod + def get_shtname(conditions, count): + # 33%负载_33%速度_1 - reach/load/speed + load = conditions[1].removeprefix('load') + speed = conditions[2].removeprefix('speed') + result_sheet_name = f"{load}%负载_{speed}%速度_{count}" + + return result_sheet_name + + def single_file_process(self, data_file, wb, count, av, rr): + df = pandas.read_csv(data_file, sep='\t') + conditions = data_file.split("/")[-2].split("_") # reach/load/speed + shtname = self.get_shtname(conditions, count) + ws = wb[shtname] + + row_start, row_end = self.get_row_range(data_file, df, conditions, av, rr) + self.data2result(df, ws, row_start, row_end) + + def data_process(self, result_file, rawdata_dirs, av, rr): + filename = result_file.split("/")[-1] + self.logger("INFO", "brake-data_process", f"正在打开文件 {filename},这可能需要一些时间......", "blue") + try: + wb = openpyxl.load_workbook(result_file) + except Exception as Err: + self.logger("ERROR", "brake-data_process", f"{filename}文件打开失败,可能是文件已损坏,确认后重新执行!
{Err}", "red", "CannotOpenFile") + + prefix = filename.split('_')[0] + for rawdata_dir in rawdata_dirs: + if rawdata_dir.split("/")[-1].split('_')[0] == prefix: + self.now_doing_msg(rawdata_dir, 'start') + _, data_files = clibs.traversal_files(rawdata_dir, self.output) + for idx in range(3): + self.single_file_process(data_files[idx], wb, idx+1, av, rr) + # threads = [ + # threading.Thread(target=self.single_file_process, args=(data_files[0], wb, 1, av, rr)), + # threading.Thread(target=self.single_file_process, args=(data_files[1], wb, 2, av, rr)), + # threading.Thread(target=self.single_file_process, args=(data_files[2], wb, 3, av, rr)) + # ] + # [t.start() for t in threads] + # [t.join() for t in threads] + self.now_doing_msg(rawdata_dir, 'done') + + self.logger("INFO", "brake-data_process", f"正在保存文件 {filename},这可能需要一些时间......
", "blue") + wb.save(result_file) + wb.close() + + def processing(self): + time_start = time.time() + clibs.running[self.idx] = 1 + + rawdata_dirs, result_files = clibs.traversal_files(self.dir_path, self.output) + config_file, result_files = self.check_files(rawdata_dirs, result_files) + av, rr = self.get_configs(config_file) + + for result_file in result_files: + self.data_process(result_file, rawdata_dirs, av, rr) + + self.logger("INFO", "brake-processing", "-"*60 + "
全部处理完毕
", "purple") + time_total = time.time() - time_start + msg = f"处理时间:{time_total // 3600:02.0f} h {time_total % 3600 // 60:02.0f} m {time_total % 60:02.0f} s" + self.logger("INFO", "brake-processing", msg) diff --git a/code/analysis/current.py b/code/analysis/current.py new file mode 100644 index 0000000..533ec1e --- /dev/null +++ b/code/analysis/current.py @@ -0,0 +1,427 @@ +import json +import openpyxl +import pandas +import re +import csv +from PySide6.QtCore import Signal, QThread +import time +from common import clibs + + +class CurrentDataProcess(QThread): + output = Signal(str, str) + + def __init__(self, dir_path, proc, /): + super().__init__() + self.dir_path = dir_path + self.proc = proc + self.idx = 1 + + def logger(self, level, module, content, color="black", error="", flag="both"): + clibs.logger(level, module, content, color, flag, signal=self.output) + if level.upper() == "ERROR": + raise Exception(f"{error} | {content}") + + def initialization(self): + _, data_files = clibs.traversal_files(self.dir_path, self.output) + count, config_file = 0, None + for data_file in data_files: + filename = data_file.split("/")[-1] + if re.match(".*\\.cfg", filename): + config_file = data_file + count += 1 + elif filename == "T_电机电流.xlsx": + count += 1 + else: + if not re.match("^j[1-7].*\\.data$", filename): + msg = f"不合规 {data_file}
" + msg += "所有数据文件必须以 j[1-7]_ 开头,以 .data 结尾,比如j1_abcdef.data,请检查整改后重新运行" + self.logger("ERROR", "current-initialization", msg, "red", "FilenameIllegal") + + if count != 2: + msg = "需要有一个机型配置文件\"*.cfg\",以及一个数据处理文件\"T_电机电流.xlsx\"表格,请检查整改后重新运行" + self.logger("ERROR", "current-initialization", msg, "red", "FilenameIllegal") + + return data_files, config_file + + def current_max(self, data_files, rts): + self.logger("INFO", "current-current_max", f"正在处理最大转矩值逻辑......") + current = {1: [], 2: [], 3: [], 4: [], 5: [], 6: []} + for data_file in data_files: + if data_file.endswith(".data"): + df = pandas.read_csv(data_file, sep="\t") + else: + continue + + self.logger("INFO", "current-current_max", f"正在处理 {data_file} ...") + cols = len(df.columns) + axis = int(data_file.split("/")[-1].split("_")[0].removeprefix("j")) + rt = rts[axis-1] + self.logger("INFO", "current-current_max", f"最大列数为 {cols},{axis} 轴的额定转矩为 {rt}") + + col = df.columns.values[clibs.c_servo_trq-1] # 获取 "device_servo_trq_feedback" + c_max = df[col].abs().max() + + scale = 1000 + _ = abs(c_max/scale*rt) + current[axis].append(_) + self.logger("INFO", "current-current_max", f"{data_file}: {_:.2f}") + self.logger("INFO", "current-current_max", f"获取到的列名为 {col},最大转矩为 {_}") + + with open(data_file, "a+") as f_data: + csv_writer = csv.writer(f_data, delimiter="\t") + csv_writer.writerow([""] * (cols-1) + [_]) + + for axis, cur in current.items(): + if not cur: + continue + else: + _ = "" + for value in cur: + _ += f"{value:.4f} " + self.logger("INFO", "current-current_max", f"{axis}轴最大转矩数据:{_}") + + self.logger("INFO", "current-current_max", f"获取最大转矩值结束 current_max = {current}") + self.logger("INFO", "current-current_max", f"最大转矩数据处理完毕......") + return current + + def current_avg(self, data_files, rts): + self.logger("INFO", "current-current_avg", f"正在处理平均转矩值逻辑......") + current = {1: [], 2: [], 3: [], 4: [], 5: [], 6: []} + for data_file in data_files: + if data_file.endswith(".data"): + df = pandas.read_csv(data_file, sep="\t") + else: + continue + + self.logger("INFO", "current-current_avg", f"正在处理 {data_file} ...") + cols = len(df.columns) + axis = int(data_file.split("/")[-1].split("_")[0].removeprefix("j")) + rt = rts[axis-1] + self.logger("INFO", "current-current_avg", f"最大列数为 {cols},{axis} 轴的额定转矩为 {rt}") + + col = df.columns.values[clibs.c_servo_trq-1] + c_std = df[col].std() + c_avg = df[col].mean() + + scale = 1000 + _ = (abs(c_avg)+c_std*3)/scale*rt + current[axis].append(_) + self.logger("INFO", "current-current_avg", f"{data_file}: {_:.2f}") + self.logger("INFO", "current-current_avg", f"获取到的列名为 {col},平均转矩为 {_}") + + with open(data_file, "a+") as f_data: + csv_writer = csv.writer(f_data, delimiter="\t") + csv_writer.writerow([""] * (cols-1) + [_]) + + for axis, cur in current.items(): + if not cur: + continue + else: + _ = "" + for value in cur: + _ += f"{value:.4f} " + self.logger("INFO", "current-current_avg", f"{axis}轴平均转矩数据:{_}") + + self.logger("INFO", "current-current_avg", f"获取平均转矩值结束 current_avg = {current}", flag="cursor") + self.logger("INFO", "current-current_avg", f"平均转矩数据处理完毕......") + return current + + def current_cycle(self, data_files, rrs, rts, params): + result, hold, single, scenario, dur_time = None, [], [], [], 0 + for data_file in data_files: + filename = data_file.split("/")[-1] + if filename == "T_电机电流.xlsx": + result = data_file + elif re.match("j[1-7]_hold_.*\\.data", filename): + hold.append(data_file) + elif re.match("j[1-7]_s_.*\\.data", filename): + scenario.append(data_file) + dur_time = float(filename.split("_")[3]) + elif re.match("j[1-7]_.*\\.data", filename): + single.append(data_file) + + clibs.stop, filename = True, result.split("/")[-1] + self.logger("INFO", "current-current_cycle", f"正在打开文件 {filename},这可能需要一些时间......", "blue") + try: + wb = openpyxl.load_workbook(result) + except Exception as Err: + self.logger("ERROR", "current-current_cycle", f"{filename}文件打开失败,可能是文件已损坏,确认后重新执行!
{Err}", "red", "CannotOpenFile") + + ws = wb["统计"] + for idx in range(len(params)-1): + row = idx + 2 + for col in range(2, 8): + ws.cell(row=row, column=col).value = params[idx][col-2] + ws.cell(row=1, column=1).value = params[-1] + + if hold: + avg = self.current_avg(hold, rts) + for axis, cur_value in avg.items(): + sht_name = f"J{axis}" + wb[sht_name]["P4"].value = float(cur_value[0]) + + if dur_time == 0: + self.p_single(wb, single, rrs) + else: + self.p_scenario(wb, scenario, rrs, dur_time) + + self.logger("INFO", "current-current_cycle", f"正在保存文件 {filename},这可能需要一些时间......", "blue") + wb.save(result) + wb.close() + + def find_point(self, data_file, df, flag, row_s, row_e, threshold, step, end_point, skip_scale, axis, seq): + if flag == "lt": + while row_e > end_point: + speed_avg = df.iloc[row_s:row_e].abs().mean() + if speed_avg < threshold: + row_e -= step + row_s -= step + continue + else: + # one more time,如果连续两次 200 个点的平均值都大于 threshold,说明已经到了临界点了(其实也不一定,只不过相对遇到一次就判定临界点更安全一点点) + # 从实际数据看,这开逻辑很小概率能触发到 + speed_avg = df.iloc[row_s-end_point*skip_scale:row_e-end_point*skip_scale].abs().mean() + if speed_avg < threshold: + self.logger("WARNING", "current-find_point", f"【lt】{axis} 轴第 {seq} 次查找数据可能有异常,row_s = {row_s}, row_e = {row_e}!", "purple") + return row_s, row_e + else: + self.logger("ERROR", "current-find_point", f"{data_file} 数据有误,需要检查,无法找到第 {seq} 个有效点......", "red", "AnchorNotFound") + elif flag == "gt": + while row_e > end_point: + speed_avg = df.iloc[row_s:row_e].abs().mean() + # if axis == 1 and seq == 1: + # insert_logdb("DEBUG", "current", f"【gt】{axis} 轴,speed_avg = {speed_avg},row_s = {row_s}, row_e = {row_e}!") + if speed_avg > threshold: + row_e -= step + row_s -= step + continue + else: + # one more time,如果连续两次 200 个点的平均值都小于 threshold,说明已经到了临界点了(其实也不一定,只不过相对遇到一次就判定临界点更安全一点点) + # 从实际数据看,这开逻辑很小概率能触发到 + speed_avg = df.iloc[row_s-end_point*skip_scale:row_e-end_point*skip_scale].abs().mean() + if speed_avg > threshold: + self.logger("WARNING", "current-find_point", f"【gt】{axis} 轴第 {seq} 次查找数据可能有异常,row_s = {row_s}, row_e = {row_e}!", "purple") + return row_s, row_e + else: + self.logger("ERROR", "current-find_point", f"{data_file} 数据有误,需要检查,无法找到第 {seq} 个有效点......", "red", "AnchorNotFound") + + def get_row_number(self, threshold, flag, df, row_s, row_e, axis): + count_1, count_2 = 0, 0 + if flag == "start" or flag == "end": + for number in df.iloc[row_s:row_e].abs(): + count_2 += 1 + if number > threshold: + count_1 += 1 + if count_1 == 10: + return row_s + count_2 - 10 + else: + count_1 = 0 + elif flag == "middle": + for number in df.iloc[row_s:row_e].abs(): + count_2 += 1 + if number < threshold: # 唯一的区别 + count_1 += 1 + if count_1 == 10: + return row_s + count_2 - 10 + else: + count_1 = 0 + + places = {"start": "起点", "middle": "中间点", "end": "终点"} # 因为是终点数据,所以可能有异常 + self.logger("WARNING", "current-get_row_number", f"{axis} 轴获取{places[flag]}数据 {row_e} 可能有异常,需关注!", "purple") + return row_e + + def p_single(self, wb, single, rrs): + # 1. 先找到第一个速度为零的点,数据从后往前找,一开始就是零的情况不予考虑 + # 2. 记录第一个点的位置,继续向前查找第二个速度为零的点,同理,一开始为零的点不予考虑 + # 3. 记录第二个点的位置,并将其中的数据拷贝至对应位置 + for data_file in single: + axis = int(data_file.split("/")[-1].split("_")[0].removeprefix("j")) + sht_name = f"J{axis}" + ws = wb[sht_name] + pandas.set_option("display.precision", 2) + df_origin = pandas.read_csv(data_file, sep="\t") + rr = rrs[axis-1] + addition = 180 / 3.1415926 * 60 / 360 * rr + + col_names = list(df_origin.columns) + df = df_origin[col_names[clibs.c_joint_vel-1]].multiply(addition) + + step = 50 # 步进值 + end_point = 200 # 有效数值的数目 + threshold = 5 # 200个点的平均阈值线 + skip_scale = 2 + row_start, row_middle, row_end = 0, 0, 0 + row_e = df.index[-1] + row_s = row_e - end_point + speed_avg = df.iloc[row_s:row_e].abs().mean() + if speed_avg < threshold: + # 第一次过滤:消除速度为零的数据,找到速度即将大于零的上升临界点 + row_s, row_e = self.find_point(data_file, df, "lt", row_s, row_e, threshold, step, end_point, skip_scale, axis, "pre-1") + row_e -= end_point*skip_scale + row_s -= end_point*skip_scale + # 第二次过滤:消除速度大于零的数据,找到速度即将趋近于零的下降临界点 + row_s, row_e = self.find_point(data_file, df, "gt", row_s, row_e, threshold, step, end_point, skip_scale, axis, "pre-2") + row_e -= end_point*skip_scale + row_s -= end_point*skip_scale + # 第三次过滤:消除速度为零的数据,找到速度即将大于零的上升临界点 + row_s, row_e = self.find_point(data_file, df, "lt", row_s, row_e, threshold, step, end_point, skip_scale, axis, "pre-3") + row_e -= end_point*skip_scale + row_s -= end_point*skip_scale + # 正式第一次采集:消除速度大于零的数据,找到速度即将趋近于零的下降临界点 + row_s, row_e = self.find_point(data_file, df, "gt", row_s, row_e, threshold, step, end_point, skip_scale, axis, 1) + row_end = self.get_row_number(threshold, "end", df, row_s, row_e, axis) + row_e -= end_point*skip_scale + row_s -= end_point*skip_scale + # 正式第二次采集:消除速度为零的数据,找到速度即将大于零的上升临界点 + row_s, row_e = self.find_point(data_file, df, "lt", row_s, row_e, threshold, step, end_point, skip_scale, axis, 2) + row_middle = self.get_row_number(threshold, "middle", df, row_s, row_e, axis) + row_e -= end_point*skip_scale + row_s -= end_point*skip_scale + # 正式第三次采集:消除速度大于零的数据,找到速度即将趋近于零的下降临界点 + row_s, row_e = self.find_point(data_file, df, "gt", row_s, row_e, threshold, step, end_point, skip_scale, axis, 3) + row_start = self.get_row_number(threshold, "start", df, row_s, row_e, axis) + elif speed_avg > threshold: + # 第一次过滤:消除速度大于零的数据,找到速度即将趋近于零的下降临界点 + row_s, row_e = self.find_point(data_file, df, "gt", row_s, row_e, threshold, step, end_point, skip_scale, axis, "pre-1") + row_e -= end_point*skip_scale + row_s -= end_point*skip_scale + # 第二次过滤:消除速度为零的数据,找到速度即将大于零的上升临界点 + row_s, row_e = self.find_point(data_file, df, "lt", row_s, row_e, threshold, step, end_point, skip_scale, axis, "pre-2") + row_e -= end_point*skip_scale + row_s -= end_point*skip_scale + # 第一次正式采集:消除速度大于零的数据,找到速度即将趋近于零的下降临界点 + row_s, row_e = self.find_point(data_file, df, "gt", row_s, row_e, threshold, step, end_point, skip_scale, axis, 1) + row_end = self.get_row_number(threshold, "end", df, row_s, row_e, axis) + row_e -= end_point*skip_scale + row_s -= end_point*skip_scale + # 第二次正式采集:消除速度为零的数据,找到速度即将大于零的上升临界点 + row_s, row_e = self.find_point(data_file, df, "lt", row_s, row_e, threshold, step, end_point, skip_scale, axis, 2) + row_middle = self.get_row_number(threshold, "middle", df, row_s, row_e, axis) + row_e -= end_point*skip_scale + row_s -= end_point*skip_scale + # 第三次正式采集:消除速度大于零的数据,找到速度即将趋近于零的下降临界点 + row_s, row_e = self.find_point(data_file, df, "gt", row_s, row_e, threshold, step, end_point, skip_scale, axis, 3) + row_start = self.get_row_number(threshold, "start", df, row_s, row_e, axis) + + self.logger("INFO", "current", f"{axis} 轴起点:{row_start}") + self.logger("INFO", "current", f"{axis} 轴中间点:{row_middle}") + self.logger("INFO", "current", f"{axis} 轴终点:{row_end}") + self.logger("INFO", "current", f"{axis} 轴数据非零段点数:{row_middle-row_start+1}") + self.logger("INFO", "current", f"{axis} 轴数据为零段点数:{row_end-row_middle+1}") + if abs(row_end+row_start-2*row_middle) > 1000: + self.logger("WARNING", "current", f"{axis} 轴数据占空比异常!", "purple") + + data, first_c, second_c, third_c, fourth_c = [], clibs.c_joint_vel-1, clibs.c_servo_trq-1, clibs.c_sensor_trq-1, clibs.c_estimate_trans_trq-1 + for row in range(row_start, row_end+1): + data.append(df_origin.iloc[row, first_c]) + data.append(df_origin.iloc[row, second_c]) + data.append(df_origin.iloc[row, third_c]) + data.append(df_origin.iloc[row, fourth_c]) + + i = 0 + for row in ws.iter_rows(min_row=2, min_col=2, max_row=150000, max_col=5): + for cell in row: + try: + if i % 4 == 0: + ws.cell((i//4)+2, 1).value = float(((i//4)+1)/1000) + _ = f"{data[i]:.2f}" + cell.value = float(_) + i += 1 + except Exception: + if i % 4 == 0: + ws.cell((i//4)+2, 1).value = None + cell.value = None + i += 1 + + def p_scenario(self, wb, scenario, rrs, dur_time): + self.logger("INFO", "current", f"本次处理的是电机电流场景数据,场景运动周期为 {dur_time}s", "blue") + for data_file in scenario: + cycle = 0.001 + axis = int(data_file.split("/")[-1].split("_")[0].removeprefix("j")) + sht_name = f"J{axis}" + ws = wb[sht_name] + pandas.set_option("display.precision", 2) + df_origin = pandas.read_csv(data_file, sep="\t") + rr = rrs[axis-1] + addition = 180 / 3.1415926 * 60 / 360 * rr + + col_names = list(df_origin.columns) + df = df_origin[col_names[clibs.c_joint_vel-1]].multiply(addition) + + row_start = 3000 + row_end = row_start + int(dur_time/cycle) + if row_end > df.index[-1]: + self.logger("ERROR", "current-p_scenario", f"位置超限:{data_file} 共有 {df.index[-1]} 条数据,无法取到第 {row_end} 条数据,需要确认场景周期时间...", "blue", "DataOverLimit") + + data, first_c, second_c, third_c, fourth_c = [], clibs.c_joint_vel-1, clibs.c_servo_trq-1, clibs.c_sensor_trq-1, clibs.c_estimate_trans_trq-1 + for row in range(row_start, row_end+1): + data.append(df_origin.iloc[row, first_c]) + data.append(df_origin.iloc[row, second_c]) + data.append(df_origin.iloc[row, third_c]) + data.append(df_origin.iloc[row, fourth_c]) + + i = 0 + for row in ws.iter_rows(min_row=2, min_col=2, max_row=250000, max_col=5): + for cell in row: + try: + if i % 4 == 0: + ws.cell((i//4)+2, 1).value = float(((i//4)+1)/1000) + _ = f"{data[i]:.2f}" + cell.value = float(_) + i += 1 + except Exception: + cell.value = None + if i % 4 == 0: + ws.cell((i//4)+2, 1).value = None + i += 1 + + def get_configs(self, config_file): + try: + if re.match("^[NXEC]B.*", config_file.split("/")[-1]): + robot_type = "工业" + else: + robot_type = "协作" + + with open(config_file, mode="r", encoding="utf-8") as f_config: + configs = json.load(f_config) + + version = configs["VERSION"] + sc = [0.001, 0.001, 0.001, 0.001, 0.001, 0.001] # 采样周期,sc for sample cycle + r_rrs = configs["TRANSMISSION"]["REDUCTION_RATIO_NUMERATOR"] # 减速比,rr for reduction ratio + m_avs = configs["MOTION"]["JOINT_MAX_SPEED"] + m_stall_ts = configs["MOTOR"]["STALL_TORQUE"] # 电机堵转转矩 + m_rts = configs["MOTOR"]["RATED_TORQUE"] # 电机额定转矩rt for rated torque + m_max_ts = configs["MOTOR"]["PEAK_TORQUE"] # 电机峰值转矩 + m_r_rpms = configs["MOTOR"]["RATED_SPEED"] # 电机额定转速 + m_max_rpms = configs["MOTOR"]["MAX_SPEED"] # 电机最大转速 + r_max_sst = configs["TRANSMISSION"]["MAX_TORQUE_FOR_START_AND_STOP"] # 减速器最大启停转矩,sst for start and stop torque + r_max_t = configs["TRANSMISSION"]["MAX_PEAK_TORQUE"] # 减速器瞬时最大转矩 + r_avg_t = configs["TRANSMISSION"]["MAX_AVERAGE_TORQUE"] # 减速器平均负载转矩允许最大值 + + self.logger("INFO", "current", f"get_configs: 机型文件版本 {config_file}_{version}") + self.logger("INFO", "current", f"get_configs: 减速比 {r_rrs}") + self.logger("INFO", "current", f"get_configs: 额定转矩 {m_rts}") + self.logger("INFO", "current", f"get_configs: 最大角速度 {m_avs}") + return sc, r_rrs, m_avs, m_stall_ts, m_rts, m_max_ts, m_r_rpms, m_max_rpms, r_max_sst, r_max_t, r_avg_t, robot_type + except Exception as Err: + self.logger("ERROR", "current", f"get_config: 无法打开 {config_file},或获取配置文件参数错误 {Err}", "red", "OpenFileError") + + def processing(self): + time_start = time.time() + clibs.running[self.idx] = 1 + + data_files, config_file = self.initialization() + params = self.get_configs(config_file) + rts, rrs = params[4], params[1] + if self.proc == "最大值": + self.current_max(data_files, rts) + elif self.proc == "平均值": + self.current_avg(data_files, rts) + elif self.proc == "周期": + self.current_cycle(data_files, rrs, rts, params) + + self.logger("INFO", "current-processing", "-"*60 + "
全部处理完毕
", "purple") + time_total = time.time() - time_start + msg = f"数据处理时间:{time_total // 3600:02.0f} h {time_total % 3600 // 60:02.0f} m {time_total % 60:02.0f} s\n" + self.logger("INFO", "current-processing", msg) diff --git a/code/analysis/iso.py b/code/analysis/iso.py new file mode 100644 index 0000000..2aeacda --- /dev/null +++ b/code/analysis/iso.py @@ -0,0 +1,213 @@ +import pdfplumber +import openpyxl +import os +import time +from PySide6.QtCore import Signal, QThread +from common import clibs + + +class IsoDataProcess(QThread): + output = Signal(str, str) + + def __init__(self, dir_path, /): + super().__init__() + self.dir_path = dir_path + self.idx = 2 + + def logger(self, level, module, content, color="black", error="", flag="both"): + clibs.logger(level, module, content, color, flag, signal=self.output) + if level.upper() == "ERROR": + raise Exception(f"{error} | {content}") + + def p_iso(self, file, p_files, ws, tmpfile): + p_files.append(file) + + pdf = pdfplumber.open(file) + with open(tmpfile, mode="w", encoding="utf-8") as fb: + for page in pdf.pages: + fb.write(page.extract_text()) + with open(tmpfile, mode="r", encoding="utf-8") as fb: + lines = fb.readlines() + lines = [line for line in lines if not line.startswith("Page ")] + for line in lines: + if line.strip() == "Pose Accuracy and Repeatability": + index = lines.index(line) + ws.cell(row=3, column=7).value = float(lines[index+4].split()[1]) + ws.cell(row=4, column=7).value = float(lines[index+5].split()[1]) + ws.cell(row=5, column=7).value = float(lines[index+6].split()[1]) + ws.cell(row=6, column=7).value = float(lines[index+7].split()[1]) + ws.cell(row=7, column=7).value = float(lines[index+8].split()[1]) + + ws.cell(row=8, column=7).value = float(lines[index+4].split()[2]) + ws.cell(row=9, column=7).value = float(lines[index+5].split()[2]) + ws.cell(row=10, column=7).value = float(lines[index+6].split()[2]) + ws.cell(row=11, column=7).value = float(lines[index+7].split()[2]) + ws.cell(row=12, column=7).value = float(lines[index+8].split()[2]) + elif line.strip() == "Pose Accuracy Variation": + index = lines.index(line) + ws.cell(row=13, column=7).value = float(lines[index+4].split()[1]) + ws.cell(row=14, column=7).value = float(lines[index+5].split()[1]) + ws.cell(row=15, column=7).value = float(lines[index+6].split()[1]) + elif line.strip() == "Distance Accuracy": + index = lines.index(line) + ws.cell(row=16, column=7).value = float(lines[index + 4].split()[1]) + ws.cell(row=17, column=7).value = float(lines[index + 4].split()[2]) + elif line.strip() == "Stabilisation Time and Overshoot": + index = lines.index(line) + ws.cell(row=18, column=7).value = float(lines[index + 7].split()[3]) + ws.cell(row=19, column=7).value = float(lines[index + 7].split()[2]) + elif line.strip() == "Velocity Accuracy and Repeatability": + index = lines.index(line) + ws.cell(row=20, column=7).value = float(lines[index + 4].split()[1]) + ws.cell(row=21, column=7).value = float(lines[index + 4].split()[2]) + ws.cell(row=22, column=7).value = float(lines[index + 4].split()[3]) + elif line.strip()[:31] == "Path Accuracy and Repeatability": + index = lines.index(line) + ws.cell(row=29, column=7).value = float(lines[index + 4].split()[1]) + ws.cell(row=30, column=7).value = float(lines[index + 4].split()[2]) + elif line.strip() == "Corner Overshoot and Roundoff": + index = lines.index(line) + ws.cell(row=35, column=7).value = float(lines[index + 4].split()[1]) + ws.cell(row=36, column=7).value = float(lines[index + 4].split()[2]) + elif line.strip() == "Robot Weaving": + index = lines.index(line) + ws.cell(row=41, column=7).value = float(lines[index + 4].split()[2]) + ws.cell(row=42, column=7).value = float(lines[index + 4].split()[3]) + ws.cell(row=43, column=7).value = float(lines[index + 4].split()[4]) + else: + pass + pdf.close() + + def p_iso_100(self, file, p_files, ws, tmpfile): + p_files.append(file) + + pdf = pdfplumber.open(file) + with open(tmpfile, mode="w", encoding="utf-8") as fb: + for page in pdf.pages: + fb.write(page.extract_text()) + with open(tmpfile, mode="r", encoding="utf-8") as fb: + lines = fb.readlines() + lines = [line for line in lines if not line.startswith("Page ")] + for line in lines: + if line.strip() == "Velocity Accuracy and Repeatability": + index = lines.index(line) + ws.cell(row=26, column=7).value = float(lines[index + 4].split()[1]) + ws.cell(row=27, column=7).value = float(lines[index + 4].split()[2]) + ws.cell(row=28, column=7).value = float(lines[index + 4].split()[3]) + elif line.strip()[:31] == "Path Accuracy and Repeatability": + index = lines.index(line) + ws.cell(row=33, column=7).value = float(lines[index + 4].split()[1]) + ws.cell(row=34, column=7).value = float(lines[index + 4].split()[2]) + elif line.strip() == "Corner Overshoot and Roundoff": + index = lines.index(line) + ws.cell(row=39, column=7).value = float(lines[index + 4].split()[1]) + ws.cell(row=40, column=7).value = float(lines[index + 4].split()[2]) + elif line.strip() == "Robot Weaving": + index = lines.index(line) + ws.cell(row=47, column=7).value = float(lines[index + 4].split()[2]) + ws.cell(row=48, column=7).value = float(lines[index + 4].split()[3]) + ws.cell(row=49, column=7).value = float(lines[index + 4].split()[4]) + else: + pass + pdf.close() + + def p_iso_1000(self, file, p_files, ws, tmpfile): + p_files.append(file) + + pdf = pdfplumber.open(file) + with open(tmpfile, mode="w", encoding="utf-8") as fb: + for page in pdf.pages: + fb.write(page.extract_text()) + with open(tmpfile, mode="r", encoding="utf-8") as fb: + lines = fb.readlines() + lines = [line for line in lines if not line.startswith("Page ")] + for line in lines: + if line.strip() == "Velocity Accuracy and Repeatability": + index = lines.index(line) + ws.cell(row=23, column=7).value = float(lines[index + 4].split()[1]) + ws.cell(row=24, column=7).value = float(lines[index + 4].split()[2]) + ws.cell(row=25, column=7).value = float(lines[index + 4].split()[3]) + elif line.strip()[:31] == "Path Accuracy and Repeatability": + index = lines.index(line) + ws.cell(row=31, column=7).value = float(lines[index + 4].split()[1]) + ws.cell(row=32, column=7).value = float(lines[index + 4].split()[2]) + elif line.strip() == "Corner Overshoot and Roundoff": + index = lines.index(line) + ws.cell(row=37, column=7).value = float(lines[index + 4].split()[1]) + ws.cell(row=38, column=7).value = float(lines[index + 4].split()[2]) + elif line.strip() == "Robot Weaving": + index = lines.index(line) + ws.cell(row=44, column=7).value = float(lines[index + 4].split()[2]) + ws.cell(row=45, column=7).value = float(lines[index + 4].split()[3]) + ws.cell(row=46, column=7).value = float(lines[index + 4].split()[4]) + else: + pass + pdf.close() + + def initialization(self): + dirs, files = clibs.traversal_files(self.dir_path, self.output) + if len(dirs) != 0: + self.logger("ERROR", "iso", f"init: 工作目录下不可以有文件夹!", "red", "InitFileError") + + for file in files: + file = file.lower() + if file.endswith("iso-results.xlsx"): + pass + elif file.endswith("iso-v1000.pdf"): + pass + elif file.endswith("iso-v100.pdf"): + pass + elif file.endswith("iso.pdf"): + pass + else: + self.logger("ERROR", "iso", f"init: 工作目录下只允许有如下四个文件,不区分大小写,pdf文件最少有一个!
1. iso-results.xlsx
2. ISO.pdf
3. ISO-V100.pdf
4. ISO-V1000.pdf", "red", "InitFileError") + + return files + + def processing(self): + time_start = time.time() + clibs.running[self.idx] = 1 + + files = self.initialization() + filename = f"{self.dir_path}/iso-results.xlsx" + tmpfile = f"{self.dir_path}/data.txt" + wb, ws = None, None + try: + wb = openpyxl.load_workbook(filename) + ws = wb.active + for i in range(3, 50): + ws.cell(row=i, column=7).value = None + except Exception as Err: + self.logger("ERROR", "iso", f"main: 无法打开文件 {filename}
{Err}", "red", "FileOpenError") + + p_files = [] + for file in files: + if file.split("/")[-1].lower() == "iso.pdf": + self.logger("INFO", "iso", f"正在处理{file}......") + self.p_iso(file, p_files, ws, tmpfile) + self.logger("INFO", "iso", f"文件{file}已处理完毕。") + + elif file.split("/")[-1].lower() == "iso-v100.pdf": + self.logger("INFO", "iso", f"正在处理{file}......") + self.p_iso_100(file, p_files, ws, tmpfile) + self.logger("INFO", "iso", f"文件{file}已处理完毕。") + + elif file.split("/")[-1].lower() == "iso-v1000.pdf": + self.logger("INFO", "iso", f"正在处理{file}......") + self.p_iso_1000(file, p_files, ws, tmpfile) + self.logger("INFO", "iso", f"文件{file}已处理完毕。") + + else: + pass + wb.save(filename) + wb.close() + + if len(p_files) == 0: + self.logger("ERROR", "iso", f"目录 {self.dir_path} 下没有需要处理的文件,需要确认......", "red", "FileNotFound") + else: + os.remove(tmpfile) + + self.logger("INFO", "current-processing", "-" * 60 + "
全部处理完毕
", "purple") + time_total = time.time() - time_start + msg = f"数据处理时间:{time_total // 3600:02.0f} h {time_total % 3600 // 60:02.0f} m {time_total % 60:02.0f} s\n" + self.logger("INFO", "current-processing", msg) diff --git a/code/analysis/wavelogger.py b/code/analysis/wavelogger.py new file mode 100644 index 0000000..1a3422f --- /dev/null +++ b/code/analysis/wavelogger.py @@ -0,0 +1,160 @@ +import pandas +import csv +import openpyxl +import chardet +import time +from PySide6.QtCore import Signal, QThread +from common import clibs + + +class WaveloggerDataProcess(QThread): + output = Signal(str, str) + + def __init__(self, dir_path, /): + super().__init__() + self.dir_path = dir_path + self.idx = 3 + + def logger(self, level, module, content, color="black", error="", flag="both"): + clibs.logger(level, module, content, color, flag, signal=self.output) + if level.upper() == "ERROR": + raise Exception(f"{error} | {content}") + + def find_point(self, bof, step, margin, threshold, pos, data_file, flag, df, row): + # bof: backward or forward + # pos: used for debug + # flag: greater than or lower than + row_target = None + row_origin = len(df) - margin + 1 + if flag == "gt": + while 0 < row < row_origin: + value = float(df.iloc[row, 2]) + if value > threshold: + row = row - step if bof == "backward" else row + step + continue + else: + row_target = row - step if bof == "backward" else row + step + break + else: + if bof == "backward": + self.logger("ERROR", "wavelogger-find_point", f"find_point-gt: [{pos}] 在 {data_file} 中,无法正确识别数据,需要确认...", "red", "DataError") + elif bof == "forward": + row_target = row + margin # to end while loop in function `single_file_proc` + elif flag == "lt": + while 0 < row < row_origin: + value = float(df.iloc[row, 2]) + if value < threshold: + row = row - step if bof == "backward" else row + step + continue + else: + row_target = row - step if bof == "backward" else row + step + break + else: + if bof == "backward": + self.logger("ERROR", "wavelogger-find_point", f"find_point-lt: [{pos}] 在 {data_file} 中,无法正确识别数据,需要确认...", "red", "DataError") + elif bof == "forward": + row_target = row + margin # to end while loop in function `single_file_proc` + return row_target + + def get_cycle_info(self, data_file, step, margin, threshold): + # end -> middle: low + # middle -> start: high + # 1. 从最后读取数据,无论是大于1还是小于1,都舍弃,找到相反的值的起始点 + # 2. 从起始点,继续往前寻找,找到与之数值相反的中间点 + # 3. 从中间点,继续往前寻找,找到与之数值相反的结束点,至此,得到了高低数值的时间区间以及一轮的周期时间 + with open(data_file, "rb") as f: + raw_data = f.read(1000) + result = chardet.detect(raw_data) + encoding = result['encoding'] + csv_reader = csv.reader(open(data_file, encoding=encoding)) + begin = int(next(csv_reader)[1]) + df = pandas.read_csv(data_file, sep=",", encoding=encoding, skip_blank_lines=False, header=begin - 1, on_bad_lines="skip") + row = len(df) - margin + if float(df.iloc[row, 2]) < threshold: + row = self.find_point("backward", step, margin, threshold, "a1", data_file, "lt", df, row) + + _row = self.find_point("backward", step, margin, threshold, "a2", data_file, "gt", df, row) + _row = self.find_point("backward", step, margin, threshold, "a3", data_file, "lt", df, _row) + row_end = self.find_point("backward", step, margin, threshold, "a4", data_file, "gt", df, _row) + row_middle = self.find_point("backward", step, margin, threshold, "a5", data_file, "lt", df, row_end) + row_start = self.find_point("backward", step, margin, threshold, "a6", data_file, "gt", df, row_middle) + # print(f"row_end = {row_end}") + # print(f"row_middle = {row_middle}") + # print(f"row_start = {row_start}") + return row_end-row_middle, row_middle-row_start, row_end-row_start, df + + def initialization(self): + _, data_files = clibs.traversal_files(self.dir_path, self.output) + + for data_file in data_files: + if not data_file.lower().endswith(".csv"): + self.logger("ERROR", "wavelogger-initialization", f"init: {data_file} 文件后缀错误,只允许 .csv 文件,需要确认!", "red", "FileTypeError") + + return data_files + + def preparation(self, data_file, step, margin, threshold, wb): + shtname = data_file.split("/")[-1].split(".")[0] + ws = wb.create_sheet(shtname) + low, high, cycle, df = self.get_cycle_info(data_file, step, margin, threshold) + + return ws, df, low, high, cycle + + def single_file_proc(self, ws, data_file, step, threshold, margin, data_length, df, cycle): + row, row_lt, row_gt, count, count_i, data = 1, 1, 1, 1, 1, {} + row_max = len(df) - margin + while row < row_max: + if count not in data.keys(): + data[count] = [] + + value = float(df.iloc[row, 2]) + if value < threshold: + row_lt = self.find_point("forward", step, margin, threshold, "c"+str(row), data_file, "lt", df, row) + start = int(row_gt + (row_lt - row_gt - data_length) / 2) + end = start + data_length + value = df.iloc[start:end, 2].astype(float).mean() + 3 * df.iloc[start:end, 2].astype(float).std() + if value > 1: + msg = f"\n" + self.logger("WARNING", "wavelogger-single_file_proc", f"{data_file} 文件第 {count} 轮 第 {count_i} 个数据可能有问题,需人工手动确认,确认有问题可删除,无问题则保留", "purple") + + data[count].append(value) + count_i += 1 + else: + row_gt = self.find_point("forward", step, margin, threshold, "c"+str(row), data_file, "gt", df, row) + if row_gt - row_lt > cycle * 2: + count += 1 + count_i = 1 + row = max(row_gt, row_lt) + for i in range(2, 10): + ws.cell(row=1, column=i).value = f"第{i-1}次测试" + ws.cell(row=i, column=1).value = f"第{i-1}次精度变化" + + for i in sorted(data.keys()): + row, column = 2, i + 1 + for value in data[i]: + ws.cell(row=row, column=column).value = float(value) + row += 1 + + def execution(self, data_files): + self.logger("INFO", "wavelogger-execution", "正在处理中......", "blue") + wb = openpyxl.Workbook() + step, margin, data_length, threshold = 5, 50, 50, 5 + for data_file in data_files: + ws, df, low, high, cycle = self.preparation(data_file, step, margin, threshold, wb) + self.single_file_proc(ws, data_file, step, threshold, margin, data_length, df, cycle) + + wd = "/".join(data_files[0].split("/")[:-1]) + filename = wd + "/result.xlsx" + wb.save(filename) + wb.close() + + def processing(self): + time_start = time.time() + clibs.running[self.idx] = 1 + + data_files = self.initialization() + self.execution(data_files) + + self.logger("INFO", "wavelogger-processing", "-" * 60 + "
全部处理完毕
", "purple") + time_total = time.time() - time_start + msg = f"数据处理时间:{time_total // 3600:02.0f} h {time_total % 3600 // 60:02.0f} m {time_total % 60:02.0f} s\n" + self.logger("INFO", "wavelogger-processing", msg) diff --git a/code/autotest/do_brake.py b/code/autotest/do_brake.py new file mode 100644 index 0000000..b30a5fb --- /dev/null +++ b/code/autotest/do_brake.py @@ -0,0 +1,374 @@ +import time +import os +import paramiko +import openpyxl +import pandas +import json +from PySide6.QtCore import Signal, QThread +from common import clibs + + +class DoBrakeTest(QThread): + output = Signal(str, str) + + def __init__(self, dir_path, tool, /): + super().__init__() + self.dir_path = dir_path + self.tool = tool + self.idx = 4 + + def logger(self, level, module, content, color="black", error="", flag="both"): + clibs.logger(level, module, content, color, flag, signal=self.output) + if level.upper() == "ERROR": + raise Exception(f"{error} | {content}") + + def initialization(self, data_dirs, data_files): + def check_files(): + msg = "初始路径下不允许有文件夹,初始路径下只能存在如下五个文件,且文件为关闭状态,确认后重新运行!
" + msg += "1. configs.xlsx
2. reach33/reach66/reach100_xxxx.xlsx
3. xxxx.zip" + if len(data_dirs) != 0 or len(data_files) != 5: + self.logger("ERROR", "do_brake-check_files", msg, "red", "InitFileError") + + config_file, reach33_file, reach66_file, reach100_file, prj_file, result_dirs = None, None, None, None, None, [] + for data_file in data_files: + filename = data_file.split("/")[-1] + if filename == "configs.xlsx": + config_file = data_file + elif filename.startswith("reach33_") and filename.endswith(".xlsx"): + reach33_file = data_file + elif filename.startswith("reach66_") and filename.endswith(".xlsx"): + reach66_file = data_file + elif filename.startswith("reach100_") and filename.endswith(".xlsx"): + reach100_file = data_file + elif filename.endswith(".zip"): + prj_file = data_file + else: + self.logger("ERROR", "do_brake-check_files", msg, "red", "InitFileError") + + if config_file and reach33_file and reach66_file and reach100_file and prj_file: + os.mkdir(f"{self.dir_path}/j1") + os.mkdir(f"{self.dir_path}/j2") + os.mkdir(f"{self.dir_path}/j3") + + load = f"load{self.tool.removeprefix('tool')}" + for reach in ["reach33", "reach66", "reach100"]: + for speed in ["speed33", "speed66", "speed100"]: + dir_name = "_".join([reach, load, speed]) + result_dirs.append(dir_name) + os.mkdir(f"{self.dir_path}/j1/{dir_name}") + os.mkdir(f"{self.dir_path}/j2/{dir_name}") + if reach == "reach100": + os.mkdir(f"{self.dir_path}/j3/{dir_name}") + + self.logger("INFO", "do_brake-check_files", "数据目录合规性检查结束,未发现问题......", "green") + return config_file, prj_file, result_dirs + else: + self.logger("ERROR", "do_brake-check_files", msg, "red", "InitFileError") + + def get_configs(): + robot_type = None + msg_id, state = clibs.c_hr.execution("controller.get_params") + records = clibs.c_hr.get_from_id(msg_id, state) + for record in records: + if "请求发送成功" not in record[0]: + robot_type = eval(record[0])["data"]["robot_type"] + server_file = f"/home/luoshi/bin/controller/robot_cfg/{robot_type}/{robot_type}.cfg" + local_file = self.dir_path + f"/{robot_type}.cfg" + clibs.c_pd.pull_file_from_server(server_file, local_file) + + try: + with open(local_file, mode="r", encoding="utf-8") as f_config: + configs = json.load(f_config) + except Exception as Err: + self.logger("ERROR", "do_brake-get_configs", f"无法打开 {local_file}
{Err}", "red", "OpenFileError") + + # 最大角速度,额定电流,减速比,额定转速 + version = configs["VERSION"] + avs = configs["MOTION"]["JOINT_MAX_SPEED"] + clibs.insert_logdb("INFO", "do_brake", f"get_configs: 机型文件版本 {robot_type}_{version}") + clibs.insert_logdb("INFO", "do_brake", f"get_configs: 各关节角速度 {avs}") + return avs + + _config_file, _prj_file, _result_dirs = check_files() + _avs = get_configs() + + return _config_file, _prj_file, _result_dirs, _avs + + def gen_result_file(self, axis, t_end, reach, load, speed, speed_max, rounds): + d_vel, d_trq, d_stop, threshold = [], [], [], 0.95 + + start_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(t_end-12)) + end_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(t_end)) + try: + clibs.lock.acquire(True) + clibs.cursor.execute(f"select content from logs where time between '{start_time}' and '{end_time}' and content like '%diagnosis.result%' order by id asc") + records = clibs.cursor.fetchall() + finally: + clibs.lock.release() + + for record in records: # 保留最后12s的数据 + data = eval(record[0])["data"] + for item in data: + d_item = reversed(item["value"]) + if item.get("channel", None) == axis-1 and item.get("name", None) == "hw_joint_vel_feedback": + d_vel.extend(d_item) + elif item.get("channel", None) == axis-1 and item.get("name", None) == "device_servo_trq_feedback": + d_trq.extend(d_item) + elif item.get("channel", None) == 0 and item.get("name", None) == "device_safety_estop": + d_stop.extend(d_item) + + idx = 0 + for idx in range(len(d_stop)-10, 0, -1): + if d_stop[idx] == 1: + break + + av_estop = abs(sum(d_vel[idx - 20:idx])/20 * clibs.RADIAN) + if av_estop / speed_max < threshold: + self.logger("WARNING", "do_brake-gen_result_file", f"[av_estop: {av_estop:.2f} | shouldbe: {speed_max:.2f}] 处理数据时,本次触发 ESTOP 时未采集到指定百分比的最大速度,即将重试!", "#8A2BE2") + clibs.count += 1 + if clibs.count < 3: + return "retry" + else: + clibs.count = 0 + self.logger("WARNING", "do_brake-gen_result_file",f"尝试三次后仍无法获取正确数据,本次数据无效,继续执行...", "red") + + df1 = pandas.DataFrame.from_dict({"hw_joint_vel_feedback": d_vel}) + df2 = pandas.DataFrame.from_dict({"device_servo_trq_feedback": d_trq}) + df3 = pandas.DataFrame.from_dict({"device_safety_estop": d_stop}) + df = pandas.concat([df1, df2, df3], axis=1) + filename = f"{self.dir_path}/j{axis}/reach{reach}_load{load}_speed{speed}/reach{reach}_load{load}_speed{speed}_{rounds}.data" + df.to_csv(filename, sep="\t", index=False) + + @staticmethod + def change_curve_state(stat): + if not stat: + display_pdo_params = [] + else: + display_pdo_params = [{"name": name, "channel": chl} for name in ["hw_joint_vel_feedback", "device_servo_trq_feedback"] for chl in range(6)] + display_pdo_params.append({"name": "device_safety_estop", "channel": 0}) + clibs.c_hr.execution("diagnosis.open", open=stat, display_open=stat, overrun=True, turn_area=True, delay_motion=False) + clibs.c_hr.execution("diagnosis.set_params", display_pdo_params=display_pdo_params, frequency=50, version="1.4.1") + + def run_rl(self, config_file, prj_file, result_dirs, avs): + count, total, speed_target = 0, 63, 0 + prj_name = ".".join(prj_file.split("/")[-1].split(".")[:-1]) + wb = openpyxl.load_workbook(config_file, read_only=True) + ws = wb["Target"] + write_diagnosis = float(ws.cell(row=2, column=2).value) + get_init_speed = float(ws.cell(row=3, column=2).value) + single_brake = str(ws.cell(row=4, column=2).value) + pon = ws.cell(row=5, column=2).value + io_name = ws.cell(row=6, column=2).value.upper().strip() + wb.close() + msg = f"基本参数配置:write_diagnosis(废弃) = {write_diagnosis}, get_init_speed = {get_init_speed}, single_brake = {single_brake}, pon = {pon}" + self.logger("INFO", "do_brake-run_rl", msg) + + if pon == "positive": + clibs.c_md.write_pon(1) + elif pon == "negative": + clibs.c_md.write_pon(0) + else: + self.logger("ERROR", "do_brake-run_rl", "configs.xlsx 中 Target 页面 B5 单元格填写不正确,检查后重新运行...", "red", "DirectionError") + + self.change_curve_state(True) + for condition in result_dirs: + reach = condition.split("_")[0].removeprefix("reach") + load = condition.split("_")[1].removeprefix("load") + speed = condition.split("_")[2].removeprefix("speed") + + # for single condition test + single_axis = -1 + if single_brake != "0": + total = 3 + single_axis = int(single_brake.split("-")[0]) + if reach != single_brake.split("-")[1] or load != single_brake.split("-")[2] or speed != single_brake.split("-")[3]: + continue + + for axis in range(1, 4): + # for single condition test + if (single_axis != -1 and single_axis != axis) or (axis == 3 and reach != "100"): + continue + + clibs.c_md.write_axis(axis) + self.logger("INFO", "brake-processing", "-" * 90, "purple", flag="signal") + speed_max = 0 + for rounds in range(1, 4): + count += 1 + _ = 3 if count % 3 == 0 else count % 3 + this_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())) + prj_path = f"{prj_name}/_build/{prj_name}.prj" + msg = f"[{this_time} | {count}/{total}] 正在执行 {axis} 轴 {condition} 的第 {_} 次制动测试..." + self.logger("INFO", "do_brake-run_rl", msg) + + # 1. 触发软急停,并解除,目的是让可能正在运行着的机器停下来,切手动模式并下电 + clibs.c_md.r_soft_estop(0) + clibs.c_md.r_soft_estop(1) + clibs.c_ec.setdo_value(io_name, "true") + clibs.c_md.r_reset_estop() + clibs.c_md.r_clear_alarm() + clibs.c_md.write_act(0) + + while count % 3 == 1: + # 2. 修改要执行的场景 + rl_cmd = "" + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect(hostname=clibs.ip_addr, port=clibs.ssh_port, username=clibs.username, password=clibs.password) + if pon == "positive": + rl_cmd = f"brake_E(j{axis}_{reach}_p, j{axis}_{reach}_n, p_speed, p_tool)" + elif pon == "negative": + rl_cmd = f"brake_E(j{axis}_{reach}_n, j{axis}_{reach}_p, p_speed, p_tool)" + rl_speed = f"VelSet {speed}" + rl_tool = f"tool p_tool = {self.tool}" + cmd = "cd /home/luoshi/bin/controller/; " + cmd += f'sudo sed -i "/brake_E/d" projects/{prj_name}/_build/brake/main.mod; ' + cmd += f'sudo sed -i "/DONOTDELETE/i {rl_cmd}" projects/{prj_name}/_build/brake/main.mod; ' + cmd += f'sudo sed -i "/VelSet/d" projects/{prj_name}/_build/brake/main.mod; ' + cmd += f'sudo sed -i "/MoveAbsJ/i {rl_speed}" projects/{prj_name}/_build/brake/main.mod; ' + cmd += f'sudo sed -i "/tool p_tool/d" projects/{prj_name}/_build/brake/main.mod; ' + cmd += f'sudo sed -i "/VelSet/i {rl_tool}" projects/{prj_name}/_build/brake/main.mod; ' + stdin, stdout, stderr = ssh.exec_command(cmd, get_pty=True) + stdin.write(clibs.password + "\n") + stdout.read().decode() # 需要read一下才能正常执行 + stderr.read().decode() + + # 3. reload工程后,pp2main,并且自动模式和上电,最后运行程序 + clibs.c_hr.execution("overview.reload", prj_path=prj_path, tasks=["brake"]) + clibs.c_hr.execution("rl_task.pp_to_main", tasks=["brake"]) + clibs.c_hr.execution("state.switch_auto") + clibs.c_hr.execution("state.switch_motor_on") + clibs.c_hr.execution("rl_task.set_run_params", loop_mode=True, override=1.0) + clibs.c_hr.execution("rl_task.run", tasks=["brake"]) + t_start = time.time() + while True: + if clibs.c_md.read_ready_to_go() == 1: + clibs.c_md.write_act(True) + break + else: + time.sleep(1) + if (time.time() - t_start) > 15: + self.logger("ERROR", "do_brake-run_rl", "15s 内未收到机器人的运行信号,需要确认 RL 程序编写正确并正常执行...", "red", "ReadySignalTimeoutError") + # 4. 找出最大速度,传递给RL程序,最后清除相关记录 + time.sleep(5) # 消除前 5s 的不稳定数据 + start_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())) + time.sleep(get_init_speed) # 指定时间后获取实际【正|负】方向的最大速度,可通过configs.xlsx配置 + end_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())) + clibs.c_hr.execution("rl_task.stop", tasks=["brake"]) + + # 找出最大速度 + @clibs.db_lock + def get_speed_max(): + _speed_max = 0 + clibs.cursor.execute(f"select content from logs where time between '{start_time}' and '{end_time}' and content like '%diagnosis.result%' order by id asc") + records = clibs.cursor.fetchall() + for record in records: + data = eval(record[0])["data"] + for item in data: + if item.get("channel", None) == axis-1 and item.get("name", None) == "hw_joint_vel_feedback": + _ = clibs.RADIAN * sum(item["value"]) / len(item["value"]) + if pon == "positive": + _speed_max = max(_, _speed_max) + elif pon == "negative": + _speed_max = min(_, _speed_max) + return _speed_max + + speed_max = abs(get_speed_max()) + speed_target = avs[axis-1] * float(speed) / 100 + clibs.insert_logdb("INFO", "do_brake", f"axis = {axis}, direction = {pon}, max speed = {speed_max}") + if speed_max < speed_target*0.95 or speed_max > speed_target*1.05: + self.logger("WARNING", "do_brake-run_rl", f"Axis: {axis}-{count} | 采集获取最大 Speed: {speed_max} | Shouldbe: {speed_target}", "indigo") + clibs.insert_logdb("WARNING", "do_brake", f"Axis: {axis}-{count} | 采集获取最大 Speed: {speed_max} | Shouldbe: {speed_target}") + clibs.c_md.write_speed_max(speed_max) + + if speed_max < 10: + clibs.c_md.r_clear_alarm() + self.logger("WARNING", "do_brake-run_rl", f"未获取到正确的速度,即将重新获取...", "red") + continue + else: + break + + while 1: + clibs.c_ec.setdo_value(io_name, "true") + clibs.c_md.r_reset_estop() + clibs.c_md.r_clear_alarm() + clibs.c_md.write_act(0) + # 5. 重新运行程序,发送继续运动信号,当速度达到最大值时,通过DO触发急停 + clibs.c_hr.execution("rl_task.pp_to_main", tasks=["brake"]) + clibs.c_hr.execution("state.switch_auto") + clibs.c_hr.execution("state.switch_motor_on") + t_start = time.time() + while 1: + clibs.c_md.r_clear_alarm() + clibs.c_hr.execution("rl_task.run", tasks=["brake"]) + time.sleep(1) + if clibs.c_md.w_program_state == 1: + break + else: + time.sleep(5) + if time.time() - t_start > 60: + self.logger("ERROR", "do_brake-run_rl","60s 内程序未能正常执行,需检查...", "red", "RlProgramStartTimeout") + + for i in range(16): + if clibs.c_md.read_ready_to_go() == 1: + clibs.c_md.write_act(1) + break + else: + time.sleep(1) + else: + self.logger("ERROR", "do_brake-run_rl", "16s 内未收到机器人的运行信号,需要确认 RL 程序配置正确并正常执行...", "red", "ReadySignalTimeoutError") + + def exec_brake(): + flag, start, data, record = True, time.time(), None, None + while flag: + time.sleep(0.05) + if time.time() - start > 20: + self.logger("ERROR", "do_brake-exec_brake", "20s 内未触发急停,需排查......", "red", "BrakeTimeoutError") + + try: + clibs.lock.acquire(True) + clibs.cursor.execute(f"select content from logs where content like '%diagnosis.result%' order by id desc limit 1") + record = clibs.cursor.fetchone() + data = eval(record[0])["data"] + finally: + clibs.lock.release() + + for item in data: + if item.get("channel", None) != axis-1 or item.get("name", None) != "hw_joint_vel_feedback": + continue + + speed_moment = clibs.RADIAN * sum(item["value"]) / len(item["value"]) + if abs(speed_moment) > speed_max - 2: + if (pon == "positive" and speed_moment > 0) or (pon == "negative" and speed_moment < 0): + clibs.c_ec.setdo_value(io_name, "false") + time.sleep(2) + flag = False + break + return time.time() + + time.sleep(11) # 排除从其他位姿到零点位姿,再到轴极限位姿的时间 + t_end = exec_brake() + # 6. 保留数据并处理输出 + ret = self.gen_result_file(axis, t_end, reach, load, speed, speed_max, rounds) + if ret != "retry": + clibs.count = 0 + break + + else: + time.sleep(50) # why? + self.change_curve_state(False) + msg = f"\n{self.tool.removeprefix('tool')}%负载的制动性能测试执行完毕,如需采集其他负载,须切换负载类型,并更换其他负载,重新执行" + self.logger("INFO", "do_brake-run_rl", msg, "green") + + def processing(self): + time_start = time.time() + clibs.running[self.idx] = 1 + + data_dirs, data_files = clibs.traversal_files(self.dir_path, self.output) + config_file, prj_file, result_dirs, avs = self.initialization(data_dirs, data_files) + clibs.c_pd.push_prj_to_server(prj_file) + self.run_rl(config_file, prj_file, result_dirs, avs) + + self.logger("INFO", "brake-processing", "-"*60 + "
全部处理完毕
", "purple") + time_total = time.time() - time_start + msg = f"处理时间:{time_total // 3600:02.0f} h {time_total % 3600 // 60:02.0f} m {time_total % 60:02.0f} s" + self.logger("INFO", "brake-processing", msg) diff --git a/code/autotest/do_current.py b/code/autotest/do_current.py new file mode 100644 index 0000000..0901d33 --- /dev/null +++ b/code/autotest/do_current.py @@ -0,0 +1,268 @@ +import os +import threading +import time +import paramiko +import pandas +from PySide6.QtCore import Signal, QThread +from common import clibs + + +class DoBrakeTest(QThread): + output = Signal(str, str) + + def __init__(self, dir_path, tool, /): + super().__init__() + self.dir_path = dir_path + self.tool = tool + self.idx = 5 + + def logger(self, level, module, content, color="black", error="", flag="both"): + clibs.logger(level, module, content, color, flag, signal=self.output) + if level.upper() == "ERROR": + raise Exception(f"{error} | {content}") + + def initialization(self, data_dirs, data_files): + def check_files(): + msg = "初始路径下不允许有文件夹,初始路径下只能存在如下两个文件,且文件为关闭状态,确认后重新运行!
" + msg += "1. T_电机电流.xlsx
2. xxxx.zip" + if len(data_dirs) != 0 or len(data_files) != 2: + self.logger("ERROR", "do_current-check_files", msg, "red", "InitFileError") + + prj_file, count = None, 0 + for data_file in data_files: + filename = data_file.split("/")[-1] + if filename == "T_电机电流.xlsx": + count += 1 + elif filename.endswith(".zip"): + count += 1 + prj_file = data_file + else: + self.logger("ERROR", "do_current-check_files", msg, "red", "InitFileError") + + if count != 2: + self.logger("ERROR", "do_current-check_files", msg, "red", "InitFileError") + + self.logger("INFO", "do_current-check_files", "数据目录合规性检查结束,未发现问题......", "green") + if self.tool == "tool100": + os.mkdir(f"{self.dir_path}/single") + os.mkdir(f"{self.dir_path}/s_1") + os.mkdir(f"{self.dir_path}/s_2") + os.mkdir(f"{self.dir_path}/s_3") + elif self.tool == "inertia": + os.mkdir(f"{self.dir_path}/inertia") + else: + self.logger("ERROR", "do_current-check_files", "负载选择错误,电机电流测试只能选择 tool100/inertia 规格!", "red", "LoadSelectError") + + return prj_file + + def get_configs(): + robot_type = None + msg_id, state = clibs.c_hr.execution("controller.get_params") + records = clibs.c_hr.get_from_id(msg_id, state) + for record in records: + if "请求发送成功" not in record[0]: + robot_type = eval(record[0])["data"]["robot_type"] + server_file = f"/home/luoshi/bin/controller/robot_cfg/{robot_type}/{robot_type}.cfg" + local_file = self.dir_path + f"/{robot_type}.cfg" + clibs.c_pd.pull_file_from_server(server_file, local_file) + + _prj_file = check_files() + get_configs() + + return _prj_file + + def single_axis_proc(self, records, number): + text = "single" if number < 6 else "hold" + number = number if number < 6 else number - 6 + d_vel, d_trq, d_sensor, d_trans = [], [], [], [] + for record in records: + data = eval(record[0])["data"] + for item in data: + d_item = reversed(item["value"]) + if item.get("channel", None) == number and item.get("name", None) == "hw_joint_vel_feedback": + d_vel.extend(d_item) + elif item.get("channel", None) == number and item.get("name", None) == "device_servo_trq_feedback": + d_trq.extend(d_item) + elif item.get("channel", None) == number and item.get("name", None) == "hw_sensor_trq_feedback": + d_sensor.extend(d_item) + elif item.get("channel", None) == number and item.get("name", None) == "hw_estimate_trans_trq_res": + d_trans.extend(d_item) + + df1 = pandas.DataFrame.from_dict({"hw_joint_vel_feedback": d_vel}) + df2 = pandas.DataFrame.from_dict({"device_servo_trq_feedback": d_trq}) + df3 = pandas.DataFrame.from_dict({"hw_sensor_trq_feedback": d_sensor}) + df4 = pandas.DataFrame.from_dict({"hw_estimate_trans_trq_res": d_trans}) + df = pandas.concat([df1, df2, df3, df4], axis=1) + filename = f"{self.dir_path}/single/j{number + 1}_{text}_{time.time()}.data" + df.to_csv(filename, sep="\t", index=False) + + def scenario_proc(self, records, number, scenario_time): + d_vel, d_trq, d_sensor, d_trans = [[], [], [], [], [], []], [[], [], [], [], [], []], [[], [], [], [], [], []], [[], [], [], [], [], []] + for record in records: + data = eval(record[0])["data"] + for item in data: + d_item = reversed(item["value"]) + for axis in range(6): + if item.get("channel", None) == axis and item.get("name", None) == "hw_joint_vel_feedback": + d_vel[axis].extend(d_item) + elif item.get("channel", None) == axis and item.get("name", None) == "device_servo_trq_feedback": + d_trq[axis].extend(d_item) + elif item.get("channel", None) == axis and item.get("name", None) == "hw_sensor_trq_feedback": + d_sensor[axis].extend(d_item) + elif item.get("channel", None) == axis and item.get("name", None) == "hw_estimate_trans_trq_res": + d_trans[axis].extend(d_item) + + for axis in range(6): + df1 = pandas.DataFrame.from_dict({"hw_joint_vel_feedback": d_vel[axis]}) + df2 = pandas.DataFrame.from_dict({"device_servo_trq_feedback": d_trq[axis]}) + df3 = pandas.DataFrame.from_dict({"hw_sensor_trq_feedback": d_sensor[axis]}) + df4 = pandas.DataFrame.from_dict({"hw_estimate_trans_trq_res": d_trans[axis]}) + df = pandas.concat([df1, df2, df3, df4], axis=1) + filename = f"{self.dir_path}/s_{number-11}/j{axis+1}_s_{number-11}_{scenario_time}_{time.time()}.data" + df.to_csv(filename, sep="\t", index=False) + + def gen_result_file(self, number, start_time, end_time, scenario_time): + def get_records(s_time, e_time): + clibs.cursor.execute(f"select content from logs where time between '{s_time}' and '{e_time}' and content like '%diagnosis.result%' order by id asc") + _ = clibs.cursor.fetchall() + return _ + + if number < 12: + records = get_records(start_time, end_time) + t = threading.Thread(target=self.single_axis_proc, args=(records, number)) + t.daemon = True + t.start() + elif number < 15: + records = get_records(start_time, end_time) + t = threading.Thread(target=self.scenario_proc, args=(records, number, scenario_time)) + t.daemon = True + t.start() + + @staticmethod + def change_curve_state(stat): + curves = ["hw_joint_vel_feedback", "device_servo_trq_feedback", "hw_sensor_trq_feedback", "hw_estimate_trans_trq_res"] + display_pdo_params = [] if not stat else [{"name": curve, "channel": chl} for curve in curves for chl in range(6)] + clibs.c_hr.execution("diagnosis.open", open=stat, display_open=stat, overrun=True, turn_area=True, delay_motion=False) + clibs.c_hr.execution("diagnosis.set_params", display_pdo_params=display_pdo_params, frequency=50, version="1.4.1") + + def run_rl(self, prj_file): + prj_name = ".".join(prj_file.split("/")[-1].split(".")[:-1]) + c_regular = [ + "scenario(0, j1_p, j1_n, p_speed, p_tool, i_tool)", + "scenario(0, j2_p, j2_n, p_speed, p_tool, i_tool)", + "scenario(0, j3_p, j3_n, p_speed, p_tool, i_tool)", + "scenario(0, j4_p, j4_n, p_speed, p_tool, i_tool)", + "scenario(0, j5_p, j5_n, p_speed, p_tool, i_tool)", + "scenario(0, j6_p, j6_n, p_speed, p_tool, i_tool)", + "scenario(4, j1_hold, j1_hold, p_speed, p_tool, i_tool)", + "scenario(4, j2_hold, j2_hold, p_speed, p_tool, i_tool)", + "scenario(4, j3_hold, j3_hold, p_speed, p_tool, i_tool)", + "scenario(4, j4_hold, j4_hold, p_speed, p_tool, i_tool)", + "scenario(4, j5_hold, j5_hold, p_speed, p_tool, i_tool)", + "scenario(4, j6_hold, j6_hold, p_speed, p_tool, i_tool)", + "scenario(1, j6_p, j6_n, p_speed, p_tool, i_tool)", + "scenario(2, j6_p, j6_n, p_speed, p_tool, i_tool)", + "scenario(3, j6_p, j6_n, p_speed, p_tool, i_tool)", + ] + c_inertia = [ + "scenario(5, j4_p_inertia, j4_n_inertia, p_speed, p_tool, i_tool)", + "scenario(5, j5_p_inertia, j5_n_inertia, p_speed, p_tool, i_tool)", + "scenario(5, j6_p_inertia, j6_n_inertia, p_speed, p_tool, i_tool)", + ] + disc_regular = ["一轴", "二轴", "三轴", "四轴", "五轴", "六轴", "一轴保持", "二轴保持", "三轴保持", "四轴保持", "五轴保持", "六轴保持", "场景一", "场景二", "场景三"] + disc_inertia = ["四轴惯量", "五轴惯量", "六轴惯量"] + conditions, disc = [], [] + if self.tool == "tool100": + conditions, disc = c_regular, disc_regular + elif self.tool == "inertia": + conditions, disc = c_inertia, disc_inertia + + # 打开诊断曲线,触发软急停,并解除,目的是让可能正在运行着的机器停下来 + clibs.c_md.r_soft_estop(0) + clibs.c_md.r_soft_estop(1) + clibs.c_md.r_clear_alarm() + + for condition in conditions: + number = conditions.index(condition) + self.logger("INFO", "do_current-run_rl", f"正在执行{disc[number]}测试......") + + # 1. 将act重置为False,并修改将要执行的场景 + clibs.c_md.write_act(False) + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect(clibs.ip_addr, clibs.ssh_port, username=clibs.username, password=clibs.password) + cmd = "cd /home/luoshi/bin/controller/; " + cmd += f'sudo sed -i "/scenario/d" projects/{prj_name}/_build/current/main.mod; ' + cmd += f'sudo sed -i "/DONOTDELETE/i {condition}" projects/{prj_name}/_build/current/main.mod' + stdin, stdout, stderr = ssh.exec_command(cmd, get_pty=True) + stdin.write(clibs.password + "\n") + stdout.read().decode() # 需要read一下才能正常执行 + stderr.read().decode() + + # 2. reload工程后,pp2main,并且自动模式和上电 + prj_path = f"{prj_name}/_build/{prj_name}.prj" + clibs.c_hr.execution("overview.reload", prj_path=prj_path, tasks=["current"]) + clibs.c_hr.execution("rl_task.pp_to_main", tasks=["current"]) + clibs.c_hr.execution("state.switch_auto") + clibs.c_hr.execution("state.switch_motor_on") + + # 3. 开始运行程序 + clibs.c_hr.execution("rl_task.set_run_params", loop_mode=True, override=1.0) + clibs.c_hr.execution("rl_task.run", tasks=["current"]) + t_start = time.time() + while True: + if clibs.c_md.read_ready_to_go() == 1: + clibs.c_md.write_act(True) + break + else: + time.sleep(1) + if (time.time() - t_start) > 15: + self.logger("ERROR", "do_current-run_rl", "15s 内未收到机器人的运行信号,需要确认RL程序和工具通信是否正常执行...", "red", "ReadySignalTimeoutError") + + # 4. 执行采集 + time.sleep(10) # 消除前 10s 的不稳定数据 + self.change_curve_state(True) + start_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())) + single_time, stall_time, scenario_time = 40, 10, 0 + if number < 6: # 单轴 + time.sleep(single_time) + elif number < 12: # 堵转 + time.sleep(stall_time) + else: # 场景 + t_start = time.time() + while True: + scenario_time = float(f"{float(clibs.c_md.read_scenario_time()):.2f}") + if float(scenario_time) != 0: + self.logger("INFO", "do_current-run_rl", f"场景{number - 11}的周期时间:{scenario_time}") + break + else: + time.sleep(1) + if (time.time()-t_start) > 180: + self.logger("ERROR", "do_current-run_rl", f"180s 内未收到场景{number - 11}的周期时间,需要确认RL程序和工具通信交互是否正常执行...", "red", "GetScenarioTimeError") + time.sleep(20) + + # 5.停止程序运行,保留数据并处理输出 + end_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())) + clibs.c_hr.execution("rl_task.stop", tasks=["current"]) + time.sleep(2) # 确保数据都拿到 + self.change_curve_state(False) + self.gen_result_file(number, start_time, end_time, scenario_time) + else: + if self.tool == "tool100": + self.logger("INFO", "do_current-run_rl", "单轴和场景电机电流采集完毕,如需采集惯量负载,须切换负载类型,并更换惯量负载,重新执行", "green") + elif self.tool == "inertia": + self.logger("INFO", "do_current-run_rl", "惯量负载电机电流采集完毕,如需采集单轴/场景/保持电机电流,须切换负载类型,并更换偏置负载,重新执行", "green") + + def processing(self): + time_start = time.time() + clibs.running[self.idx] = 1 + + data_dirs, data_files = clibs.traversal_files(self.dir_path, self.output) + prj_file = self.initialization(data_dirs, data_files) + clibs.c_pd.push_prj_to_server(prj_file) + self.run_rl(prj_file) + + self.logger("INFO", "brake-processing", "-" * 60 + "
全部处理完毕
", "purple") + time_total = time.time() - time_start + msg = f"处理时间:{time_total // 3600:02.0f} h {time_total % 3600 // 60:02.0f} m {time_total % 60:02.0f} s" + self.logger("INFO", "brake-processing", msg) diff --git a/code/common/clibs.py b/code/common/clibs.py new file mode 100644 index 0000000..8a42f5b --- /dev/null +++ b/code/common/clibs.py @@ -0,0 +1,62 @@ +import os +import os.path +import threading + + +def traversal_files(dir_path, signal): + # 功能:以列表的形式分别返回指定路径下的文件和文件夹,不包含子目录 + # 参数:路径/信号/游标/功能编号 + # 返回值:路径下的文件夹列表 路径下的文件列表 + global cursor, tb_name + + if not os.path.exists(dir_path): + logger("ERROR", "clibs", f"数据文件夹{dir_path}不存在,请确认后重试......", "red", signal=signal) + else: + dirs, files = [], [] + for item in os.scandir(dir_path): + if item.is_dir(): + dirs.append(item.path.replace("\\", "/")) + elif item.is_file(): + files.append(item.path.replace("\\", "/")) + + return dirs, files + + +def db_lock(func): + def wrapper(*args, **kwargs): + try: + lock.acquire(True) + ret = func(*args, **kwargs) + finally: + lock.release() + return ret + + return wrapper + + +@db_lock +def logger(level, module, content, color="black", flag="both", signal=""): + global cursor, tb_name + if flag == "signal": + signal.emit(content, color) + elif flag == "cursor": + cursor.execute(f"INSERT INTO {tb_name} (level, module, content) VALUES (%s, %s, %s)", (level, module, content)) + elif flag == "both": + signal.emit(content, color) + cursor.execute(f"INSERT INTO {tb_name} (level, module, content) VALUES (%s, %s, %s)", (level, module, content)) + + +# PREFIX = "assets" # for pyinstaller packaging +PREFIX = "../assets" # for source code testing and debug +lock = threading.Lock() + +running = [0, 0, 0, 0, 0, 0, 0] # 制动数据/转矩数据/激光数据/精度数据/制动自动化/转矩自动化/耐久数据采集 +functions = ["制动数据处理", "转矩数据处理", "激光数据处理", "精度数据处理", "制动自动化测试", "转矩自动化测试", "耐久数据采集"] + +log_name = "" +ip_addr, ssh_port, socket_port, xService_port, external_port, modbus_port, upgrade_port = "", 22, 5050, 6666, 8080, 502, 4567 +username, password = "luoshi", "123456" +INTERVAL, RADIAN, MAX_FRAME_SIZE, MAX_LOG_NUMBER = 0.5, 57.3, 1024, 10 +c_md, c_hr, c_ec, c_pd, cursor, tb_name = None, None, None, None, None, "" +status = {"mysql": 0, "hmi": 0, "md": 0, "ec": 0} +c_joint_vel, c_servo_trq, c_sensor_trq, c_estimate_trans_trq, c_safety_estop = 1, 2, 3, 4, 3 # 各个指标所在列 diff --git a/code/common/openapi.py b/code/common/openapi.py new file mode 100644 index 0000000..24c3c16 --- /dev/null +++ b/code/common/openapi.py @@ -0,0 +1,2577 @@ +import json +import socket +from inspect import currentframe +import threading +import functools +from paramiko import SSHClient, AutoAddPolicy +from pymodbus.client.tcp import ModbusTcpClient +import selectors +import time +import os.path +from ctypes import * +import hashlib +import struct +from PySide6.QtCore import Signal, QThread +from common import clibs + + +class ModbusRequest(QThread): + output = Signal(str, str) + + def __init__(self, ip, port, /): + super().__init__() + self.ip = ip + self.port = port + + def net_conn(self): + RobotInit.modbus_init() + self.__c = ModbusTcpClient(host=self.ip, port=self.port) + if self.__c.connect(): + self.logger("DEBUG", "openapi", f"Modbus connection({clibs.ip_addr}:{clibs.modbus_port}) success...", "green") + else: + self.logger("ERROR", "openapi", f"Modbus connection({clibs.ip_addr}:{clibs.modbus_port}) failed...", "red", "MdConnFailed") + + def logger(self, level, module, content, color="black", error="", flag="both"): + clibs.logger(level, module, content, color, flag, signal=self.output) + if level.upper() == "ERROR": + raise Exception(f"{error} | {content}") + + def close(self): + if self.__c.connect(): + try: + self.__c.close() + except Exception as err: + self.logger("ERROR", "openapi", f"modbus: 关闭 Modbus 连接失败:{err}", "red", "MdCloseFailed") + + def __reg_high_pulse(self, addr: int) -> None: + self.__c.write_register(addr, 0) + time.sleep(clibs.INTERVAL) + self.__c.write_register(addr, 1) + time.sleep(clibs.INTERVAL+1) + self.__c.write_register(addr, 0) + + def r_clear_alarm(self): # OK + self.__reg_high_pulse(40000) + self.logger("INFO", "openapi", "modbus: 40000-010 执行清除告警信息") + + def r_reset_estop(self): # OK + self.__reg_high_pulse(40001) + self.logger("INFO", "openapi", "modbus: 40001-010 执行复位急停状态(非软急停)") + + def r_reset_estop_clear_alarm(self): # OK + self.__reg_high_pulse(40002) + self.logger("INFO", "openapi", "modbus: 40002-010 执行复位急停状态(非软急停),并清除告警信息") + + def r_motor_off(self): # OK + self.__reg_high_pulse(40003) + self.logger("INFO", "openapi", "modbus: 40003-010 执行机器人下电") + + def r_motor_on(self): # OK + self.__reg_high_pulse(40004) + self.logger("INFO", "openapi", "modbus: 40004-010 执行机器人上电") + + def r_motoron_pp2main_start(self): # OK + self.__reg_high_pulse(40005) + self.logger("INFO", "openapi", "modbus: 40005-010 执行机器人上电/pp2main/开始运行程序,需自动模式执行,若运行失败,可清除告警后再次尝试") + + def r_motoron_start(self): # OK + self.__reg_high_pulse(40006) + self.logger("INFO", "openapi", "modbus: 40006-010 执行机器人上电/开始运行程序,需自动模式执行,若运行失败,可清除告警、执行pp2main后再次尝试") + + def r_pulse_motoroff(self): # OK + self.__reg_high_pulse(40007) + self.logger("INFO", "openapi", "modbus: 40007-010 执行机器人停止,并下电,手动模式下可停止程序运行,但不能下电,若运行失败,可清除告警后再次尝试") + + def r_pp2main(self): # OK + self.__reg_high_pulse(40008) + self.logger("INFO", "openapi", "modbus: 40008-010 执行机器人 pp2main,需自动模式执行,若运行失败,可清除告警后再次尝试") + + def r_program_start(self): # OK + self.__reg_high_pulse(40009) + self.logger("INFO", "openapi", "modbus: 40009-010 执行机器人默认程序运行,需有 pp2main 前置操作,若运行失败,可清除告警后再次尝试") + + def r_program_stop(self): # OK + self.__reg_high_pulse(40010) + self.logger("INFO", "openapi", "modbus: 40010-010 执行机器人默认程序停止,需有 pp2main 前置操作,若运行失败,可清除告警后再次尝试") + + def r_reduced_mode(self, action: int): # OK + self.__c.write_register(40011, action) + actions = "进入" if action == 1 else "退出" + self.logger("INFO", "openapi", f"modbus: 40011-{action} 执行机器人{actions}缩减模式") + time.sleep(clibs.INTERVAL) + + def r_soft_estop(self, action: int): # OK + self.__c.write_register(40012, action) + actions = "解除" if action == 1 else "触发" + self.logger("INFO", "openapi", f"modbus: 40012-{action} 执行{actions}急停动作") + time.sleep(clibs.INTERVAL) + + def r_switch_auto_motoron(self): # OK + self.__reg_high_pulse(40013) + self.logger("INFO", "openapi", "modbus: 40013-010 执行切换为自动模式,并上电,初始状态 !!不能是!! 手动上电模式") + + def r_switch_auto(self): # OK + self.__reg_high_pulse(40014) + self.logger("INFO", "openapi", "modbus: 40014-010 执行切换为自动模式") + + def r_switch_manual(self): # OK + self.__reg_high_pulse(40015) + self.logger("INFO", "openapi", "modbus: 40015-010 执行切换为手动模式") + + def r_switch_safe_region01(self, action: bool): # OK | 上升沿打开,下降沿关闭 + if action: + self.__c.write_register(40016, False) + time.sleep(clibs.INTERVAL) + self.__c.write_register(40016, True) + else: + self.__c.write_register(40016, True) + time.sleep(clibs.INTERVAL) + self.__c.write_register(40016, False) + actions = "打开" if action else "关闭" + self.logger("INFO", "openapi", f"modbus: 40016-{action} 执行{actions}安全区 safe region 01") + time.sleep(clibs.INTERVAL) + + def r_switch_safe_region02(self, action: bool): # OK | 上升沿打开,下降沿关闭 + if action: + self.__c.write_register(40017, False) + time.sleep(clibs.INTERVAL) + self.__c.write_register(40017, True) + else: + self.__c.write_register(40017, True) + time.sleep(clibs.INTERVAL) + self.__c.write_register(40017, False) + actions = "打开" if action else "关闭" + self.logger("INFO", "openapi", f"modbus: 40017-{action} 执行{actions}安全区 safe region 02") + time.sleep(clibs.INTERVAL) + + def r_switch_safe_region03(self, action: bool): # OK | 上升沿打开,下降沿关闭 + if action: + self.__c.write_register(40018, False) + time.sleep(clibs.INTERVAL) + self.__c.write_register(40018, True) + else: + self.__c.write_register(40018, True) + time.sleep(clibs.INTERVAL) + self.__c.write_register(40018, False) + actions = "打开" if action else "关闭" + self.logger("INFO", "openapi", f"modbus: 40018-{action} 执行{actions}安全区 safe region 03") + time.sleep(clibs.INTERVAL) + + def write_act(self, number): + self.__c.write_register(40100, number) + self.logger("INFO", "openapi", f"modbus: 40100 将 {number} 写入") + + def write_probe(self, probe): + self.__c.write_register(40101, probe) + self.logger("INFO", "openapi", f"modbus: 40101 将 {probe} 写入") + + def write_pon(self, pon): + self.__c.write_register(40102, pon) + self.logger("INFO", "openapi", f"modbus: 40102 将 {pon} 写入") + + def write_axis(self, axis): + result = self.__c.convert_to_registers(int(axis), self.__c.DATATYPE.INT32, "little") + self.__c.write_registers(40103, result) + self.logger("INFO", "openapi", f"modbus: 40103 将 {axis} 写入") + + def write_speed_max(self, speed): + result = self.__c.convert_to_registers(float(speed), self.__c.DATATYPE.FLOAT32, "little") + self.__c.write_registers(40105, result) + self.logger("INFO", "openapi", f"modbus: 40105 将 {speed} 写入") + + def r_write_signals(self, addr: int, value): # OK | 40100 - 40109: signal_0 ~ signal_9 + if -1 < addr < 10 and addr.is_integer(): + self.__c.write_register(40100+addr, value) + self.logger("INFO", "openapi", f"modbus: {40100+addr}-{value} 将寄存器 signal_{addr} 赋值为 {value}") + else: + self.logger("INFO", "openapi", f"modbus: {40100+addr}-{value} 地址错误,无法赋值!") + + @property + def w_alarm_state(self): # OK + res = self.__c.read_holding_registers(40500, count=1).registers[0] + self.logger("INFO", "openapi", f"modbus: 40500 获取告警状态,结果为 {res} :--: 0 表示无告警,,1 表示有告警") + return res + + @property + def w_collision_alarm_state(self): # OK + res = self.__c.read_holding_registers(40501, count=1).registers[0] + self.logger("INFO", "openapi", f"modbus: 40501 获取碰撞告警状态,结果为 {res} :--: 0 表示未触发,1 表示已触发") + return res + + @property + def w_collision_open_state(self): # OK + res = self.__c.read_holding_registers(40502, count=1).registers[0] + self.logger("INFO", "openapi", f"modbus: 40502 获取碰撞检测开启状态,结果为 {res} :--: 0 表示关闭,1 表示开启") + return res + + @property + def w_controller_is_running(self): # OK + res = self.__c.read_holding_registers(40503, count=1).registers[0] + self.logger("INFO", "openapi", f"modbus: 40503 获取控制器运行状态,结果为 {res} :--: 0 表示运行异常,1 表示运行正常") + return res + + @property + def w_encoder_low_battery(self): # OK + res = self.__c.read_holding_registers(40504, count=1).registers[0] + self.logger("INFO", "openapi", f"modbus: 40504 获取编码器低电压状态,结果为 {res} :--: 0 表示非低电压,1 表示低电压 需关注") + return res + + @property + def w_estop_state(self): # OK + res = self.__c.read_holding_registers(40505, count=1).registers[0] + self.logger("INFO", "openapi", f"modbus: 40505 获取机器人急停状态(非软急停),结果为 {res} :--: 0 表示未触发,1 表示已触发") + return res + + @property + def w_motor_state(self): # OK + res = self.__c.read_holding_registers(40506, count=1).registers[0] + self.logger("INFO", "openapi", f"modbus: 40506 获取机器人上电状态,结果为 {res} :--: 0 表示未上电,1 表示已上电") + return res + + @property + def w_operation_mode(self): # OK + res = self.__c.read_holding_registers(40507, count=1).registers[0] + self.logger("INFO", "openapi", f"modbus: 40507 获取机器人操作模式,结果为 {res} :--: 0 表示手动模式,1 表示自动模式") + return res + + @property + def w_program_state(self): # OK + res = self.__c.read_holding_registers(40508, count=1).registers[0] + self.logger("INFO", "openapi", f"modbus: 40508 获取程序的运行状态,结果为 {res} :--: 0 表示未运行,1 表示正在运行") + return res + + @property + def w_program_not_run(self): # OK + res = self.__c.read_holding_registers(40509, count=1).registers[0] + self.logger("INFO", "openapi", f"modbus: 40509 判定程序为未运行状态,结果为 {res} :--: 0 表示正在运行,1 表示未运行") + return res + + @property + def w_program_reset(self): # OK + res = self.__c.read_holding_registers(40510, count=1).registers[0] + self.logger("INFO", "openapi", f"modbus: 40510 判定程序指针为 pp2main 状态,结果为 {res} :--: 0 表示指针不在 main 函数,1 表示指针在 main 函数") + return res + + @property + def w_reduce_mode_state(self): # OK + res = self.__c.read_holding_registers(40511, count=1).registers[0] + self.logger("INFO", "openapi", f"modbus: 40511 获取机器人缩减模式状态,结果为 {res} :--: 0 表示非缩减模式,1 表示缩减模式") + return res + + @property + def w_robot_is_busy(self): # OK + res = self.__c.read_holding_registers(40512, count=1).registers[0] + self.logger("INFO", "openapi", f"modbus: 40512 获取机器人是否处于 busy 状态,结果为 {res} :--: 0 表示未处于 busy 状态,1 表示处于 busy 状态") + return res + + @property + def w_robot_is_moving(self): # OK + res = self.__c.read_holding_registers(40513, count=1).registers[0] + self.logger("INFO", "openapi", f"modbus: 40513 获取机器人是否处于运动状态,结果为 {res} :--: 0 表示未运动,1 表示正在运动") + return res + + @property + def w_safe_door_state(self): # OK + res = self.__c.read_holding_registers(40514, count=1).registers[0] + self.logger("INFO", "openapi", f"modbus: 40514 获取机器人是否处于安全门打开状态,需自动模式下执行,结果为 {res} :--: 0 表示未触发安全门,1 表示已触发安全门") + return res + + @property + def w_safe_region01_trig_state(self): # OK + res = self.__c.read_holding_registers(40515, count=1).registers[0] + self.logger("INFO", "openapi", f"modbus: 40515 获取安全区域 safe region01 的触发状态,结果为 {res} :--: 0 表示未触发,1 表示已触发") + return res + + @property + def w_safe_region02_trig_state(self): # OK + res = self.__c.read_holding_registers(40516, count=1).registers[0] + self.logger("INFO", "openapi", f"modbus: 40516 获取安全区域 safe region02 的触发状态,结果为 {res} :--: 0 表示未触发,1 表示已触发") + return res + + @property + def w_safe_region03_trig_state(self): # OK + res = self.__c.read_holding_registers(40517, count=1).registers[0] + self.logger("INFO", "openapi", f"modbus: 40517 获取安全区域 safe region03 的触发状态,结果为 {res} :--: 0 表示未触发,1 表示已触发") + return res + + @property + def w_soft_estop_state(self): # OK + res = self.__c.read_holding_registers(40518, count=1).registers[0] + self.logger("INFO", "openapi", f"modbus: 40518 获取机器人软急停状态,结果为 {res} :--: 0 表示未触发软急停,1 表示已触发软急停") + return res + + def io_write_coils(self, addr, action): # OK | 名字叫写线圈,其实是写 modbus 的 discrete inputs(DI) + # e.g. io_write_coils(0, 1) + # e.g. io_write_coils(1, 1) + # e.g. io_write_coils(0, [1, 1, 1]) + self.__c.write_coils(addr, action) + self.logger("INFO", "openapi", f"modbus: 执行给 DI 地址 {addr} 赋值为 {action},可根据情况传递列表,实现一次性赋值多个") + time.sleep(clibs.INTERVAL) + + def io_read_coils(self): # OK | 读 modbus 的 16 个 discrete inputs(DI) + res = self.__c.read_coils(0, count=16).bits + self.logger("INFO", "openapi", f"modbus: 执行读取所有 DI 的结果为 {res}") + return res + + def io_read_discretes(self): # OK | 读 modbus 的 coil outputs(DO) + res = self.__c.read_discrete_inputs(0, count=16).bits + self.logger("INFO", "openapi", f"modbus: 执行读取所有 DO 的结果为 {res}") + return res + + def read_ready_to_go(self): + result = self.__c.read_holding_registers(40600, count=1) + return result.registers[0] + + def read_scenario_time(self): + results = self.__c.read_holding_registers(40601, count=2) + result = self.__c.convert_from_registers(registers=results.registers, data_type=self.__c.DATATYPE.FLOAT32, word_order="little") + return result + + def read_capture_start(self): + result = self.__c.read_holding_registers(40603, count=1) + return result.registers[0] + + +class HmiRequest(QThread): + output = Signal(str, str) + socket.setdefaulttimeout(clibs.INTERVAL * 3) + + def __init__(self, ip, port, port_xs, /): + super().__init__() + self.__ip = ip + self.__port = port + self.__port_xs = port_xs + self.__close_hmi = False + self.__is_connected = False + self.__index = 0 + self.__previous_data = b"" + self.__valid_data_length = 0 + self.__leftovers = 0 + self.__response = b"" + self.__response_xs = "" + self.__half_frm = b"" + self.__half_frm_flag = -1 + self.__half_pkg = b"" + self.__half_pkg_flag = False + self.__is_first_frame = True + self.__is_debug = True + + def net_conn(self): + self.__socket_conn() + self.__t_heartbeat = threading.Thread(target=self.__heartbeat) + self.__t_heartbeat.daemon = True + self.__t_heartbeat.start() + self.__t_unpackage = threading.Thread(target=self.__unpackage, args=(self.c,)) + self.__t_unpackage.daemon = True + self.__t_unpackage.start() + self.__t_unpackage_xs = threading.Thread(target=self.__unpackage_xs, args=(self.c_xs,)) + self.__t_unpackage_xs.daemon = True + self.__t_unpackage_xs.start() + + def logger(self, level, module, content, color="black", error="", flag="both"): + clibs.logger(level, module, content, color, flag, signal=self.output) + if level.upper() == "ERROR": + raise Exception(f"{error} | {content}") + + @property + def status(self): + return self.__is_connected + + def close(self): + if self.__is_connected: + try: + self.__is_connected = False + time.sleep(clibs.INTERVAL*2) + self.c.close() + self.c_xs.close() + time.sleep(clibs.INTERVAL*2) + clibs.status["hmi"] = 0 + except Exception as err: + self.logger("ERROR", "openapi", f"hmi: 关闭 Socket 连接失败 {err}", "red", "HmiCloseFailed") + + def __socket_conn(self): + self.close() + try: + self.c = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.c.connect((self.__ip, self.__port)) + self.c_xs = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.c_xs.connect((self.__ip, self.__port_xs)) + # 尝试连续三次发送心跳包,确认建联成功 + state = None + for i in range(3): + _, state = self.execution.__wrapped__(self, "controller.heart") + time.sleep(clibs.INTERVAL/5) + if state is not None: + self.__is_connected = True + self.logger("INFO", "openapi", "hmi: HMI connection success...", "green") + else: + self.logger("ERROR", "openapi", "hmi: HMI connection failed...", "red", "HmiConnFailed") + except Exception as err: + self.logger("ERROR", "openapi", f"hmi: HMI connection timeout...
err = {err}", "red", "HmiConnTimeout") + + def __heartbeat(self): + while self.__is_connected: + self.execution("controller.heart") + time.sleep(clibs.INTERVAL*2) + + @staticmethod + def package(cmd): + frm_value = (len(cmd) + 6).to_bytes(length=2, byteorder="big") + pkg_value = len(cmd).to_bytes(length=4, byteorder="big") + protocol = int(2).to_bytes(length=1, byteorder="big") + reserved = int(0).to_bytes(length=1, byteorder="big") + return frm_value + pkg_value + protocol + reserved + cmd.encode() + + def __unpackage(self, sock): + def to_read(conn, mask): + data = conn.recv(clibs.MAX_FRAME_SIZE) + if data: + # print(f"data = {data}") + # with open(f"{clibs.log_path}/logs.txt", mode="a", encoding="utf-8") as f_logs: + # f_logs.write(str(data) + "\n") + self.__get_response(data) + else: + sel.unregister(conn) + conn.close() + + try: + sel = selectors.DefaultSelector() + sel.register(sock, selectors.EVENT_READ, to_read) + while self.__is_connected: + events = sel.select() + for key, mask in events: + callback = key.data + callback(key.fileobj, mask) + except Exception as Err: + self.logger("INFO", "openapi", f"hmi: 老协议解包报错 {Err}") + + def __get_headers(self, index, data): + if index + 8 < len(data): + frm_value = int.from_bytes(data[index:index + 2], byteorder="big") + pkg_value = int.from_bytes(data[index + 2:index + 6], byteorder="big") + protocol = int.from_bytes(data[index + 6:index + 7], byteorder="big") + reserved = int.from_bytes(data[index + 7:index + 8], byteorder="big") + if reserved == 0 and protocol == 2: + return index + 8, frm_value, pkg_value + else: + # print("hr-get_headers: 解包数据有误,需要确认!") + # print(data) + self.logger("ERROR", "openapi", f"hmi: 解包数据有误,需要确认,最后一个数据包如下 {data}") + else: + self.__half_pkg = data[index:] + self.__half_pkg_flag = True + return -1, 0, 0 + + def __get_response(self, data): + frm_value, pkg_value, self.__index = 0, 0, 0 + + while self.__index < len(data): + if self.__is_first_frame: + if self.__half_pkg_flag: + len_one = len(self.__half_pkg) + len_another = 8 - len_one + headers = self.__half_pkg + data[:len_another] + self.__index = len_another + frm_value = int.from_bytes(headers[0:2], byteorder="big") + pkg_value = int.from_bytes(headers[2:6], byteorder="big") + self.__half_pkg_flag = False + else: + self.__index, frm_value, pkg_value = self.__get_headers(self.__index, data) + if self.__half_pkg_flag: + break + + if frm_value - pkg_value == 6: + if len(data[self.__index:]) >= pkg_value: + self.__response = data[self.__index:self.__index + pkg_value] + self.__index += pkg_value + self.logger("INFO", "openapi", str(json.loads(self.__response.decode()))) + self.__response = b"" + self.__leftovers = 0 + self.__is_first_frame = True + elif len(data[self.__index:]) < pkg_value: + self.__response = data[self.__index:] + self.__leftovers = pkg_value - len(data[self.__index:]) + self.__index += clibs.MAX_FRAME_SIZE # whatever + self.__is_first_frame = False + elif frm_value < pkg_value: + self.__valid_data_length = pkg_value + if len(data[self.__index:]) >= frm_value - 6: + self.__is_first_frame = False + self.__leftovers = 0 + self.__response = data[self.__index:self.__index + frm_value - 6] + self.__index += (frm_value - 6) + self.__valid_data_length -= (frm_value - 6) + if len(data[self.__index:]) == 2: + self.__half_frm = data[self.__index:] + self.__index += clibs.MAX_FRAME_SIZE + self.__half_frm_flag = 2 + elif len(data[self.__index:]) == 1: + self.__half_frm = data[self.__index:] + self.__index += clibs.MAX_FRAME_SIZE + self.__half_frm_flag = 1 + elif len(data[self.__index:]) == 0: + self.__index += clibs.MAX_FRAME_SIZE + self.__half_frm_flag = 0 + else: + self.__half_frm_flag = -1 + elif len(data[self.__index:]) < frm_value - 6: + self.__response = data[self.__index:] + self.__leftovers = frm_value - 6 - len(data[self.__index:]) + self.__valid_data_length -= len(data[self.__index:]) + self.__index += clibs.MAX_FRAME_SIZE + self.__is_first_frame = False + elif not self.__is_first_frame: # 不是首帧 + if self.__leftovers > 0 and self.__valid_data_length > 0: + if len(data) >= self.__leftovers: + self.__is_first_frame = False + self.__response += data[:self.__leftovers] + self.__index = self.__leftovers + self.__valid_data_length -= self.__leftovers + self.__leftovers = 0 + + if self.__valid_data_length == 0: + # with open(f"{clibs.log_path}/response.txt", mode="a", encoding="utf-8") as f_res: + # f_res.write(f"{json.loads(self.__response.decode())}" + "\n") + self.logger("INFO", "openapi", str(json.loads(self.__response.decode()))) + self.__response = b"" + self.__is_first_frame = True + continue # 此时应该重新 get_headers + + if len(data[self.__index:]) == 2: + self.__half_frm = data[self.__index:] + self.__index += clibs.MAX_FRAME_SIZE + self.__half_frm_flag = 2 + elif len(data[self.__index:]) == 1: + self.__half_frm = data[self.__index:] + self.__index += clibs.MAX_FRAME_SIZE + self.__half_frm_flag = 1 + elif len(data[self.__index:]) == 0: + self.__index += clibs.MAX_FRAME_SIZE + self.__half_frm_flag = 0 + else: + self.__half_frm_flag = -1 + + elif len(data) < self.__leftovers: + self.__response += data + self.__leftovers -= len(data) + self.__valid_data_length -= len(data) + self.__index += clibs.MAX_FRAME_SIZE + + elif self.__leftovers > 0 and self.__valid_data_length == 0: + if len(data) >= self.__leftovers: + self.__response += data[:self.__leftovers] + self.__index = self.__leftovers + self.__leftovers = 0 + self.logger("DEBUG", "openapi", str(json.loads(self.__response.decode()))) + self.__response = b"" + self.__is_first_frame = True + elif len(data) < self.__leftovers: + self.__response += data + self.__leftovers -= len(data) + self.__index += clibs.MAX_FRAME_SIZE + + elif self.__leftovers == 0 and self.__valid_data_length > 0: + # 1. len(data[self.__index:]) > 2 + # 2. len(data[self.__index:]) = 2 + # 3. len(data[self.__index:]) = 1 + # 4. len(data[self.__index:]) = 0 + if self.__half_frm_flag != -1: + if self.__half_frm_flag == 2: + frm_value = int.from_bytes(self.__half_frm) + if len(data) >= frm_value: + self.__response += data[:frm_value] + self.__leftovers = 0 + self.__valid_data_length -= len(data[:frm_value]) + self.__index = frm_value + elif len(data) < frm_value: + self.__response += data + self.__leftovers = frm_value - len(data) + self.__valid_data_length -= len(data) + self.__index += clibs.MAX_FRAME_SIZE + self.__half_frm_flag = -1 + elif self.__half_frm_flag == 1: + frm_value = int.from_bytes(self.__half_frm + data[0:1]) + if len(data[1:]) >= frm_value: + self.__response += data[1:frm_value+1] + self.__leftovers = 0 + self.__valid_data_length -= len(data[1:frm_value+1]) + self.__index = frm_value + 1 + elif len(data[1:]) < frm_value: + self.__response += data[1:] + self.__leftovers = frm_value - len(data[1:]) + self.__valid_data_length -= len(data[1:]) + self.__index += clibs.MAX_FRAME_SIZE + self.__half_frm_flag = -1 + elif self.__half_frm_flag == 0: + frm_value = int.from_bytes(data[0:2]) + if len(data[2:]) >= frm_value: + self.__response += data[2:frm_value+2] + self.__leftovers = 0 + self.__valid_data_length -= len(data[2:frm_value+2]) + self.__index = frm_value + 2 + elif len(data[2:]) < frm_value: + self.__response += data[2:] + self.__leftovers = frm_value - len(data[2:]) + self.__valid_data_length -= len(data[2:]) + self.__index += clibs.MAX_FRAME_SIZE + self.__half_frm_flag = -1 + + if self.__valid_data_length == 0: + self.logger("INFO", "openapi", str(json.loads(self.__response.decode()))) + self.__response = b"" + self.__is_first_frame = True + continue + + if len(data[self.__index:]) == 2: + self.__half_frm = data[self.__index:] + self.__index += clibs.MAX_FRAME_SIZE + self.__half_frm_flag = 2 + elif len(data[self.__index:]) == 1: + self.__half_frm = data[self.__index:] + self.__index += clibs.MAX_FRAME_SIZE + self.__half_frm_flag = 1 + elif len(data[self.__index:]) == 0: + self.__index += clibs.MAX_FRAME_SIZE + self.__half_frm_flag = 0 + else: + self.__half_frm_flag = -1 + + else: + frm_value = int.from_bytes(data[self.__index:self.__index + 2]) + self.__index += 2 + if len(data[self.__index:]) >= frm_value: + self.__leftovers = 0 + self.__response += data[self.__index:self.__index + frm_value] + self.__index += frm_value + self.__valid_data_length -= frm_value + if self.__valid_data_length == 0: + self.logger("INFO", "openapi", str(json.loads(self.__response.decode()))) + self.__response = b"" + self.__is_first_frame = True + continue + + if len(data[self.__index:]) == 2: + self.__index += clibs.MAX_FRAME_SIZE + self.__half_frm = data[self.__index:] + self.__half_frm_flag = 2 + elif len(data[self.__index:]) == 1: + self.__index += clibs.MAX_FRAME_SIZE + self.__half_frm = data[self.__index:] + self.__half_frm_flag = 1 + elif len(data[self.__index:]) == 0: + self.__half_frm_flag = 0 + else: + self.__half_frm_flag = -1 + + elif len(data[self.__index:]) < frm_value: + self.__response += data[self.__index:] + self.__leftovers = frm_value - len(data[self.__index:]) + self.__valid_data_length -= len(data[self.__index:]) + self.__index += clibs.MAX_FRAME_SIZE + else: + # DEBUG INFO + # if self.__is_debug: + # print(f"12 index = {self.__index}") + # print(f"12 frm_value = {frm_value}") + # print(f"12 leftovers = {self.__leftovers}") + # print(f"12 valid_data_length = {self.__valid_data_length}") + # print(f"12 is_first_frame = {self.__is_first_frame}") + # if self.__valid_data_length < 0 or self.__leftovers > 1024: + # print(f"data = {data}") + # raise Exception("DataError") + self.logger("ERROR", "openapi", "hmi: Will never be here", "red", "WillNeverBeHere") + + @staticmethod + def package_xs(cmd): + return "".join([json.dumps(cmd, separators=(",", ":")), "\r"]).encode() + + def __unpackage_xs(self, sock): + def to_read(conn, mask): + data = conn.recv(1024) # Should be ready + if data: + # print(data) + self.get_response_xs(data) + else: + sel.unregister(conn) + conn.close() + + try: + sel = selectors.DefaultSelector() + sel.register(sock, selectors.EVENT_READ, to_read) + + while self.__is_connected: + events = sel.select() + for key, mask in events: + callback = key.data + callback(key.fileobj, mask) + except Exception as err: + self.logger("INFO", "openapi", f"hmi: xService解包报错 {err}", "red") + + def get_response_xs(self, data): + char, response = "", self.__response_xs + for char in data.decode(): + if char != "\r": + response = "".join([response, char]) + else: + self.logger("INFO", "openapi", response) + self.response_xs = response + response = "" + else: + self.__response_xs = response + + def get_from_id(self, msg_id, state): + if state == "done_xs": + return + f_text = f"%{msg_id}%" + for _ in range(3): + time.sleep(clibs.INTERVAL * 4) + try: + clibs.lock.acquire(True) + clibs.cursor.execute(f"select content from {clibs.tb_name} where content like '{f_text}'") + records = clibs.cursor.fetchall() + finally: + clibs.lock.release() + if len(records) == 2: + return records + else: + self.close() + self.logger("ERROR", "openapi", f"hmi: 无法找到请求 {msg_id} 的响应", "red", "ResponseNotFound") + + @staticmethod + def validate_req(func): + @functools.wraps(func) + def wrapper(self, *args, **kwargs): + msg_id, state = func(self, *args, **kwargs) + t = threading.Thread(target=self.get_from_id, args=(msg_id, state)) + t.daemon = True + t.start() + return msg_id, state + return wrapper + + @validate_req + def execution(self, command, p_flag=0, **kwargs): + req, state = None, None + try: + with open(f"{clibs.PREFIX}/files/protocols/hmi/{command}.json", encoding="utf-8", mode="r") as f_json: + req = json.load(f_json) + except Exception as err: + self.logger("ERROR", "openapi", f"hmi: 暂不支持 {command} 功能,或确认该功能存在... {err}", "red", "CommandNotSupport") + + if p_flag == 0: # for old protocols + match command: + case "state.set_tp_mode": + req["data"]["tp_mode"] = kwargs["tp_mode"] + case "overview.set_autoload": + req["data"]["autoload_prj_path"] = kwargs["autoload_prj_path"] + case "overview.reload": + req["data"]["prj_path"] = kwargs["prj_path"] + req["data"]["tasks"] = kwargs["tasks"] + case "rl_task.pp_to_main" | "rl_task.run" | "rl_task.stop": + req["data"]["tasks"] = kwargs["tasks"] + case "rl_task.set_run_params": + req["data"]["loop_mode"] = kwargs["loop_mode"] + req["data"]["override"] = kwargs["override"] + case "diagnosis.set_params": + req["data"]["display_pdo_params"] = kwargs["display_pdo_params"] + req["data"]["frequency"] = kwargs["frequency"] + req["data"]["version"] = kwargs["version"] + case "diagnosis.open": + req["data"]["open"] = kwargs["open"] + req["data"]["display_open"] = kwargs["display_open"] + req["data"]["overrun"] = kwargs["overrun"] + req["data"]["turn_area"] = kwargs["turn_area"] + req["data"]["delay_motion"] = kwargs["delay_motion"] + case "register.set_value": + req["data"]["name"] = kwargs["name"] + req["data"]["type"] = kwargs["type"] + req["data"]["bias"] = kwargs["bias"] + req["data"]["value"] = kwargs["value"] + case "diagnosis.save": + req["data"]["save"] = kwargs["save"] + case "socket.set_params": + req["data"] = kwargs["data"] + case "fieldbus_device.set_params": + req["data"]["device_name"] = kwargs["device_name"] + req["data"]["enable"] = kwargs["enable"] + case "soft_limit.set_params": + req["data"] = kwargs["data"] + case "move.set_quickturn_pos": + req["data"]["enable_home"] = kwargs["enable_home"] + req["data"]["enable_drag"] = kwargs["enable_drag"] + req["data"]["enable_transport"] = kwargs["enable_transport"] + req["data"]["end_posture"] = kwargs["end_posture"] + case "move.quick_turn": + req["data"]["name"] = kwargs["name"] + case "move.stop": + req["data"]["stoptype"] = kwargs["stoptype"] + case "jog.start": + req["data"]["index"] = kwargs["index"] + req["data"]["direction"] = kwargs["direction"] + req["data"]["is_ext"] = kwargs["is_ext"] + case "jog.set_params": + req["data"]["step"] = kwargs["step"] + req["data"]["override"] = kwargs["override"] + req["data"]["space"] = kwargs["space"] + case "diagnosis.get_params": + req["data"]["version"] = kwargs["version"] + case "system_io.update_configuration": + req["data"]["input_system_io"] = kwargs["input_system_io"] + req["data"]["output_system_io"] = kwargs["output_system_io"] + case "modbus.set_params": + req["data"]["enable_slave"] = kwargs["enable_slave"] + req["data"]["ip"] = kwargs["ip"] + req["data"]["port"] = kwargs["port"] + req["data"]["slave_id"] = kwargs["slave_id"] + req["data"]["enable_master"] = kwargs["enable_master"] + case "modbus.get_values": + req["data"]["mode"] = kwargs["mode"] + case "move.set_monitor_cfg": + req["data"]["ref_coordinate"] = kwargs["ref_coordinate"] + case "move.set_params": + req["data"]["MOTION"] = kwargs["data"] + case "move.set_quickstop_distance": + req["data"]["distance"] = kwargs["distance"] + case "collision.set_params": + req["data"] = kwargs["data"] + case "collision.set_state": + req["data"]["collision_state"] = kwargs["collision_state"] + case "controller.set_params": + req["data"]["time"] = kwargs["time"] + case "drag.set_params": + req["data"]["enable"] = kwargs["enable"] + req["data"]["space"] = kwargs["space"] + req["data"]["type"] = kwargs["type"] + case _: + pass + + req["id"] = f"{command}-{time.time()}" + cmd = json.dumps(req, separators=(",", ":")) + print(f"correct cmd = {cmd}") + try: + self.c.send(self.package(cmd)) + self.logger("INFO", "openapi", f"hmi: 老协议请求发送成功 {cmd}") + state = "done" + except Exception as err: + self.logger("ERROR", "openapi", f"hmi: 老协议请求发送失败 {cmd},报错信息 {err}", "red", "CommandSendFailed") + return req["id"], state + elif p_flag == 1: # for xService + match command: + case "safety.safety_area.signal_enable": + req["c"]["safety.safety_area.signal_enable"]["signal"] = kwargs["signal"] + case "safety.safety_area.overall_enable": + req["c"]["safety.safety_area.overall_enable"]["enable"] = kwargs["enable"] + case "log_code.data.code_list": + req["s"]["log_code.data"]["code_list"] = kwargs["code_list"] + case "safety.safety_area.safety_area_enable": + req["c"]["safety.safety_area.safety_area_enable"]["id"] = kwargs["id"] + req["c"]["safety.safety_area.safety_area_enable"]["enable"] = kwargs["enable"] + case "safety.safety_area.set_param": + req["c"]["safety.safety_area.set_param"] = kwargs["data"] + case _: + pass + + try: + self.c_xs.send(self.package_xs(req)) + self.logger("INFO", "openapi", f"hmi: xService请求发送成功 {req}") + state = "done" + except Exception as Err: + self.logger("ERROR", "openapi", f"hr: xService请求发送失败 {req} 报错信息 {Err}", "red", "CommandSendFailed") + return command, state + + # =================================== ↓↓↓ specific functions ↓↓↓ =================================== + + def switch_motor_state(self, state: str): # OK + """ + 切换上电/下电的状态 + :param state: on/off + :return: None + """ + match state: + case "on": + self.execution("state.switch_motor_on") + case "off": + self.execution("state.switch_motor_off") + case _: + self.logger("ERROR", "openapi", f"hmi: switch_motor_state 参数错误 {state}, 非法参数,只接受 on/off", "red", "ArgumentError") + + def switch_operation_mode(self, mode: str): # OK + """ + 切换自动/手动操作模式 + :param mode: auto/manual + :return: None + """ + match mode: + case "auto": + self.execution("state.switch_auto") + case "manual": + self.execution("state.switch_manual") + case _: + self.logger("ERROR", "openapi", f"hmi: switch_operation_mode 参数错误 {mode},非法参数,只接受 auto/manual", "red", "ArgumentError") + + def reload_project(self, prj_name: str, tasks: list): # OK + """ + 重新加载指定工程 + :param prj_name: 工程名,也即 zip 文件的名字 + :param tasks: 要加载的任务列表 + :return: None + """ + prj_path = f"{prj_name}/_build/{prj_name}.prj" + self.execution("overview.reload", prj_path=prj_path, tasks=tasks) + + def set_project_auto_reload(self, prj_name: str): # OK + """ + 将指定工程设置为开机自动加载,也即默认工程 + :param prj_name: 工程名,也即 zip 文件的名字 + :return: None + """ + autoload_prj_path = f"{prj_name}/_build/{prj_name}.prj" + self.execution("overview.set_autoload", autoload_prj_path=autoload_prj_path) + + def pp_to_main(self, tasks: list): # OK + """ + 将指定的任务列表的指针,指向 main 函数 + :param tasks: 任务列表 + :return: None + """ + self.execution("rl_task.pp_to_main", tasks=tasks) + + def program_start(self, tasks: list): # OK + """ + 开始执行程序任务,必须是自动模式下执行 + :param tasks: 任务列表 + :return: None + """ + self.execution("rl_task.run", tasks=tasks) + + def program_stop(self, tasks: list): # OK + """ + 停止执行程序任务 + :param tasks: 人物列表 + :return: None + """ + self.execution("rl_task.stop", tasks=tasks) + + def set_program_loop_speed(self, loop_mode: bool = True, override: float = 0.5): # OK + """ + :param loop_mode: True为循环模式,False为单次模式 + :param override: HMI 左下方的速度滑块,取值范围 [0, 1] + :return: None + """ + self.execution("rl_task.set_run_params", loop_mode=loop_mode, override=override) + + def clear_alarm(self): # OK + """ + 清除伺服告警 + :return: None + """ + self.execution("servo.clear_alarm") + + def reboot_robot(self): # OK + """ + 重启控制器 + :return: None + """ + self.execution("controller.reboot") + self.logger("INFO", "openapi", f"hmi: 控制器重启中,重连预计需要等待 100s 左右...") + ts = time.time() + time.sleep(30) + while True: + time.sleep(5) + te = time.time() + if te - ts > 180: + self.__silence = False + self.__sth_wrong("3min 内未能完成重新连接,需要查看后台控制器是否正常启动,或者 ip/port 是否正确") + break + for _ in range(3): + if not self.__is_connected: + break + time.sleep(2) + else: + self.logger("INFO", "openapi", "hr: HMI 重新连接成功...", "green") + break + + def reload_io(self): + """ + 触发控制器重新加载 IO 设备列表 + :return: None + """ + self.execution("io_device.load_cfg") + + @property + def get_quickturn_pos(self): # OK + """ + 获取机器人的home位姿、拖动位姿和发货位姿,轴关节角度,end_posture 取值如下: + 0 法兰平面与地面平行 + 1 工具坐标系X轴与地面垂直,正向朝下 + 2 工具坐标系X轴与地面垂直,正向朝上 + 3 工具坐标系Y轴与地面垂直,正向朝下 + 4 工具坐标系Y轴与地面垂直,正向朝上 + 5 工具坐标系Z轴与地面垂直,正向朝下 + 6 工具坐标系Z轴与地面垂直,正向朝上 + :return: as below + { + "enable_home": false, // 是否开启 home 点快速调整 + "enable_drag": false, // 是否开启拖动位姿点快速调整 + "enable_transport": false, // 是否开启发货位姿点快速调整 + "joint_home": [0.0,0.0,0.0,0.0,0.0,0.0,0.0], // home 位姿的关节角度 + "joint_drag": [0.0,0.0,0.0,0.0,0.0,0.0,0.0], // 拖动位姿的关节角度 + "joint_transport": [0.0,0.0,0.0,0.0,0.0,0.0,0.0], // 发货位姿的关节角度 + "end_posture":0, // 末端姿态的调整方式,取值 0-6 + "home_error_range":[0.0,0.0,0.0,0.0,0.0,0.0,0.0] // home点误差范围 + } + """ + return self.__get_data(currentframe().f_code.co_name, "move.get_quickturn_pos") + + def set_quickturn_pos(self, enable_home: bool = False, enable_drag: bool = False, enable_transport: bool = False, end_posture: int = 0): # OK + """ + 设置机器人的home位姿、拖动位姿、发货位姿,轴关节角度,Home点误差范围,详见上一个 get_quickturn_pos 功能实现 + :param enable_home: 是否开启 home 点快速调整 + :param enable_drag: 是否开启拖动位姿点快速调整 + :param enable_transport:是否开启发货位姿点快速调整 + :param end_posture: 末端姿态的调整方式,取值 0-6,详见 get_quickturn_pos 注释 + :return: None + """ + self.execution("move.set_quickturn_pos", enable_home=enable_home, enable_drag=enable_drag, enable_transport=enable_transport, end_posture=end_posture) + + def move2quickturn(self, name: str): # OK + """ + 运动到指定的快速调整位姿 + :param name: 指定快速调整的名称,home/drag/transport + :return: None + """ + self.execution("move.quick_turn", name=name) + + def stop_move(self, stoptype=0): # OK + """ + 停止运动 + TS_READY | TS_JOG | TS_LOADIDENTIFY | TS_DYNAMICIDENTIFY | TS_DRAG | TS_PROGRAM | TS_DEMO | TS_RCI | TS_DEBUG | TS_FRICTIONIDENTIFY + :param stoptype: 对应控制器的任务空间类型TaskSpace的枚举值,0-7 + :return: None + """ + self.execution("move.stop", stoptype=stoptype) + + @property + def get_jog_params(self): # OK + """ + 获取JOG的参数 + 世界坐标系 WORLD_COORDINATE 0 + 法兰盘坐标系 FLANGE_COORDINATE 1 + 基坐标系 BASE_COORDINATE 2 + 工具坐标系 TOOL_COORDINATE 3 + 工件坐标系 FRAME_COORDINATE 4 + 关节空间 JOINT_SPACE 5 + :return: + { + "step": 1000 [1000-连续] [10/1/0.1/0.001-点动] + "override": 0.2 速度比率 + "space": 5 JOG的空间 + } + """ + return self.__get_data(currentframe().f_code.co_name, "jog.get_params") + + def set_jog_params(self, step, override, space): # OK + """ + 设置JOG的参数,包含步长,空间,速度倍率 + :param step: [1000-连续] [10/1/0.1/0.001-点动] + :param override: 速度比率 + :param space: JOG的空间 + :return: None + """ + self.execution("jog.set_params", step=step, override=override, space=space) + + def start_jog(self, index: int, direction: bool = False, is_ext: bool = False): # OK + """ + 开始 JOG 运动 + :param index: 0-6,若选轴空间,则 0-6 对应 1-7 轴,若选笛卡尔空间,则 0-6 对应 xyzabc elb + :param direction: True 正方向,False 反方向 + :param is_ext: 是否是外部轴 jog + :return: None + """ + self.execution("jog.start", index=index, direction=direction, is_ext=is_ext) + + @property + def get_socket_params(self): # OK + """ + 获取socket参数 + :return: + { + "auto_connect": true, // True 开机启动,False 不开机启动 + "disconnection_detection_time": 10, // 链接断开检测周期(s) + "disconnection_triggering_behavior": 0, // 断开连接触发行为 0无动作 1暂停程序 2暂停并下电 + "enable": true, // True 开启或者 False 关闭 + "ip": "", // 仅限于客户端,用于指定服务端 IP;当作为服务端时,该参数设置为空字符串,否则会报错!!! + "name": "name", // 连接名称 + "port": "8080", // 连接端口 + "reconnect_flag": true, // True 自动重连,False 不自动重连 + "suffix": "\r", // 指定发送分隔符 + "type": 1 // 连接类型 0 client | 1 server + } + """ + return self.__get_data(currentframe().f_code.co_name, "socket.get_params") + + def set_socket_params(self, enable: bool, ip: str, port: str, suffix: str, type: int = 1, **kwargs): # OK + """ + 设置 socket 参数,一般作为服务器使用 + :param enable: True 开启或者 False 关闭 + :param ip: 连接ip + :param port: 连接端口 + :param suffix: 指定发送分隔符 + :param type: 0 client | 1 server + :return: None + """ + data = self.get_socket_params + keys = data.keys() + kwargs.update({"enable": enable, "ip": ip, "port": port, "suffix": suffix, "type": type}) + for _ in keys: + if _ in kwargs.keys(): + data[_] = kwargs[_] + self.execution("socket.set_params", data=data) + + @property + def get_diagnosis_params(self, version="1.4.1"): # OK + """ + 获取诊断功能开启状态,以及相应其他信息 + :param version: 指定诊断工具版本 + :return: + { + "delay_motion": false, // - + "display_open": false, // 诊断显示功能开启状态 + "ecat_diagnosis_state": false, // - + "overrun": false, // 是否开启实时线程超时监控上报 + "pdo_params": [...], // 指定版本支持的所有曲线信息 + "state": true, // 诊断功能的开启状态 + "turn_area": false // 转弯区是否上报 + } + """ + return self.__get_data(currentframe().f_code.co_name, "diagnosis.get_params", version=version) + + def set_diagnosis_params(self, display_pdo_params: list, frequency: int = 50, version: str = "1.4.1"): # OK + """ + 设置诊断功能显示参数 [{"name": "hw_joint_vel_feedback", "channel": 0}, ] + :param display_pdo_params: 指定要采集的曲线名称,具体可通过 get_diagnosis_params 函数功能获取所有目前已支持的曲线 + :param frequency: 采样频率,默认 50ms + :param version: xDiagnose的版本号 + :return: None + """ + self.execution("diagnosis.set_params", display_pdo_params=display_pdo_params, frequency=frequency, version=version) + + def open_diagnosis(self, open: bool, display_open: bool, overrun: bool = False, turn_area: bool = False, delay_motion: bool = False): # OK + """ + 打开或者关闭诊断曲线,并定义其他功能的开关(调试相关功能,比如是否开启线程超时监控和上报,转弯区以及运动延迟等) + :param open: 诊断功能,控制HMI->日志->诊断设置->私服诊断开关,一般设置成 True + :param display_open: 诊断显示功能,指的是在线诊断插件中的打开 switch 的状态,需要诊断数据的情况,设置成 True + :param overrun: 实时线程超时监控上报 + :param turn_area: 转弯区上报 + :param delay_motion: 延迟运动 + :return: None + """ + self.execution("diagnosis.open", open=open, display_open=display_open, overrun=overrun, turn_area=turn_area, delay_motion=delay_motion) + + def save_diagnosis(self, save: bool = True): # OK + """ + 保存诊断数据,也就是主动写诊断动作,HMI日志->诊断设置->保存诊断数据 + :param save: 保存数据开关 + :return: None + """ + self.execution("diagnosis.save", save=save) + + @property + def qurry_system_io_configuration(self): # OK + """ + 系统IO配置的查询协议,trigger 参数取值参照如下: + FLANKS 0, //边缘触发 + POS_FLANK 1, //上升沿 + NEG_FLANK 2, //下降沿 + HIGH_LEVEL 3, //高电平 + LOW_LEVEL 4 //低电平 + :return: + { + "input_system_io": { + "motor_on": { + "signal":"DI0_0", + "trigger":1 + }, + "motor_off": { + "signal":"DI0_0", + "trigger":2 + } + }, + "output_system_io": { + "sta_motor_on": { + "signal":"DO0_0" + }, + "sta_robot_running": { + "signal":"DO0_1" + } + } + } + """ + return self.__get_data(currentframe().f_code.co_name, "system_io.query_configuration") + + @property + def qurry_system_io_event_configuration(self): # OK + """ + 查询当前系统支持的系统IO事件列表,包括事件key、名称、支持的触发方式等配置 + :return: + { + "input_system_event": [ + { + "key": "ctrl_motor_on", + "name": "上电", + "trigger_types": [ + 1, + 2 + ] + }, + { + "key": "ctrl_motor_off", + "name": "下电", + "trigger_types": [ + 1, + 2 + ] + } + ], + "output_system_event": [ + { + "key": "sta_motor_on", + "name": "上下电状态" + }, + { + "key": "sta_program", + "name": "运行状态" + } + ], + "input_mutex_event": [ + { + "key0": "ctrl_motor_on", + "key1": "ctrl_motor_off" + }, + { + "key0": "ctrl_switch_auto", + "key1": "ctrl_switch_manu" + } + ] + } + """ + return self.__get_data(currentframe().f_code.co_name, "system_io.query_configuration") + + def update_system_io_configuration(self, i_funcs: list, o_funcs: list, i_signals: list, o_signals: list, trig_types: list): # OK + """ + 配置系统 IO + :param i_funcs: 输入,只写功能码列表 + :param o_funcs: 输出,只读功能码列表 + :param i_signals: DI 信号列表 + :param o_signals: DO 信号列表 + :param trig_types: 触发条件列表,可参考 qurry_system_io_configuration 中的触发条件 + :return: None + """ + input_system_io, output_system_io = {}, {} + for i_func in i_funcs: + _index = i_funcs.index(i_func) + input_system_io[i_func] = {"signal": i_signals[_index], "trigger": trig_types[_index]} + for o_func in o_funcs: + _index = o_funcs.index(o_func) + output_system_io[o_func] = {"signal": o_signals[_index]} + self.execution("system_io.update_configuration", input_system_io=input_system_io, output_system_io=output_system_io) + + @property + def get_fieldbus_device_params(self): # OK + """ + 获取的是 HMI通讯->总线设备列表的信息,以及开关状态 + :return: + { + "device_list": [ + { + "device_name": "modbus_1", + "enable": true + }, { + "device_name": "modbus_2", + "enable": false + } + ] + } + """ + return self.__get_data(currentframe().f_code.co_name, "fieldbus_device.get_params") + + def set_fieldbus_device_params(self, device_name: str, enable: bool): # OK + """ + 定义开关设备的协议,一次只能打开一个设备 + :param device_name: 设备列表中的名称 + :param enable: 是否开启,这里操作的是 HMI通信->IO设备里面的开关状态 + :return: None + """ + self.execution("fieldbus_device.set_params", device_name=device_name, enable=enable) + + def reload_fieldbus(self): # OK + """ + 触发控制器重新加载总线设备 + :return: None + """ + self.execution("fieldbus_device.load_cfg") + + @property + def get_modbus_params(self): # OK + """ + 获取modbus参数 + :return: + { + "connect_state": true, + "enable_master": false, + "enable_slave": false, + "ip": "192.168.0.160", + "is_convert": true, + "port": 502, + "slave_id": 1 + } + """ + return self.__get_data(currentframe().f_code.co_name, "modbus.get_params") + + def set_modbus_params(self, enable_slave: bool, ip: str, port: int, slave_id: int, enable_master: bool): # OK + """ + 设置modbus参数,相当于新建 + :param enable_slave: Modbus从站是否自动开启 + :param ip: ip 地址 + :param port: 端口 + :param slave_id: 从站 ID + :param enable_master: Modbus主站是否自动开启 + :return: + """ + self.execution("modbus.set_params", enable_slave=enable_slave, ip=ip, port=port, slave_id=slave_id, enable_master=enable_master) + + def reload_registers(self): # OK + """ + 触发控制器重新加载寄存器列表 + :return: None + """ + self.execution("modbus.load_cfg") + + def get_modbus_values(self, mode: str = "all"): # OK + """ + 用于获取 modbus 寄存器变量值的更新信息,展示在状态监控界面 + :param mode: all/change + :return: + """ + return self.__get_data(currentframe().f_code.co_name, "modbus.get_values", mode=mode) + + @property + def get_soft_limit_params(self): # OK + """ + 获取软限位参数 + :return: + { + "enable":true + "upper":[0,0,0,0,0,0,0], + "lower":[0,0,0,0,0,0,0], + "reduced_upper":[0,0,0,0,0,0,0], + "reduced_lower":[0,0,0,0,0,0,0] + } + """ + return self.__get_data(currentframe().f_code.co_name, "soft_limit.get_params") + + def set_soft_limit_params(self, **kwargs): # OK + """ + 设定软限位参数 enable: bool, upper: list, lower: list, reduced_upper: list, reduced_lower: list + :enable: 是否启用软限位 + :upper: 软限位上限 + :lower: 软限位下限 + :reduced_upper: 缩减模式软限位上限 + :reduced_lower: 缩减模式软限位下限 + :return: None + """ + data = self.get_soft_limit_params + keys = data.keys() + for _ in keys: + if _ in kwargs.keys(): + data[_] = kwargs[_] + self.execution("soft_limit.set_params", data=data) + + @property + def get_device_params(self): # OK + """ + 获取设备信息 + :return: + """ + return self.__get_data(currentframe().f_code.co_name, "device.get_params") + + @property + def get_cart_pos(self): # OK + """ + 获取机器人的当前位姿:包括轴关节角度,笛卡尔关节角度,四元数,欧拉角(臂角) + :return: + { + "joint":[0.0,0.0,0.0,0.0,0.0,0.0], + "position":[0.0,0.0,0.0,0.0,0.0,0.0], + "euler":[0.0,0.0,0.0], + "quaternion"[0.0,0.0,0.0,0.0], + "elb":0.0, // 可缺省 + "ext_joint":[0.0,0.0,0.0,0.0,0.0] + } + """ + return self.__get_data(currentframe().f_code.co_name, "move.get_pos") + + @property + def get_joint_pos(self): # OK + """ + 获取机器人的当前关节角度:包括内部轴和外部轴 + :return: + { + "inner_pos": [0,0,0,0,0,0,0], + "extern_pos": [0,0,0,0,0,0,0] + } + """ + return self.__get_data(currentframe().f_code.co_name, "move.get_joint_pos") + + @property + def get_monitor_cfg(self): # OK + """ + 获取机器人的监控配置参数,RefCoordinateType 类型数据,表示当前控制器位置监测的相对坐标系 + 基坐标系 REF_COORDINATE_BASE 0 + 世界坐标系 REF_COORDINATE_WORLD 1 + 工件坐标系 REF_COORDINATE_WOBJ 2 + :return: + { + "ref_coordinate": 0 + } + """ + return self.__get_data(currentframe().f_code.co_name, "move.get_monitor_cfg") + + def set_monitor_cfg(self, ref_coordinate): # OK + """ + 设置机器人的监控配置参数 + :ref_coordinate: RefCoordinateType类型数据,用来设置当前控制器位置监测的相对坐标系 + :return: None + """ + self.execution("move.set_monitor_cfg", ref_coordinate=ref_coordinate) + + @property + def get_move_params(self): # OK + """ + 获取机器人的运动参数:包括减速比、耦合比、最大速度、加速度、加加速度、acc ramp time、规划步长等信息 + :return: + { + "MOTION": { + "ACC_RAMPTIME_JOG": 0.5, + "ACC_RAMPTIME_STOP": 0.5, + "DEFAULT_ACC_PARAMS": [1.0, 0.5], + "JERK_LIMIT_CART": 0, + "JERK_LIMIT_JOINT": 0, + "JERK_LIMIT_ROT": 0, + "JOINT_MAX_ACC": [500, 500, 1000, 1000, 1000], + "JOINT_MAX_JERK": [2000, 2000, 2000, 4000, 4000, 4000], + "JOINT_MAX_SPEED": [120.0, 120.0, 180.0, 180.0, 180.0, 180.0], + "MAX_ACC_PARAMS": [1.0, 1], + "MIN_ACC_PARAMS": [0.3, 0.05], + "TCP_MAX_ACC": 5000, + "TCP_MAX_JERK": 10000, + "TCP_MAX_SPEED": 1000, + "TCP_ROTATE_MAX_ACC": 1800, + "TCP_ROTATE_MAX_JERK": 3600, + "TCP_ROTATE_MAX_SPEED": 180, + "VEL_SMOOTH_FACTOR": 1.0, + "VEL_SMOOTH_FACTOR_RANGE": [1.0, 10.0] + } + } + """ + return self.__get_data(currentframe().f_code.co_name, "move.get_params") + + def set_move_params(self, **kwargs): # OK + """ + 设置机器人的运动参数:轴最大速度、轴最大加加速度、速度、加加速度、加速度、加加速度、acc ramp time、规划步长等信息 + 可选参数:参考 get_move_params 函数返回值 MOTION 里面的选项 + :return: None + """ + data = self.get_move_params["MOTION"] + print(f"res = {data}") + keys = data.keys() + for _ in keys: + if _ in kwargs.keys(): + data[_] = kwargs[_] + self.execution("move.set_params", data=data) + + @property + def get_quick_stop_distance(self): # OK + """ + 获取机器人 search 指令最大停止距离 + :return: + { + "distance":2.0 + } + """ + return self.__get_data(currentframe().f_code.co_name, "move.get_quickstop_distance") + + def set_quick_stop_distance(self, distance: float): # OK + """ + 设置机器人 search 指令最大停止距离 + :param distance: 停止距离,单位 mm + :return: None + """ + self.execution("move.set_quickstop_distance", distance=distance) + + @property + def get_collision_params(self): # OK + """ + 获取碰撞检测相关参数 + :return: + { + "action": 1, // 触发行为:1-安全停止;2-触发暂停;3-柔顺停止 + "coeff": [100, 100, 100, 100, 100, 100], // 0-整机灵敏度百分比,1-单轴灵敏度百分比,2-单轴和整机灵敏度百分比 + "coeff_level": 0, // 灵敏度等级:0-低,1-中,2-高 + "compliance": 0, // 柔顺功能比例系数,[0-1] + "enable": true, // 功能使能开关 + "fallback_distance": 3, // 回退距离 + "mode": 0, // 力传感器系数,0 整机 1 单轴 + "percent": 100, // 0-200,整机灵敏度百分比 + "percent_axis": [100, 100, 100, 100, 100, 100], // 0-200,单轴灵敏度百分比 + "reduced_percent": 100, // 0-200,整机缩减模式灵敏度百分比 + "reduced_percent_axis": [100, 100, 100, 100, 100, 100] // 0-200,单轴缩减模式灵敏度百分比 + } + """ + return self.__get_data(currentframe().f_code.co_name, "collision.get_params") + + def set_collision_params(self, enable, mode, action, percent, **kwargs): # OK + """ + 设置碰撞检测相关参数 + :param enable: 功能使能开关 + :param mode: 力传感器系数,0 整机 1 单轴 + :param action: 触发行为:1-安全停止;2-触发暂停;3-柔顺停止 + :param percent: 0-200,整机灵敏度百分比 + :return: + """ + data = self.get_collision_params + keys = data.keys() + kwargs.update({"enable": enable, "mode": mode, "action": action, "percent": percent}) + for _ in keys: + if _ in kwargs.keys(): + data[_] = kwargs[_] + self.execution("collision.set_params", data=data) + + def set_collision_state(self, collision_state: bool): # NG + """ + 开启或者关闭虚拟墙碰撞检测,测试该函数功能无效!!! + :param collision_state: 碰撞检测的开关状态 + :return: None + """ + self.execution("collision.set_state", collision_state=collision_state) + + @property + def get_robot_state(self): # OK + """ + { + "rc_state":"normal", # "fatal" 、"error"、"block"、"normal" + "engine":"on", # "fatal" 、"error"、"GStop"、"EStop"、"on"、"off" + "servo_mode":"position", # "torque"、"position" + "operate": "auto", # "auto"、"manual" + "task_space": "program", # "jog"、"drag"、"ready"、"load_identify"、"demo"、"rci"、"dynamic_identify"、"program"、"debug" + "robot_action": "idle", # "idle"、"busy" + "safety_mode": "collision" # "normal"、"collision"、"collaboration" + } + """ + return self.__get_data(currentframe().f_code.co_name, "state.get_state") + + def set_controller_params(self, robot_time: str): # OK + """ + 设置控制器系统时间 + :param robot_time: 系统时间,"2020-02-28 15:28:30" + :return: None + """ + self.execution("controller.set_params", time=robot_time) + + @property + def get_robot_params(self): # OK + """ + "alias": "", + "auth_state": + "controller_type": "XBC_XMATE", + "controller_types": ["XBC_3", "XBC_5", "XBC_XMATE"], + "disk_serial_number": "2338020401535", + "mac_addr": "34:df:20:03:1b:45", + "model":, + "nic_list": ["enp1s0", "enp2s0"], + "occupied_addr": "192.168.2.123:49269", + "period": 0.002, + "period_types": [0.001, 0.002, 0.003, 0.004], + "robot_template": 10, + "robot_type": "XMC12-R1300-W7G3B1C", + "robot_types": ["XMC12-R1300-B7S3B0C", "XMC12-R1300-W7G3B1C", "XMC17_5-R1900-W7G3B1C", "XMC20-R1650-B7G3Z0C"], + "security_type": "ROKAE_RSC", + "security_types": ["ROKAE_MINI", "ROKAE_RSC"], + "time": "2024-09-13 12:36:38", + "version": "2.3.0.4" + """ + return self.__get_data(currentframe().f_code.co_name, "controller.get_params") + + def switch_tp_mode(self, mode: str): # OK + """ + 切换示教器模式 + :param mode: with/without + :return: None + """ + match mode: + case "with": + self.execution("state.set_tp_mode", tp_mode="with") + case "without": + self.execution("state.set_tp_mode", tp_mode="without") + case _: + self.logger("ERROR", "openapi", f"hmi: switch_tp_mode 参数错误{mode}, 非法参数,只接受 with/without", "red", "ArgumentError") + + @property + def get_tp_mode(self): # OK + """ + 获取示教器连接状态 + :return: + { + "tp_mode":"with" + } + """ + return self.__get_data(currentframe().f_code.co_name, "state.get_tp_mode") + + @property + def get_drag_params(self): # OK + """ + 获取拖动模式参数 + :return: + { + "enable": true, + "space": 0, + "type": 0 + } + """ + return self.__get_data(currentframe().f_code.co_name, "drag.get_params") + + def set_drag_params(self, enable: bool, space: int = 1, type: int = 2): # OK + """ + 设置拖动模式开关以及参数 + :param enable: 是否启用拖动 + :param space: 拖动空间 - 0 代表关节 1 代表笛卡尔 + :param type: 拖动类型 - 0 只平移 1 只旋转 2 自由拖动 + :return: None + """ + self.execution("drag.set_params", enable=enable, space=space, type=type) + + def set_safety_area_signal(self, signal: bool): # OK + """ + 设置安全区域信号控制使能开关 + :param signal: True 打开 False 关闭 + :return: None + """ + self.execution("safety.safety_area.signal_enable", protocol_flag=1, signal=signal) + + def set_safety_area_overall(self, enable: bool): # OK + """ + 设置安全区域整体控制使能开关 + :param enable: True 打开 False 关闭 + :return: None + """ + self.execution("safety.safety_area.overall_enable", protocol_flag=1, enable=enable) + + @property + def get_safety_area_params(self): # OK + """ + 获取安全区所有的配置信息 + :return: + "g": { + "safety_area_data": { + "overall_enable": true, + "safety_area_setting": [ + { + "box": { + "Lx": 100.0, + "Ly": 100.0, + "Lz": 100.0, + "direction": false, + "ori": { + "euler": { + "a": 179.9963851353547, + "b": -0.006653792532429416, + "c": 179.9934560302729 + }, + "quaternion": { + "q1": 0.0, + "q2": 0.0, + "q3": 0.0, + "q4": 0.0 + } + }, + "pos": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "enable": false, + "id": 0, + "name": "region1", + "plane": { + "direction": true, + "point": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "vector": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "shape": 0, + "shared_bind_di": "", + "shared_bind_do": "", + "sphere": { + "ori": { + "euler": { + "a": 0.0, + "b": 0.0, + "c": 0.0 + }, + "quaternion": { + "q1": 0.0, + "q2": 0.0, + "q3": 0.0, + "q4": 0.0 + } + }, + "pos": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "radius": 0.0 + }, + "state": true, + "trigger": 0, + "type": 0, + "vertebral": { + "high": 0.0, + "ori": { + "euler": { + "a": 0.0, + "b": 0.0, + "c": 0.0 + }, + "quaternion": { + "q1": 0.0, + "q2": 0.0, + "q3": 0.0, + "q4": 0.0 + } + }, + "pos": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "radius": 0.0 + } + }, + ... // 剩余 9 个安全区域的配置信息 + ], + "signal_enable": true + } + } + } + """ + return self.__get_data(currentframe().f_code.co_name, "safety_area_data", flag=1) + + def set_safety_area_enable(self, id: int, enable: bool): # OK + """ + 设置每个安全区域的开关 + :param id: 安全区域开关,0-9 + :param enable: True 打开,False 关闭 + :return: None + """ + self.execution("safety.safety_area.safety_area_enable", protocol_flag=1, id=id, enable=enable) + + def set_safety_area_param(self, id: int, enable: bool, **kwargs): # OK + """ + 设定单独安全区的参数 + :param id: 安全区 id + :param enable: 是否开启 + :param kwargs: 其他参数,参考 get_safety_area_params 的返回值形式 + :return: None + """ + data = self.get_safety_area_params["g"]["safety_area_data"]["safety_area_setting"][id] + keys = data.keys() + kwargs.update({"id": id, "enable": enable}) + for _ in keys: + if _ in kwargs.keys(): + data[_] = kwargs[_] + self.execution("safety.safety_area.set_param", protocol_flag=1, data=data) + self.execution("safety.safety_area.safety_area_enable", protocol_flag=1, id=id, enable=enable) + + @property + def get_filtered_error_code(self): # OK + """ + 获取已设定的错误码过滤列表 + :return: + { + "g": { + "log_code.data": { + "code_list": [ + { + "id": 100, + "title": "DEMO\\u610f\\u5916\\u505c\\u6b62" + }, + { + "id": 10000, + "title": "HMI\\u8bf7\\u6c42\\u5305\\u89e3\\u6790\\u9519\\u8bef" + }, + { + "id": 10002, + "title": "\\u5feb\\u901f\\u8c03\\u6574\\u542f\\u52a8\\u5931\\u8d25" + } + ], + "version": 1 + } + } + } + """ + return self.__get_data(currentframe().f_code.co_name, "log_code.data", flag=1) + + def set_filtered_error_code(self, action: str, code_list: list): # OK + """ + 清空,增加,删除错误过滤码 + :param action: 支持 add/remove/clear,当为 clear 时,code_list 可为任意列表 + :param code_list: 需要添加/删除的过滤码列表 + :return: None + """ + origin_code_list = self.get_filtered_error_code["g"]["log_code.data"]["code_list"] + # [{"id": 10000, "title": "HMI请求包解析错误"}, {"id": 10002, "title": "快速调整启动失败"}] + if action == "clear": + code_list = [] + elif action == "add": + for error_code in code_list: + for item in origin_code_list: + if error_code == item["id"]: + break + else: + origin_code_list.append({"id": error_code, "title": ""}) + code_list = origin_code_list + elif action == "remove": + for error_code in code_list: + for item in origin_code_list: + if error_code == item["id"]: + origin_code_list.remove(item) + code_list = origin_code_list + + self.execution("log_code.data.code_list", protocol_flag=1, code_list=code_list) + + def __get_data(self, upper_func, command, flag=0, **kwargs): + msg_id, state = self.execution(command, protocol_flag=flag, **kwargs) + records = clibs.c_hr.get_from_id(msg_id, state) + for record in records: + if "请求发送成功" not in record[0]: + data = eval(record[0])["data"] + return data + # =================================== ↑↑↑ specific functions ↑↑↑ =================================== + + +class ExternalCommunication(QThread): + output = Signal(str, str) + + def __init__(self, ip, port, /): + super().__init__() + self.__c = None + self.ip = ip + self.port = int(port) + self.suffix = "\r" + self.exec_desc = " :--: 返回 true 表示执行成功,false 失败" + + def logger(self, level, module, content, color="black", error="", flag="both"): + clibs.logger(level, module, content, color, flag, signal=self.output) + if level.upper() == "ERROR": + raise Exception(f"{error} | {content}") + + def net_conn(self): + clibs.c_hr.set_socket_params(False, "0.0.0.0", self.port, "\r") + clibs.c_hr.set_socket_params(True, "0.0.0.0", self.port, "\r") + self.__c = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + self.__c.connect((self.ip, self.port)) + self.logger("INFO", "openapi", f"ec: 外部通信连接成功...", "green") + return self.__c + except Exception as err: + self.logger("ERROR", "openapi", f"ec: 外部通信连接失败... {err}", "red", "EcConnFailed") + + def close(self): + if clibs.status["ec"]: + try: + self.__c.close() + except Exception as err: + self.logger("ERROR", "openapi", f"ec: 关闭 EC 连接失败:{err}", "red", "EcCloseFailed") + + def s_string(self, directive): + order = "".join([directive, self.suffix]) + self.__c.send(order.encode()) + time.sleep(clibs.INTERVAL) + + def r_string(self, directive): + result, char = "", "" + while char != self.suffix: + try: + char = self.__c.recv(1).decode(encoding="unicode_escape") + except Exception as err: + self.logger("ERROR", "openapi", f"ec: 获取请求指令 {directive} 的返回数据超时,需确认指令发送格式以及内容正确!具体报错信息如下 {err}", "red", "RecvMsgFailed") + result = "".join([result, char]) + time.sleep(clibs.INTERVAL) + return result + + # =================================== ↓↓↓ specific functions ↓↓↓ =================================== + def motor_on(self): # OK + return self.__exec_cmd("motor_on", "电机上电", self.exec_desc) + + def motor_off(self): # OK + return self.__exec_cmd("motor_off", "电机下电", self.exec_desc) + + def pp_to_main(self): # OK + return self.__exec_cmd("pp_to_main", "程序指针到", self.exec_desc) + + def program_start(self): # OK + return self.__exec_cmd("start", "程序启动(可能需先清告警)", self.exec_desc) + + def program_stop(self): # OK + return self.__exec_cmd("stop", "程序停止", self.exec_desc) + + def clear_alarm(self): # OK + return self.__exec_cmd("clear_alarm", "清除伺服报警", self.exec_desc) + + def switch_operation_auto(self): # OK + return self.__exec_cmd("switch_mode:auto", "切换到自动模式", self.exec_desc) + + def switch_operation_manual(self): # OK + return self.__exec_cmd("switch_mode:manual", "切换到手动模式", self.exec_desc) + + def open_drag_mode(self): # OK + return self.__exec_cmd("open_drag", "打开拖动", self.exec_desc) + + def close_drag_mode(self): # OK + return self.__exec_cmd("close_drag", "关闭拖动", self.exec_desc) + + def get_program_list(self): # OK + return self.__exec_cmd("list_prog", "获取工程列表") + + def get_current_program(self): # OK + return self.__exec_cmd("current_prog", "当前工程") + + def load_program(self, program_name): # OK + return self.__exec_cmd(f"load_prog:{program_name}", "加载工程", self.exec_desc) + + def estop_reset(self): # OK | 复原外部 IO 急停和安全门急停,前提是硬件已复原 + return self.__exec_cmd("estop_reset", "急停复位", self.exec_desc) + + def estopreset_and_clearalarm(self): # OK | 外部 IO/安全门急停/安全碰撞告警都可以清除,前提是硬件已复原 + return self.__exec_cmd("estopreset_and_clearalarm", "急停复位并清除报警", self.exec_desc) + + def motoron_pp2main_start(self): # OK + return self.__exec_cmd("motoron_pptomain_start", "依次执行上电,程序指针到main,启动程序(可以是手动模式,必要时需清告警)", self.exec_desc) + + def motoron_start(self): # OK + return self.__exec_cmd("motoron_start", "依次执行上电,启动程序(可以是手动模式,必要时需清告警)", self.exec_desc) + + def pause_motoroff(self): # OK + return self.__exec_cmd("pause_motoroff", "暂停程序并下电", self.exec_desc) + + def set_program_speed(self, speed: int): # OK | 1-100 + return self.__exec_cmd(f"set_program_speed:{speed}", "设置程序运行速率(滑块)", self.exec_desc) + + def set_soft_estop(self, enable: str): # OK + return self.__exec_cmd(f"set_soft_estop:{enable}", "触发(true)/解除(false)机器人软急停", self.exec_desc) + + def switch_auto_motoron(self): # OK + return self.__exec_cmd("switch_auto_motoron", "切换自动模式并上电", self.exec_desc) + + def open_safe_region(self, number: int): # OK | 1-10 + return self.__exec_cmd(f"open_safe_region:{number}", f"打开第 {number} 个安全区域(1-10,信号控制开关需打开,不限操作模式)", self.exec_desc) + + def close_safe_region(self, number: int): # OK | 1-10 + return self.__exec_cmd(f"close_safe_region:{number}", f"关闭第 {number} 个安全区域(1-10,信号控制开关需打开,不限操作模式)", self.exec_desc) + + def open_reduced_mode(self): # OK + return self.__exec_cmd("open_reduced_mode", "开启缩减模式(不限操作模式)", self.exec_desc) + + def close_reduced_mode(self): # OK + return self.__exec_cmd("close_reduced_mode", "关闭缩减模式(不限操作模式)", self.exec_desc) + + def setdo_value(self, do_name: str, do_value: str): # OK | do_value 为 true/false + return self.__exec_cmd(f"setdo:{do_name},{do_value}", f"设置 {do_name} 的值为 {do_value} ", self.exec_desc) + + def modify_system_time(self, robot_time): # OK + return self.__exec_cmd(f"set_robot_time:{robot_time}", f"修改控制器和示教器的时间为 {robot_time} 的", self.exec_desc) + + # -------------------------------------------------------------------------------------------------- + @property + def motor_on_state(self): # OK + return self.__exec_cmd("motor_on_state", "获取上电状态", " :--: 返回 true 表示已上电,false 已下电") + + @property + def robot_running_state(self): # OK + return self.__exec_cmd("robot_running_state", "获取程序运行状态", " :--: 返回 true 表示正在运行,false 未运行") + + @property + def estop_state(self): # OK | 只表示外部急停,安全门触发不会返回 true,只要有急停标识 E 字母,就会返回 true + return self.__exec_cmd("estop_state", "急停状态", " :--: 返回 true 表示处于急停状态,false 非急停") + + @property + def operation_mode(self): # OK + return self.__exec_cmd("operating_mode", "获取工作模式", " :--: 返回 true 表示自动模式,false 手动模式") + + @property + def home_state(self): # OK | 需要设置一下 "HMI 设置->快速调整" + return self.__exec_cmd("home_state", "获取 HOME 输出状态", " :--: 返回 true 表示法兰中心处于 HOME 点,false 未处于 HOME 点") + + @property + def fault_state(self): # OK + return self.__exec_cmd("fault_state", "获取 故障状态", " :--: 返回 true 表示处于故障状态,false 非故障状态") + + @property + def collision_state(self): # OK | 但是触发后,无法清除? + return self.__exec_cmd("collision_state", "获取碰撞触发状态", " :--: 返回 true 表示碰撞已触发,false 未触发") + + @property + def task_state(self): # OK + return self.__exec_cmd("task_state", "获取机器人运行任务状态", " :--: 返回 program 表示任务正在运行,ready 未运行") + + @property + def get_cart_pos(self): # OK | cart_pos/cart_pos_name 都可以正常返回,区别在返回的前缀,可测试辨别 + return self.__exec_cmd("cart_pos", "获取笛卡尔位置") + + @property + def get_joint_pos(self): # OK | jnt_pos/jnt_pos_name 都可以正常返回,区别在返回的前缀,可测试辨别 + return self.__exec_cmd("jnt_pos", "获取轴位置") + + @property + def get_axis_vel(self): # OK | jnt_vel/jnt_vel_name 都可以正常返回,区别在返回的前缀,可测试辨别 + return self.__exec_cmd("jnt_vel", "获取轴速度") + + @property + def get_axis_trq(self): # OK | jnt_trq/jnt_trq_name 都可以正常返回,区别在返回的前缀,可测试辨别 + return self.__exec_cmd("jnt_trq", "获取轴力矩") + + @property + def reduced_mode_state(self): # OK + return self.__exec_cmd("reduced_mode_state", "获取缩减模式状态", " :--: 返回 true 表示缩减模式,false 非缩减模式") + + def get_io_state(self, io_list: str): # OK | DO0_0,DI1_3,DO2_5,不能有空格 + return self.__exec_cmd(f"io_state:{io_list}", "获取 IO 状态值") + + @property + def alarm_state(self): # OK + return self.__exec_cmd("alarm_state", "获取报警状态", " :--: 返回 true 表示当前有告警,false 没有告警") + + @property + def collision_alarm_state(self): # OK + return self.__exec_cmd("collision_alarm_state", "获取碰撞报警状态", " :--: 返回 true 表示有碰撞告警,false 没有碰撞告警") + + @property + def collision_open_state(self): # OK + return self.__exec_cmd("collision_open_state", "获取碰撞检测开启状态", " :--: 返回 true 表示已开启碰撞检测,false 未开启") + + @property + def controller_is_running(self): # OK + return self.__exec_cmd("controller_is_running", "判断控制器是否开机", " :--: 返回 true 表示控制器正在运行,false 未运行") + + @property + def encoder_low_battery_state(self): # OK + return self.__exec_cmd("encoder_low_battery_state", "编码器低电压报警状态", " :--: 返回 true 表示编码器处于低电压状态,false 电压正常") + + @property + def robot_error_code(self): # OK + return self.__exec_cmd("robot_error_code", "获取机器人错误码") + + @property + def rl_pause_state(self): # OK + # 0 -- 初始化状态,刚开机上电时 + # 1 -- RL 运行中 + # 2 -- HMI 暂停 + # 3 -- 系统 IO 暂停 + # 4 -- 寄存器功能码暂停 + # 5 -- 外部通讯暂停 + # 6 -- + # 7 -- Pause 指令暂停 + # 8 -- + # 9 -- + # 10 -- 外部 IO 急停 + # 11 -- 安全门急停 + # 12 -- 其他因素停止,比如碰撞检测 + return self.__exec_cmd("program_full", "获取 RL 的暂停状态", " :--: 返回值含义详见功能定义") + + @property + def program_reset_state(self): # OK + return self.__exec_cmd("program_reset_state", "获取程序复位状态", " :--: 返回 true 表示指针指向 main,false 未指向 main") + + @property + def program_speed_value(self): # OK | 速度滑块 + return self.__exec_cmd("program_speed", "获取程序运行速度") + + @property + def robot_is_busy(self): # OK | 触发条件为 pp2main/重载工程/推送到控制器,最好测试工程大一些,比较容易触发 + return self.__exec_cmd("robot_is_busy", "获取程序忙碌状态", " :--: 返回 1 表示控制器忙碌,0 非忙碌状态") + + @property + def robot_is_moving(self): # OK + return self.__exec_cmd("robot_is_moving", "获取程序运行状态", " :--: 返回 true 表示机器人正在运动,false 未运动") + + @property + def safe_door_state(self): # OK + return self.__exec_cmd("safe_door_state", "获取安全门状态", " :--: 返回 true 表示安全门已触发,false 未触发") + + @property + def soft_estop_state(self): # OK + return self.__exec_cmd("soft_estop_state", "获取软急停状态", " :--: 返回 true 表示软急停已触发,false 未触发") + + @property + def get_cart_vel(self): # OK + return self.__exec_cmd("cart_vel", "获取笛卡尔速度") + + @property + def get_tcp_pos(self): # OK + return self.__exec_cmd("tcp_pose", "获取 TCP 位姿") + + @property + def get_tcp_vel(self): # OK + return self.__exec_cmd("tcp_vel", "获取 TCP 速度") + + @property + def get_tcp_vel_mag(self): # OK + return self.__exec_cmd("tcp_vel_mag", "获取 TCP 合成线速度") + + @property + def ext_estop_state(self): # OK + return self.__exec_cmd("ext_estop_state", "获取外部轴急停状态", " :--: 返回 true 表示外部轴急停已触发,false 未触发") + + @property + def hand_estop_state(self): # OK + return self.__exec_cmd("hand_estop_state", "获取手持急停状态", " :--: 返回 true 表示手持急停已触发,false 未触发") + + @property + def collaboration_state(self): # OK + return self.__exec_cmd("collaboration_state", "获取协作模式状态(其实就是缩减模式)", " :--: 返回 true 表示协作模式,false 非协作模式") + + def __exec_cmd(self, directive, description, more_desc=""): + self.s_string(directive) + result = self.r_string(directive).strip() + self.logger("INFO", "openapi", f"ec: 执行{description}指令是 {directive},返回值为 {result}{more_desc}") + return result + + +class PreDos(object): + def __init__(self, ip, ssh_port, username, password): + self.__ssh = None + self.__sftp = None + self.ip = ip + self.ssh_port = ssh_port + self.username = username + self.password = password + + def __ssh2server(self): + try: + self.__ssh = SSHClient() + self.__ssh.set_missing_host_key_policy(AutoAddPolicy()) + self.__ssh.connect(hostname=self.ip, port=self.ssh_port, username=self.username, password=self.password) + self.__sftp = self.__ssh.open_sftp() + except Exception as err: + print(f"predos: SSH 无法连接到 {self.ip}:{self.ssh_port},需检查网络连通性或者登录信息是否正确 {err}") + raise Exception("SshConnFailed") + + def push_prj_to_server(self, prj_file): + # prj_file:本地工程完整路径 + self.__ssh2server() + prj_name = prj_file.split("/")[-1].split(".")[0] + self.__sftp.put(prj_file, f"/tmp/{prj_name}.zip") + cmd = f"cd /tmp; mkdir {prj_name}; unzip -d {prj_name} -q {prj_name}.zip; rm -rf /tmp/{prj_name}.zip; " + cmd += f"sudo rm -rf /home/luoshi/bin/controller/projects/{prj_name}; " + cmd += f"sudo mv /tmp/{prj_name}/ /home/luoshi/bin/controller/projects/" + stdin, stdout, stderr = self.__ssh.exec_command(cmd, get_pty=True) + stdin.write(self.password + "\n") + stdout.read().decode() # 需要read一下才能正常执行 + stderr.read().decode() + self.__ssh.close() + + def pull_prj_from_server(self, prj_name, local_prj_path): # NG | 可以拉取文件,但是导入之后,有问题 + # prj_name:要拉取的服务端工程名 + # local_prj_path:本地工程文件的完整路径 + self.__ssh2server() + cmd = f"cd /tmp/; sudo rm -rf {prj_name}*; sudo cp -rf /home/luoshi/bin/controller/projects/{prj_name} .; " + cmd += f"sudo zip -q -r {prj_name}.zip {prj_name}/; sudo rm -rf {prj_name}" + stdin, stdout, stderr = self.__ssh.exec_command(cmd, get_pty=True) + stdin.write(self.password + "\n") + print(stdout.read().decode()) # 需要read一下才能正常执行 + print(stderr.read().decode()) + + self.__sftp.get(f"/tmp/{prj_name}.zip", local_prj_path) + cmd = f"sudo rm -rf /tmp/{prj_name}.zip" + stdin, stdout, stderr = self.__ssh.exec_command(cmd, get_pty=True) + stdin.write(self.password + "\n") + print(stdout.read().decode()) # 需要read一下才能正常执行 + print(stderr.read().decode()) + + self.__ssh.close() + + def push_file_to_server(self, local_file, server_file): + # local_file:本地文件完整路径 + # server_file:服务端文件完整路径 + self.__ssh2server() + filename = local_file.split("\\")[-1].split("/")[-1] + self.__sftp.put(local_file, f"/tmp/{filename}") + cmd = f"sudo mv /tmp/{filename} {server_file}" + stdin, stdout, stderr = self.__ssh.exec_command(cmd, get_pty=True) + stdin.write(self.password + "\n") + stdout.read().decode() # 需要read一下才能正常执行 + stderr.read().decode() + self.__ssh.close() + + def pull_file_from_server(self, server_file, local_file): + # local_file:本地文件完整路径 + # server_file:服务端文件完整路径 + self.__ssh2server() + cmd = f"sudo cp {server_file} /tmp/" + stdin, stdout, stderr = self.__ssh.exec_command(cmd, get_pty=True) + stdin.write(self.password + "\n") + stdout.read().decode() # 需要read一下才能正常执行 + stderr.read().decode() + filename = server_file.split("/")[-1] + self.__sftp.get(f"/tmp/{filename}", f"{local_file}") + cmd = f"sudo rm -rf /tmp/{filename}" + stdin, stdout, stderr = self.__ssh.exec_command(cmd, get_pty=True) + stdin.write(self.password + "\n") + stdout.read().decode() # 需要read一下才能正常执行 + stderr.read().decode() + self.__ssh.close() + + +class RobotInit(object): + @staticmethod + def modbus_init(): + pd = PreDos(clibs.ip_addr, clibs.ssh_port, clibs.username, clibs.password) + # 推送配置文件 + print("init: 推送配置文件 fieldbus_device.json/registers.json/registers.xml 到控制器,并配置 IO 设备,设备号为 7...") + robot_params = clibs.c_hr.get_robot_params + robot_type = robot_params["robot_type"] + security_type = robot_params["security_type"] + controller_type = robot_params["controller_type"] + io_device_file = "_".join(["io_device", controller_type, security_type, "1"]) + + user_settings = "/home/luoshi/bin/controller/user_settings" + interactive_data = f"/home/luoshi/bin/controller/interactive_data/{robot_type}" + + config_files = [ + f"{clibs.PREFIX}/files/projects/fieldbus_device.json", + f"{clibs.PREFIX}/files/projects/registers.json", + f"{clibs.PREFIX}/files/projects/registers.xml" + ] + for config_file in config_files: + filename = config_file.split("/")[-1] + pd.push_file_to_server(config_file, f"{user_settings}/{filename}") + pd.push_file_to_server(config_file, f"{interactive_data}/{filename}") + + io_device_autotest = {"ai_num": 0, "ao_num": 0, "di_num": 16, "do_num": 16, "extend_attr": {"mode": "slaver", "name": "autotest", "type": "MODBUS"}, "id": 7, "name": "autotest", "type": 6} + + io_device_file_local = f"{io_device_file}" + io_device_file_local_tmp = f"{io_device_file}_tmp" + io_device_file_remote = f"{user_settings}/{io_device_file}" + pd.pull_file_from_server(io_device_file_remote, io_device_file_local) + with open(io_device_file_local, mode="r", encoding="utf-8") as f: + data = json.load(f) + for _ in data["device_list"]: + if _["extend_attr"].get("name", None) == "autotest": + break + else: + data["device_list"].append(io_device_autotest) + with open(io_device_file_local_tmp, mode="w", encoding="utf-8") as f_tmp: + json.dump(data, f_tmp, indent=4) + pd.push_file_to_server(io_device_file_local_tmp, f"{user_settings}/{io_device_file}") + pd.push_file_to_server(io_device_file_local_tmp, f"{interactive_data}/{io_device_file}") + + # os.remove(io_device_file_local) + # os.remove(io_device_file_local_tmp) + clibs.c_hr.reload_io() + clibs.c_hr.reload_registers() + clibs.c_hr.reload_fieldbus() + clibs.c_hr.set_fieldbus_device_params("autotest", True) + + def robot_init(self): + pd = PreDos(clibs.ip_addr, clibs.ssh_port, clibs.username, clibs.password) + # 推送配置文件 + print("init: 推送配置文件 fieldbus_device.json/registers.json/registers.xml 到控制器,并配置 IO 设备,设备号为 7...") + robot_params = clibs.c_hr.get_robot_params + robot_type = robot_params["robot_type"] + security_type = robot_params["security_type"] + controller_type = robot_params["controller_type"] + io_device_file = "_".join(["io_device", controller_type, security_type, "1"]) + + user_settings = "/home/luoshi/bin/controller/user_settings" + interactive_data = f"/home/luoshi/bin/controller/interactive_data/{robot_type}" + + config_files = [ + "..\\assets\\configs\\fieldbus_device.json", + "..\\assets\\configs\\registers.json", + "..\\assets\\configs\\registers.xml" + ] + for config_file in config_files: + filename = config_file.split("\\")[-1] + pd.push_file_to_server(config_file, f"{user_settings}/{filename}") + pd.push_file_to_server(config_file, f"{interactive_data}/{filename}") + + io_device_autotest = {"ai_num": 0, "ao_num": 0, "di_num": 16, "do_num": 16, "extend_attr": {"mode": "slaver", "name": "autotest", "type": "MODBUS"}, "id": 7, "name": "autotest", "type": 6} + io_device_file_local = f"..\\assets\\configs\\{io_device_file}" + io_device_file_local_tmp = f"..\\assets\\configs\\{io_device_file}_tmp" + io_device_file_remote = f"{user_settings}/{io_device_file}" + pd.pull_file_from_server(io_device_file_remote, io_device_file_local) + with open(io_device_file_local, mode="r", encoding="utf-8") as f: + data = json.load(f) + for _ in data["device_list"]: + if _["extend_attr"].get("name", None) == "autotest": + break + else: + data["device_list"].append(io_device_autotest) + with open(io_device_file_local_tmp, mode="w", encoding="utf-8") as f_tmp: + json.dump(data, f_tmp, indent=4) + pd.push_file_to_server(io_device_file_local_tmp, f"{user_settings}/{io_device_file}") + pd.push_file_to_server(io_device_file_local_tmp, f"{interactive_data}/{io_device_file}") + + clibs.c_hr.reload_io() + clibs.c_hr.reload_registers() + clibs.c_hr.reload_fieldbus() + clibs.c_hr.set_fieldbus_device_params("autotest", True) + + # 触发急停并恢复 + clibs.c_md.r_soft_estop(0) + clibs.c_md.r_soft_estop(1) + + # 断开示教器连接 + print("init: 断开示教器连接...") + clibs.c_hr.switch_tp_mode("without") + + # 清空 system IO 配置 + print("init: 清空所有的 System IO 功能配置...") + clibs.c_hr.update_system_io_configuration([], [], [], [], []) + + # 关闭缩减模式 + clibs.c_md.r_reduced_mode(0) + + # 打开软限位 + print("init: 打开软限位开关...") + clibs.c_hr.set_soft_limit_params(enable=True) + + # 关闭安全区域 + print("init: 正在关闭所有的安全区,并关闭总使能开关...") + clibs.c_hr.set_safety_area_overall(False) + clibs.c_hr.set_safety_area_signal(False) + for i in range(10): + clibs.c_hr.set_safety_area_enable(i, False) + + # 打开外部通信,并设置控制器时间同步 + print("init: 配置并打开外部通信,默认服务器,8080端口,后缀为 \"\\r\"...") + clibs.c_hr.set_socket_params(True, "8080", "\r", 1) + clibs.c_ec.modify_system_time(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))) + + # 关闭拖动 + if robot_type.upper()[:2] not in ["XB", "NB"]: + print("init: 关闭拖动模式...") + clibs.c_hr.set_drag_params(False, 1, 2) + + # 关闭碰撞检测 + print("init: 关闭碰撞检测...") + clibs.c_hr.set_collision_params(False, 0, 1, 100) + + # 清除所有过滤错误码 + print("init: 清除所有过滤错误码设定...") + clibs.c_hr.set_filtered_error_code("clear", []) + + # 回拖动位姿 + print("init: 正在回拖动位姿...") + clibs.c_hr.switch_operation_mode("manual") + clibs.c_hr.switch_motor_state("on") + clibs.c_hr.set_quickturn_pos(enable_drag=True) + clibs.c_hr.move2quickturn("drag") + while True: + if clibs.c_md.w_robot_is_moving: + time.sleep(1) + else: + break + clibs.c_hr.stop_move(1) + clibs.c_hr.switch_motor_state("off") + clibs.c_hr.close() + + # 清除所有告警 + clibs.c_md.r_clear_alarm() + + def fw_updater(self, local_file_path): + fw_size = os.path.getsize(local_file_path) + if fw_size > 10000000: + # get previous version of xCore + version_previous = clibs.hr.get_robot_params["version"] + + # var def + remote_file_path = "./upgrade/lircos.zip" + fw_content = bytearray() + package_data = bytearray() + package_data_with_head = bytearray() + + # socket connect + print(f"update firmware: 正在连接 {clibs.ip_addr}:{clibs.upgrade_port}...") + try: + tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + tcp_socket.connect((clibs.ip_addr, clibs.upgrade_port)) + tcp_socket.setblocking(True) + except Exception as Err: + print(f"update firmware: {Err} | 连接 {clibs.ip_addr}:{clibs.upgrade_port} 失败...") + raise Exception("UpgradeFailed") + + # get firmware content of binary format + print(f"update firmware: 正在读取 {local_file_path} 文件内容...") + with open(local_file_path, "rb") as f_fw: + fw_content += f_fw.read() + + # construct package data: http://confluence.i.rokae.com/pages/viewpage.action?pageId=15634148 + print("update firmware: 正在构造数据包,以及包头...") + # 1 protocol id + protocol_id = c_ushort(socket.htons(0)) # 2 bytes + package_data += protocol_id + + # 2 write type + write_type = c_ubyte(0) + package_data += bytes(write_type) + + # 3 md5 + md5_hash = hashlib.md5() + md5_hash.update(fw_content) + fw_md5 = md5_hash.hexdigest() + i = 0 + while i < len(fw_md5): + _ = (fw_md5[i:i + 2]) + package_data += bytes.fromhex(_) + i += 2 + + # 4 remote path len + remote_file_path_len = c_ushort(socket.htons(len(remote_file_path))) + package_data += remote_file_path_len + + # 5 remote path + package_data += remote_file_path.encode("ascii") + + # 6 file + package_data += fw_content + + # construct communication protocol: http://confluence.i.rokae.com/pages/viewpage.action?pageId=15634045 + # 1 get package data with header + package_size = c_uint(socket.htonl(len(package_data))) + package_type = c_ubyte(1) # 0-无协议 1-文件 2-json + package_reserve = c_ubyte(0) # 预留位 + + package_data_with_head += package_size + package_data_with_head += package_type + package_data_with_head += package_reserve + package_data_with_head += package_data + + # 2 send data to server + print("update firmware: 正在发送数据到 xCore,升级控制器无需重启,升级配置文件会自动软重启...") + start = 0 + len_of_package = len(package_data_with_head) + while start < len_of_package: + end = 10240 + start + if end > len_of_package: + end = len_of_package + sent = tcp_socket.send((package_data_with_head[start:end])) + time.sleep(0.01) + if sent == 0: + raise RuntimeError("socket connection broken") + else: + start += sent + + waited = 5 if fw_size > 10000000 else 25 + time.sleep(waited) + + if fw_size > 10000000: + # get current version of xCore + version_current = clibs.c_hr.get_robot_params["version"] + print(f"update firmware: 控制器升级成功:from {version_previous} to {version_current} :)") + else: + print(f"update firmware: 配置文件升级成功 :)") + + tcp_socket.close() + + +class UpgradeJsonCmd(object): + def __init__(self): + self.__c = None + self.__sock_conn() + + def __sock_conn(self): + # socket connect + print(f"正在连接 {clibs.ip_addr}:{clibs.upgrade_port}...") + try: + self.__c = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.__c.connect((clibs.ip_addr, clibs.upgrade_port)) + self.__c.setblocking(True) + self.__c.settimeout(3) + except Exception as Err: + print(f"upgrade: {Err} | 连接 {clibs.ip_addr}:{clibs.upgrade_port} 失败...") + raise Exception("UpgradePortConnFailed") + + @staticmethod + def __do_package(cmd): + package_size = struct.pack("!I", len(cmd)) + package_type = struct.pack("B", 2) + reserved_byte = struct.pack("B", 0) + return package_size + package_type + reserved_byte + cmd + + def __recv_result(self, cmd): + time.sleep(clibs.INTERVAL) + try: + res = self.__c.recv(10240).decode() + except Exception as err: + print(f"upgrade: 请求命令 {cmd.decode()} 的返回错误 {err}") + raise Exception("ResponseError") + + time.sleep(clibs.INTERVAL) + return res + + def __exec(self, command: dict): + try: + self.__c.recv(10240) + except Exception: + pass + cmd = json.dumps(command, separators=(",", ":")).encode("utf-8") + self.__c.sendall(self.__do_package(cmd)) + res = self.__recv_result(cmd) + print(f"upgrade: 请求命令 {cmd.decode()} 的返回信息:{res}") + + def erase_cfg(self): + # 一键抹除机器人配置(.rc_cfg)、交互数据配置(interactive_data),但保留用户日志 + # 场景:如果xCore版本升级跨度过大,配置文件可能不兼容导致无法启动,可以使用该功能抹除配置,重新生成配置。 + # 机器人参数、零点等会丢失! + self.__exec({"cmd": "erase"}) + + def clear_rubbish(self): + self.__exec({"cmd": "clearRubbish"}) + + def soft_reboot(self): + self.__exec({"cmd": "soft_reboot"}) + + def version_query(self): + self.__exec({"query": "version"}) + + def robot_reboot(self): + self.__exec({"cmd": "reboot"}) + + def reset_passwd(self): + # 不生效,有问题 + self.__exec({"cmd": "passwd"}) + + def backup_origin(self): + # xCore + # .rc_cfg / + # interactive_data / + # module / + # demo_project / + # robot_cfg / + # dev_eni.xml + # ecat_license + # libemllI8254x.so & libemllI8254x_v3.so + # set_network_parameters + self.__exec({"cmd": "backup_origin"}) + + def origin_recovery(self): + self.__exec({"cmd": "recover"}) diff --git a/code/durable/create_plot.py b/code/durable/create_plot.py new file mode 100644 index 0000000..3074b22 --- /dev/null +++ b/code/durable/create_plot.py @@ -0,0 +1,85 @@ +import os.path +import matplotlib.pyplot as plt +import pandas +from matplotlib.widgets import Slider +from PySide6.QtCore import Signal, QThread +from common import clibs + + +# class DrawCurves(QThread): +# output = Signal(str, str) +# +# def __init__(self, /): +# super().__init__() +# +# @staticmethod +# def logger(level, module, content, color="black", error="", flag="both", signal=output): +# clibs.logger(level, module, content, color, flag, signal) +# if level.upper() == "ERROR": +# raise Exception(error) +# +# def initialization(self): +# path, curves = None, None +# try: +# path = clibs.data_dd["path"] +# curves = clibs.data_dd["curves"] +# except Exception: +# clibs.w2t("程序未开始运行,暂无数据可以展示......\n", "red") +# return None, None +# +# for curve in curves: +# if not os.path.exists(f"{path}/{curve}.csv"): +# clibs.w2t(f"{curve}曲线数据暂未生成,稍后再试......\n", "orange") +# return None, None +# +# return path, curves +# +# +# def data_plot(path, curve): +# titles = {"hw_joint_vel_feedback": "各关节最大速度曲线", "device_servo_trq_feedback": "各关节平均有效转矩曲线"} +# ylabels = {"hw_joint_vel_feedback": "速度(rad/s)", "device_servo_trq_feedback": "转矩(Nm)"} +# +# fig, axes = plt.subplots(figsize=(10, 4.5), dpi=100) +# cols = [f"{curve}_{i}" for i in range(6)] +# cols.insert(0, "time") +# df = pandas.read_csv(f"{path}/{curve}.csv") +# plt.plot(df[cols[1]], label="一轴") +# plt.plot(df[cols[2]], label="二轴") +# plt.plot(df[cols[3]], label="三轴") +# plt.plot(df[cols[4]], label="四轴") +# plt.plot(df[cols[5]], label="五轴") +# plt.plot(df[cols[6]], label="六轴") +# axes.set_title(titles[curve]) +# axes.set_ylabel(ylabels[curve]) +# axes.legend(loc="upper right") +# +# slider_position = plt.axes((0.1, 0.01, 0.8, 0.05), facecolor="blue") # (left, bottom, width, height) +# scrollbar = Slider(slider_position, 'Time', 1, int(len(df)), valstep=1) +# +# def update(val): +# pos = scrollbar.val +# axes.set_xlim([pos, pos + 10]) +# fig.canvas.draw_idle() +# +# scrollbar.on_changed(update) +# fig.tight_layout(rect=(0, 0.02, 0.96, 1)) # tuple (left, bottom, right, top) +# +# +# def main(): +# path, curves = initialization() +# if not path or not curves: +# return +# +# for curve in curves: +# data_plot(path, curve) +# +# plt.show() +# +# +# plt.rcParams['font.sans-serif'] = ['SimHei'] +# plt.rcParams['axes.unicode_minus'] = False +# plt.rcParams['figure.dpi'] = 100 +# plt.rcParams['font.size'] = 14 +# plt.rcParams['lines.marker'] = 'o' +# plt.rcParams["figure.autolayout"] = True +# \ No newline at end of file diff --git a/code/durable/factory_test.py b/code/durable/factory_test.py new file mode 100644 index 0000000..ed146f8 --- /dev/null +++ b/code/durable/factory_test.py @@ -0,0 +1,239 @@ +import json +import threading +import time +import pandas +import numpy +import math +import csv +from PySide6.QtCore import Signal, QThread +from common import clibs + + +class DoBrakeTest(QThread): + output = Signal(str, str) + + def __init__(self, dir_path, interval, proc, /): + super().__init__() + self.dir_path = dir_path + self.interval = interval + self.proc = proc + self.idx = 6 + + def logger(self, level, module, content, color="black", error="", flag="both"): + clibs.logger(level, module, content, color, flag, signal=self.output) + if level.upper() == "ERROR": + raise Exception(f"{error} | {content}") + + def initialization(self, data_dirs, data_files): + def check_files(): + if len(curves) == 0: + self.logger("ERROR", "factory-check_files", "未查询到需要记录数据的曲线,至少选择一个!", "red", "CurveNameError") + + if len(data_dirs) != 0 or len(data_files) != 1: + self.logger("ERROR", "factory-check_files", "初始路径下不允许有文件夹,且初始路径下只能存在一个工程文件 —— *.zip,确认后重新运行!", "red", "InitFileError") + + if not data_files[0].endswith(".zip"): + self.logger("ERROR", "factory-check_files", f"{data_files[0]} 不是一个有效的工程文件,需确认!", "red", "ProjectFileError") + + return data_files[0], interval + + def get_configs(): + robot_type, records = None, None + msg_id, state = clibs.c_hr.execution("controller.get_params") + records = clibs.c_hr.get_from_id(msg_id, state) + for record in records: + if "请求发送成功" not in record[0]: + robot_type = eval(record[0])["data"]["robot_type"] + server_file = f"/home/luoshi/bin/controller/robot_cfg/{robot_type}/{robot_type}.cfg" + local_file = self.dir_path + f"/{robot_type}.cfg" + clibs.c_pd.pull_file_from_server(server_file, local_file) + + try: + with open(local_file, mode="r", encoding="utf-8") as f_config: + configs = json.load(f_config) + except Exception as Err: + self.logger("ERROR", "factory-get_configs", f"无法打开 {local_file}
{Err}", "red", "OpenFileError") + + # 最大角速度,额定电流,减速比,额定转速 + version = configs["VERSION"] + m_avs = configs["MOTION"]["JOINT_MAX_SPEED"] + m_rts = configs["MOTOR"]["RATED_TORQUE"] # 电机额定转矩rt for rated torque + m_tcs = [1, 1, 1, 1, 1, 1] # 电机转矩常数,tc for torque constant + m_rcs = [] + for i in range(len(m_tcs)): + m_rcs.append(m_rts[i] / m_tcs[i]) # 电机额定电流,rc for rated current + clibs.insert_logdb("INFO", "do_brake", f"get_configs: 机型文件版本 {robot_type}_{version}") + clibs.insert_logdb("INFO", "do_brake", f"get_configs: 各关节角速度 {m_avs}") + clibs.insert_logdb("INFO", "do_brake", f"get_configs: 各关节额定电流 {m_rcs}") + return m_avs, m_rcs + + prj_file, interval = check_files() + avs, rcs = get_configs() + params = { + "prj_file": prj_file, + "interval": interval, + "avs": avs, + "rcs": rcs, + } + self.logger("INFO", "factory-initialization", "数据目录合规性检查结束,未发现问题......", "green") + return params + + @staticmethod + def change_curve_state(curves, stat): + display_pdo_params = [{"name": name, "channel": chl} for name in curves for chl in range(6)] + clibs.c_hr.execution("diagnosis.open", open=stat, display_open=stat, overrun=True, turn_area=True, delay_motion=False) + clibs.c_hr.execution("diagnosis.set_params", display_pdo_params=display_pdo_params, frequency=50, version="1.4.1") + + def run_rl(self, params, curves): + prj_file, interval = params["prj_file"], params["interval"] + # 1. 关闭诊断曲线,触发软急停,并解除,目的是让可能正在运行着的机器停下来,切手动模式并下电 + self.change_curve_state(curves, False) + clibs.c_md.r_soft_estop(0) + clibs.c_md.r_soft_estop(1) + clibs.c_md.r_clear_alarm() + clibs.c_md.write_act(False) + time.sleep(1) # 让曲线彻底关闭 + + # 2. reload工程后,pp2main,并且自动模式和上电 + prj_name = ".".join(prj_file.split("/")[-1].split(".")[:-1]) + prj_path = f"{prj_name}/_build/{prj_name}.prj" + clibs.c_hr.execution("overview.reload", prj_path=prj_path, tasks=["factory"]) + clibs.c_hr.execution("rl_task.pp_to_main", tasks=["factory"]) + clibs.c_hr.execution("state.switch_auto") + clibs.c_hr.execution("state.switch_motor_on") + + # 3. 开始运行程序 + clibs.c_hr.execution("rl_task.set_run_params", loop_mode=True, override=1.0) + clibs.c_hr.execution("rl_task.run", tasks=["factory"]) + t_start = time.time() + while True: + if clibs.c_md.read_ready_to_go() == 1: + clibs.c_md.write_act(True) + break + else: + if (time.time() - t_start) > 15: + self.logger("ERROR", "factory-run_rl", "15s 内未收到机器人的运行信号,需要确认RL程序编写正确并正常执行...", "red", "ReadySignalTimeoutError") + else: + time.sleep(1) + + # 4. 获取初始数据,周期时间,首次的各轴平均电流值,打开诊断曲线,并执行采集 + time.sleep(10) # 等待 RL 程序中 scenario_time 初始化 + t_start = time.time() + while True: + scenario_time = float(f"{float(clibs.c_md.read_scenario_time()):.2f}") + if scenario_time != 0: + self.logger("INFO", "factory-run_rl", f"耐久工程的周期时间:{scenario_time}s | 单轮次执行时间:{scenario_time+interval}~{scenario_time*2+interval}") + break + else: + time.sleep(1) + if (time.time() - t_start) > 900: + self.logger("ERROR", "factory-run_rl", f"900s 内未收到耐久工程的周期时间,需要确认RL程序和工具通信交互是否正常执行...", "red", "GetScenarioTimeError") + + # 6. 准备数据保存文件 + for curve in curves: + with open(f"{self.dir_path}/{curve}.csv", mode="a+", newline="") as f_csv: + titles = [f"{curve}_{i}" for i in range(6)] + titles.insert(0, "time") + csv_writer = csv.writer(f_csv) + csv_writer.writerow(titles) + + # 7. 开始采集 + count = 0 + while clibs.running: + this_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())) + next_time_1 = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()+scenario_time+interval+1)) + next_time_2 = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()+scenario_time+interval+1+scenario_time)) + self.logger("INFO", "factory-run_rl", f"[{this_time}] 当前次数:{count:09d} | 预计下次数据更新时间:{next_time_1}~{next_time_2}", "#008B8B") + count += 1 + # 固定间隔,更新一次数据,打开曲线,获取周期内电流,关闭曲线 + time.sleep(interval) + while True: + capture_start = clibs.c_md.read_capture_start() + if capture_start == 1: + break + else: + time.sleep(0.1) + + self.change_curve_state(curves, True) + time.sleep(scenario_time) + end_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())) + start_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()-scenario_time)) + self.change_curve_state(curves, False) + # 保留数据并处理输出 + self.gen_results(params, curves, start_time, end_time) + else: + self.change_curve_state(curves, False) + self.logger("INFO", "factory", "后台数据清零完成,现在可以重新运行其他程序。", "green") + + def gen_results(self, params, curves, start_time, end_time): + clibs.cursor.execute(f"select content from logs where time between '{start_time}' and '{end_time}' and content like '%diagnosis.result%' order by id asc") + records = clibs.cursor.fetchall() + self.data_proc(records, params, curves) + + def data_proc(self, records, params, curves): + for curve in curves: + if curve == "device_servo_trq_feedback": + # proc_device_servo_trq_feedback(records, params, w2t) + t = threading.Thread(target=self.proc_device_servo_trq_feedback, args=(records, params)) + t.daemon = True + t.start() + elif curve == "hw_joint_vel_feedback": + # proc_hw_joint_vel_feedback(records, params, w2t) + t = threading.Thread(target=self.proc_hw_joint_vel_feedback, args=(records, params)) + t.daemon = True + t.start() + + def proc_device_servo_trq_feedback(self, records, params): + d_trq, rcs, results = [[], [], [], [], [], []], params["rcs"], [time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))] + for record in records: + data = eval(record[0])["data"] + for item in data: + d_item = reversed(item["value"]) + for axis in range(6): + if item.get("channel", None) == axis and item.get("name", None) == "device_servo_trq_feedback": + d_trq[axis].extend(d_item) + + for axis in range(6): + df = pandas.DataFrame.from_dict({"device_servo_trq_feedback": d_trq[axis]}) + _ = math.sqrt(numpy.square(df[df.columns[0]].values * 1.27 / 1000).sum() / len(df)) + results.append(_) + + path = "/".join(params["prj_file"].split("/")[:-1]) + with open(f"{path}/device_servo_trq_feedback.csv", mode="a+", newline="") as f_csv: + csv_writer = csv.writer(f_csv) + csv_writer.writerow(results) + + def proc_hw_joint_vel_feedback(self, records, params): + d_trq, rcs, results = [[], [], [], [], [], []], params["rcs"], [time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))] + for record in records: + data = eval(record[0])["data"] + for item in data: + d_item = reversed(item["value"]) + for axis in range(6): + if item.get("channel", None) == axis and item.get("name", None) == "hw_joint_vel_feedback": + d_trq[axis].extend(d_item) + + for axis in range(6): + df = pandas.DataFrame.from_dict({"hw_joint_vel_feedback": d_trq[axis]}) + _ = df.max().iloc[0] + results.append(_) + + path = "/".join(params["prj_file"].split("/")[:-1]) + with open(f"{path}/hw_joint_vel_feedback.csv", mode="a+", newline="") as f_csv: + csv_writer = csv.writer(f_csv) + csv_writer.writerow(results) + + def processing(self): + time_start = time.time() + clibs.running[self.idx] = 1 + + data_dirs, data_files = clibs.traversal_files(self.dir_path, self.output) + params = self.initialization(data_dirs, data_files) + prj_file = params["prj_file"] + clibs.c_pd.push_prj_to_server(prj_file) + self.run_rl(params) + + self.logger("INFO", "brake-processing", "-"*60 + "
全部处理完毕
", "purple") + time_total = time.time() - time_start + msg = f"处理时间:{time_total // 3600:02.0f} h {time_total % 3600 // 60:02.0f} m {time_total % 60:02.0f} s" + self.logger("INFO", "brake-processing", msg) \ No newline at end of file diff --git a/code/test.py b/code/test.py new file mode 100644 index 0000000..2265c7f --- /dev/null +++ b/code/test.py @@ -0,0 +1,113 @@ +import time + +# import common.openapi as openapi +# +# hr = openapi.HmiRequest("10.2.21.252", 5050, 6666) +# for _ in range(3): +# hr.execution("controller.heart") +# time.sleep(1) +# +# hr.close() + + + +import pymysql + +conn = pymysql.connect(host='10.2.20.216', user='root', password='Rokae_123457', port=13306, charset='utf8') +cursor = conn.cursor() +cursor.execute("SET autocommit = 1;") +cursor.execute("use fanmingfu;") +# cursor.execute("insert into 20250315153551_log (module, level, content) values (%s, %s, %s)", ("aioaaaaaa", "debug", "testing information")) +# logger("ERROR", "clibs", f"数据文件夹{dir_path}不存在,请确认后重试......\n", signal, "red", "PathNotExistError", idx) + +level = "ERROR" +module = "clibs" +content = "{'data': {'name': 'xCore'}, 'id': 'controller.heart-1742374255.8898985'}" +tb_name = "20250319162718_log" +cursor.execute(f"INSERT INTO {tb_name} (level, module, content) VALUES (%s, %s, %s)", (level, module, content)) + +# conn.commit() +# ============================================ +# def tttt(flag, signal, cursor, **data): +# if flag == "signal": +# print(f"data = {data['signals']}") +# elif flag == "cursor": +# print(f"data = {data['cursors']}") +# elif flag == "both": +# print(f"data = {data}") +# print(f"data = {data['signals']}") +# print(f"data = {data['cursors']}") +# +# +# tttt("both", 1, 1, signals=123, cursors=456) + +# ============================================ + +# import sys +# from time import sleep +# from PySide6.QtCore import * +# from PySide6.QtGui import * +# from PySide6.QtWidgets import * +# +# +# class MyWindow(QMainWindow): +# range_number = Signal(int) +# +# def __init__(self) -> None: +# super().__init__() +# self.setWindowTitle("QThread学习") +# self.resize(800, 600) +# self.setup_ui() +# self.setup_thread() +# +# def setup_ui(self): +# self.mylistwidget = QListWidget(self) +# self.mylistwidget.resize(500, 500) +# self.mylistwidget.move(20, 20) +# +# self.additem_button = QPushButton(self) +# self.additem_button.resize(150, 30) +# self.additem_button.setText("填充QListWidget") +# self.additem_button.move(530, 20) +# +# def setup_thread(self): +# self.thread1 = QThread(self) # 创建一个线程 +# self.range_thread = WorkThread() # 实例化线程类 +# self.range_thread.moveToThread(self.thread1) # 将类移动到线程中运行 +# # 线程数据传回信号,用add_item函数处理 +# self.range_thread.range_requested.connect(self.add_item) +# self.additem_button.clicked.connect(self.start_thread) +# self.range_number.connect(self.range_thread.range_proc) +# # self.additem_button.clicked.connect(self.range_thread.range_proc) # 连接到线程类的函数 +# +# def start_thread(self): +# self.thread1.start() +# range_number = 30 +# self.range_number.emit(range_number) # 发射信号让线程接收需要range多少 +# +# def add_item(self, requested_number): # 线程传回参数 +# text = f"第{requested_number}项————Item" +# item = QListWidgetItem() +# item.setIcon(QPixmap()) +# item.setText(text) +# self.mylistwidget.addItem(item) +# +# +# class WorkThread(QObject): +# range_requested = Signal(int) # 括号里是传出的参数的类型 +# +# def __init__(self): +# super().__init__() +# +# def range_proc(self, number): # number即为从主线程接收的参数 +# print(number) +# for i in range(number): +# self.range_requested.emit(i) # 发射信号 +# sleep(0.5) +# +# +# if __name__ == "__main__": +# app = QApplication(sys.argv) +# window = MyWindow() +# window.show() +# app.exec() \ No newline at end of file diff --git a/code/ui/login_window.py b/code/ui/login_window.py new file mode 100644 index 0000000..df2f7b4 --- /dev/null +++ b/code/ui/login_window.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'login.ui' +## +## Created by: Qt User Interface Compiler version 6.8.2 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QApplication, QHBoxLayout, QLabel, QLineEdit, + QPushButton, QSizePolicy, QVBoxLayout, QWidget) + +class Ui_Form(QWidget): + def setupUi(self, Form): + if not Form.objectName(): + Form.setObjectName(u"Form") + Form.setWindowModality(Qt.WindowModality.WindowModal) + Form.resize(500, 270) + sizePolicy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(Form.sizePolicy().hasHeightForWidth()) + Form.setSizePolicy(sizePolicy) + Form.setMinimumSize(QSize(500, 270)) + Form.setMaximumSize(QSize(500, 270)) + font = QFont() + font.setFamilies([u"Consolas"]) + font.setPointSize(14) + Form.setFont(font) + icon = QIcon() + icon.addFile(u"../assets/media/icon.ico", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + Form.setWindowIcon(icon) + self.widget = QWidget(Form) + self.widget.setObjectName(u"widget") + self.widget.setGeometry(QRect(41, 41, 411, 211)) + self.verticalLayout = QVBoxLayout(self.widget) + self.verticalLayout.setSpacing(2) + self.verticalLayout.setObjectName(u"verticalLayout") + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.horizontalLayout_2 = QHBoxLayout() + self.horizontalLayout_2.setSpacing(2) + self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") + self.label = QLabel(self.widget) + self.label.setObjectName(u"label") + self.label.setFont(font) + + self.horizontalLayout_2.addWidget(self.label) + + self.le_username = QLineEdit(self.widget) + self.le_username.setObjectName(u"le_username") + self.le_username.setFont(font) + + self.horizontalLayout_2.addWidget(self.le_username) + + + self.verticalLayout.addLayout(self.horizontalLayout_2) + + self.horizontalLayout_3 = QHBoxLayout() + self.horizontalLayout_3.setSpacing(2) + self.horizontalLayout_3.setObjectName(u"horizontalLayout_3") + self.label_2 = QLabel(self.widget) + self.label_2.setObjectName(u"label_2") + self.label_2.setFont(font) + + self.horizontalLayout_3.addWidget(self.label_2) + + self.le_password = QLineEdit(self.widget) + self.le_password.setObjectName(u"le_password") + self.le_password.setFont(font) + self.le_password.setEchoMode(QLineEdit.EchoMode.Password) + + self.horizontalLayout_3.addWidget(self.le_password) + + + self.verticalLayout.addLayout(self.horizontalLayout_3) + + self.label_hint = QLabel(self.widget) + self.label_hint.setObjectName(u"label_hint") + sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) + sizePolicy1.setHorizontalStretch(0) + sizePolicy1.setVerticalStretch(0) + sizePolicy1.setHeightForWidth(self.label_hint.sizePolicy().hasHeightForWidth()) + self.label_hint.setSizePolicy(sizePolicy1) + font1 = QFont() + font1.setFamilies([u"Consolas"]) + font1.setPointSize(12) + self.label_hint.setFont(font1) + + self.verticalLayout.addWidget(self.label_hint) + + self.horizontalLayout = QHBoxLayout() + self.horizontalLayout.setSpacing(2) + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.btn_login = QPushButton(self.widget) + self.btn_login.setObjectName(u"btn_login") + self.btn_login.setFont(font) + + self.horizontalLayout.addWidget(self.btn_login) + + self.btn_reset = QPushButton(self.widget) + self.btn_reset.setObjectName(u"btn_reset") + self.btn_reset.setFont(font) + + self.horizontalLayout.addWidget(self.btn_reset) + + + self.verticalLayout.addLayout(self.horizontalLayout) + + + self.retranslateUi(Form) + self.btn_login.clicked.connect(Form.user_login) + self.le_password.returnPressed.connect(Form.user_login) + self.le_username.returnPressed.connect(Form.user_login) + self.btn_reset.clicked.connect(Form.reset_password) + + QMetaObject.connectSlotsByName(Form) + # setupUi + + def retranslateUi(self, Form): + Form.setWindowTitle(QCoreApplication.translate("Form", u"\u767b\u5f55", None)) + self.label.setText(QCoreApplication.translate("Form", u"\u7528\u6237\u540d", None)) + self.label_2.setText(QCoreApplication.translate("Form", u"\u5bc6 \u7801", None)) + self.label_hint.setText("") + self.btn_login.setText(QCoreApplication.translate("Form", u"\u767b\u5f55", None)) + self.btn_reset.setText(QCoreApplication.translate("Form", u"\u91cd\u7f6e", None)) + # retranslateUi + diff --git a/code/ui/main_window.py b/code/ui/main_window.py new file mode 100644 index 0000000..9d6e659 --- /dev/null +++ b/code/ui/main_window.py @@ -0,0 +1,954 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'main.ui' +## +## Created by: Qt User Interface Compiler version 6.8.2 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QApplication, QCheckBox, QComboBox, QFormLayout, + QFrame, QHBoxLayout, QHeaderView, QLabel, + QLineEdit, QMainWindow, QPlainTextEdit, QPushButton, + QScrollArea, QSizePolicy, QSpacerItem, QStackedWidget, + QStatusBar, QTabWidget, QTreeWidget, QTreeWidgetItem, + QVBoxLayout, QWidget) + +class Ui_MainWindow(QMainWindow): + def setupUi(self, MainWindow): + if not MainWindow.objectName(): + MainWindow.setObjectName(u"MainWindow") + MainWindow.setEnabled(True) + MainWindow.resize(1002, 555) + sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(MainWindow.sizePolicy().hasHeightForWidth()) + MainWindow.setSizePolicy(sizePolicy) + MainWindow.setMinimumSize(QSize(1000, 550)) + font = QFont() + font.setFamilies([u"Consolas"]) + font.setPointSize(14) + MainWindow.setFont(font) + icon = QIcon() + icon.addFile(u"../assets/media/icon.ico", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + MainWindow.setWindowIcon(icon) + MainWindow.setStyleSheet(u"background-color: rgb(233, 233, 233);") + MainWindow.setDocumentMode(False) + self.centralwidget = QWidget(MainWindow) + self.centralwidget.setObjectName(u"centralwidget") + self.formLayout = QFormLayout(self.centralwidget) + self.formLayout.setObjectName(u"formLayout") + self.vl_1_left = QVBoxLayout() + self.vl_1_left.setObjectName(u"vl_1_left") + self.label = QLabel(self.centralwidget) + self.label.setObjectName(u"label") + sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) + sizePolicy1.setHorizontalStretch(0) + sizePolicy1.setVerticalStretch(0) + sizePolicy1.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth()) + self.label.setSizePolicy(sizePolicy1) + self.label.setMinimumSize(QSize(200, 100)) + self.label.setMaximumSize(QSize(240, 120)) + font1 = QFont() + font1.setFamilies([u"Segoe Print"]) + font1.setPointSize(24) + font1.setBold(True) + self.label.setFont(font1) + self.label.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.label.setMargin(0) + + self.vl_1_left.addWidget(self.label, 0, Qt.AlignmentFlag.AlignHCenter|Qt.AlignmentFlag.AlignVCenter) + + self.btn_start = QPushButton(self.centralwidget) + self.btn_start.setObjectName(u"btn_start") + sizePolicy1.setHeightForWidth(self.btn_start.sizePolicy().hasHeightForWidth()) + self.btn_start.setSizePolicy(sizePolicy1) + self.btn_start.setMinimumSize(QSize(150, 36)) + self.btn_start.setMaximumSize(QSize(180, 45)) + font2 = QFont() + font2.setFamilies([u"Consolas"]) + font2.setPointSize(14) + font2.setBold(True) + self.btn_start.setFont(font2) + self.btn_start.setFlat(False) + + self.vl_1_left.addWidget(self.btn_start, 0, Qt.AlignmentFlag.AlignHCenter|Qt.AlignmentFlag.AlignVCenter) + + self.btn_stop = QPushButton(self.centralwidget) + self.btn_stop.setObjectName(u"btn_stop") + sizePolicy1.setHeightForWidth(self.btn_stop.sizePolicy().hasHeightForWidth()) + self.btn_stop.setSizePolicy(sizePolicy1) + self.btn_stop.setMinimumSize(QSize(150, 36)) + self.btn_stop.setMaximumSize(QSize(180, 45)) + self.btn_stop.setFont(font2) + self.btn_stop.setFlat(False) + + self.vl_1_left.addWidget(self.btn_stop, 0, Qt.AlignmentFlag.AlignHCenter|Qt.AlignmentFlag.AlignVCenter) + + self.btn_reset = QPushButton(self.centralwidget) + self.btn_reset.setObjectName(u"btn_reset") + sizePolicy1.setHeightForWidth(self.btn_reset.sizePolicy().hasHeightForWidth()) + self.btn_reset.setSizePolicy(sizePolicy1) + self.btn_reset.setMinimumSize(QSize(150, 36)) + self.btn_reset.setMaximumSize(QSize(180, 45)) + self.btn_reset.setFont(font2) + self.btn_reset.setFlat(False) + + self.vl_1_left.addWidget(self.btn_reset, 0, Qt.AlignmentFlag.AlignHCenter|Qt.AlignmentFlag.AlignVCenter) + + self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) + + self.vl_1_left.addItem(self.verticalSpacer) + + self.vl_1_left.setStretch(0, 4) + self.vl_1_left.setStretch(1, 1) + self.vl_1_left.setStretch(2, 1) + self.vl_1_left.setStretch(3, 1) + self.vl_1_left.setStretch(4, 10) + + self.formLayout.setLayout(0, QFormLayout.LabelRole, self.vl_1_left) + + self.vl_1_right = QVBoxLayout() + self.vl_1_right.setObjectName(u"vl_1_right") + self.tw_funcs = QTabWidget(self.centralwidget) + self.tw_funcs.setObjectName(u"tw_funcs") + sizePolicy.setHeightForWidth(self.tw_funcs.sizePolicy().hasHeightForWidth()) + self.tw_funcs.setSizePolicy(sizePolicy) + self.tw_funcs.setMinimumSize(QSize(0, 0)) + font3 = QFont() + font3.setPointSize(14) + font3.setBold(True) + self.tw_funcs.setFont(font3) + self.tw_funcs.setElideMode(Qt.TextElideMode.ElideNone) + self.tw_funcs.setUsesScrollButtons(True) + self.tw_funcs.setDocumentMode(True) + self.tw_funcs.setTabsClosable(False) + self.tw_funcs.setTabBarAutoHide(False) + self.tab_data = QWidget() + self.tab_data.setObjectName(u"tab_data") + self.verticalLayout = QVBoxLayout(self.tab_data) + self.verticalLayout.setObjectName(u"verticalLayout") + self.horizontalLayout = QHBoxLayout() + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.cb_data_func = QComboBox(self.tab_data) + self.cb_data_func.addItem("") + self.cb_data_func.addItem("") + self.cb_data_func.addItem("") + self.cb_data_func.addItem("") + self.cb_data_func.setObjectName(u"cb_data_func") + self.cb_data_func.setMinimumSize(QSize(100, 0)) + font4 = QFont() + font4.setFamilies([u"Consolas"]) + font4.setPointSize(12) + self.cb_data_func.setFont(font4) + + self.horizontalLayout.addWidget(self.cb_data_func) + + self.cb_data_current = QComboBox(self.tab_data) + self.cb_data_current.addItem("") + self.cb_data_current.addItem("") + self.cb_data_current.addItem("") + self.cb_data_current.setObjectName(u"cb_data_current") + self.cb_data_current.setMinimumSize(QSize(100, 0)) + self.cb_data_current.setFont(font4) + + self.horizontalLayout.addWidget(self.cb_data_current) + + self.label_4 = QLabel(self.tab_data) + self.label_4.setObjectName(u"label_4") + self.label_4.setFont(font4) + self.label_4.setAlignment(Qt.AlignmentFlag.AlignRight|Qt.AlignmentFlag.AlignTrailing|Qt.AlignmentFlag.AlignVCenter) + + self.horizontalLayout.addWidget(self.label_4) + + self.le_data_path = QLineEdit(self.tab_data) + self.le_data_path.setObjectName(u"le_data_path") + self.le_data_path.setFont(font4) + self.le_data_path.setAlignment(Qt.AlignmentFlag.AlignLeading|Qt.AlignmentFlag.AlignLeft|Qt.AlignmentFlag.AlignVCenter) + + self.horizontalLayout.addWidget(self.le_data_path) + + self.btn_data_open = QPushButton(self.tab_data) + self.btn_data_open.setObjectName(u"btn_data_open") + self.btn_data_open.setMaximumSize(QSize(30, 16777215)) + self.btn_data_open.setFont(font4) + + self.horizontalLayout.addWidget(self.btn_data_open) + + self.horizontalLayout.setStretch(0, 1) + self.horizontalLayout.setStretch(1, 1) + self.horizontalLayout.setStretch(2, 1) + self.horizontalLayout.setStretch(3, 10) + self.horizontalLayout.setStretch(4, 1) + + self.verticalLayout.addLayout(self.horizontalLayout) + + self.verticalSpacer_2 = QSpacerItem(20, 161, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) + + self.verticalLayout.addItem(self.verticalSpacer_2) + + self.tw_funcs.addTab(self.tab_data, "") + self.tab_unit = QWidget() + self.tab_unit.setObjectName(u"tab_unit") + self.verticalLayout_2 = QVBoxLayout(self.tab_unit) + self.verticalLayout_2.setObjectName(u"verticalLayout_2") + self.hl_2_unit1 = QHBoxLayout() + self.hl_2_unit1.setObjectName(u"hl_2_unit1") + self.cb_unit_func = QComboBox(self.tab_unit) + self.cb_unit_func.addItem("") + self.cb_unit_func.addItem("") + self.cb_unit_func.setObjectName(u"cb_unit_func") + self.cb_unit_func.setMinimumSize(QSize(100, 0)) + self.cb_unit_func.setFont(font4) + + self.hl_2_unit1.addWidget(self.cb_unit_func) + + self.cb_unit_tool = QComboBox(self.tab_unit) + self.cb_unit_tool.addItem("") + self.cb_unit_tool.addItem("") + self.cb_unit_tool.addItem("") + self.cb_unit_tool.addItem("") + self.cb_unit_tool.setObjectName(u"cb_unit_tool") + self.cb_unit_tool.setMinimumSize(QSize(100, 0)) + self.cb_unit_tool.setFont(font4) + + self.hl_2_unit1.addWidget(self.cb_unit_tool) + + self.label_6 = QLabel(self.tab_unit) + self.label_6.setObjectName(u"label_6") + sizePolicy1.setHeightForWidth(self.label_6.sizePolicy().hasHeightForWidth()) + self.label_6.setSizePolicy(sizePolicy1) + self.label_6.setFont(font4) + self.label_6.setAlignment(Qt.AlignmentFlag.AlignRight|Qt.AlignmentFlag.AlignTrailing|Qt.AlignmentFlag.AlignVCenter) + + self.hl_2_unit1.addWidget(self.label_6) + + self.le_unit_path = QLineEdit(self.tab_unit) + self.le_unit_path.setObjectName(u"le_unit_path") + self.le_unit_path.setFont(font4) + + self.hl_2_unit1.addWidget(self.le_unit_path) + + self.btn_unit_open = QPushButton(self.tab_unit) + self.btn_unit_open.setObjectName(u"btn_unit_open") + self.btn_unit_open.setMaximumSize(QSize(30, 16777215)) + self.btn_unit_open.setFont(font4) + + self.hl_2_unit1.addWidget(self.btn_unit_open) + + self.hl_2_unit1.setStretch(0, 1) + self.hl_2_unit1.setStretch(1, 1) + self.hl_2_unit1.setStretch(2, 1) + self.hl_2_unit1.setStretch(3, 10) + self.hl_2_unit1.setStretch(4, 1) + + self.verticalLayout_2.addLayout(self.hl_2_unit1) + + self.verticalSpacer_3 = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) + + self.verticalLayout_2.addItem(self.verticalSpacer_3) + + self.tw_funcs.addTab(self.tab_unit, "") + self.tab_durable = QWidget() + self.tab_durable.setObjectName(u"tab_durable") + self.horizontalLayout_11 = QHBoxLayout(self.tab_durable) + self.horizontalLayout_11.setObjectName(u"horizontalLayout_11") + self.horizontalLayout_10 = QHBoxLayout() + self.horizontalLayout_10.setObjectName(u"horizontalLayout_10") + self.verticalLayout_9 = QVBoxLayout() + self.verticalLayout_9.setObjectName(u"verticalLayout_9") + self.frame = QFrame(self.tab_durable) + self.frame.setObjectName(u"frame") + self.frame.setMinimumSize(QSize(200, 0)) + self.frame.setMaximumSize(QSize(300, 16777215)) + self.frame.setFrameShape(QFrame.Shape.StyledPanel) + self.frame.setFrameShadow(QFrame.Shadow.Raised) + self.verticalLayout_8 = QVBoxLayout(self.frame) + self.verticalLayout_8.setObjectName(u"verticalLayout_8") + self.verticalLayout_7 = QVBoxLayout() + self.verticalLayout_7.setObjectName(u"verticalLayout_7") + self.label_11 = QLabel(self.frame) + self.label_11.setObjectName(u"label_11") + sizePolicy1.setHeightForWidth(self.label_11.sizePolicy().hasHeightForWidth()) + self.label_11.setSizePolicy(sizePolicy1) + self.label_11.setFont(font2) + self.label_11.setAlignment(Qt.AlignmentFlag.AlignLeading|Qt.AlignmentFlag.AlignLeft|Qt.AlignmentFlag.AlignVCenter) + + self.verticalLayout_7.addWidget(self.label_11) + + self.scrollArea = QScrollArea(self.frame) + self.scrollArea.setObjectName(u"scrollArea") + self.scrollArea.setWidgetResizable(True) + self.scrollAreaWidgetContents = QWidget() + self.scrollAreaWidgetContents.setObjectName(u"scrollAreaWidgetContents") + self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 211, 78)) + self.horizontalLayout_9 = QHBoxLayout(self.scrollAreaWidgetContents) + self.horizontalLayout_9.setObjectName(u"horizontalLayout_9") + self.verticalLayout_5 = QVBoxLayout() + self.verticalLayout_5.setObjectName(u"verticalLayout_5") + self.cb_1 = QCheckBox(self.scrollAreaWidgetContents) + self.cb_1.setObjectName(u"cb_1") + self.cb_1.setFont(font4) + + self.verticalLayout_5.addWidget(self.cb_1) + + self.cb_2 = QCheckBox(self.scrollAreaWidgetContents) + self.cb_2.setObjectName(u"cb_2") + self.cb_2.setFont(font4) + + self.verticalLayout_5.addWidget(self.cb_2) + + self.verticalSpacer_5 = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) + + self.verticalLayout_5.addItem(self.verticalSpacer_5) + + + self.horizontalLayout_9.addLayout(self.verticalLayout_5) + + self.scrollArea.setWidget(self.scrollAreaWidgetContents) + + self.verticalLayout_7.addWidget(self.scrollArea) + + + self.verticalLayout_8.addLayout(self.verticalLayout_7) + + + self.verticalLayout_9.addWidget(self.frame) + + + self.horizontalLayout_10.addLayout(self.verticalLayout_9) + + self.verticalLayout_6 = QVBoxLayout() + self.verticalLayout_6.setObjectName(u"verticalLayout_6") + self.horizontalLayout_6 = QHBoxLayout() + self.horizontalLayout_6.setObjectName(u"horizontalLayout_6") + self.label_8 = QLabel(self.tab_durable) + self.label_8.setObjectName(u"label_8") + sizePolicy1.setHeightForWidth(self.label_8.sizePolicy().hasHeightForWidth()) + self.label_8.setSizePolicy(sizePolicy1) + self.label_8.setFont(font4) + self.label_8.setAlignment(Qt.AlignmentFlag.AlignCenter) + + self.horizontalLayout_6.addWidget(self.label_8) + + self.le_durable_path = QLineEdit(self.tab_durable) + self.le_durable_path.setObjectName(u"le_durable_path") + self.le_durable_path.setFont(font4) + + self.horizontalLayout_6.addWidget(self.le_durable_path) + + self.btn_durable_open = QPushButton(self.tab_durable) + self.btn_durable_open.setObjectName(u"btn_durable_open") + self.btn_durable_open.setMaximumSize(QSize(30, 16777215)) + self.btn_durable_open.setFont(font4) + + self.horizontalLayout_6.addWidget(self.btn_durable_open) + + + self.verticalLayout_6.addLayout(self.horizontalLayout_6) + + self.horizontalLayout_7 = QHBoxLayout() + self.horizontalLayout_7.setObjectName(u"horizontalLayout_7") + self.label_9 = QLabel(self.tab_durable) + self.label_9.setObjectName(u"label_9") + sizePolicy1.setHeightForWidth(self.label_9.sizePolicy().hasHeightForWidth()) + self.label_9.setSizePolicy(sizePolicy1) + self.label_9.setFont(font4) + self.label_9.setAlignment(Qt.AlignmentFlag.AlignCenter) + + self.horizontalLayout_7.addWidget(self.label_9) + + self.le_durable_interval = QLineEdit(self.tab_durable) + self.le_durable_interval.setObjectName(u"le_durable_interval") + self.le_durable_interval.setFont(font4) + self.le_durable_interval.setInputMethodHints(Qt.InputMethodHint.ImhNone) + + self.horizontalLayout_7.addWidget(self.le_durable_interval) + + self.label_10 = QLabel(self.tab_durable) + self.label_10.setObjectName(u"label_10") + sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred) + sizePolicy2.setHorizontalStretch(0) + sizePolicy2.setVerticalStretch(0) + sizePolicy2.setHeightForWidth(self.label_10.sizePolicy().hasHeightForWidth()) + self.label_10.setSizePolicy(sizePolicy2) + self.label_10.setMinimumSize(QSize(30, 0)) + + self.horizontalLayout_7.addWidget(self.label_10) + + + self.verticalLayout_6.addLayout(self.horizontalLayout_7) + + self.horizontalLayout_8 = QHBoxLayout() + self.horizontalLayout_8.setObjectName(u"horizontalLayout_8") + self.cb_durable_total = QCheckBox(self.tab_durable) + self.cb_durable_total.setObjectName(u"cb_durable_total") + font5 = QFont() + font5.setFamilies([u"Consolas"]) + font5.setPointSize(12) + font5.setBold(True) + self.cb_durable_total.setFont(font5) + + self.horizontalLayout_8.addWidget(self.cb_durable_total) + + self.btn_draw = QPushButton(self.tab_durable) + self.btn_draw.setObjectName(u"btn_draw") + self.btn_draw.setFont(font5) + + self.horizontalLayout_8.addWidget(self.btn_draw) + + self.label_3 = QLabel(self.tab_durable) + self.label_3.setObjectName(u"label_3") + + self.horizontalLayout_8.addWidget(self.label_3) + + self.horizontalLayout_8.setStretch(0, 2) + self.horizontalLayout_8.setStretch(2, 8) + + self.verticalLayout_6.addLayout(self.horizontalLayout_8) + + self.verticalSpacer_4 = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) + + self.verticalLayout_6.addItem(self.verticalSpacer_4) + + + self.horizontalLayout_10.addLayout(self.verticalLayout_6) + + self.horizontalLayout_10.setStretch(0, 1) + self.horizontalLayout_10.setStretch(1, 2) + + self.horizontalLayout_11.addLayout(self.horizontalLayout_10) + + self.tw_funcs.addTab(self.tab_durable, "") + self.tab_network = QWidget() + self.tab_network.setObjectName(u"tab_network") + self.horizontalLayout_13 = QHBoxLayout(self.tab_network) + self.horizontalLayout_13.setObjectName(u"horizontalLayout_13") + self.horizontalLayout_12 = QHBoxLayout() + self.horizontalLayout_12.setObjectName(u"horizontalLayout_12") + self.sw_network = QStackedWidget(self.tab_network) + self.sw_network.setObjectName(u"sw_network") + self.page = QWidget() + self.page.setObjectName(u"page") + self.horizontalLayout_14 = QHBoxLayout(self.page) + self.horizontalLayout_14.setObjectName(u"horizontalLayout_14") + self.verticalLayout_10 = QVBoxLayout() + self.verticalLayout_10.setObjectName(u"verticalLayout_10") + self.horizontalLayout_3 = QHBoxLayout() + self.horizontalLayout_3.setObjectName(u"horizontalLayout_3") + self.label_2 = QLabel(self.page) + self.label_2.setObjectName(u"label_2") + sizePolicy1.setHeightForWidth(self.label_2.sizePolicy().hasHeightForWidth()) + self.label_2.setSizePolicy(sizePolicy1) + self.label_2.setMinimumSize(QSize(70, 0)) + self.label_2.setFont(font5) + self.label_2.setAlignment(Qt.AlignmentFlag.AlignCenter) + + self.horizontalLayout_3.addWidget(self.label_2) + + self.le_hmi_ip = QLineEdit(self.page) + self.le_hmi_ip.setObjectName(u"le_hmi_ip") + self.le_hmi_ip.setMinimumSize(QSize(150, 0)) + self.le_hmi_ip.setFont(font4) + + self.horizontalLayout_3.addWidget(self.le_hmi_ip) + + self.btn_hmi_conn = QPushButton(self.page) + self.btn_hmi_conn.setObjectName(u"btn_hmi_conn") + self.btn_hmi_conn.setFont(font5) + + self.horizontalLayout_3.addWidget(self.btn_hmi_conn) + + self.label_5 = QLabel(self.page) + self.label_5.setObjectName(u"label_5") + self.label_5.setFont(font4) + + self.horizontalLayout_3.addWidget(self.label_5) + + self.cb_hmi_cmd = QComboBox(self.page) + self.cb_hmi_cmd.addItem("") + self.cb_hmi_cmd.addItem("") + self.cb_hmi_cmd.addItem("") + self.cb_hmi_cmd.setObjectName(u"cb_hmi_cmd") + self.cb_hmi_cmd.setMinimumSize(QSize(240, 0)) + self.cb_hmi_cmd.setFont(font4) + + self.horizontalLayout_3.addWidget(self.cb_hmi_cmd) + + self.btn_hmi_send = QPushButton(self.page) + self.btn_hmi_send.setObjectName(u"btn_hmi_send") + self.btn_hmi_send.setFont(font5) + + self.horizontalLayout_3.addWidget(self.btn_hmi_send) + + self.horizontalLayout_3.setStretch(0, 1) + self.horizontalLayout_3.setStretch(1, 4) + self.horizontalLayout_3.setStretch(2, 1) + self.horizontalLayout_3.setStretch(3, 4) + self.horizontalLayout_3.setStretch(4, 8) + self.horizontalLayout_3.setStretch(5, 1) + + self.verticalLayout_10.addLayout(self.horizontalLayout_3) + + self.horizontalLayout_5 = QHBoxLayout() + self.horizontalLayout_5.setObjectName(u"horizontalLayout_5") + self.pte_hmi_send = QPlainTextEdit(self.page) + self.pte_hmi_send.setObjectName(u"pte_hmi_send") + + self.horizontalLayout_5.addWidget(self.pte_hmi_send) + + self.pte_him_recv = QPlainTextEdit(self.page) + self.pte_him_recv.setObjectName(u"pte_him_recv") + + self.horizontalLayout_5.addWidget(self.pte_him_recv) + + + self.verticalLayout_10.addLayout(self.horizontalLayout_5) + + + self.horizontalLayout_14.addLayout(self.verticalLayout_10) + + self.sw_network.addWidget(self.page) + self.page_2 = QWidget() + self.page_2.setObjectName(u"page_2") + self.horizontalLayout_17 = QHBoxLayout(self.page_2) + self.horizontalLayout_17.setObjectName(u"horizontalLayout_17") + self.verticalLayout_11 = QVBoxLayout() + self.verticalLayout_11.setObjectName(u"verticalLayout_11") + self.horizontalLayout_15 = QHBoxLayout() + self.horizontalLayout_15.setObjectName(u"horizontalLayout_15") + self.label_7 = QLabel(self.page_2) + self.label_7.setObjectName(u"label_7") + sizePolicy1.setHeightForWidth(self.label_7.sizePolicy().hasHeightForWidth()) + self.label_7.setSizePolicy(sizePolicy1) + self.label_7.setMinimumSize(QSize(70, 0)) + self.label_7.setFont(font5) + self.label_7.setAlignment(Qt.AlignmentFlag.AlignCenter) + + self.horizontalLayout_15.addWidget(self.label_7) + + self.le_md_port = QLineEdit(self.page_2) + self.le_md_port.setObjectName(u"le_md_port") + self.le_md_port.setMinimumSize(QSize(150, 0)) + self.le_md_port.setFont(font4) + + self.horizontalLayout_15.addWidget(self.le_md_port) + + self.btn_md_conn = QPushButton(self.page_2) + self.btn_md_conn.setObjectName(u"btn_md_conn") + self.btn_md_conn.setFont(font5) + + self.horizontalLayout_15.addWidget(self.btn_md_conn) + + self.label_12 = QLabel(self.page_2) + self.label_12.setObjectName(u"label_12") + self.label_12.setFont(font4) + + self.horizontalLayout_15.addWidget(self.label_12) + + self.cb_md_cmd = QComboBox(self.page_2) + self.cb_md_cmd.addItem("") + self.cb_md_cmd.addItem("") + self.cb_md_cmd.setObjectName(u"cb_md_cmd") + self.cb_md_cmd.setMinimumSize(QSize(240, 0)) + self.cb_md_cmd.setFont(font4) + + self.horizontalLayout_15.addWidget(self.cb_md_cmd) + + self.btn_md_send = QPushButton(self.page_2) + self.btn_md_send.setObjectName(u"btn_md_send") + self.btn_md_send.setFont(font5) + + self.horizontalLayout_15.addWidget(self.btn_md_send) + + self.horizontalLayout_15.setStretch(0, 1) + self.horizontalLayout_15.setStretch(1, 4) + self.horizontalLayout_15.setStretch(2, 1) + self.horizontalLayout_15.setStretch(3, 4) + self.horizontalLayout_15.setStretch(4, 8) + self.horizontalLayout_15.setStretch(5, 1) + + self.verticalLayout_11.addLayout(self.horizontalLayout_15) + + self.horizontalLayout_16 = QHBoxLayout() + self.horizontalLayout_16.setObjectName(u"horizontalLayout_16") + self.pte_md_send = QPlainTextEdit(self.page_2) + self.pte_md_send.setObjectName(u"pte_md_send") + + self.horizontalLayout_16.addWidget(self.pte_md_send) + + self.pte_md_recv = QPlainTextEdit(self.page_2) + self.pte_md_recv.setObjectName(u"pte_md_recv") + + self.horizontalLayout_16.addWidget(self.pte_md_recv) + + + self.verticalLayout_11.addLayout(self.horizontalLayout_16) + + + self.horizontalLayout_17.addLayout(self.verticalLayout_11) + + self.sw_network.addWidget(self.page_2) + self.page_3 = QWidget() + self.page_3.setObjectName(u"page_3") + self.horizontalLayout_26 = QHBoxLayout(self.page_3) + self.horizontalLayout_26.setObjectName(u"horizontalLayout_26") + self.verticalLayout_14 = QVBoxLayout() + self.verticalLayout_14.setObjectName(u"verticalLayout_14") + self.horizontalLayout_24 = QHBoxLayout() + self.horizontalLayout_24.setObjectName(u"horizontalLayout_24") + self.label_17 = QLabel(self.page_3) + self.label_17.setObjectName(u"label_17") + sizePolicy1.setHeightForWidth(self.label_17.sizePolicy().hasHeightForWidth()) + self.label_17.setSizePolicy(sizePolicy1) + self.label_17.setMinimumSize(QSize(70, 0)) + self.label_17.setFont(font5) + self.label_17.setAlignment(Qt.AlignmentFlag.AlignCenter) + + self.horizontalLayout_24.addWidget(self.label_17) + + self.le_ec_port = QLineEdit(self.page_3) + self.le_ec_port.setObjectName(u"le_ec_port") + self.le_ec_port.setMinimumSize(QSize(150, 0)) + self.le_ec_port.setFont(font4) + + self.horizontalLayout_24.addWidget(self.le_ec_port) + + self.btn_ec_conn = QPushButton(self.page_3) + self.btn_ec_conn.setObjectName(u"btn_ec_conn") + self.btn_ec_conn.setFont(font5) + + self.horizontalLayout_24.addWidget(self.btn_ec_conn) + + self.label_18 = QLabel(self.page_3) + self.label_18.setObjectName(u"label_18") + self.label_18.setFont(font4) + + self.horizontalLayout_24.addWidget(self.label_18) + + self.cb_ec_cmd = QComboBox(self.page_3) + self.cb_ec_cmd.addItem("") + self.cb_ec_cmd.addItem("") + self.cb_ec_cmd.setObjectName(u"cb_ec_cmd") + self.cb_ec_cmd.setMinimumSize(QSize(240, 0)) + self.cb_ec_cmd.setFont(font4) + + self.horizontalLayout_24.addWidget(self.cb_ec_cmd) + + self.btn_ec_send = QPushButton(self.page_3) + self.btn_ec_send.setObjectName(u"btn_ec_send") + self.btn_ec_send.setFont(font5) + + self.horizontalLayout_24.addWidget(self.btn_ec_send) + + self.horizontalLayout_24.setStretch(0, 1) + self.horizontalLayout_24.setStretch(1, 4) + self.horizontalLayout_24.setStretch(2, 1) + self.horizontalLayout_24.setStretch(3, 4) + self.horizontalLayout_24.setStretch(4, 8) + self.horizontalLayout_24.setStretch(5, 1) + + self.verticalLayout_14.addLayout(self.horizontalLayout_24) + + self.horizontalLayout_25 = QHBoxLayout() + self.horizontalLayout_25.setObjectName(u"horizontalLayout_25") + self.pte_ec_send = QPlainTextEdit(self.page_3) + self.pte_ec_send.setObjectName(u"pte_ec_send") + + self.horizontalLayout_25.addWidget(self.pte_ec_send) + + self.pte_ec_recv = QPlainTextEdit(self.page_3) + self.pte_ec_recv.setObjectName(u"pte_ec_recv") + + self.horizontalLayout_25.addWidget(self.pte_ec_recv) + + + self.verticalLayout_14.addLayout(self.horizontalLayout_25) + + + self.horizontalLayout_26.addLayout(self.verticalLayout_14) + + self.sw_network.addWidget(self.page_3) + + self.horizontalLayout_12.addWidget(self.sw_network) + + self.verticalLayout_4 = QVBoxLayout() + self.verticalLayout_4.setObjectName(u"verticalLayout_4") + self.pushButton = QPushButton(self.tab_network) + self.pushButton.setObjectName(u"pushButton") + self.pushButton.setFont(font5) + + self.verticalLayout_4.addWidget(self.pushButton) + + self.pushButton_2 = QPushButton(self.tab_network) + self.pushButton_2.setObjectName(u"pushButton_2") + self.pushButton_2.setFont(font5) + + self.verticalLayout_4.addWidget(self.pushButton_2) + + self.pushButton_3 = QPushButton(self.tab_network) + self.pushButton_3.setObjectName(u"pushButton_3") + self.pushButton_3.setFont(font5) + + self.verticalLayout_4.addWidget(self.pushButton_3) + + + self.horizontalLayout_12.addLayout(self.verticalLayout_4) + + self.horizontalLayout_12.setStretch(0, 11) + self.horizontalLayout_12.setStretch(1, 1) + + self.horizontalLayout_13.addLayout(self.horizontalLayout_12) + + self.tw_funcs.addTab(self.tab_network, "") + + self.vl_1_right.addWidget(self.tw_funcs) + + self.tw_docs = QTabWidget(self.centralwidget) + self.tw_docs.setObjectName(u"tw_docs") + self.tw_docs.setFont(font3) + self.tw_docs.setElideMode(Qt.TextElideMode.ElideNone) + self.tw_docs.setDocumentMode(True) + self.tab_output = QWidget() + self.tab_output.setObjectName(u"tab_output") + self.horizontalLayout_4 = QHBoxLayout(self.tab_output) + self.horizontalLayout_4.setObjectName(u"horizontalLayout_4") + self.pte_output = QPlainTextEdit(self.tab_output) + self.pte_output.setObjectName(u"pte_output") + self.pte_output.setFont(font4) + self.pte_output.setLineWrapMode(QPlainTextEdit.LineWrapMode.WidgetWidth) + self.pte_output.setReadOnly(True) + + self.horizontalLayout_4.addWidget(self.pte_output) + + self.tw_docs.addTab(self.tab_output, "") + self.tab_log = QWidget() + self.tab_log.setObjectName(u"tab_log") + self.verticalLayout_3 = QVBoxLayout(self.tab_log) + self.verticalLayout_3.setObjectName(u"verticalLayout_3") + self.treew_log = QTreeWidget(self.tab_log) + self.treew_log.setObjectName(u"treew_log") + self.treew_log.setFont(font4) + self.treew_log.header().setVisible(True) + + self.verticalLayout_3.addWidget(self.treew_log) + + self.horizontalLayout_2 = QHBoxLayout() + self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") + self.label_page = QLabel(self.tab_log) + self.label_page.setObjectName(u"label_page") + self.label_page.setMinimumSize(QSize(100, 0)) + font6 = QFont() + font6.setFamilies([u"Consolas"]) + font6.setPointSize(10) + font6.setBold(True) + self.label_page.setFont(font6) + self.label_page.setStyleSheet(u"background-color: rgb(222, 222, 222);") + self.label_page.setAlignment(Qt.AlignmentFlag.AlignCenter) + + self.horizontalLayout_2.addWidget(self.label_page) + + self.btn_docs_previous = QPushButton(self.tab_log) + self.btn_docs_previous.setObjectName(u"btn_docs_previous") + self.btn_docs_previous.setFont(font4) + + self.horizontalLayout_2.addWidget(self.btn_docs_previous) + + self.btn_docs_realtime = QPushButton(self.tab_log) + self.btn_docs_realtime.setObjectName(u"btn_docs_realtime") + self.btn_docs_realtime.setFont(font4) + + self.horizontalLayout_2.addWidget(self.btn_docs_realtime) + + self.btn_docs_next = QPushButton(self.tab_log) + self.btn_docs_next.setObjectName(u"btn_docs_next") + self.btn_docs_next.setFont(font4) + + self.horizontalLayout_2.addWidget(self.btn_docs_next) + + self.btn_docs_load = QPushButton(self.tab_log) + self.btn_docs_load.setObjectName(u"btn_docs_load") + self.btn_docs_load.setFont(font4) + + self.horizontalLayout_2.addWidget(self.btn_docs_load) + + self.btn_docs_search = QPushButton(self.tab_log) + self.btn_docs_search.setObjectName(u"btn_docs_search") + self.btn_docs_search.setFont(font4) + + self.horizontalLayout_2.addWidget(self.btn_docs_search) + + self.le_docs_search = QLineEdit(self.tab_log) + self.le_docs_search.setObjectName(u"le_docs_search") + self.le_docs_search.setFont(font4) + + self.horizontalLayout_2.addWidget(self.le_docs_search) + + self.horizontalLayout_2.setStretch(0, 1) + self.horizontalLayout_2.setStretch(1, 1) + self.horizontalLayout_2.setStretch(2, 1) + self.horizontalLayout_2.setStretch(3, 1) + self.horizontalLayout_2.setStretch(4, 1) + self.horizontalLayout_2.setStretch(5, 1) + self.horizontalLayout_2.setStretch(6, 10) + + self.verticalLayout_3.addLayout(self.horizontalLayout_2) + + self.tw_docs.addTab(self.tab_log, "") + + self.vl_1_right.addWidget(self.tw_docs) + + self.vl_1_right.setStretch(0, 1) + self.vl_1_right.setStretch(1, 3) + + self.formLayout.setLayout(0, QFormLayout.FieldRole, self.vl_1_right) + + MainWindow.setCentralWidget(self.centralwidget) + self.statusbar = QStatusBar(MainWindow) + self.statusbar.setObjectName(u"statusbar") + sizePolicy3 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + sizePolicy3.setHorizontalStretch(0) + sizePolicy3.setVerticalStretch(0) + sizePolicy3.setHeightForWidth(self.statusbar.sizePolicy().hasHeightForWidth()) + self.statusbar.setSizePolicy(sizePolicy3) + self.statusbar.setMinimumSize(QSize(0, 27)) + self.statusbar.setStyleSheet(u"background-color: rgb(200, 200, 200);") + MainWindow.setStatusBar(self.statusbar) + + self.retranslateUi(MainWindow) + self.btn_start.clicked.connect(MainWindow.prog_start) + self.btn_stop.clicked.connect(MainWindow.prog_stop) + self.btn_reset.clicked.connect(MainWindow.prog_reset) + self.btn_durable_open.clicked.connect(MainWindow.file_browser) + self.btn_draw.clicked.connect(MainWindow.curve_draw) + self.cb_durable_total.checkStateChanged.connect(MainWindow.durable_cb_change) + self.btn_unit_open.clicked.connect(MainWindow.file_browser) + self.btn_data_open.clicked.connect(MainWindow.file_browser) + self.btn_docs_previous.clicked.connect(MainWindow.pre_page) + self.btn_docs_realtime.clicked.connect(MainWindow.realtime_page) + self.btn_docs_next.clicked.connect(MainWindow.next_page) + self.btn_docs_load.clicked.connect(MainWindow.load_sql) + self.btn_docs_search.clicked.connect(MainWindow.search_keyword) + self.le_docs_search.returnPressed.connect(MainWindow.search_keyword) + self.cb_hmi_cmd.currentTextChanged.connect(MainWindow.hmi_cb_change) + self.btn_hmi_send.clicked.connect(MainWindow.hmi_send) + self.pushButton.clicked.connect(MainWindow.hmi_page) + self.pushButton_2.clicked.connect(MainWindow.md_page) + self.pushButton_3.clicked.connect(MainWindow.ec_page) + self.cb_md_cmd.currentTextChanged.connect(MainWindow.md_cb_change) + self.btn_md_send.clicked.connect(MainWindow.md_send) + self.btn_ec_send.clicked.connect(MainWindow.ec_send) + self.btn_hmi_conn.clicked.connect(MainWindow.hmi_conn) + self.btn_md_conn.clicked.connect(MainWindow.md_conn) + self.btn_ec_conn.clicked.connect(MainWindow.ec_conn) + self.le_durable_interval.editingFinished.connect(MainWindow.check_interval) + self.cb_ec_cmd.currentTextChanged.connect(MainWindow.ec_cb_change) + self.le_hmi_ip.returnPressed.connect(MainWindow.hmi_conn) + + self.tw_funcs.setCurrentIndex(0) + self.sw_network.setCurrentIndex(0) + self.tw_docs.setCurrentIndex(1) + + + QMetaObject.connectSlotsByName(MainWindow) + # setupUi + + def retranslateUi(self, MainWindow): + MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"Rokae AIO", None)) + self.label.setText(QCoreApplication.translate("MainWindow", u"Rokae AIO", None)) + self.btn_start.setText(QCoreApplication.translate("MainWindow", u"\u5f00\u59cb\u6267\u884c", None)) + self.btn_stop.setText(QCoreApplication.translate("MainWindow", u"\u505c\u6b62\u6267\u884c", None)) + self.btn_reset.setText(QCoreApplication.translate("MainWindow", u"\u72b6\u6001\u91cd\u7f6e", None)) + self.cb_data_func.setItemText(0, QCoreApplication.translate("MainWindow", u"\u5236\u52a8", None)) + self.cb_data_func.setItemText(1, QCoreApplication.translate("MainWindow", u"\u8f6c\u77e9", None)) + self.cb_data_func.setItemText(2, QCoreApplication.translate("MainWindow", u"\u6fc0\u5149", None)) + self.cb_data_func.setItemText(3, QCoreApplication.translate("MainWindow", u"\u7cbe\u5ea6", None)) + + self.cb_data_current.setItemText(0, QCoreApplication.translate("MainWindow", u"\u5468\u671f", None)) + self.cb_data_current.setItemText(1, QCoreApplication.translate("MainWindow", u"\u6700\u5927\u503c", None)) + self.cb_data_current.setItemText(2, QCoreApplication.translate("MainWindow", u"\u5e73\u5747\u503c", None)) + + self.label_4.setText(QCoreApplication.translate("MainWindow", u"\u8def\u5f84", None)) + self.btn_data_open.setText(QCoreApplication.translate("MainWindow", u"...", None)) + self.tw_funcs.setTabText(self.tw_funcs.indexOf(self.tab_data), QCoreApplication.translate("MainWindow", u"\u6570\u636e\u5904\u7406", None)) + self.cb_unit_func.setItemText(0, QCoreApplication.translate("MainWindow", u"\u5236\u52a8", None)) + self.cb_unit_func.setItemText(1, QCoreApplication.translate("MainWindow", u"\u8f6c\u77e9", None)) + + self.cb_unit_tool.setItemText(0, QCoreApplication.translate("MainWindow", u"tool33", None)) + self.cb_unit_tool.setItemText(1, QCoreApplication.translate("MainWindow", u"tool66", None)) + self.cb_unit_tool.setItemText(2, QCoreApplication.translate("MainWindow", u"tool100", None)) + self.cb_unit_tool.setItemText(3, QCoreApplication.translate("MainWindow", u"inertia", None)) + + self.label_6.setText(QCoreApplication.translate("MainWindow", u"\u8def\u5f84", None)) + self.btn_unit_open.setText(QCoreApplication.translate("MainWindow", u"...", None)) + self.tw_funcs.setTabText(self.tw_funcs.indexOf(self.tab_unit), QCoreApplication.translate("MainWindow", u"\u6574\u673a\u6d4b\u8bd5", None)) + self.label_11.setText(QCoreApplication.translate("MainWindow", u"\u9009\u62e9\u6307\u6807", None)) + self.cb_1.setText(QCoreApplication.translate("MainWindow", u"\u5468\u671f\u5185\u5e73\u5747\u8f6c\u77e9", None)) + self.cb_2.setText(QCoreApplication.translate("MainWindow", u"\u5468\u671f\u5185\u5e73\u5747\u8f6c\u77e9", None)) + self.label_8.setText(QCoreApplication.translate("MainWindow", u"\u8def\u5f84", None)) + self.btn_durable_open.setText(QCoreApplication.translate("MainWindow", u"...", None)) + self.label_9.setText(QCoreApplication.translate("MainWindow", u"\u95f4\u9694", None)) +#if QT_CONFIG(whatsthis) + self.le_durable_interval.setWhatsThis("") +#endif // QT_CONFIG(whatsthis) + self.le_durable_interval.setPlaceholderText(QCoreApplication.translate("MainWindow", u"\u6bcf\u6b21\u6570\u636e\u91c7\u96c6\u7684\u65f6\u95f4\u95f4\u9694\uff0c\u9ed8\u8ba4(\u6700\u5c0f)300s", None)) + self.label_10.setText("") + self.cb_durable_total.setText(QCoreApplication.translate("MainWindow", u"\u5168\u90e8\u6253\u5f00/\u5173\u95ed", None)) + self.btn_draw.setText(QCoreApplication.translate("MainWindow", u"\u7ed8\u56fe", None)) + self.label_3.setText("") + self.tw_funcs.setTabText(self.tw_funcs.indexOf(self.tab_durable), QCoreApplication.translate("MainWindow", u"\u8010\u4e45\u91c7\u96c6", None)) + self.label_2.setText(QCoreApplication.translate("MainWindow", u"HMI IP", None)) + self.le_hmi_ip.setText(QCoreApplication.translate("MainWindow", u"192.168.0.160", None)) + self.btn_hmi_conn.setText(QCoreApplication.translate("MainWindow", u"\u8fde\u63a5", None)) + self.label_5.setText("") + self.cb_hmi_cmd.setItemText(0, QCoreApplication.translate("MainWindow", u"controller.heart", None)) + self.cb_hmi_cmd.setItemText(1, QCoreApplication.translate("MainWindow", u"device.get_params", None)) + self.cb_hmi_cmd.setItemText(2, QCoreApplication.translate("MainWindow", u"safety_area_data", None)) + + self.btn_hmi_send.setText(QCoreApplication.translate("MainWindow", u"\u53d1\u9001", None)) + self.label_7.setText(QCoreApplication.translate("MainWindow", u"MD Port", None)) + self.le_md_port.setText(QCoreApplication.translate("MainWindow", u"502", None)) + self.btn_md_conn.setText(QCoreApplication.translate("MainWindow", u"\u8fde\u63a5", None)) + self.label_12.setText("") + self.cb_md_cmd.setItemText(0, QCoreApplication.translate("MainWindow", u"ctrl_motor_on", None)) + self.cb_md_cmd.setItemText(1, QCoreApplication.translate("MainWindow", u"ctrl_motor_off", None)) + + self.btn_md_send.setText(QCoreApplication.translate("MainWindow", u"\u53d1\u9001", None)) + self.label_17.setText(QCoreApplication.translate("MainWindow", u"EC Port", None)) + self.le_ec_port.setText(QCoreApplication.translate("MainWindow", u"8080", None)) + self.btn_ec_conn.setText(QCoreApplication.translate("MainWindow", u"\u8fde\u63a5", None)) + self.label_18.setText("") + self.cb_ec_cmd.setItemText(0, QCoreApplication.translate("MainWindow", u"motor_on_state", None)) + self.cb_ec_cmd.setItemText(1, QCoreApplication.translate("MainWindow", u"robot_running_state", None)) + + self.btn_ec_send.setText(QCoreApplication.translate("MainWindow", u"\u53d1\u9001", None)) + self.pushButton.setText(QCoreApplication.translate("MainWindow", u"HMI", None)) + self.pushButton_2.setText(QCoreApplication.translate("MainWindow", u"Modbus", None)) + self.pushButton_3.setText(QCoreApplication.translate("MainWindow", u"EC", None)) + self.tw_funcs.setTabText(self.tw_funcs.indexOf(self.tab_network), QCoreApplication.translate("MainWindow", u"\u7f51\u7edc\u8bbe\u7f6e", None)) + self.tw_docs.setTabText(self.tw_docs.indexOf(self.tab_output), QCoreApplication.translate("MainWindow", u"\u8f93\u51fa", None)) + ___qtreewidgetitem = self.treew_log.headerItem() + ___qtreewidgetitem.setText(4, QCoreApplication.translate("MainWindow", u"Content", None)); + ___qtreewidgetitem.setText(3, QCoreApplication.translate("MainWindow", u"Module", None)); + ___qtreewidgetitem.setText(2, QCoreApplication.translate("MainWindow", u"Level", None)); + ___qtreewidgetitem.setText(1, QCoreApplication.translate("MainWindow", u"Timestamp", None)); + ___qtreewidgetitem.setText(0, QCoreApplication.translate("MainWindow", u"ID", None)); + self.label_page.setText(QCoreApplication.translate("MainWindow", u"0/0", None)) + self.btn_docs_previous.setText(QCoreApplication.translate("MainWindow", u"\u4e0a\u4e00\u9875", None)) + self.btn_docs_realtime.setText(QCoreApplication.translate("MainWindow", u"\u5b9e\u65f6", None)) + self.btn_docs_next.setText(QCoreApplication.translate("MainWindow", u"\u4e0b\u4e00\u9875", None)) + self.btn_docs_load.setText(QCoreApplication.translate("MainWindow", u"\u52a0\u8f7d", None)) + self.btn_docs_search.setText(QCoreApplication.translate("MainWindow", u"\u67e5\u627e", None)) + self.le_docs_search.setPlaceholderText(QCoreApplication.translate("MainWindow", u"[id/level/module] \u67e5\u627e\u5185\u5bb9", None)) + self.tw_docs.setTabText(self.tw_docs.indexOf(self.tab_log), QCoreApplication.translate("MainWindow", u"\u65e5\u5fd7", None)) + # retranslateUi + diff --git a/code/ui/reset_window.py b/code/ui/reset_window.py new file mode 100644 index 0000000..ec6c7d6 --- /dev/null +++ b/code/ui/reset_window.py @@ -0,0 +1,170 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'reset.ui' +## +## Created by: Qt User Interface Compiler version 6.8.2 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QApplication, QHBoxLayout, QLabel, QLineEdit, + QPushButton, QSizePolicy, QVBoxLayout, QWidget) + +class Ui_Form(QWidget): + def setupUi(self, Form): + if not Form.objectName(): + Form.setObjectName(u"Form") + Form.setWindowModality(Qt.WindowModality.WindowModal) + Form.resize(500, 270) + sizePolicy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(Form.sizePolicy().hasHeightForWidth()) + Form.setSizePolicy(sizePolicy) + Form.setMinimumSize(QSize(500, 270)) + Form.setMaximumSize(QSize(500, 270)) + font = QFont() + font.setFamilies([u"Consolas"]) + font.setPointSize(14) + Form.setFont(font) + icon = QIcon() + icon.addFile(u"../assets/media/icon.ico", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + Form.setWindowIcon(icon) + self.widget = QWidget(Form) + self.widget.setObjectName(u"widget") + self.widget.setGeometry(QRect(40, 27, 411, 211)) + self.verticalLayout = QVBoxLayout(self.widget) + self.verticalLayout.setObjectName(u"verticalLayout") + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.horizontalLayout_5 = QHBoxLayout() + self.horizontalLayout_5.setObjectName(u"horizontalLayout_5") + self.label = QLabel(self.widget) + self.label.setObjectName(u"label") + self.label.setFont(font) + + self.horizontalLayout_5.addWidget(self.label) + + self.le_username = QLineEdit(self.widget) + self.le_username.setObjectName(u"le_username") + self.le_username.setFont(font) + + self.horizontalLayout_5.addWidget(self.le_username) + + + self.verticalLayout.addLayout(self.horizontalLayout_5) + + self.horizontalLayout_4 = QHBoxLayout() + self.horizontalLayout_4.setObjectName(u"horizontalLayout_4") + self.label_2 = QLabel(self.widget) + self.label_2.setObjectName(u"label_2") + self.label_2.setFont(font) + + self.horizontalLayout_4.addWidget(self.label_2) + + self.le_old_password = QLineEdit(self.widget) + self.le_old_password.setObjectName(u"le_old_password") + self.le_old_password.setFont(font) + self.le_old_password.setEchoMode(QLineEdit.EchoMode.Password) + + self.horizontalLayout_4.addWidget(self.le_old_password) + + + self.verticalLayout.addLayout(self.horizontalLayout_4) + + self.horizontalLayout_3 = QHBoxLayout() + self.horizontalLayout_3.setObjectName(u"horizontalLayout_3") + self.label_3 = QLabel(self.widget) + self.label_3.setObjectName(u"label_3") + self.label_3.setFont(font) + + self.horizontalLayout_3.addWidget(self.label_3) + + self.le_new_password_1 = QLineEdit(self.widget) + self.le_new_password_1.setObjectName(u"le_new_password_1") + self.le_new_password_1.setFont(font) + self.le_new_password_1.setEchoMode(QLineEdit.EchoMode.Password) + + self.horizontalLayout_3.addWidget(self.le_new_password_1) + + + self.verticalLayout.addLayout(self.horizontalLayout_3) + + self.horizontalLayout_2 = QHBoxLayout() + self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") + self.label_4 = QLabel(self.widget) + self.label_4.setObjectName(u"label_4") + self.label_4.setFont(font) + + self.horizontalLayout_2.addWidget(self.label_4) + + self.le_new_password_2 = QLineEdit(self.widget) + self.le_new_password_2.setObjectName(u"le_new_password_2") + self.le_new_password_2.setFont(font) + self.le_new_password_2.setEchoMode(QLineEdit.EchoMode.Password) + + self.horizontalLayout_2.addWidget(self.le_new_password_2) + + + self.verticalLayout.addLayout(self.horizontalLayout_2) + + self.label_hint = QLabel(self.widget) + self.label_hint.setObjectName(u"label_hint") + sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) + sizePolicy1.setHorizontalStretch(0) + sizePolicy1.setVerticalStretch(0) + sizePolicy1.setHeightForWidth(self.label_hint.sizePolicy().hasHeightForWidth()) + self.label_hint.setSizePolicy(sizePolicy1) + font1 = QFont() + font1.setFamilies([u"Consolas"]) + font1.setPointSize(12) + self.label_hint.setFont(font1) + + self.verticalLayout.addWidget(self.label_hint) + + self.horizontalLayout = QHBoxLayout() + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.btn_reset = QPushButton(self.widget) + self.btn_reset.setObjectName(u"btn_reset") + self.btn_reset.setFont(font) + + self.horizontalLayout.addWidget(self.btn_reset) + + self.btn_cancel = QPushButton(self.widget) + self.btn_cancel.setObjectName(u"btn_cancel") + self.btn_cancel.setFont(font) + + self.horizontalLayout.addWidget(self.btn_cancel) + + + self.verticalLayout.addLayout(self.horizontalLayout) + + QWidget.setTabOrder(self.le_username, self.le_old_password) + QWidget.setTabOrder(self.le_old_password, self.le_new_password_1) + QWidget.setTabOrder(self.le_new_password_1, self.le_new_password_2) + + self.retranslateUi(Form) + self.btn_reset.clicked.connect(Form.reset_password) + self.btn_cancel.clicked.connect(Form.reset_cancel) + + QMetaObject.connectSlotsByName(Form) + # setupUi + + def retranslateUi(self, Form): + Form.setWindowTitle(QCoreApplication.translate("Form", u"\u91cd\u7f6e\u5bc6\u7801", None)) + self.label.setText(QCoreApplication.translate("Form", u"\u7528\u6237\u540d", None)) + self.label_2.setText(QCoreApplication.translate("Form", u"\u65e7\u5bc6\u7801", None)) + self.label_3.setText(QCoreApplication.translate("Form", u"\u65b0\u5bc6\u7801", None)) + self.label_4.setText(QCoreApplication.translate("Form", u"\u786e \u8ba4", None)) + self.label_hint.setText("") + self.btn_reset.setText(QCoreApplication.translate("Form", u"\u786e\u5b9a", None)) + self.btn_cancel.setText(QCoreApplication.translate("Form", u"\u53d6\u6d88", None)) + # retranslateUi + diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..689079b --- /dev/null +++ b/readme.md @@ -0,0 +1,129 @@ +# 珞石测试部自动化工具 + +针对珞石工业协作六轴机器人的整机测试,该工具可以执行如下自动化操作,以减少人工处理时长,提高测试数据处理的效率和准确度: + +1. 制动数据,单轴数据处理 5min 以内 +2. 电机电流数据,全部轴数据处理 3min 以内 +3. ISO 激光数据整理,1min 以内 +4. wavelogger 波形处理,几乎不花费时间 +5. 制动自动化测试,40min 左右 +6. 电机电流自动化测试,15min 左右 +7. 耐久工程相关指标采集记录,可定制曲线并绘图 + +## 一、处理逻辑以及原理 + +该部分简要介绍了工具实现的核心逻辑原理,以及执行的必要条件。 + +### 1. 数据处理 + +#### A. 制动数据 + +- 原理:找到 `device_safety_estop` 曲线值由 1 -> 0 的位置,并预留 20 个数据余量截取 1000 个点 +- 必要条件: + - 根目录命名为 j1/2/3 + - 数据文件夹命名参照执行制动数据采集 + - 需要有三个结果文件,命名为 reach33/66/100_xxxxxxxx.xlsx + - 需要有一个机型文件,比如 NB4h-R580-3G.cfg,此文件会在执行制动测试的时候自动生成,注意保存 + +#### B. 电机电流 + +- 单轴原理:找到一个周期的起点和终点 +- 堵转原理:求平均均值 +- 场景原理:获取场景的执行周期时间,并做数据截取 +- 必要条件: + - 数据文件命名参照电机电流测试生成的格式 + - 需要有一个结果文件,命名为 T_电机电流.xlsx + - 需要有一个机型文件,比如 NB4h-R580-3G.cfg,此文件会在执行制动测试的时候自动生成,注意保存 + +#### C. 激光 + +- 原理:根据要获取的数据所在位置的特征,过滤提取 +- 必要条件: + - 数据文件命名必须参照如下: + - ISO-.pdf + - ISO-V100.pdf + - ISO-V1000.pdf + - 需要有一个结果文件,iso-results.xlsx + +#### D. 基恩士数据 + +- 原理:根据数据大小以及出现的周期规律,进行相应的处理,处理完成之后结果文件会自动生成 +- 必要条件:需要提前将 `.xdt` 波形数据转换成 `.csv` 文件,数据文件命名必须以 `.csv` 结尾 + +## 二、自动测试 + +### 1. 协议封包解包 + +详见 `assets/files/protocols/` + +### 2. 制动测试 + +- 原理:使用 xCore socket 协议获取诊断数据,包括速度,力矩等信息,结合 modbus 和外部通信执行急停等动作 +- 必要条件: + - RL 工程(brake任务),参考 `assets/files/projects/`,需要详细阅读对应的注释 + - 需要有一个配置文件,命名为 configs.xlsx + - 需要有三个结果文件,命名为 reach33/66/100_xxxxxxxx.xlsx + +### 3. 电机电流测试 + +- 原理:使用 xCore socket 协议获取诊断数据,包括速度,力矩等信息,结合 modbus 和外部通信执行急停等动作 +- 必要条件: + - RL 工程(current任务),参考 `assets/files/projects/`,需要详细阅读对应的注释 + - 需要有一个结果文件,命名为 T_电机电流.xlsx + +## 三、耐久测试数据采集 + +- 原理:根据设置的时间间隔,采集相应的场景周期指标数据 +- 必要条件: + - RL 工程(factory任务),参考 `assets/files/projects/`,需要详细阅读对应的注释 + +## 三、注意事项 + +> **!!仅内网使用!!** + +1. 仅适用于 xCore 2.3.0.7 及以上的版本 +2. 仅适配了六轴工业/协作机型,其他机型可能会存在使用上的问题,具体可以找fanmingfu@rokae.com确认 +3. 单轴电机电流数据处理,至少需要三个完整周期,使用本工具采集一般不会有问题,主要是针对手动采集命名的数据 +4. 执行制动测试/电机电流采集/耐久测试的时候,执行过程中停止,需要重新关闭软件再次打开,才能正常使用 +5. RL工程/寄存器文件/configs.xlsx文件已更新,需要使用新版,其他使用方法和之前工具一致 +6. 基恩士采集数据时,不同轮次(每5次测试)数据时间间隔最好大于 2 倍的周期时间,否则会出现采集的轮数不正确的情况,但数据是完整的 +7. 激光数据处理只支持英文版本的数据文件 +8. 耐久(场景)指标数据采集的工程运行周期需要小于 300s,否则会出现异常 + +> **需要使用 assets/files/projects/ 下的工程,寄存器文件以及配置文件等!!!** + +## 四、发版记录 + +详见 `assets/files/version/release_change.txt` 文件 + +## 五、其他 + +### 1. 打包命令 + +打包时,只需要修改 clibs.py 中的 PREFIX 即可,调试时再修改回来,第三方库依赖详见 `assets/files/version/requirements.txt` 文件 + +``` +pyinstaller --noconfirm --onedir --windowed --optimize 2 --contents-directory . --upx-dir "D:/Syncthing/common/A_Program/upx-4.2.4-win64/" --add-data "../.venv/Lib/site-packages/customtkinter;customtkinter/" --add-data "../assets:assets" --version-file ../assets/files/version/file_version_info.txt -i ../assets/media/icon.ico ../code/aio.py -p ../code/common/clibs.py -p ../code/commom/openapi.py -p ../code/data_process/brake.py -p ../code/data_process/iso.py -p ../code/data_process/current.py -p ../code/data_process/wavelogger.py -p ../code/automatic_test/do_current.py -p ../code/automatic_test/do_brake.py -p ../code/durable_docs/factory_test.py -p ../code/durable_docs/create_plot.py --exclude-module=scipy +``` + +### 2. tabview 组件字体修改 + +customtkinter的tabview组件不支持修改字体大小,解决方法可参考如下: + +- Method 1:可以参考 [Changing Font of a Tabview](https://github.com/TomSchimansky/CustomTkinter/issues/2296) 进行手动修改源码实现: + + + 运行 `pip show customtkinter`,获取到库的路径 + + 修改.../windows/widgets/ctk_tabview.py + + 增加 from .font.ctk_font import CTkFont + + 在大概 78 行的位置,增加 font=CTkFont(family="Consolas", size=18, weight='bold') + +- Method 2: + + 直接在源码中修改:`self.tabview_bottom._segmented_button.configure(font=ctk.CTkFont(family="Consolas", size=18, weight="bold"))` + +### 3. scroll frame 不支持修改高度和宽度 + +https://github.com/TomSchimansky/CustomTkinter/pull/1765/files + + +--- diff --git a/ui/login.ui b/ui/login.ui new file mode 100644 index 0000000..2f976bc --- /dev/null +++ b/ui/login.ui @@ -0,0 +1,249 @@ + + + Form + + + Qt::WindowModality::WindowModal + + + + 0 + 0 + 500 + 270 + + + + + 0 + 0 + + + + + 500 + 270 + + + + + 500 + 270 + + + + + Consolas + 14 + + + + 登录 + + + + ../assets/media/icon.ico../assets/media/icon.ico + + + + + 41 + 41 + 411 + 211 + + + + + 2 + + + + + 2 + + + + + + Consolas + 14 + + + + 用户名 + + + + + + + + Consolas + 14 + + + + + + + + + + 2 + + + + + + Consolas + 14 + + + + 密 码 + + + + + + + + Consolas + 14 + + + + QLineEdit::EchoMode::Password + + + + + + + + + + 0 + 0 + + + + + Consolas + 12 + + + + + + + + + + + 2 + + + + + + Consolas + 14 + + + + 登录 + + + + + + + + Consolas + 14 + + + + 重置 + + + + + + + + + + + + btn_login + clicked() + Form + user_login() + + + 85 + 130 + + + 34 + 112 + + + + + le_password + returnPressed() + Form + user_login() + + + 178 + 82 + + + 11 + 70 + + + + + le_username + returnPressed() + Form + user_login() + + + 169 + 42 + + + 10 + 33 + + + + + btn_reset + clicked() + Form + reset_password() + + + 311 + 138 + + + 367 + 113 + + + + + + user_login() + reset_password() + + diff --git a/ui/main.ui b/ui/main.ui new file mode 100644 index 0000000..53452a8 --- /dev/null +++ b/ui/main.ui @@ -0,0 +1,2004 @@ + + + MainWindow + + + true + + + + 0 + 0 + 1002 + 555 + + + + + 0 + 0 + + + + + 1000 + 550 + + + + + Consolas + 14 + + + + Rokae AIO + + + + ../assets/media/icon.ico../assets/media/icon.ico + + + background-color: rgb(233, 233, 233); + + + false + + + + + + + + + + 0 + 0 + + + + + 200 + 100 + + + + + 240 + 120 + + + + + Segoe Print + 24 + true + + + + Rokae AIO + + + Qt::AlignmentFlag::AlignCenter + + + 0 + + + + + + + + 0 + 0 + + + + + 150 + 36 + + + + + 180 + 45 + + + + + Consolas + 14 + true + + + + 开始执行 + + + false + + + + + + + + 0 + 0 + + + + + 150 + 36 + + + + + 180 + 45 + + + + + Consolas + 14 + true + + + + 停止执行 + + + false + + + + + + + + 0 + 0 + + + + + 150 + 36 + + + + + 180 + 45 + + + + + Consolas + 14 + true + + + + 状态重置 + + + false + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 14 + true + + + + 0 + + + Qt::TextElideMode::ElideNone + + + true + + + true + + + false + + + false + + + + 数据处理 + + + + + + + + + 100 + 0 + + + + + Consolas + 12 + + + + + 制动 + + + + + 转矩 + + + + + 激光 + + + + + 精度 + + + + + + + + + 100 + 0 + + + + + Consolas + 12 + + + + + 周期 + + + + + 最大值 + + + + + 平均值 + + + + + + + + + Consolas + 12 + + + + 路径 + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + + + + + + Consolas + 12 + + + + Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter + + + + + + + + 30 + 16777215 + + + + + Consolas + 12 + + + + ... + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 161 + + + + + + + + + 整机测试 + + + + + + + + + 100 + 0 + + + + + Consolas + 12 + + + + + 制动 + + + + + 转矩 + + + + + + + + + 100 + 0 + + + + + Consolas + 12 + + + + + tool33 + + + + + tool66 + + + + + tool100 + + + + + inertia + + + + + + + + + 0 + 0 + + + + + Consolas + 12 + + + + 路径 + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + + + + + + Consolas + 12 + + + + + + + + + 30 + 16777215 + + + + + Consolas + 12 + + + + ... + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + 耐久采集 + + + + + + + + + + + 200 + 0 + + + + + 300 + 16777215 + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + + + + + + 0 + 0 + + + + + Consolas + 14 + true + + + + 选择指标 + + + Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter + + + + + + + true + + + + + 0 + 0 + 211 + 78 + + + + + + + + + + Consolas + 12 + + + + 周期内平均转矩 + + + + + + + + Consolas + 12 + + + + 周期内平均转矩 + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + + + + + Consolas + 12 + + + + 路径 + + + Qt::AlignmentFlag::AlignCenter + + + + + + + + Consolas + 12 + + + + + + + + + 30 + 16777215 + + + + + Consolas + 12 + + + + ... + + + + + + + + + + + + 0 + 0 + + + + + Consolas + 12 + + + + 间隔 + + + Qt::AlignmentFlag::AlignCenter + + + + + + + + Consolas + 12 + + + + + + + Qt::InputMethodHint::ImhNone + + + 每次数据采集的时间间隔,默认(最小)300s + + + + + + + + 0 + 0 + + + + + 30 + 0 + + + + + + + + + + + + + + + + Consolas + 12 + true + + + + 全部打开/关闭 + + + + + + + + Consolas + 12 + true + + + + 绘图 + + + + + + + + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + + + + + 网络设置 + + + + + + + + 0 + + + + + + + + + + + + 0 + 0 + + + + + 70 + 0 + + + + + Consolas + 12 + true + + + + HMI IP + + + Qt::AlignmentFlag::AlignCenter + + + + + + + + 150 + 0 + + + + + Consolas + 12 + + + + 192.168.0.160 + + + + + + + + Consolas + 12 + true + + + + 连接 + + + + + + + + Consolas + 12 + + + + + + + + + + + + 240 + 0 + + + + + Consolas + 12 + + + + + controller.heart + + + + + device.get_params + + + + + safety_area_data + + + + + + + + + Consolas + 12 + true + + + + 发送 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + + + + + 70 + 0 + + + + + Consolas + 12 + true + + + + MD Port + + + Qt::AlignmentFlag::AlignCenter + + + + + + + + 150 + 0 + + + + + Consolas + 12 + + + + 502 + + + + + + + + Consolas + 12 + true + + + + 连接 + + + + + + + + Consolas + 12 + + + + + + + + + + + + 240 + 0 + + + + + Consolas + 12 + + + + + ctrl_motor_on + + + + + ctrl_motor_off + + + + + + + + + Consolas + 12 + true + + + + 发送 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + + + + + 70 + 0 + + + + + Consolas + 12 + true + + + + EC Port + + + Qt::AlignmentFlag::AlignCenter + + + + + + + + 150 + 0 + + + + + Consolas + 12 + + + + 8080 + + + + + + + + Consolas + 12 + true + + + + 连接 + + + + + + + + Consolas + 12 + + + + + + + + + + + + 240 + 0 + + + + + Consolas + 12 + + + + + motor_on_state + + + + + robot_running_state + + + + + + + + + Consolas + 12 + true + + + + 发送 + + + + + + + + + + + + + + + + + + + + + + + + + + + + Consolas + 12 + true + + + + HMI + + + + + + + + Consolas + 12 + true + + + + Modbus + + + + + + + + Consolas + 12 + true + + + + EC + + + + + + + + + + + + + + + + 14 + true + + + + 1 + + + Qt::TextElideMode::ElideNone + + + true + + + + 输出 + + + + + + + Consolas + 12 + + + + QPlainTextEdit::LineWrapMode::WidgetWidth + + + true + + + + + + + + 日志 + + + + + + + Consolas + 12 + + + + true + + + + ID + + + + + Timestamp + + + + + Level + + + + + Module + + + + + Content + + + + + + + + + + + 100 + 0 + + + + + Consolas + 10 + true + + + + background-color: rgb(222, 222, 222); + + + 0/0 + + + Qt::AlignmentFlag::AlignCenter + + + + + + + + Consolas + 12 + + + + 上一页 + + + + + + + + Consolas + 12 + + + + 实时 + + + + + + + + Consolas + 12 + + + + 下一页 + + + + + + + + Consolas + 12 + + + + 加载 + + + + + + + + Consolas + 12 + + + + 查找 + + + + + + + + Consolas + 12 + + + + [id/level/module] 查找内容 + + + + + + + + + + + + + + + + + 0 + 0 + + + + + 0 + 27 + + + + background-color: rgb(200, 200, 200); + + + + + + + btn_start + clicked() + MainWindow + prog_start() + + + 89 + 158 + + + 18 + 130 + + + + + btn_stop + clicked() + MainWindow + prog_stop() + + + 43 + 176 + + + 15 + 172 + + + + + btn_reset + clicked() + MainWindow + prog_reset() + + + 41 + 217 + + + 15 + 214 + + + + + btn_durable_open + clicked() + MainWindow + file_browser() + + + 964 + 69 + + + 990 + 97 + + + + + btn_draw + clicked() + MainWindow + curve_draw() + + + 692 + 136 + + + 701 + 179 + + + + + cb_durable_total + checkStateChanged(Qt::CheckState) + MainWindow + durable_cb_change() + + + 566 + 135 + + + 546 + 180 + + + + + btn_unit_open + clicked() + MainWindow + file_browser() + + + 981 + 78 + + + 974 + 181 + + + + + btn_data_open + clicked() + MainWindow + file_browser() + + + 964 + 73 + + + 941 + 180 + + + + + btn_docs_previous + clicked() + MainWindow + pre_page() + + + 408 + 507 + + + 307 + 517 + + + + + btn_docs_realtime + clicked() + MainWindow + realtime_page() + + + 489 + 507 + + + 435 + 520 + + + + + btn_docs_next + clicked() + MainWindow + next_page() + + + 570 + 507 + + + 520 + 519 + + + + + btn_docs_load + clicked() + MainWindow + load_sql() + + + 651 + 507 + + + 606 + 521 + + + + + btn_docs_search + clicked() + MainWindow + search_keyword() + + + 732 + 507 + + + 688 + 521 + + + + + le_docs_search + returnPressed() + MainWindow + search_keyword() + + + 838 + 505 + + + 932 + 525 + + + + + cb_hmi_cmd + currentTextChanged(QString) + MainWindow + hmi_cb_change() + + + 806 + 89 + + + 585 + 185 + + + + + btn_hmi_send + clicked() + MainWindow + hmi_send() + + + 887 + 89 + + + 789 + 185 + + + + + pushButton + clicked() + MainWindow + hmi_page() + + + 980 + 85 + + + 744 + 2 + + + + + pushButton_2 + clicked() + MainWindow + md_page() + + + 980 + 124 + + + 784 + 6 + + + + + pushButton_3 + clicked() + MainWindow + ec_page() + + + 980 + 163 + + + 969 + 9 + + + + + cb_md_cmd + currentTextChanged(QString) + MainWindow + md_cb_change() + + + 805 + 89 + + + 612 + 181 + + + + + btn_md_send + clicked() + MainWindow + md_send() + + + 887 + 89 + + + 795 + 181 + + + + + btn_ec_send + clicked() + MainWindow + ec_send() + + + 887 + 89 + + + 652 + 7 + + + + + btn_hmi_conn + clicked() + MainWindow + hmi_conn() + + + 545 + 89 + + + 372 + 186 + + + + + btn_md_conn + clicked() + MainWindow + md_conn() + + + 545 + 89 + + + 398 + 182 + + + + + btn_ec_conn + clicked() + MainWindow + ec_conn() + + + 545 + 89 + + + 412 + -1 + + + + + le_durable_interval + editingFinished() + MainWindow + check_interval() + + + 844 + 97 + + + 821 + -3 + + + + + cb_ec_cmd + currentTextChanged(QString) + MainWindow + ec_cb_change() + + + 805 + 89 + + + 540 + -2 + + + + + le_hmi_ip + returnPressed() + MainWindow + hmi_conn() + + + 420 + 79 + + + 216 + 130 + + + + + + prog_start() + prog_stop() + prog_reset() + file_browser() + curve_draw() + durable_cb_change() + pre_page() + realtime_page() + next_page() + load_sql() + search_keyword() + hmi_conn() + md_conn() + ec_conn() + hmi_page() + md_page() + ec_page() + hmi_send() + md_send() + ec_send() + hmi_cb_change() + md_cb_change() + ec_cb_change() + check_interval() + + diff --git a/ui/reset.ui b/ui/reset.ui new file mode 100644 index 0000000..2f34e7d --- /dev/null +++ b/ui/reset.ui @@ -0,0 +1,271 @@ + + + Form + + + Qt::WindowModality::WindowModal + + + + 0 + 0 + 500 + 270 + + + + + 0 + 0 + + + + + 500 + 270 + + + + + 500 + 270 + + + + + Consolas + 14 + + + + 重置密码 + + + + ../assets/media/icon.ico../assets/media/icon.ico + + + + + 40 + 27 + 411 + 211 + + + + + + + + + + Consolas + 14 + + + + 用户名 + + + + + + + + Consolas + 14 + + + + + + + + + + + + + Consolas + 14 + + + + 旧密码 + + + + + + + + Consolas + 14 + + + + QLineEdit::EchoMode::Password + + + + + + + + + + + + Consolas + 14 + + + + 新密码 + + + + + + + + Consolas + 14 + + + + QLineEdit::EchoMode::Password + + + + + + + + + + + + Consolas + 14 + + + + 确 认 + + + + + + + + Consolas + 14 + + + + QLineEdit::EchoMode::Password + + + + + + + + + + 0 + 0 + + + + + Consolas + 12 + + + + + + + + + + + + + + Consolas + 14 + + + + 确定 + + + + + + + + Consolas + 14 + + + + 取消 + + + + + + + + + + le_username + le_old_password + le_new_password_1 + le_new_password_2 + + + + + btn_reset + clicked() + Form + reset_password() + + + 85 + 175 + + + 22 + 149 + + + + + btn_cancel + clicked() + Form + reset_cancel() + + + 295 + 177 + + + 386 + 148 + + + + + + reset_password() + reset_cancel() + +

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