From da5ddcea0aa1a14aad468f490bb946210ea8dc1a Mon Sep 17 00:00:00 2001 From: gitea Date: Wed, 17 Jul 2024 10:09:06 +0800 Subject: [PATCH] =?UTF-8?q?v0.1.9.4(2024/07/15)=201.=20[profile:=20aio.py]?= =?UTF-8?q?=EF=BC=9A=E5=AE=8C=E5=96=84durable=20text=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E9=80=BB=E8=BE=91=202.=20[profile:=20do=5Fbrake/do=5Fcurrent/b?= =?UTF-8?q?tn=5Ffunctions.py]=EF=BC=9A=E5=88=A0=E9=99=A4validate=5Fresp?= =?UTF-8?q?=E5=87=BD=E6=95=B0=EF=BC=8C=E4=BF=AE=E6=94=B9execution=E5=87=BD?= =?UTF-8?q?=E6=95=B0=203.=20[profile:=20factory=5Ftest.py]=20=20=20=20-=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=80=90=E4=B9=85/=E8=80=81=E5=8C=96?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=A8=8B=E5=BA=8F=20=20=20=20-=20=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E5=85=AD=E8=BD=B4=E6=8A=98=E7=BA=BF=E5=9B=BE=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=204.=20[profile:=20openapi.py]=EF=BC=9A=E5=A4=9A?= =?UTF-8?q?=E6=AC=A1=E5=90=88=E5=B9=B6=E9=81=97=E7=95=99=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E5=A4=84=E7=90=86=205.=20templates=E6=96=87=E4=BB=B6=E5=A4=B9?= =?UTF-8?q?=E7=BB=84=E7=BB=87=E6=9E=B6=E6=9E=84=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- aio/README.md | 11 +- aio/assets/layout.xlsx | Bin 1225937 -> 1226005 bytes .../durable/durable_data_current.xlsx | Bin 0 -> 6796 bytes .../durable/durable_data_velocity.xlsx | Bin 0 -> 6796 bytes aio/assets/templates/heartbeat | 2 +- .../{ => json}/controller.heart.json | 0 .../{ => json}/device.get_params.json | 0 .../{ => json}/diagnosis.get_params.json | 0 .../templates/{ => json}/diagnosis.open.json | 0 .../templates/{ => json}/diagnosis.save.json | 0 .../{ => json}/diagnosis.set_params.json | 0 .../{ => json}/overview.get_autoload.json | 0 .../{ => json}/overview.get_cur_prj.json | 0 .../templates/{ => json}/overview.reload.json | 0 .../{ => json}/overview.set_autoload.json | 0 .../{ => json}/register.set_value.json | 0 .../{ => json}/rl_task.pp_to_main.json | 0 .../templates/{ => json}/rl_task.run.json | 0 .../templates/{ => json}/rl_task.stop.json | 0 .../templates/{ => json}/state.get_state.json | 0 .../{ => json}/state.get_tp_mode.json | 0 .../{ => json}/state.set_tp_mode.json | 0 .../{ => json}/state.switch_auto.json | 0 .../{ => json}/state.switch_manual.json | 0 .../{ => json}/state.switch_motor_off.json | 0 .../{ => json}/state.switch_motor_on.json | 0 aio/code/aio.py | 164 +++++++++--- aio/code/automatic_test/btn_functions.py | 15 +- aio/code/automatic_test/do_brake.py | 15 +- aio/code/automatic_test/do_current.py | 15 +- aio/code/durable_action/__init__.py | 1 + aio/code/durable_action/factory_test.py | 252 ++++++++++++++++++ aio/code/openapi.py | 10 +- aio/code/profile/__init__.py | 1 - aio/code/profile/do_profile.py | 0 36 files changed, 402 insertions(+), 87 deletions(-) create mode 100644 aio/assets/templates/durable/durable_data_current.xlsx create mode 100644 aio/assets/templates/durable/durable_data_velocity.xlsx rename aio/assets/templates/{ => json}/controller.heart.json (100%) rename aio/assets/templates/{ => json}/device.get_params.json (100%) rename aio/assets/templates/{ => json}/diagnosis.get_params.json (100%) rename aio/assets/templates/{ => json}/diagnosis.open.json (100%) rename aio/assets/templates/{ => json}/diagnosis.save.json (100%) rename aio/assets/templates/{ => json}/diagnosis.set_params.json (100%) rename aio/assets/templates/{ => json}/overview.get_autoload.json (100%) rename aio/assets/templates/{ => json}/overview.get_cur_prj.json (100%) rename aio/assets/templates/{ => json}/overview.reload.json (100%) rename aio/assets/templates/{ => json}/overview.set_autoload.json (100%) rename aio/assets/templates/{ => json}/register.set_value.json (100%) rename aio/assets/templates/{ => json}/rl_task.pp_to_main.json (100%) rename aio/assets/templates/{ => json}/rl_task.run.json (100%) rename aio/assets/templates/{ => json}/rl_task.stop.json (100%) rename aio/assets/templates/{ => json}/state.get_state.json (100%) rename aio/assets/templates/{ => json}/state.get_tp_mode.json (100%) rename aio/assets/templates/{ => json}/state.set_tp_mode.json (100%) rename aio/assets/templates/{ => json}/state.switch_auto.json (100%) rename aio/assets/templates/{ => json}/state.switch_manual.json (100%) rename aio/assets/templates/{ => json}/state.switch_motor_off.json (100%) rename aio/assets/templates/{ => json}/state.switch_motor_on.json (100%) create mode 100644 aio/code/durable_action/__init__.py create mode 100644 aio/code/durable_action/factory_test.py delete mode 100644 aio/code/profile/__init__.py delete mode 100644 aio/code/profile/do_profile.py diff --git a/.gitignore b/.gitignore index 7eb83ee..bea7ae6 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ aio/venv aio/__pycache__/ aio/code/automatic_test/__pycache__/ aio/code/data_process/__pycache__/ -aio/assets/templates/c_msg.log \ No newline at end of file +aio/assets/templates/c_msg.log +aio/code/durable_action/__pycache__/ \ No newline at end of file diff --git a/aio/README.md b/aio/README.md index 1289907..294c7a2 100644 --- a/aio/README.md +++ b/aio/README.md @@ -486,7 +486,16 @@ v0.1.9.3(2024/07/15) - 修改modbus连接失败报错输出形式,使之只在automatic test页面显示 - 将该文件移动至toplevel,为后面扩展做准备 - 修改heartbeat文件路径,使后续打包的时候更方便 -2. [APIs: aio.py]: +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文件夹组织架构调整 + diff --git a/aio/assets/layout.xlsx b/aio/assets/layout.xlsx index c7934cee3d34d7a0282ce91cc4ee83f9b0541d0a..735de053d795474cc6eaafa886d9667f8769904d 100644 GIT binary patch delta 13020 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 literal 0 HcmV?d00001 diff --git a/aio/assets/templates/durable/durable_data_velocity.xlsx b/aio/assets/templates/durable/durable_data_velocity.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..f438a2ad364c7fd7e5be6d73da826901e3ad0a49 GIT binary patch literal 6796 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 literal 0 HcmV?d00001 diff --git a/aio/assets/templates/heartbeat b/aio/assets/templates/heartbeat index c227083..56a6051 100644 --- a/aio/assets/templates/heartbeat +++ b/aio/assets/templates/heartbeat @@ -1 +1 @@ -0 \ No newline at end of file +1 \ No newline at end of file diff --git a/aio/assets/templates/controller.heart.json b/aio/assets/templates/json/controller.heart.json similarity index 100% rename from aio/assets/templates/controller.heart.json rename to aio/assets/templates/json/controller.heart.json diff --git a/aio/assets/templates/device.get_params.json b/aio/assets/templates/json/device.get_params.json similarity index 100% rename from aio/assets/templates/device.get_params.json rename to aio/assets/templates/json/device.get_params.json diff --git a/aio/assets/templates/diagnosis.get_params.json b/aio/assets/templates/json/diagnosis.get_params.json similarity index 100% rename from aio/assets/templates/diagnosis.get_params.json rename to aio/assets/templates/json/diagnosis.get_params.json diff --git a/aio/assets/templates/diagnosis.open.json b/aio/assets/templates/json/diagnosis.open.json similarity index 100% rename from aio/assets/templates/diagnosis.open.json rename to aio/assets/templates/json/diagnosis.open.json diff --git a/aio/assets/templates/diagnosis.save.json b/aio/assets/templates/json/diagnosis.save.json similarity index 100% rename from aio/assets/templates/diagnosis.save.json rename to aio/assets/templates/json/diagnosis.save.json diff --git a/aio/assets/templates/diagnosis.set_params.json b/aio/assets/templates/json/diagnosis.set_params.json similarity index 100% rename from aio/assets/templates/diagnosis.set_params.json rename to aio/assets/templates/json/diagnosis.set_params.json diff --git a/aio/assets/templates/overview.get_autoload.json b/aio/assets/templates/json/overview.get_autoload.json similarity index 100% rename from aio/assets/templates/overview.get_autoload.json rename to aio/assets/templates/json/overview.get_autoload.json diff --git a/aio/assets/templates/overview.get_cur_prj.json b/aio/assets/templates/json/overview.get_cur_prj.json similarity index 100% rename from aio/assets/templates/overview.get_cur_prj.json rename to aio/assets/templates/json/overview.get_cur_prj.json diff --git a/aio/assets/templates/overview.reload.json b/aio/assets/templates/json/overview.reload.json similarity index 100% rename from aio/assets/templates/overview.reload.json rename to aio/assets/templates/json/overview.reload.json diff --git a/aio/assets/templates/overview.set_autoload.json b/aio/assets/templates/json/overview.set_autoload.json similarity index 100% rename from aio/assets/templates/overview.set_autoload.json rename to aio/assets/templates/json/overview.set_autoload.json diff --git a/aio/assets/templates/register.set_value.json b/aio/assets/templates/json/register.set_value.json similarity index 100% rename from aio/assets/templates/register.set_value.json rename to aio/assets/templates/json/register.set_value.json diff --git a/aio/assets/templates/rl_task.pp_to_main.json b/aio/assets/templates/json/rl_task.pp_to_main.json similarity index 100% rename from aio/assets/templates/rl_task.pp_to_main.json rename to aio/assets/templates/json/rl_task.pp_to_main.json diff --git a/aio/assets/templates/rl_task.run.json b/aio/assets/templates/json/rl_task.run.json similarity index 100% rename from aio/assets/templates/rl_task.run.json rename to aio/assets/templates/json/rl_task.run.json diff --git a/aio/assets/templates/rl_task.stop.json b/aio/assets/templates/json/rl_task.stop.json similarity index 100% rename from aio/assets/templates/rl_task.stop.json rename to aio/assets/templates/json/rl_task.stop.json diff --git a/aio/assets/templates/state.get_state.json b/aio/assets/templates/json/state.get_state.json similarity index 100% rename from aio/assets/templates/state.get_state.json rename to aio/assets/templates/json/state.get_state.json diff --git a/aio/assets/templates/state.get_tp_mode.json b/aio/assets/templates/json/state.get_tp_mode.json similarity index 100% rename from aio/assets/templates/state.get_tp_mode.json rename to aio/assets/templates/json/state.get_tp_mode.json diff --git a/aio/assets/templates/state.set_tp_mode.json b/aio/assets/templates/json/state.set_tp_mode.json similarity index 100% rename from aio/assets/templates/state.set_tp_mode.json rename to aio/assets/templates/json/state.set_tp_mode.json diff --git a/aio/assets/templates/state.switch_auto.json b/aio/assets/templates/json/state.switch_auto.json similarity index 100% rename from aio/assets/templates/state.switch_auto.json rename to aio/assets/templates/json/state.switch_auto.json diff --git a/aio/assets/templates/state.switch_manual.json b/aio/assets/templates/json/state.switch_manual.json similarity index 100% rename from aio/assets/templates/state.switch_manual.json rename to aio/assets/templates/json/state.switch_manual.json diff --git a/aio/assets/templates/state.switch_motor_off.json b/aio/assets/templates/json/state.switch_motor_off.json similarity index 100% rename from aio/assets/templates/state.switch_motor_off.json rename to aio/assets/templates/json/state.switch_motor_off.json diff --git a/aio/assets/templates/state.switch_motor_on.json b/aio/assets/templates/json/state.switch_motor_on.json similarity index 100% rename from aio/assets/templates/state.switch_motor_on.json rename to aio/assets/templates/json/state.switch_motor_on.json diff --git a/aio/code/aio.py b/aio/code/aio.py index c61d407..89c9406 100644 --- a/aio/code/aio.py +++ b/aio/code/aio.py @@ -7,17 +7,43 @@ import customtkinter from time import time, strftime, localtime, sleep from urllib.request import urlopen from socket import setdefaulttimeout +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from data_process import * from automatic_test import * +from durable_action import * import openapi +import matplotlib.pyplot as plt +import matplotlib +import pandas as pd +matplotlib.use('Agg') heartbeat = f'{dirname(__file__)}/../assets/templates/heartbeat' +durable_data_current_xlsx = f'{dirname(__file__)}/../assets/templates/durable/durable_data_current.xlsx' +durable_data_velocity_xlsx = f'{dirname(__file__)}/../assets/templates/durable/durable_data_velocity.xlsx' customtkinter.set_appearance_mode("System") # Modes: "System" (standard), "Dark", "Light" customtkinter.set_default_color_theme("blue") # Themes: "blue" (standard), "green", "dark-blue" customtkinter.set_widget_scaling(1.1) # widget dimensions and text size customtkinter.set_window_scaling(1.1) # window geometry dimensions setdefaulttimeout(3) # global vars +durable_data_current = { + 'time': list(range(1, 26)), + 'axis1': [0 for _ in range(25)], + 'axis2': [0 for _ in range(25)], + 'axis3': [0 for _ in range(25)], + 'axis4': [0 for _ in range(25)], + 'axis5': [0 for _ in range(25)], + 'axis6': [0 for _ in range(25)], +} +durable_data_velocity = { + 'time': list(range(1, 26)), + 'axis1': [0 for _ in range(25)], + 'axis2': [0 for _ in range(25)], + 'axis3': [0 for _ in range(25)], + 'axis4': [0 for _ in range(25)], + 'axis5': [0 for _ in range(25)], + 'axis6': [0 for _ in range(25)], +} btns_func = { 'start': {'btn': '', 'row': 1, 'text': '开始运行'}, 'check': {'btn': '', 'row': 2, 'text': '检查参数'}, @@ -36,11 +62,9 @@ widgits_at = { 'path': {'label': '', 'entry': '', 'row': 2, 'col': 2, 'text': '数据文件夹路径'}, 'loadsel': {'label': '', 'optionmenu': '', 'row': 2, 'col': 1, 'text': '负载信息'}, } -widgits_pr = { +widgits_da = { 'path': {'label': '', 'entry': '', 'row': 1, 'col': 2, 'text': '数据文件夹路径'}, - 'loadsel': {'label': '', 'optionmenu': '', 'row': 2, 'col': 1, 'text': '负载选择'}, - 'actionsel': {'label': '', 'optionmenu': '', 'row': 3, 'col': 1, 'text': '动作选择'}, - 'inertiasel': {'label': '', 'optionmenu': '', 'row': 4, 'col': 1, 'text': '惯量选择'}, + 'loadsel': {'label': '', 'optionmenu': '', 'row': 1, 'col': 1, 'text': '负载选择'}, } @@ -51,6 +75,9 @@ class App(customtkinter.CTk): self.w_param = 84 self.hr = None self.md = None + self.canvas = None + self.flg = 0 + self.df_copy = None # ===================================================================== # configure window self.title("AIO - All in one automatic toolbox") @@ -86,7 +113,7 @@ class App(customtkinter.CTk): self.tabview.grid(row=0, column=1, padx=10, pady=5, sticky="nsew") self.tabview.add("Data Process") self.tabview.add("Automatic Test") - self.tabview.add("Profile Test") + self.tabview.add("Durable Action") # create main menu for data process self.menu_main_dp = customtkinter.CTkOptionMenu(self.tabview.tab('Data Process'), values=["init", "brake", "current", "iso", "wavelogger"], font=self.my_font, text_color='yellow', button_color='red', fg_color='green', command=self.func_main_callback) self.menu_main_dp.grid(row=1, column=1, sticky='we', padx=5, pady=10) @@ -122,7 +149,7 @@ class App(customtkinter.CTk): # For automatic test tab START ===================================================================== # create buttons self.seg_button = customtkinter.CTkSegmentedButton(self.tabview.tab('Automatic Test'), dynamic_resizing=False, font=customtkinter.CTkFont(size=16, weight='bold'), command=lambda value='机器状态': self.thread_it(self.segmented_button_callback)) - self.seg_button.grid(row=1, column=2, columnspan=12, padx=(20, 10), pady=(10, 10), sticky="ew") + self.seg_button.grid(row=1, column=2, columnspan=12, padx=(65, 10), pady=(10, 10), sticky="ew") self.seg_button.configure(dynamic_resizing=False, values=["功能切换", "触发急停", "恢复急停", "待定功能", "功能待定", "机器状态", "告警信息"]) self.seg_button.set("功能切换") # create progress bar @@ -134,7 +161,7 @@ class App(customtkinter.CTk): for widgit in widgits_at: if widgit == 'path': widgits_at[widgit]['label'] = customtkinter.CTkLabel(self.tabview.tab('Automatic Test'), text=f'{widgit.upper()}', font=self.my_font) - widgits_at[widgit]['label'].grid(row=widgits_at[widgit]['row'], column=widgits_at[widgit]['col'], sticky='e', pady=5) + widgits_at[widgit]['label'].grid(row=widgits_at[widgit]['row'], column=widgits_at[widgit]['col'], sticky='e', padx=(20, 5), pady=5) widgits_at[widgit]['entry'] = customtkinter.CTkEntry(self.tabview.tab('Automatic Test'), width=670, placeholder_text=widgits_at[widgit]['text'], font=self.my_font) widgits_at[widgit]['entry'].grid(row=widgits_at[widgit]['row'], column=widgits_at[widgit]['col']+1, columnspan=11, padx=(5, 10), pady=5, sticky='we') widgits_at[widgit]['entry'].configure(state='disabled') @@ -144,33 +171,23 @@ class App(customtkinter.CTk): widgits_at[widgit]['optionmenu'].set(widgits_at[widgit]['text']) widgits_at[widgit]['optionmenu'].configure(state='disabled') # For automatic test tab END ===================================================================== - # For profile tab START ===================================================================== - for widgit in widgits_pr: - if widgit == 'path': - widgits_pr[widgit]['label'] = customtkinter.CTkLabel(self.tabview.tab('Profile Test'), text=f'{widgit.upper()}', font=self.my_font) - widgits_pr[widgit]['label'].grid(row=widgits_pr[widgit]['row'], column=widgits_pr[widgit]['col'], sticky='e', pady=5) - widgits_pr[widgit]['entry'] = customtkinter.CTkEntry(self.tabview.tab('Profile Test'), width=670, placeholder_text=widgits_pr[widgit]['text'], font=self.my_font) - widgits_pr[widgit]['entry'].grid(row=widgits_pr[widgit]['row'], column=widgits_pr[widgit]['col']+1, columnspan=11, padx=(5, 10), pady=5, sticky='we') - # widgits_pr[widgit]['entry'].configure(state='disabled') - elif widgit in ['loadsel', 'actionsel', 'inertiasel']: - match widgit: - case 'loadsel': - values = ["tool100", "inertia"] - case 'actionsel': - values = ["signle", "box", "cart"] - case 'inertiasel': - values = ["high", "middle", "low"] - - widgits_pr[widgit]['optionmenu'] = customtkinter.CTkOptionMenu(self.tabview.tab('Profile Test'), button_color='#708090', fg_color='#778899', values=values, width=self.w_param, font=self.my_font) - widgits_pr[widgit]['optionmenu'].grid(row=widgits_pr[widgit]['row'], column=widgits_pr[widgit]['col'], padx=5, pady=5, sticky='we') - widgits_pr[widgit]['optionmenu'].set(widgits_pr[widgit]['text']) - # widgits_pr[widgit]['optionmenu'].configure(state='disabled') + # For durable_action tab START ===================================================================== # create progress bar - self.progressbar_pr = customtkinter.CTkProgressBar(self.tabview.tab('Profile Test')) - self.progressbar_pr.grid(row=5, column=1, padx=5, pady=5, sticky="ew") - self.progressbar_pr.configure(mode="determinnate", width=10) - self.progressbar_pr.start() - # For profile tab END ===================================================================== + self.progressbar_da = customtkinter.CTkProgressBar(self.tabview.tab('Durable Action')) + self.progressbar_da.grid(row=2, column=1, padx=5, pady=5, sticky="ew") + self.progressbar_da.configure(mode="determinnate", width=self.w_param) + self.progressbar_da.start() + for widgit in widgits_da: + if widgit == 'path': + widgits_da[widgit]['label'] = customtkinter.CTkLabel(self.tabview.tab('Durable Action'), text=f'{widgit.upper()}', font=self.my_font) + widgits_da[widgit]['label'].grid(row=widgits_da[widgit]['row'], column=widgits_da[widgit]['col'], sticky='e', padx=(20, 5), pady=10) + widgits_da[widgit]['entry'] = customtkinter.CTkEntry(self.tabview.tab('Durable Action'), width=670, placeholder_text=widgits_da[widgit]['text'], font=self.my_font) + widgits_da[widgit]['entry'].grid(row=widgits_da[widgit]['row'], column=widgits_da[widgit]['col']+1, columnspan=11, padx=(5, 10), pady=10, sticky='we') + elif widgit in ['loadsel']: + widgits_da[widgit]['optionmenu'] = customtkinter.CTkOptionMenu(self.tabview.tab('Durable Action'), button_color='#708090', fg_color='#778899', values=["tool100", "inertia"], font=self.my_font) + widgits_da[widgit]['optionmenu'].grid(row=widgits_da[widgit]['row'], column=widgits_da[widgit]['col'], padx=5, pady=10, sticky='we') + widgits_da[widgit]['optionmenu'].set(widgits_da[widgit]['text']) + # For durable_action tab END ===================================================================== # create textbox self.textbox = customtkinter.CTkTextbox(self, wrap='none', font=customtkinter.CTkFont(family="consolas", size=14), text_color="blue", fg_color='#E9E9E9', border_width=2, border_color='#CDCDCD', border_spacing=5) self.textbox.grid(row=6, column=1, columnspan=13, ipadx=10, ipady=10, padx=10, pady=(5, 10), sticky='nsew') @@ -188,6 +205,35 @@ class App(customtkinter.CTk): tkinter.messagebox.showwarning(title="版本更新", message="连接服务器失败,无法确认当前是否是最新版本......") # functions below ↓ ---------------------------------------------------------------------------------------- + def create_canvas(self, figure): + self.canvas = FigureCanvasTkAgg(figure, self) + self.canvas.draw() + self.canvas.get_tk_widget().configure(height=640) + self.canvas.get_tk_widget().grid(row=3, column=1, rowspan=3, columnspan=13, padx=20, pady=10, sticky="nsew") + + def create_plot(self): + 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' + + df = pd.read_excel(durable_data_current_xlsx) + if not df.equals(self.df_copy) or self.flg == 0: + self.flg = 1 + self.df_copy = df.copy() + figure = plt.figure(frameon=True, facecolor='#E9E9E9') + plt.subplots_adjust(left=0.04, right=0.98, bottom=0.05, top=0.98) + ax = figure.add_subplot(1, 1, 1) + df.plot(grid=True, x='time', y='axis1', ax=ax) + df.plot(grid=True, x='time', y='axis2', ax=ax) + df.plot(grid=True, x='time', y='axis3', ax=ax) + df.plot(grid=True, x='time', y='axis4', ax=ax) + df.plot(grid=True, x='time', y='axis5', ax=ax) + df.plot(grid=True, x='time', y='axis6', ax=ax) + + self.create_canvas(figure) + def thread_it(self, func, *args): """ 将函数打包进线程 """ self.myThread = Thread(target=func, args=args) @@ -216,17 +262,23 @@ class App(customtkinter.CTk): # self.tabview.configure(state='normal') def detect_network(self): + df = pd.DataFrame(durable_data_current) + df.to_excel(durable_data_current_xlsx, index=False) + with open(heartbeat, "w", encoding='utf-8') as f_hb: f_hb.write('0') self.hr = openapi.HmiRequest(self.write2textbox) self.md = openapi.ModbusRequest(self.write2textbox) while True: + if self.tabview.get() == 'Durable Action': + self.create_plot() + with open(heartbeat, 'r', encoding='utf-8') as f_hb: c_state = f_hb.read().strip() pb_color = 'green' if c_state == '1' else 'red' self.progressbar.configure(progress_color=pb_color) - self.progressbar_pr.configure(progress_color=pb_color) + self.progressbar_da.configure(progress_color=pb_color) if c_state == '0': self.hr.t_bool = False sleep(3) @@ -239,10 +291,22 @@ class App(customtkinter.CTk): tab_name = self.tabview.get() if tab_name == 'Data Process': + self.flg = 0 self.menu_main_dp.set("Start Here!") + try: + self.canvas.get_tk_widget().grid_forget() + except: + pass elif tab_name == 'Automatic Test': + self.flg = 0 self.menu_main_at.set("Start Here!") self.seg_button.configure(state='normal') + try: + self.canvas.get_tk_widget().grid_forget() + except: + pass + elif tab_name == 'Durable Action': + pass def initialization(self): tab_name = self.tabview.get() @@ -263,7 +327,7 @@ class App(customtkinter.CTk): self.menu_sub_dp.grid_forget() elif tab_name == 'Automatic Test': for widgit in widgits_at: - if widgit in ['path', 'av1', 'av2', 'av3', 'av4', 'av5', 'av6', 'rc1', 'rc2', 'rc3', 'rc4', 'rc5', 'rc6', 'rr1', 'rr2', 'rr3', 'rr4', 'rr5', 'rr6']: + if widgit in ['path', ]: widgits_at[widgit]['label'].configure(text=f'{widgit.upper()}', text_color='black') widgits_at[widgit]['entry'].delete(0, tkinter.END) widgits_at[widgit]['entry'].configure(placeholder_text=widgits_at[widgit]['text'], state='normal') @@ -273,6 +337,15 @@ class App(customtkinter.CTk): widgits_at[widgit]['optionmenu'].set(widgits_at[widgit]['text']) widgits_at[widgit]['optionmenu'].configure(state='disabled') self.seg_button.set("功能切换") + elif tab_name == 'Durable Action': + for widgit in widgits_da: + if widgit in ['path', ]: + widgits_da[widgit]['label'].configure(text=f'{widgit.upper()}', text_color='black') + widgits_da[widgit]['entry'].delete(0, tkinter.END) + widgits_da[widgit]['entry'].configure(placeholder_text=widgits_at[widgit]['text'], state='normal') + elif widgit in ['loadsel']: + widgits_da[widgit]['optionmenu'].configure(state='normal') + widgits_da[widgit]['optionmenu'].set(widgits_at[widgit]['text']) def func_main_callback(self, func_name): self.initialization() @@ -369,7 +442,7 @@ class App(customtkinter.CTk): self.textbox.tag_config(tagName=color, foreground=color) tab_name_cur = self.tabview.get() - if tab_name == 'openapi' and tab_name_cur == 'Automatic Test': + if tab_name == tab_name_cur: if wait != 0: self.textbox.insert(index='end', text=text, tags=color) self.textbox.update() @@ -383,7 +456,7 @@ class App(customtkinter.CTk): self.textbox.insert(index='end', text=text + '\n', tags=color) self.textbox.update() self.textbox.see('end') - elif tab_name == tab_name_cur: + elif tab_name == 'openapi' and tab_name_cur == 'Automatic Test' or tab_name_cur == 'Durable Action': if wait != 0: self.textbox.insert(index='end', text=text, tags=color) self.textbox.update() @@ -489,12 +562,24 @@ class App(customtkinter.CTk): return 0, 0 else: return 0, 0 + elif tab_name == 'Durable Action': + path = widgits_da['path']['entry'].get().strip() + loadsel = widgits_da['loadsel']['optionmenu'].get() + c1 = exists(path) + c2 = loadsel in ['tool100', 'inertia'] + if c1 and c2: + return 7, path, loadsel + else: + return 0, 0 def func_start_callback(self): self.textbox.delete(index1='1.0', index2='end') flag, *args = self.check_param() - func_dict = {1: brake.main, 2: current.main, 3: iso.main, 4: wavelogger.main, 5: do_brake.main, 6: do_current.main} + func_dict = { + 1: brake.main, 2: current.main, 3: iso.main, 4: wavelogger.main, 5: do_brake.main, 6: do_current.main, + 7: factory_test.main + } if flag == 1: func_dict[flag](path=args[0], vel=args[1], trq=args[2], estop=args[3], w2t=self.write2textbox) elif flag == 2: @@ -509,6 +594,9 @@ class App(customtkinter.CTk): elif flag == 6: self.pre_warning() func_dict[flag](path=args[0], hr=self.hr, md=self.md, loadsel=args[1], w2t=self.write2textbox) + elif flag == 7: + self.pre_warning() + func_dict[flag](path=args[0], hr=self.hr, md=self.md, w2t=self.write2textbox) else: tkinter.messagebox.showerror(title="参数错误", message="请检查对应参数是否填写正确!", ) diff --git a/aio/code/automatic_test/btn_functions.py b/aio/code/automatic_test/btn_functions.py index 884a2b0..2f57992 100644 --- a/aio/code/automatic_test/btn_functions.py +++ b/aio/code/automatic_test/btn_functions.py @@ -2,18 +2,6 @@ from json import loads from sys import argv -def validate_resp(_id, response, w2t): - match _id: - case 'DATA ERR': - w2t(f"数据处理错误,需要确认", 0, 4, 'red', tab_name='Automatic Test') - case 'DATA READ ERR': - w2t(f"无法读取数据,需要确认", 0, 3, 'red', tab_name='Automatic Test') - case 'NOT SUPPORT': - w2t(f"不支持的功能,需要确认", 0, 2, 'red', tab_name='Automatic Test') - if not response: - w2t(f"无法获取{id}请求的响应信息", 0, 1, 'red', tab_name='Automatic Test') - - def execution(cmd, hr, w2t, **kwargs): _id = hr.execution(cmd, **kwargs) _msg = hr.get_from_id(_id) @@ -21,7 +9,8 @@ def execution(cmd, hr, w2t, **kwargs): w2t(f"无法获取{_id}请求的响应信息", 0, 6, 'red', tab_name='Automatic Test') else: _response = loads(_msg) - validate_resp(_id, _response, w2t) + if not _response: + w2t(f"无法获取{id}请求的响应信息", 0, 1, 'red', tab_name='Automatic Test') return _response diff --git a/aio/code/automatic_test/do_brake.py b/aio/code/automatic_test/do_brake.py index b937264..408fd3e 100644 --- a/aio/code/automatic_test/do_brake.py +++ b/aio/code/automatic_test/do_brake.py @@ -101,18 +101,6 @@ def prj_to_xcore(prj_file): ssh.close() -def validate_resp(_id, response, w2t): - match _id: - case 'DATA ERR': - w2t(f"数据处理错误,需要确认", 0, 4, 'red', tab_name='Automatic Test') - case 'DATA READ ERR': - w2t(f"无法读取数据,需要确认", 0, 3, 'red', tab_name='Automatic Test') - case 'NOT SUPPORT': - w2t(f"不支持的功能,需要确认", 0, 2, 'red', tab_name='Automatic Test') - if not response: - w2t(f"无法获取{id}请求的响应信息", 0, 1, 'red', tab_name='Automatic Test') - - def execution(cmd, hr, w2t, **kwargs): _id = hr.execution(cmd, **kwargs) _msg = hr.get_from_id(_id) @@ -120,7 +108,8 @@ def execution(cmd, hr, w2t, **kwargs): w2t(f"无法获取{_id}请求的响应信息", 0, 6, 'red', tab_name='Automatic Test') else: _response = loads(_msg) - validate_resp(_id, _response, w2t) + if not _response: + w2t(f"无法获取{id}请求的响应信息", 0, 1, 'red', tab_name='Automatic Test') return _response diff --git a/aio/code/automatic_test/do_current.py b/aio/code/automatic_test/do_current.py index 245fe0f..1dff222 100644 --- a/aio/code/automatic_test/do_current.py +++ b/aio/code/automatic_test/do_current.py @@ -86,18 +86,6 @@ def prj_to_xcore(prj_file): ssh.close() -def validate_resp(_id, response, w2t): - match _id: - case 'DATA ERR': - w2t(f"数据处理错误,需要确认", 0, 4, 'red', tab_name='Automatic Test') - case 'DATA READ ERR': - w2t(f"无法读取数据,需要确认", 0, 3, 'red', tab_name='Automatic Test') - case 'NOT SUPPORT': - w2t(f"不支持的功能,需要确认", 0, 2, 'red', tab_name='Automatic Test') - if not response: - w2t(f"无法获取{id}请求的响应信息", 0, 1, 'red', tab_name='Automatic Test') - - def execution(cmd, hr, w2t, **kwargs): _id = hr.execution(cmd, **kwargs) _msg = hr.get_from_id(_id) @@ -105,7 +93,8 @@ def execution(cmd, hr, w2t, **kwargs): w2t(f"无法获取{_id}请求的响应信息", 0, 7, 'red', tab_name='Automatic Test') else: _response = loads(_msg) - validate_resp(_id, _response, w2t) + if not _response: + w2t(f"无法获取{id}请求的响应信息", 0, 1, 'red', tab_name='Automatic Test') return _response diff --git a/aio/code/durable_action/__init__.py b/aio/code/durable_action/__init__.py new file mode 100644 index 0000000..913ac28 --- /dev/null +++ b/aio/code/durable_action/__init__.py @@ -0,0 +1 @@ +__all__ = ['factory_test'] \ No newline at end of file diff --git a/aio/code/durable_action/factory_test.py b/aio/code/durable_action/factory_test.py new file mode 100644 index 0000000..f68036f --- /dev/null +++ b/aio/code/durable_action/factory_test.py @@ -0,0 +1,252 @@ +from sys import argv +from os.path import exists, dirname +from os import scandir +from paramiko import SSHClient, AutoAddPolicy +from json import loads +from time import sleep, time +import pandas as pd +from openpyxl import load_workbook +from math import sqrt + +tab_name = 'Durable Action' +durable_data_current_xlsx = f'{dirname(__file__)}/../../assets/templates/durable/durable_data_current.xlsx' +durable_data_velocity_xlsx = f'{dirname(__file__)}/../../assets/templates/durable/durable_data_velocity.xlsx' +display_pdo_params = [ + {"name": "hw_joint_vel_feedback", "channel": 0}, + {"name": "hw_joint_vel_feedback", "channel": 1}, + {"name": "hw_joint_vel_feedback", "channel": 2}, + {"name": "hw_joint_vel_feedback", "channel": 3}, + {"name": "hw_joint_vel_feedback", "channel": 4}, + {"name": "hw_joint_vel_feedback", "channel": 5}, + {"name": "device_servo_trq_feedback", "channel": 0}, + {"name": "device_servo_trq_feedback", "channel": 1}, + {"name": "device_servo_trq_feedback", "channel": 2}, + {"name": "device_servo_trq_feedback", "channel": 3}, + {"name": "device_servo_trq_feedback", "channel": 4}, + {"name": "device_servo_trq_feedback", "channel": 5}, +] +durable_data_current = { + 'time': list(range(1, 26)), + 'axis1': [0 for _ in range(25)], + 'axis2': [0 for _ in range(25)], + 'axis3': [0 for _ in range(25)], + 'axis4': [0 for _ in range(25)], + 'axis5': [0 for _ in range(25)], + 'axis6': [0 for _ in range(25)], +} +durable_data_velocity = { + 'time': list(range(1, 26)), + 'axis1': [0 for _ in range(25)], + 'axis2': [0 for _ in range(25)], + 'axis3': [0 for _ in range(25)], + 'axis4': [0 for _ in range(25)], + 'axis5': [0 for _ in range(25)], + 'axis6': [0 for _ in range(25)], +} + +def traversal_files(path, w2t): + if not exists(path): + msg = f'数据文件夹{path}不存在,请确认后重试......' + w2t(msg, 0, 1, 'red', tab_name=tab_name) + else: + dirs = [] + files = [] + for item in scandir(path): + if item.is_dir(): + dirs.append(item.path) + elif item.is_file(): + files.append(item.path) + + return dirs, files + + +def check_files(data_dirs, data_files, w2t): + if len(data_dirs) != 0 or len(data_files) != 2: + w2t('初始路径下不允许有文件夹,且初始路径下只能存在如下文件,确认后重新运行!\n1. target.zip\n2.configs.xlsx', 0, + 10, 'red', tab_name) + + _files = [data_files[0].split('\\')[-1], data_files[1].split('\\')[-1]] + _files.sort() + if _files != ['configs.xlsx', 'target.zip']: + w2t('初始路径下只能存在如下文件,确认后重新运行!\n1. target.zip\n2.configs.xlsx', 0, 10, 'red', tab_name) + + data_files.sort() + return data_files + + +def prj_to_xcore(prj_file): + ssh = SSHClient() + ssh.set_missing_host_key_policy(AutoAddPolicy()) + ssh.connect('192.168.0.160', 22, username='luoshi', password='luoshi2019') + sftp = ssh.open_sftp() + sftp.put(prj_file, '/tmp/target.zip') + cmd = 'cd /tmp; rm -rf target/; mkdir target; unzip -d target/ -q target.zip; ' + cmd += 'chmod 777 -R target/; rm target.zip' + ssh.exec_command(cmd) + + cmd = 'sudo rm -rf /home/luoshi/bin/controller/projects/target; ' + cmd += 'sudo mv /tmp/target/ /home/luoshi/bin/controller/projects/' + stdin, stdout, stderr = ssh.exec_command(cmd, get_pty=True) + stdin.write('luoshi2019' + '\n') + stdin.flush() + print(stdout.read().decode()) # 必须得输出一下stdout,才能正确执行sudo + print(stderr.read().decode()) # 顺便也执行以下stderr + + _prj_name = prj_file.split('\\')[-1].removesuffix('.zip') + cmd = 'cd /home/luoshi/bin/controller/; ' + cmd += f'sudo mv projects/target/_build/{_prj_name}.prj projects/target/_build/target.prj' + stdin, stdout, stderr = ssh.exec_command(cmd, get_pty=True) + stdin.write('luoshi2019' + '\n') + stdin.flush() + print(stdout.read().decode()) # 必须得输出一下stdout,才能正确执行sudo + print(stderr.read().decode()) # 顺便也执行以下stderr + ssh.close() + + +def execution(cmd, hr, w2t, **kwargs): + _id = hr.execution(cmd, **kwargs) + _msg = hr.get_from_id(_id) + if not _msg: + w2t(f"无法获取{_id}请求的响应信息", 0, 7, 'red', tab_name=tab_name) + else: + _response = loads(_msg) + if not _response: + w2t(f"无法获取{id}请求的响应信息", 0, 1, 'red', tab_name=tab_name) + return _response + + +def run_rl(path, config_file, hr, md, w2t): + # 1. 关闭诊断曲线,触发软急停,并解除,目的是让可能正在运行着的机器停下来,切手动模式并下电 + _response = execution('diagnosis.open', hr, w2t, open=False, display_open=False) + md.trigger_estop() + md.reset_estop() + md.write_act(False) + sleep(1) # 让曲线彻底关闭 + _response = execution('state.switch_manual', hr, w2t) + _response = execution('state.switch_motor_off', hr, w2t) + + # 2. reload工程后,pp2main,并且自动模式和上电 + prj_path = 'target/_build/target.prj' + _response = execution('overview.reload', hr, w2t, prj_path=prj_path, tasks=['current']) + _response = execution('overview.get_cur_prj', hr, w2t) + _response = execution('rl_task.pp_to_main', hr, w2t, tasks=['current']) + _response = execution('state.switch_auto', hr, w2t) + _response = execution('state.switch_motor_on', hr, w2t) + + # 3. 开始运行程序 + _response = execution('rl_task.run', hr, w2t, tasks=['current']) + _t_start = time() + while True: + if md.read_ready_to_go() == 1: + md.write_act(True) + break + else: + if (time() - _t_start) // 20 > 1: + w2t("20s内未收到机器人的运行信号,需要确认RL程序编写正确并正常执行...", 0, 111, 'red', tab_name) + else: + sleep(1) + + # 4. 获取初始数据,周期时间,首次的各轴平均电流值,打开诊断曲线,并执行采集 + _response = execution('diagnosis.open', hr, w2t, open=True, display_open=True) + _response = execution('diagnosis.set_params', hr, w2t, display_pdo_params=display_pdo_params) + _t_start = time() + while True: + scenario_time = md.read_scenario_time() + if float(scenario_time) > 1: + w2t(f"场景的周期时间:{scenario_time}", 0, 0, 'green', tab_name) + break + else: + if (time() - _t_start) // 60 > 3: + w2t(f"未收到场景的周期时间,需要确认RL程序编写正确并正常执行...", 0, 111, 'red', tab_name) + else: + sleep(5) + sleep(1) # 一定要延迟一秒再读一次scenario time寄存器,因为一开始读取的数值不准确 + scenario_time = float(md.read_scenario_time()) + sleep(scenario_time * 0.2) # 再运行周期的20%即可 + + # 6. 关闭诊断曲线,停止程序运行,下电并且换成手动模式 + _response = execution('diagnosis.open', hr, w2t, open=False, display_open=False) + _response = execution('diagnosis.set_params', hr, w2t, display_pdo_params=[]) + _response = execution('rl_task.stop', hr, w2t, tasks=['current']) + sleep(1) # 保证所有数据均已返回 + # 7. 保留数据并处理输出 + data = get_durable_data(path, config_file, durable_data_current, scenario_time, hr, w2t) + df = pd.DataFrame(data) + df.to_excel(durable_data_current_xlsx, index=False) + + # 8. 继续运行 + _response = execution('rl_task.run', hr, w2t, tasks=['current']) + while True: + # 每3分钟,更新一次数据,打开曲线,获取周期内电流,关闭曲线 + sleep(180) + _response = execution('diagnosis.open', hr, w2t, open=True, display_open=True) + _response = execution('diagnosis.set_params', hr, w2t, display_pdo_params=display_pdo_params) + sleep(scenario_time + 10) + _response = execution('diagnosis.open', hr, w2t, open=False, display_open=False) + _response = execution('diagnosis.set_params', hr, w2t, display_pdo_params=[]) + # 7. 保留数据并处理输出 + data = get_durable_data(path, config_file, durable_data_current, scenario_time, hr, w2t) + df = pd.DataFrame(data) + df.to_excel(durable_data_current_xlsx, index=False) + + +def get_durable_data(path, config_file, data, scenario_time, hr, w2t): + _data_list = [] + for _msg in hr.c_msg: + if 'diagnosis.result' in _msg: + _data_list.insert(0, loads(_msg)) + else: + _index = 210 + for _msg in hr.c_msg: + if 'diagnosis.result' in _msg: + _index = hr.c_msg.index(_msg) + break + del hr.c_msg[_index:] + hr.c_msg_xs.clear() + + # with open('log.txt', 'w', encoding='utf-8') as f_obj: + # for _ in _data_list: + # f_obj.write(f"{_}\n") + + _wb = load_workbook(config_file, read_only=True) + _ws = _wb['Target'] + rcs = [] + for i in range(6): + rcs.append(float(_ws.cell(row=6, column=i + 2).value)) + + _d2d_trq = { + 'device_servo_trq_feedback_0': [], 'device_servo_trq_feedback_1': [], 'device_servo_trq_feedback_2': [], + 'device_servo_trq_feedback_3': [], 'device_servo_trq_feedback_4': [], 'device_servo_trq_feedback_5': [], + } + for line in _data_list: + for item in line['data']: + for i in range(6): + item['value'].reverse() + if item.get('channel', None) == i and item.get('name', None) == 'device_servo_trq_feedback': + _d2d_trq[f'device_servo_trq_feedback_{i}'].extend(item['value']) + + if len(_d2d_trq['device_servo_trq_feedback_0']) / 1000 > scenario_time + 1: + _df = pd.DataFrame(_d2d_trq) + for i in range(6): + _ = sqrt(_df[f'device_servo_trq_feedback_{i}'].apply(lambda x: (rcs[i]*x/1000)**2).sum()/len(_df[f'device_servo_trq_feedback_{i}'])) + del data[f"axis{i + 1}"][0] + data[f"axis{i + 1}"].append(_) + break + else: + with open(f'{path}\\device_servo_trq_feedback_0.txt', 'w', encoding='utf-8') as f_obj: + for _ in _d2d_trq['device_servo_trq_feedback_0']: + f_obj.write(f"{_}\n") + w2t("采集的数据时间长度不够,需要确认。", 0, 2, 'red', tab_name) + + return data + + +def main(path, hr, md, w2t): + data_dirs, data_files = traversal_files(path, w2t) + config_file, prj_file = check_files(data_dirs, data_files, w2t) + prj_to_xcore(prj_file) + run_rl(path, config_file, hr, md, w2t) + + +if __name__ == '__main__': + main(*argv[1:]) diff --git a/aio/code/openapi.py b/aio/code/openapi.py index 4cb454c..4ca716b 100644 --- a/aio/code/openapi.py +++ b/aio/code/openapi.py @@ -250,9 +250,8 @@ class HmiRequest(object): with open(heartbeat, "w", encoding='utf-8') as f_hb: f_hb.write(_flag) if _flag == '0': - # self.w2t(f"{_id} 心跳丢失,连接失败,重新连接中...", 0, 7, 'red', tab_name=self.tab_name) - self.w2t(f"", 0, 7, 'red', tab_name=self.tab_name) - sleep(1.5) + self.w2t(f"{_id} 心跳丢失,连接失败,重新连接中...", 0, 7, 'red', tab_name=self.tab_name) + sleep(2) # with open(f"{current_path}/../assets/templates/c_msg.log", "w", encoding='utf-8') as f: # for msg in self.c_msg: # f.write(str(loads(msg)) + '\n') @@ -566,7 +565,7 @@ class HmiRequest(object): if flg == 0: # for old protocols req = None try: - with open(f'{current_path}/../assets/templates/{command}.json', encoding='utf-8', + with open(f'{current_path}/../assets/templates/json/{command}.json', encoding='utf-8', mode='r') as f_json: req = load(f_json) except: @@ -602,8 +601,7 @@ class HmiRequest(object): self.c.send(self.package(cmd)) sleep(0.5) except Exception as Err: - # self.w2t(f"{cmd}\n请求发送失败...{Err}", 0, 0, 'red', tab_name=self.tab_name) - self.w2t(f"", 0, 0, 'red', tab_name=self.tab_name) + self.w2t(f"{cmd}\n请求发送失败...{Err}", 0, 0, 'red', tab_name=self.tab_name) return req['id'] diff --git a/aio/code/profile/__init__.py b/aio/code/profile/__init__.py deleted file mode 100644 index 7824c48..0000000 --- a/aio/code/profile/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__all__ = ['do_profile'] \ No newline at end of file diff --git a/aio/code/profile/do_profile.py b/aio/code/profile/do_profile.py deleted file mode 100644 index e69de29..0000000