diff --git a/1257-18A.sln b/1257-18A.sln
new file mode 100644
index 0000000..522366a
--- /dev/null
+++ b/1257-18A.sln
@@ -0,0 +1,141 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.5.33530.505
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JinYuan.Helper", "JinYuan.Helper\JinYuan.Helper.csproj", "{258D0AB7-2B4F-4D8F-A3CD-7A8CB4A85360}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JinYuan.VirtualDataLibrary", "JinYuan.VirtualDataLibrary\JinYuan.VirtualDataLibrary.csproj", "{2AB7E380-7795-448A-933C-0F6A081B5E7E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JinYuan.Models", "JinYuan.Models\JinYuan.Models.csproj", "{4C0BCB84-5CD7-4846-8363-C4C10D46E391}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JinYuan.ControlCenters", "JinYuan.ControlCenter\JinYuan.ControlCenters.csproj", "{1D0FA5A4-07F0-4621-B6BB-683AEC91E658}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JinYuan.DAL", "JinYuan.DAL\JinYuan.DAL.csproj", "{114AA012-29C9-418C-A2D6-3173C5341F51}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JinYuan.MES", "JinYuan.MES\JinYuan.MES.csproj", "{530B6426-08E7-461C-91DC-787F876620FC}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LargeSquareOne", "LargeSquareOne\LargeSquareOne.csproj", "{29E1F0D0-43DC-4077-A136-F25477E56881}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PLC", "PLC\PLC.csproj", "{486AECD0-C2BF-43C5-B7D7-88E565ADE4B7}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JinYuan.ControlLib", "JinYuan.ControlLib\JinYuan.ControlLib.csproj", "{1BB7ED33-D2CB-4CDD-AB39-F7F9AE948CA2}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Language", "Language\Language.csproj", "{292A5C9C-7A82-4D25-A885-DDCEDC93F03C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IOT.Services", "IOT.Services\IOT.Services.csproj", "{242400F4-9B01-4AEE-A6F7-05083757FCDD}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "解决方案项", "解决方案项", "{9FA3D6BD-1EC1-3BA5-80CB-CE02773A58D5}"
+ ProjectSection(SolutionItems) = preProject
+ 数据库\gluingmachineoftabflatteningandwelding_a.sql = 数据库\gluingmachineoftabflatteningandwelding_a.sql
+ 数据库\hbg_cupdataturnover.sql = 数据库\hbg_cupdataturnover.sql
+ TextFile1.txt = TextFile1.txt
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {258D0AB7-2B4F-4D8F-A3CD-7A8CB4A85360}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {258D0AB7-2B4F-4D8F-A3CD-7A8CB4A85360}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {258D0AB7-2B4F-4D8F-A3CD-7A8CB4A85360}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {258D0AB7-2B4F-4D8F-A3CD-7A8CB4A85360}.Debug|x64.Build.0 = Debug|Any CPU
+ {258D0AB7-2B4F-4D8F-A3CD-7A8CB4A85360}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {258D0AB7-2B4F-4D8F-A3CD-7A8CB4A85360}.Release|Any CPU.Build.0 = Release|Any CPU
+ {258D0AB7-2B4F-4D8F-A3CD-7A8CB4A85360}.Release|x64.ActiveCfg = Release|Any CPU
+ {258D0AB7-2B4F-4D8F-A3CD-7A8CB4A85360}.Release|x64.Build.0 = Release|Any CPU
+ {2AB7E380-7795-448A-933C-0F6A081B5E7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2AB7E380-7795-448A-933C-0F6A081B5E7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2AB7E380-7795-448A-933C-0F6A081B5E7E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {2AB7E380-7795-448A-933C-0F6A081B5E7E}.Debug|x64.Build.0 = Debug|Any CPU
+ {2AB7E380-7795-448A-933C-0F6A081B5E7E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2AB7E380-7795-448A-933C-0F6A081B5E7E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2AB7E380-7795-448A-933C-0F6A081B5E7E}.Release|x64.ActiveCfg = Release|Any CPU
+ {2AB7E380-7795-448A-933C-0F6A081B5E7E}.Release|x64.Build.0 = Release|Any CPU
+ {4C0BCB84-5CD7-4846-8363-C4C10D46E391}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4C0BCB84-5CD7-4846-8363-C4C10D46E391}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4C0BCB84-5CD7-4846-8363-C4C10D46E391}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {4C0BCB84-5CD7-4846-8363-C4C10D46E391}.Debug|x64.Build.0 = Debug|Any CPU
+ {4C0BCB84-5CD7-4846-8363-C4C10D46E391}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4C0BCB84-5CD7-4846-8363-C4C10D46E391}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4C0BCB84-5CD7-4846-8363-C4C10D46E391}.Release|x64.ActiveCfg = Release|Any CPU
+ {4C0BCB84-5CD7-4846-8363-C4C10D46E391}.Release|x64.Build.0 = Release|Any CPU
+ {1D0FA5A4-07F0-4621-B6BB-683AEC91E658}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1D0FA5A4-07F0-4621-B6BB-683AEC91E658}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1D0FA5A4-07F0-4621-B6BB-683AEC91E658}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {1D0FA5A4-07F0-4621-B6BB-683AEC91E658}.Debug|x64.Build.0 = Debug|Any CPU
+ {1D0FA5A4-07F0-4621-B6BB-683AEC91E658}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1D0FA5A4-07F0-4621-B6BB-683AEC91E658}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1D0FA5A4-07F0-4621-B6BB-683AEC91E658}.Release|x64.ActiveCfg = Release|Any CPU
+ {1D0FA5A4-07F0-4621-B6BB-683AEC91E658}.Release|x64.Build.0 = Release|Any CPU
+ {114AA012-29C9-418C-A2D6-3173C5341F51}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {114AA012-29C9-418C-A2D6-3173C5341F51}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {114AA012-29C9-418C-A2D6-3173C5341F51}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {114AA012-29C9-418C-A2D6-3173C5341F51}.Debug|x64.Build.0 = Debug|Any CPU
+ {114AA012-29C9-418C-A2D6-3173C5341F51}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {114AA012-29C9-418C-A2D6-3173C5341F51}.Release|Any CPU.Build.0 = Release|Any CPU
+ {114AA012-29C9-418C-A2D6-3173C5341F51}.Release|x64.ActiveCfg = Release|Any CPU
+ {114AA012-29C9-418C-A2D6-3173C5341F51}.Release|x64.Build.0 = Release|Any CPU
+ {530B6426-08E7-461C-91DC-787F876620FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {530B6426-08E7-461C-91DC-787F876620FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {530B6426-08E7-461C-91DC-787F876620FC}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {530B6426-08E7-461C-91DC-787F876620FC}.Debug|x64.Build.0 = Debug|Any CPU
+ {530B6426-08E7-461C-91DC-787F876620FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {530B6426-08E7-461C-91DC-787F876620FC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {530B6426-08E7-461C-91DC-787F876620FC}.Release|x64.ActiveCfg = Release|Any CPU
+ {530B6426-08E7-461C-91DC-787F876620FC}.Release|x64.Build.0 = Release|Any CPU
+ {29E1F0D0-43DC-4077-A136-F25477E56881}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {29E1F0D0-43DC-4077-A136-F25477E56881}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {29E1F0D0-43DC-4077-A136-F25477E56881}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {29E1F0D0-43DC-4077-A136-F25477E56881}.Debug|x64.Build.0 = Debug|Any CPU
+ {29E1F0D0-43DC-4077-A136-F25477E56881}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {29E1F0D0-43DC-4077-A136-F25477E56881}.Release|Any CPU.Build.0 = Release|Any CPU
+ {29E1F0D0-43DC-4077-A136-F25477E56881}.Release|x64.ActiveCfg = Release|Any CPU
+ {29E1F0D0-43DC-4077-A136-F25477E56881}.Release|x64.Build.0 = Release|Any CPU
+ {486AECD0-C2BF-43C5-B7D7-88E565ADE4B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {486AECD0-C2BF-43C5-B7D7-88E565ADE4B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {486AECD0-C2BF-43C5-B7D7-88E565ADE4B7}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {486AECD0-C2BF-43C5-B7D7-88E565ADE4B7}.Debug|x64.Build.0 = Debug|Any CPU
+ {486AECD0-C2BF-43C5-B7D7-88E565ADE4B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {486AECD0-C2BF-43C5-B7D7-88E565ADE4B7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {486AECD0-C2BF-43C5-B7D7-88E565ADE4B7}.Release|x64.ActiveCfg = Release|Any CPU
+ {486AECD0-C2BF-43C5-B7D7-88E565ADE4B7}.Release|x64.Build.0 = Release|Any CPU
+ {1BB7ED33-D2CB-4CDD-AB39-F7F9AE948CA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1BB7ED33-D2CB-4CDD-AB39-F7F9AE948CA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1BB7ED33-D2CB-4CDD-AB39-F7F9AE948CA2}.Debug|x64.ActiveCfg = Debug|x64
+ {1BB7ED33-D2CB-4CDD-AB39-F7F9AE948CA2}.Debug|x64.Build.0 = Debug|x64
+ {1BB7ED33-D2CB-4CDD-AB39-F7F9AE948CA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1BB7ED33-D2CB-4CDD-AB39-F7F9AE948CA2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1BB7ED33-D2CB-4CDD-AB39-F7F9AE948CA2}.Release|x64.ActiveCfg = Release|x64
+ {1BB7ED33-D2CB-4CDD-AB39-F7F9AE948CA2}.Release|x64.Build.0 = Release|x64
+ {292A5C9C-7A82-4D25-A885-DDCEDC93F03C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {292A5C9C-7A82-4D25-A885-DDCEDC93F03C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {292A5C9C-7A82-4D25-A885-DDCEDC93F03C}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {292A5C9C-7A82-4D25-A885-DDCEDC93F03C}.Debug|x64.Build.0 = Debug|Any CPU
+ {292A5C9C-7A82-4D25-A885-DDCEDC93F03C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {292A5C9C-7A82-4D25-A885-DDCEDC93F03C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {292A5C9C-7A82-4D25-A885-DDCEDC93F03C}.Release|x64.ActiveCfg = Release|Any CPU
+ {292A5C9C-7A82-4D25-A885-DDCEDC93F03C}.Release|x64.Build.0 = Release|Any CPU
+ {242400F4-9B01-4AEE-A6F7-05083757FCDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {242400F4-9B01-4AEE-A6F7-05083757FCDD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {242400F4-9B01-4AEE-A6F7-05083757FCDD}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {242400F4-9B01-4AEE-A6F7-05083757FCDD}.Debug|x64.Build.0 = Debug|Any CPU
+ {242400F4-9B01-4AEE-A6F7-05083757FCDD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {242400F4-9B01-4AEE-A6F7-05083757FCDD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {242400F4-9B01-4AEE-A6F7-05083757FCDD}.Release|x64.ActiveCfg = Release|Any CPU
+ {242400F4-9B01-4AEE-A6F7-05083757FCDD}.Release|x64.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ RESX_AutoCreateNewLanguageFiles = False
+ RESX_SortFileContentOnSave = True
+ RESX_NeutralResourcesLanguage = zh-Hans
+ SolutionGuid = {E8421996-BAC4-4509-9C3E-BC424197F2D8}
+ EndGlobalSection
+EndGlobal
diff --git a/BTW/LabellingMachineA2.btw b/BTW/LabellingMachineA2.btw
new file mode 100644
index 0000000..b3feb2e
Binary files /dev/null and b/BTW/LabellingMachineA2.btw differ
diff --git a/IOT.Services/App.config b/IOT.Services/App.config
new file mode 100644
index 0000000..62f6cc9
--- /dev/null
+++ b/IOT.Services/App.config
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/IOT.Services/Config.xml b/IOT.Services/Config.xml
new file mode 100644
index 0000000..a0fbd14
--- /dev/null
+++ b/IOT.Services/Config.xml
@@ -0,0 +1,3207 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/IOT.Services/IOT.Services.csproj b/IOT.Services/IOT.Services.csproj
new file mode 100644
index 0000000..6da8971
--- /dev/null
+++ b/IOT.Services/IOT.Services.csproj
@@ -0,0 +1,127 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {242400F4-9B01-4AEE-A6F7-05083757FCDD}
+ WinExe
+ IOT.Services
+ IOT.Services
+ v4.8
+ 512
+ true
+ true
+
+ publish\
+ true
+ Disk
+ false
+ Foreground
+ 7
+ Days
+ false
+ false
+ true
+ 0
+ 1.0.0.%2a
+ false
+ false
+ true
+ 10.0
+
+
+ AnyCPU
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+ False
+ Lib\DataConvertLib.dll
+
+
+ False
+ Lib\Newtonsoft.Json.dll
+
+
+ ..\packages\NLog.6.0.2\lib\net46\NLog.dll
+
+
+ False
+ Lib\PLCCommunication.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Component
+
+
+ Service1.cs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
+ False
+ Microsoft .NET Framework 4.8 %28x86 和 x64%29
+ true
+
+
+ False
+ .NET Framework 3.5 SP1
+ false
+
+
+
+
\ No newline at end of file
diff --git a/IOT.Services/Lib/Newtonsoft.Json.dll b/IOT.Services/Lib/Newtonsoft.Json.dll
new file mode 100644
index 0000000..7af125a
Binary files /dev/null and b/IOT.Services/Lib/Newtonsoft.Json.dll differ
diff --git a/IOT.Services/Lib/PLCCommunication.dll b/IOT.Services/Lib/PLCCommunication.dll
new file mode 100644
index 0000000..1993b11
Binary files /dev/null and b/IOT.Services/Lib/PLCCommunication.dll differ
diff --git a/IOT.Services/Logger.cs b/IOT.Services/Logger.cs
new file mode 100644
index 0000000..4d6a8d7
--- /dev/null
+++ b/IOT.Services/Logger.cs
@@ -0,0 +1,135 @@
+using NLog;
+using System;
+using System.IO;
+using System.Timers;
+
+
+public class Logger
+{
+
+
+ private static readonly NLog.Logger loginfo = LogManager.GetLogger("loginfo");
+ private static readonly NLog.Logger logerror = LogManager.GetLogger("logerror");
+ private static readonly NLog.Logger logmqtt = LogManager.GetLogger("logmqtt");
+
+ private static readonly NLog.Logger logsInfo = LogManager.GetLogger("logsInfo");
+ private static readonly NLog.Logger logsMEStIME = LogManager.GetLogger("logsMEStIME");
+ private static readonly NLog.Logger logsSql = LogManager.GetLogger("logsSql");
+ private static readonly NLog.Logger logsSysErrorLog = LogManager.GetLogger("logsSysErrorLog");
+ private static readonly NLog.Logger logsSysErrorLogcn = LogManager.GetLogger("logsSysErrorLogcn");
+ private static readonly Timer cleanupTimer;
+ private const int MAX_DAYS = 7;
+ private const string LOG_BASE_PATH = "Log";
+
+ static Logger()
+ {
+ try
+ {
+ // 创建一个每天执行一次的定时器
+ cleanupTimer = new Timer(24 * 60 * 60 * 1000); // 24小时
+ cleanupTimer.Elapsed += CleanupOldLogs;
+ cleanupTimer.Start();
+
+ // 启动时执行一次清理
+ CleanupOldLogs(null, null);
+ }
+ catch (Exception ex)
+ {
+ WriteError("Failed to initialize logger cleanup timer", ex);
+ }
+ }
+
+ public static void WriteInfo(string info)
+ {
+ if (loginfo.IsInfoEnabled)
+ {
+ loginfo.Info(info);
+ }
+ }
+
+ public static void WriteMqtt(string info)
+ {
+ if (logmqtt.IsInfoEnabled)
+ {
+ logmqtt.Info(info);
+ }
+ }
+
+ public static void WriteError(string error)
+ {
+ if (logerror.IsErrorEnabled)
+ {
+ logerror.Error(error);
+ }
+ }
+
+ public static void WriteError(string info, Exception ex)
+ {
+ if (logerror.IsErrorEnabled)
+ {
+ logerror.Error(info, ex);
+ }
+ }
+
+ private static void CleanupOldLogs(object sender, ElapsedEventArgs e)
+ {
+ try
+ {
+ if (!Directory.Exists(LOG_BASE_PATH))
+ return;
+
+ string[] logTypes = { "LogError", "LogInfo", "LogMqtt" };
+ DateTime cutoffDate = DateTime.Now.AddDays(-MAX_DAYS);
+
+ foreach (string logType in logTypes)
+ {
+ string logTypeDir = Path.Combine(LOG_BASE_PATH, logType);
+ if (!Directory.Exists(logTypeDir))
+ continue;
+
+ // 获取所有日期文件夹
+ string[] dateDirs = Directory.GetDirectories(logTypeDir);
+ foreach (string dateDir in dateDirs)
+ {
+ string dirName = Path.GetFileName(dateDir);
+ if (DateTime.TryParseExact(dirName, "yyyyMMdd", null,
+ System.Globalization.DateTimeStyles.None, out DateTime dirDate))
+ {
+ if (dirDate < cutoffDate)
+ {
+ try
+ {
+ Directory.Delete(dateDir, true);
+ WriteInfo($"Deleted old log directory: {dateDir}");
+ }
+ catch (Exception ex)
+ {
+ WriteError($"Failed to delete log directory: {dateDir}", ex);
+ }
+ }
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ WriteError("Error during log cleanup", ex);
+ }
+ }
+
+ // 提供手动清理方法
+ public static void ForceCleanupOldLogs()
+ {
+ CleanupOldLogs(null, null);
+ }
+
+ // 在应用程序退出时调用此方法
+ public static void Shutdown()
+ {
+ if (cleanupTimer != null)
+ {
+ cleanupTimer.Stop();
+ cleanupTimer.Dispose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/IOT.Services/Models.cs b/IOT.Services/Models.cs
new file mode 100644
index 0000000..97e069c
--- /dev/null
+++ b/IOT.Services/Models.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Collections.Generic;
+
+namespace WindowsService1
+{
+ #region 1、系统内部
+ public enum AcquisitionType
+ {
+ PPM,
+ Axis,
+ Cylinder,
+ LRobot
+ }
+
+ public enum PLCDataType
+ {
+ Short,
+ Float
+ }
+
+ public class StationData
+ {
+ public string Name { get; set; }
+ public object MainValue { get; set; }
+ public DateTime Timestamp { get; set; }
+ }
+
+ public class TypeConfig
+ {
+ public string Name { get; set; }
+ public string Address { get; set; }
+ public string DataType { get; set; }
+ }
+
+
+
+ public class CollectionGroupConfig
+ {
+ public string Name { get; set; }
+ public string StartAddress { get; set; }
+ public int Length { get; set; }
+ public string DefaultDataType { get; set; }
+ public Dictionary Types { get; set; }
+ }
+ #endregion
+
+ #region 2、PPM点位
+ #endregion
+
+ #region 3、预测性点位
+ #endregion
+
+ #region 4、机械臂
+ //直角坐标机械臂
+ public class LRobot
+ {
+ public string bu_id { get; set; }
+ public string district_id { get; set; }
+ public string factory_id { get; set; }
+ public string production_line_id { get; set; }
+ public string production_processes_id { get; set; }
+ public string work_center_id { get; set; }
+ public string station_id { get; set; }
+ public string device_name { get; set; } = "601PWM01";
+ public string robot_name { get; set; }
+ public List taglist { get; set; }
+ }
+ public class Taglist
+ {
+ public string device_time { get; set; }
+ public Collection collections_items { get; set; }
+
+ }
+ public class Collection
+ {
+ public string robot_status { get; set; } = "1";
+ public string x_value { get; set; }
+ public string y_value { get; set; }
+ public string z_value { get; set; }
+ public string time_span { get; set; } = "2.5";
+ public string hand_status { get; set; } = "1";
+
+ }
+ #endregion
+}
diff --git a/IOT.Services/MqttClientWrapper.cs b/IOT.Services/MqttClientWrapper.cs
new file mode 100644
index 0000000..1e77e14
--- /dev/null
+++ b/IOT.Services/MqttClientWrapper.cs
@@ -0,0 +1,376 @@
+using Newtonsoft.Json.Linq;
+using PLCCommunication;
+using PLCCommunication.LogNet;
+using PLCCommunication.MQTT;
+using System;
+using System.Text;
+using System.Threading.Tasks;
+using System.Xml.Linq;
+
+namespace WindowsService1
+{
+ public class MqttClientWrapper
+ {
+ private MqttClient mqttClient;
+ private bool mqttIsConnected;
+ private const int MaxReconnectAttempts = 5;
+ private const int ReconnectDelay = 5000; // 5 seconds
+
+ private MqttConnectionOptions options;
+ public event Action OnMessageReceived;
+ public long ReceiveCount { get; private set; }
+
+ public PLCCommunication.LogNet.ILogNet LogNet = new LogNetDateTime("D:\\APILog\\Logs\\MQTTLogs", GenerateMode.ByEveryDay, 20);
+
+ public enum MessageFormat
+ {
+ Binary,
+ Text,
+ Xml,
+ Json
+ }
+
+
+ public MqttClientWrapper(string ipAddress, int port, string clientId, MqttCredential credentials)
+ {
+ InitializeMqttOptions(ipAddress, port, clientId, credentials);
+ }
+
+ private void InitializeMqttOptions(string ipAddress, int port, string clientId, MqttCredential credentials)
+ {
+ options = new MqttConnectionOptions
+ {
+ IpAddress = ipAddress,
+ Port = port,
+ ClientId = clientId,
+ Credentials = credentials,
+ UseRSAProvider = false,
+ CleanSession = false,
+ UseSSL = false,
+ SSLSecure = false,
+ CertificateFile = "",
+ KeepAlivePeriod = TimeSpan.FromSeconds(30),
+ KeepAliveSendInterval = TimeSpan.FromSeconds(10),
+ ConnectTimeout = 5,
+ WillMessage = null
+ };
+ }
+
+ public async Task InitializeAndConnectAsync()
+ {
+ try
+ {
+ mqttClient = new MqttClient(options);
+ Logger.WriteMqtt("MQTT 初始化成功");
+
+ await ConnectMqttAsync();
+ }
+ catch (Exception ex)
+ {
+ Logger.WriteError($"MQTT 初始化失败: {ex.Message}");
+ }
+ }
+
+ private async Task ConnectMqttAsync()
+ {
+ try
+ {
+ JYResult connect = await mqttClient.ConnectServerAsync();
+ if (connect.IsSuccess)
+ {
+ mqttIsConnected = true;
+ Logger.WriteMqtt("MQTT 连接服务器成功");
+ }
+ else
+ {
+ Logger.WriteError("MQTT 无法连接到服务器");
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.WriteError($"MQTT 无法连接到服务器: {ex.Message}");
+ }
+ }
+
+
+ public async Task SendMessageAsync(string source, string publishTopic, string type, JObject messageObj)
+ {
+ if (!mqttClient.IsConnected)
+ {
+ mqttIsConnected = false;
+
+ Logger.WriteMqtt("客户端未连接到服务器。正在尝试重新连接...");
+
+ await ReconnectWithRetry();
+ if (!mqttClient.IsConnected)
+ {
+ throw new InvalidOperationException("无法与 MQTT 代理建立连接.");
+ }
+ }
+ try
+ {
+ if (messageObj != null)
+ {
+ string json = messageObj.ToString();
+ //string json = JsonConvert.SerializeObject(messageObj);
+ JYResult result = await mqttClient.PublishMessageAsync(new MqttApplicationMessage
+ {
+ Topic = publishTopic,
+ QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce,
+ Payload = Encoding.UTF8.GetBytes(json),
+ Retain = false,
+ });
+ if (result.IsSuccess)
+ {
+ if (Service1.SwitchLog)
+ {
+ Logger.WriteMqtt($"线程 [{source}] 发送 [{type}] 消息到主题 [{publishTopic}]: {json}");
+ }
+ }
+ else
+ {
+ Logger.WriteMqtt($"线程 [{source}] 发送 [{type}] 消息到主题 [{publishTopic}] 失败: {result.Message}");
+
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.WriteError($"发送消息错误: {ex.Message}");
+ }
+ }
+
+
+ public async Task SendMessageAsync(string source, string publishTopic, string type, string messageObj)
+ {
+ if (!mqttClient.IsConnected)
+ {
+ mqttIsConnected = false;
+
+ Logger.WriteMqtt("客户端未连接到服务器。正在尝试重新连接...");
+
+
+ await ReconnectWithRetry();
+ if (!mqttClient.IsConnected)
+ {
+ throw new InvalidOperationException("无法与 MQTT 代理建立连接.");
+ }
+ }
+ try
+ {
+ if (messageObj != null)
+ {
+ JYResult result = await mqttClient.PublishMessageAsync(new MqttApplicationMessage
+ {
+ Topic = publishTopic,
+ QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce,
+ Payload = Encoding.UTF8.GetBytes(messageObj),
+ Retain = false,
+ });
+
+ if (result.IsSuccess)
+ {
+ if (Service1.SwitchLog)
+ {
+ Logger.WriteMqtt($"线程 [{source}] 发送 [{type}] 消息到主题 [{publishTopic}]: {messageObj}");
+ }
+ }
+ else
+ {
+ Logger.WriteMqtt($"线程 [{source}] 发送 [{type}] 消息到主题 [{publishTopic}] 失败: {result.Message}");
+
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.WriteError($"发送消息错误: {ex.Message}");
+ }
+ }
+
+
+
+ public void SubscribeMessage(string topic, MessageFormat format = MessageFormat.Text, bool truncateLongMessage = false)
+ {
+ if (!mqttIsConnected)
+ {
+ ConnectMqttAsync().Wait();
+ }
+
+ try
+ {
+ JYResult operateResult = mqttClient.SubscribeMessage(new string[] { topic });
+ if (!operateResult.IsSuccess)
+ {
+ Logger.WriteMqtt($"订阅消息失败: {operateResult.Message}");
+ return;
+ }
+
+ SubscribeTopic subscribeTopic = mqttClient.GetSubscribeTopic(topic);
+ if (subscribeTopic != null)
+ {
+ subscribeTopic.OnMqttMessageReceived += (sender, args) =>
+ {
+ HandleReceivedMessage(args.Topic, args.Payload, format, truncateLongMessage);
+ };
+ Logger.WriteMqtt($"成功订阅主题: {topic}");
+ }
+ else
+ {
+ Logger.WriteMqtt($"无法获取订阅主题 {topic}");
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.WriteError($"订阅主题时出错 {topic}: {ex.Message}");
+ }
+ }
+
+ private void HandleReceivedMessage(string topic, byte[] payload, MessageFormat format, bool truncateLongMessage)
+ {
+ ReceiveCount++;
+ string text = string.Empty;
+
+ try
+ {
+ switch (format)
+ {
+ case MessageFormat.Binary:
+ text = BitConverter.ToString(payload).Replace("-", " ");
+ break;
+ case MessageFormat.Text:
+ text = Encoding.UTF8.GetString(payload);
+ break;
+ case MessageFormat.Xml:
+ try
+ {
+ text = XElement.Parse(Encoding.UTF8.GetString(payload)).ToString();
+ }
+ catch
+ {
+ text = Encoding.UTF8.GetString(payload);
+ }
+ break;
+ case MessageFormat.Json:
+ try
+ {
+ text = JObject.Parse(Encoding.UTF8.GetString(payload)).ToString();
+ }
+ catch
+ {
+ text = Encoding.UTF8.GetString(payload);
+ }
+ break;
+ }
+ //if (Service1.SwitchLog)
+ //{
+ // LogNet.WriteDebug($"收到主题 {topic}: {text}");
+ //}
+ //if (truncateLongMessage && text.Length > 200)
+ //{
+ // text = text.Substring(0, 200) + "...";
+ //}
+
+ string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
+ OnMessageReceived?.Invoke(timestamp, topic, text);
+ }
+ catch (Exception ex)
+ {
+ Logger.WriteError($"处理收到的消息时出错: {ex.Message}");
+ }
+ }
+
+ public void UnsubscribeMessage(string topic)
+ {
+ try
+ {
+ JYResult operateResult = mqttClient.UnSubscribeMessage(new string[] { topic });
+ if (operateResult.IsSuccess)
+ {
+ Logger.WriteMqtt($"成功取消订阅主题: {topic}");
+ }
+ else
+ {
+ Logger.WriteMqtt($"取消订阅主题失败 {topic}: {operateResult.Message}");
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.WriteError($"取消订阅主题时出错 {topic}: {ex.Message}");
+ }
+ }
+
+ ///
+ /// 重连
+ ///
+ ///
+ private async Task ReconnectWithRetry()
+ {
+ int attempts = 0;
+ while (!mqttClient.IsConnected && attempts < MaxReconnectAttempts)
+ {
+ try
+ {
+ await mqttClient.ConnectServerAsync();
+ Logger.WriteMqtt("重新连接成功.");
+
+ return;
+ }
+ catch (Exception ex)
+ {
+ attempts++;
+ Logger.WriteMqtt($"重新连接尝试 {attempts} 失败的: {ex.Message}");
+ await Task.Delay(ReconnectDelay);
+ }
+ }
+ if (!mqttClient.IsConnected)
+ {
+ Logger.WriteMqtt("尝试次数达到上限后仍无法重新连接.");
+ }
+ }
+
+
+ private async Task ReconnectAsync()
+ {
+ Logger.WriteMqtt("正在尝试重新连接...");
+ mqttIsConnected = false;
+ int retryCount = 0;
+ const int maxRetries = 5;
+ const int retryDelayMs = 5000;
+
+ while (!mqttIsConnected && retryCount < maxRetries)
+ {
+ try
+ {
+ await ConnectMqttAsync();
+ if (mqttIsConnected)
+ {
+ Logger.WriteMqtt("重新连接成功");
+ return;
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.WriteMqtt($"重新连接尝试 {retryCount + 1} 失败的: {ex.Message}");
+ }
+
+ retryCount++;
+ await Task.Delay(retryDelayMs);
+ }
+
+ if (!mqttIsConnected)
+ {
+ Logger.WriteMqtt("多次尝试后仍无法重新连接");
+ }
+ }
+
+
+
+ public async Task Disconnect()
+ {
+ await mqttClient.ConnectCloseAsync();
+ mqttIsConnected = false;
+ Logger.WriteMqtt("与 MQTT服务端断开连接!!!");
+ }
+ }
+}
diff --git a/IOT.Services/MqttDataService.cs b/IOT.Services/MqttDataService.cs
new file mode 100644
index 0000000..e6efb3c
--- /dev/null
+++ b/IOT.Services/MqttDataService.cs
@@ -0,0 +1,730 @@
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace WindowsService1
+{
+ public class MqttDataService
+ {
+
+ public static string BU_id;
+ public static string District_id;
+ public static string Factory_id;
+ public static string Production_line_id;
+ public static string Device_name;
+ public static string Topic;
+ public static string Work_Center_id;
+ public class OperationData
+ {
+ public int Item_Value { get; set; }
+ public string Start_Time { get; set; }
+ public string End_Time { get; set; }
+ }
+
+
+ public class MqttMessage
+ {
+ public string bu_id { get; set; }
+ public string district_id { get; set; }
+ public string factory_id { get; set; }
+ public string production_line_id { get; set; }
+ public string production_processes_id { get; set; }
+ public string work_center_id { get; set; }
+ public string station_id { get; set; }
+ public string device_name { get; set; }
+ public List taglist { get; set; }
+ }
+
+ public class TagList
+ {
+ public string device_time { get; set; }
+ public Dictionary collection_items { get; set; }
+ }
+
+
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static MqttMessage CreateMqttMessage(string source, AcquisitionType type, List processedValues,
+ Dictionary> _plcAddresses,
+ Dictionary _collectionGroups)
+ {
+ try
+ {
+ Logger.WriteInfo($"开始创建 MQTT 消息, 类型: {type}, 数据点数量: {processedValues?.Count ?? 0}");
+
+ if (processedValues == null || processedValues.Count == 0)
+ {
+ Logger.WriteInfo("没有需要处理的数据,返回 null");
+ return null;
+ }
+
+ var addresses = _plcAddresses[type];
+ var config = _collectionGroups[type];
+ var collectionItems = new Dictionary();
+
+ // 获取当前时间作为设备时间
+ var currentTime = DateTime.Now;
+ var deviceTimeStr = currentTime.ToString("yyyy-MM-dd HH:mm:ss.fff");
+
+ Logger.WriteInfo($"处理数据类型: {type}, 当前时间: {deviceTimeStr}");
+
+ switch (type)
+ {
+ case AcquisitionType.PPM:
+ ProcessPPMData(processedValues, addresses, config, collectionItems);
+ break;
+
+ case AcquisitionType.Axis:
+ case AcquisitionType.Cylinder:
+ ProcessFloatData(processedValues, addresses, config, currentTime, collectionItems);
+ break;
+
+ default:
+ Logger.WriteError($"未知的数据类型: {type}");
+ return null;
+ }
+
+ if (collectionItems.Count == 0)
+ {
+ Logger.WriteInfo("没有有效的采集项,返回 null");
+ return null;
+ }
+
+ var message = new MqttMessage
+ {
+ bu_id = BU_id,
+ district_id = District_id,
+ factory_id = Factory_id,
+ production_line_id = Production_line_id,
+ production_processes_id = string.Empty,
+ work_center_id = Work_Center_id,
+ station_id = "1",
+ device_name = Device_name,
+ taglist = new List
+ {
+ new TagList
+ {
+ device_time = deviceTimeStr,
+ collection_items = collectionItems
+ }
+ }
+ };
+
+ // 记录创建的消息内容
+ Logger.WriteInfo($"MQTT 消息创建完成: {JsonConvert.SerializeObject(message, Formatting.None)}");
+
+ return message;
+ }
+ catch (Exception ex)
+ {
+ Logger.WriteError($"创建 MQTT 消息时发生错误: {ex.Message}");
+ return null;
+ }
+ }
+
+ ///
+ /// PPM点位数据
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ // 添加静态字段来存储上次发送的数据
+ private static Dictionary _lastSentData = new Dictionary();
+
+ public static void ProcessPPMData(List processedValues, Dictionary addresses, CollectionGroupConfig config, Dictionary collectionItems)
+ {
+ try
+ {
+ Logger.WriteInfo($"开始处理 PPM 数据,数据点数量: {processedValues.Count}");
+
+ // 创建临时字典存储当前所有数据点
+ var tempItems = new Dictionary();
+
+ foreach (var value in processedValues)
+ {
+ var addressPair = addresses.FirstOrDefault(x => x.Key == value.Name);
+ if (!string.IsNullOrEmpty(addressPair.Key))
+ {
+ int itemValue = Convert.ToInt32(ConvertToIntegerWithThreeDecimals(value.MainValue));
+
+ // 计算开始时间 (结束时间 - 时长)
+ var startTime = value.Timestamp.AddMilliseconds(-itemValue);
+
+ var operationData = new OperationData
+ {
+ Item_Value = itemValue,
+ Start_Time = startTime.ToString("yyyy-MM-dd HH:mm:ss.fff"),
+ End_Time = value.Timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff")
+ };
+
+ // 检查数据是否发生变化
+ bool shouldAdd = true;
+
+ // 检查是否与上次发送的数据重复
+ if (_lastSentData.TryGetValue(addressPair.Key, out var lastData))
+ {
+ var currentEndTime = operationData.End_Time.ToString();
+ var lastEndTime = lastData.End_Time.ToString();
+
+ if (string.Equals(lastEndTime, currentEndTime, StringComparison.OrdinalIgnoreCase))
+ {
+ shouldAdd = false;
+ Logger.WriteInfo($"跳过重复数据点: {addressPair.Key}");
+ continue;
+ }
+ }
+
+ if (shouldAdd)
+ {
+ tempItems[addressPair.Key] = operationData;
+ Logger.WriteInfo($"处理数据点: {addressPair.Key}, 值: {itemValue}, " +
+ $"开始时间: {startTime:yyyy-MM-dd HH:mm:ss.fff}, " +
+ $"结束时间: {operationData.End_Time}");
+ }
+ //if (shouldAdd)
+ //{
+ // // 检查是否与当前批次的其他数据有重复的结束时间
+ // var duplicateEndTimeItem = tempItems.FirstOrDefault(x =>
+ // string.Equals(x.Value.End_Time.ToString(), operationData.End_Time.ToString(), StringComparison.OrdinalIgnoreCase));
+
+ // if (duplicateEndTimeItem.Key != null)
+ // {
+ // Logger.WriteError($"检测到结束时间重复: {operationData.End_Time} 对应地址: {addressPair.Key}, " +
+ // $"与地址 {duplicateEndTimeItem.Key} 的数据重复");
+
+ // // 根据地址的字母顺序决定哪个需要调整时间,确保调整的一致性
+ // if (string.Compare(addressPair.Key, duplicateEndTimeItem.Key) > 0)
+ // {
+ // operationData.End_Time = value.Timestamp.AddMilliseconds(1).ToString("yyyy-MM-dd HH:mm:ss.fff");
+ // Logger.WriteInfo($"调整后的结束时间: {operationData.End_Time}");
+ // }
+ // }
+
+ // tempItems[addressPair.Key] = operationData;
+ // Logger.WriteInfo($"处理数据点: {addressPair.Key}, 值: {itemValue}, " +
+ // $"开始时间: {startTime:yyyy-MM-dd HH:mm:ss.fff}, " +
+ // $"结束时间: {operationData.End_Time}");
+ //}
+ }
+ else
+ {
+ Logger.WriteError($"未找到数据点 {value.Name} 对应的地址配置");
+ }
+ }
+
+ // 将处理后的数据添加到最终结果并更新上次发送的数据
+ foreach (var item in tempItems)
+ {
+ collectionItems[item.Key] = item.Value;
+ _lastSentData[item.Key] = item.Value; // 更新上次发送的数据
+ }
+
+ Logger.WriteInfo($"PPM 数据处理完成,处理后的数据点数量: {collectionItems.Count}");
+ }
+ catch (Exception ex)
+ {
+ Logger.WriteError($"处理 PPM 数据时出错: {ex.Message}");
+ throw;
+ }
+ }
+
+ private static bool IsDataEqual(OperationData data1, OperationData data2, string addressKey)
+ {
+ if (data1 == null || data2 == null)
+ return false;
+
+ bool isEqual = data1.Item_Value == data2.Item_Value &&
+ data1.Start_Time == data2.Start_Time &&
+ data1.End_Time == data2.End_Time;
+
+ if (isEqual)
+ {
+ Logger.WriteInfo($"检测到重复数据 - 地址: {addressKey}, " +
+ $"值: {data1.Item_Value}, " +
+ $"开始时间: {data1.Start_Time}, " +
+ $"结束时间: {data1.End_Time}");
+ }
+
+ return isEqual;
+ }
+
+
+
+ ///
+ /// 预测性点位
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static void ProcessFloatData(List processedValues, Dictionary addresses, CollectionGroupConfig config, DateTime endTime, Dictionary collectionItems)
+ {
+ for (int i = 0; i < processedValues.Count && i < config.Length; i++)
+ {
+ string address = $"D{int.Parse(config.StartAddress.Substring(1)) + i * 2}";
+ if (addresses.ContainsValue(address))
+ {
+ var key = addresses.FirstOrDefault(x => x.Value == address).Key;
+ int itemValue = Convert.ToInt32(ConvertToIntegerWithThreeDecimals(processedValues[i].MainValue));
+
+ // 计算开始时间 (结束时间 - 时长)
+ //var startTime = endTime.AddMilliseconds(-itemValue);
+
+ collectionItems[key] = new OperationData
+ {
+ Item_Value = (int)itemValue,
+ Start_Time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
+ End_Time = endTime.ToString("yyyy-MM-dd HH:mm:ss.fff")
+ };
+ }
+ }
+ }
+
+ ///
+ /// 机械臂数据
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static LRobot LRobotMessage(string source, AcquisitionType type, List processedValues, Dictionary> _plcAddresses,
+ Dictionary _collectionGroups)
+ {
+ if (processedValues == null || processedValues.Count == 0)
+ {
+ return null;
+ }
+ var addresses = _plcAddresses[type];
+ var config = _collectionGroups[type];
+ return new LRobot()
+ {
+ bu_id = BU_id,
+ district_id = District_id,
+ factory_id = Factory_id,
+ production_line_id = Production_line_id,
+ production_processes_id = "",
+ work_center_id = "",
+ station_id = "",
+ device_name = "601PWM01",
+ robot_name = "LUR01",
+ taglist = new List
+ {
+ new Taglist//上料动子1
+ {
+ device_time=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
+ collections_items=new Collection()
+ {
+ robot_status = "1",
+ x_value=processedValues[0].MainValue.ToString(),
+ y_value="0",
+ z_value=processedValues[1].MainValue.ToString(),
+ time_span="2.5",
+ hand_status="1",
+ }
+ },
+ new Taglist//上料动子2
+ {
+ device_time=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
+ collections_items=new Collection()
+ {
+ robot_status = "1",
+ x_value=processedValues[2].MainValue.ToString(),
+ y_value="0",
+ z_value=processedValues[3].MainValue.ToString(),
+ time_span="2.5",
+ hand_status="1",
+ }
+ },
+ new Taglist//上料动子3
+ {
+ device_time=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
+ collections_items=new Collection()
+ {
+ robot_status = "1",
+ x_value=processedValues[4].MainValue.ToString(),
+ y_value="0",
+ z_value=processedValues[5].MainValue.ToString(),
+ time_span="2.5",
+ hand_status="1",
+ }
+ },
+ new Taglist//上料动子4
+ {
+ device_time=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
+ collections_items=new Collection()
+ {
+ robot_status = "1",
+ x_value=processedValues[6].MainValue.ToString(),
+ y_value="0",
+ z_value=processedValues[7].MainValue.ToString(),
+ time_span="2.5",
+ hand_status="1",
+ }
+ },
+ new Taglist//上料动子5
+ {
+ device_time=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
+ collections_items=new Collection()
+ {
+ robot_status = "1",
+ x_value=processedValues[8].MainValue.ToString(),
+ y_value="0",
+ z_value=processedValues[9].MainValue.ToString(),
+ time_span="2.5",
+ hand_status="1",
+ }
+ },
+ new Taglist//上料动子6
+ {
+ device_time=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
+ collections_items=new Collection()
+ {
+ robot_status = "1",
+ x_value=processedValues[10].MainValue.ToString(),
+ y_value="0",
+ z_value=processedValues[11].MainValue.ToString(),
+ time_span="2.5",
+ hand_status="1",
+ }
+ },
+ new Taglist//上料动子7
+ {
+ device_time=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
+ collections_items=new Collection()
+ {
+ robot_status = "1",
+ x_value=processedValues[13].MainValue.ToString(),
+ y_value="0",
+ z_value=processedValues[14].MainValue.ToString(),
+ time_span="2.5",
+ hand_status="1",
+ }
+ },
+ new Taglist//上料动子8
+ {
+ device_time=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
+ collections_items=new Collection()
+ {
+ robot_status = "1",
+ x_value=processedValues[16].MainValue.ToString(),
+ y_value="0",
+ z_value=processedValues[17].MainValue.ToString(),
+ time_span="2.5",
+ hand_status="1",
+ }
+ },
+ new Taglist//上料动子9
+ {
+ device_time=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
+ collections_items=new Collection()
+ {
+ robot_status = "1",
+ x_value=processedValues[18].MainValue.ToString(),
+ y_value="0",
+ z_value=processedValues[19].MainValue.ToString(),
+ time_span="2.5",
+ hand_status="1",
+ }
+ },
+ new Taglist//焊机1
+ {
+ device_time=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
+ collections_items=new Collection()
+ {
+ robot_status = "1",
+ x_value=processedValues[20].MainValue.ToString(),
+ y_value=processedValues[21].MainValue.ToString(),
+ z_value=processedValues[22].MainValue.ToString(),
+ time_span="2.5",
+ hand_status="1",
+ }
+ },
+ new Taglist//焊机2
+ {
+ device_time=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
+ collections_items=new Collection()
+ {
+ robot_status = "1",
+ x_value=processedValues[23].MainValue.ToString(),
+ y_value=processedValues[24].MainValue.ToString(),
+ z_value=processedValues[25].MainValue.ToString(),
+ time_span="2.5",
+ hand_status="1",
+ }
+ },
+ new Taglist//焊机3
+ {
+ device_time=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
+ collections_items=new Collection()
+ {
+ robot_status = "1",
+ x_value=processedValues[26].MainValue.ToString(),
+ y_value=processedValues[27].MainValue.ToString(),
+ z_value=processedValues[28].MainValue.ToString(),
+ time_span="2.5",
+ hand_status="1",
+ }
+ },
+ new Taglist//焊机4
+ {
+ device_time=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
+ collections_items=new Collection()
+ {
+ robot_status = "1",
+ x_value=processedValues[29].MainValue.ToString(),
+ y_value=processedValues[30].MainValue.ToString(),
+ z_value=processedValues[31].MainValue.ToString(),
+ time_span="2.5",
+ hand_status="1",
+ }
+ },
+ new Taglist//焊机5
+ {
+ device_time=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
+ collections_items=new Collection()
+ {
+ robot_status = "1",
+ x_value=processedValues[32].MainValue.ToString(),
+ y_value=processedValues[33].MainValue.ToString(),
+ z_value=processedValues[34].MainValue.ToString(),
+ time_span="2.5",
+ hand_status="1",
+ }
+ },
+ new Taglist//焊机6
+ {
+ device_time=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
+ collections_items=new Collection()
+ {
+ robot_status = "1",
+ x_value=processedValues[35].MainValue.ToString(),
+ y_value=processedValues[36].MainValue.ToString(),
+ z_value=processedValues[37].MainValue.ToString(),
+ time_span="2.5",
+ hand_status="1",
+ }
+ },
+ new Taglist//焊机7
+ {
+ device_time=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
+ collections_items=new Collection()
+ {
+ robot_status = "1",
+ x_value=processedValues[38].MainValue.ToString(),
+ y_value=processedValues[39].MainValue.ToString(),
+ z_value=processedValues[40].MainValue.ToString(),
+ time_span="2.5",
+ hand_status="1",
+ }
+ },
+ new Taglist//焊机8
+ {
+ device_time=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
+ collections_items=new Collection()
+ {
+ robot_status = "1",
+ x_value=processedValues[41].MainValue.ToString(),
+ y_value=processedValues[42].MainValue.ToString(),
+ z_value=processedValues[43].MainValue.ToString(),
+ time_span="2.5",
+ hand_status="1",
+ }
+ },
+ new Taglist//焊机9
+ {
+ device_time=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
+ collections_items=new Collection()
+ {
+ robot_status = "1",
+ x_value=processedValues[44].MainValue.ToString(),
+ y_value=processedValues[45].MainValue.ToString(),
+ z_value=processedValues[46].MainValue.ToString(),
+ time_span="2.5",
+ hand_status="1",
+ }
+ },
+ new Taglist//焊机10
+ {
+ device_time=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
+ collections_items=new Collection()
+ {
+ robot_status = "1",
+ x_value=processedValues[47].MainValue.ToString(),
+ y_value=processedValues[48].MainValue.ToString(),
+ z_value=processedValues[49].MainValue.ToString(),
+ time_span="2.5",
+ hand_status="1",
+ }
+ },
+ new Taglist//焊机11
+ {
+ device_time=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
+ collections_items=new Collection()
+ {
+ robot_status = "1",
+ x_value=processedValues[50].MainValue.ToString(),
+ y_value=processedValues[51].MainValue.ToString(),
+ z_value=processedValues[52].MainValue.ToString(),
+ time_span="2.5",
+ hand_status="1",
+ }
+ },
+ new Taglist//焊机12
+ {
+ device_time=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
+ collections_items=new Collection()
+ {
+ robot_status = "1",
+ x_value=processedValues[53].MainValue.ToString(),
+ y_value=processedValues[54].MainValue.ToString(),
+ z_value=processedValues[55].MainValue.ToString(),
+ time_span="2.5",
+ hand_status="1",
+ }
+ },
+ new Taglist//下料动子1
+ {
+ device_time=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
+ collections_items=new Collection()
+ {
+ robot_status = "1",
+ x_value=processedValues[56].MainValue.ToString(),
+ y_value="0",
+ z_value=processedValues[57].MainValue.ToString(),
+ time_span="2.5",
+ hand_status="1",
+ }
+ },
+ new Taglist//下料动子2
+ {
+ device_time=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
+ collections_items=new Collection()
+ {
+ robot_status = "1",
+ x_value=processedValues[58].MainValue.ToString(),
+ y_value="0",
+ z_value=processedValues[59].MainValue.ToString(),
+ time_span="2.5",
+ hand_status="1",
+ }
+ },
+ new Taglist//下料动子3
+ {
+ device_time=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
+ collections_items=new Collection()
+ {
+ robot_status = "1",
+ x_value=processedValues[61].MainValue.ToString(),
+ y_value="0",
+ z_value=processedValues[62].MainValue.ToString(),
+ time_span="2.5",
+ hand_status="1",
+ }
+ },
+ new Taglist//下料动子4
+ {
+ device_time=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
+ collections_items=new Collection()
+ {
+ robot_status = "1",
+ x_value=processedValues[63].MainValue.ToString(),
+ y_value="0",
+ z_value=processedValues[64].MainValue.ToString(),
+ time_span="2.5",
+ hand_status="1",
+ }
+ },
+ new Taglist//下料动子5
+ {
+ device_time=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
+ collections_items=new Collection()
+ {
+ robot_status = "1",
+ x_value=processedValues[65].MainValue.ToString(),
+ y_value="0",
+ z_value=processedValues[66].MainValue.ToString(),
+ time_span="2.5",
+ hand_status="1",
+ }
+ },
+ new Taglist//下料动子6
+ {
+ device_time=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
+ collections_items=new Collection()
+ {
+ robot_status = "1",
+ x_value=processedValues[67].MainValue.ToString(),
+ y_value="0",
+ z_value=processedValues[68].MainValue.ToString(),
+ time_span="2.5",
+ hand_status="1",
+ }
+ }
+
+ }
+
+ };
+ }
+
+
+
+
+ // 辅助方法
+ private static List ConvertToShortList(object[] data)
+ {
+ return data.Select(x => Convert.ToInt16(x)).ToList();
+ }
+
+ private static List ConvertToFloatList(object[] data)
+ {
+ return data.Select(x => Convert.ToSingle(x)).ToList();
+ }
+
+
+ private static object ConvertToIntegerWithThreeDecimals(object value)
+ {
+ try
+ {
+ if (value == null) return 0;
+
+ if (value is float floatValue)
+ {
+ return (int)Math.Round(floatValue * 1000);
+ }
+ else if (value is double doubleValue)
+ {
+ return (int)Math.Round(doubleValue * 1000);
+ }
+ else if (value is decimal decimalValue)
+ {
+ return (int)Math.Round(decimalValue * 1000);
+ }
+ else
+ {
+ // 尝试转换其他类型
+ float converted = Convert.ToSingle(value);
+ return (int)Math.Round(converted * 1000);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.WriteError($"转换值到整数时出错: {ex.Message}, 值: {value}");
+ return 0;
+ }
+ }
+ }
+}
diff --git a/IOT.Services/NLog.config b/IOT.Services/NLog.config
new file mode 100644
index 0000000..4ca60d8
--- /dev/null
+++ b/IOT.Services/NLog.config
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/IOT.Services/PLCDataReader.cs b/IOT.Services/PLCDataReader.cs
new file mode 100644
index 0000000..4bd584b
--- /dev/null
+++ b/IOT.Services/PLCDataReader.cs
@@ -0,0 +1,155 @@
+using JinYuan.DataConvertLib;
+using PLCCommunication.PLCType.Omron;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace WindowsService1
+{
+
+
+ ///
+ /// 定义每个数据读取请求的参数。
+ ///
+ public class PLCDataReadRequest
+ {
+ ///
+ /// PLC起始地址
+ ///
+ public string StartAddress { get; set; }
+ ///
+ /// 读取长度
+ ///
+ public ushort Length { get; set; }
+ ///
+ /// 读取区域名称
+ ///
+ public string StorageName { get; set; }
+ ///
+ /// 读取数据类型
+ ///
+ public PLCDataType DataType { get; set; }
+ }
+
+ ///
+ /// 存储每次读取操作的结果。
+ ///
+ public class PLCDataResult
+ {
+ ///
+ /// 读取区域名称
+ ///
+ public string StorageName { get; set; }
+ ///
+ /// 读取数据类型
+ ///
+ public PLCDataType DataType { get; set; }
+ ///
+ /// 数据
+ ///
+ public byte[] Data { get; set; }
+ ///
+ /// 是否读取成功
+ ///
+ public bool IsSuccess { get; set; }
+ ///
+ /// 读取失败的错误信息
+ ///
+ public string ErrorMessage { get; set; }
+ }
+
+ public class PLCDataReader
+ {
+ private readonly OmronFinsNet _plc;
+
+
+ public PLCDataReader(OmronFinsNet plc)
+ {
+ _plc = plc ?? throw new ArgumentNullException(nameof(plc));
+ }
+
+ public async Task ReadDataAsync(PLCDataReadRequest request)
+ {
+ try
+ {
+ var data = await _plc.ReadAsync(request.StartAddress, request.Length);
+ if (data.IsSuccess && data.Content.Length > 0)
+ {
+ Logger.WriteInfo($"成功读取PLC数据,起始地址:{request.StartAddress},长度:{request.Length},类型:{request.DataType}");
+ return new PLCDataResult
+ {
+ StorageName = request.StorageName,
+ DataType = request.DataType,
+ Data = data.Content,
+ IsSuccess = true
+ };
+ }
+ else
+ {
+ Logger.WriteInfo($"读取PLC数据失败,起始地址:{request.StartAddress},长度:{request.Length},类型:{request.DataType}");
+ return new PLCDataResult
+ {
+ StorageName = request.StorageName,
+ DataType = request.DataType,
+ IsSuccess = false,
+ ErrorMessage = "读取PLC数据失败"
+ };
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.WriteError($"读取PLC数据时发生错误:{ex.Message}");
+ return new PLCDataResult
+ {
+ StorageName = request.StorageName,
+ DataType = request.DataType,
+ IsSuccess = false,
+ ErrorMessage = ex.Message
+ };
+ }
+ }
+
+ private object[] ParseData(byte[] content, PLCDataType dataType)
+ {
+ switch (dataType)
+ {
+ case PLCDataType.Short:
+ return ParseShortData(content);
+ case PLCDataType.Float:
+ return ParseFloatData(content);
+ default:
+ throw new ArgumentException($"不支持的数据类型:{dataType}");
+ }
+ }
+
+ private object[] ParseShortData(byte[] content)
+ {
+ int count = content.Length / 2;
+ return Enumerable.Range(0, count)
+ .Select(i => (object)ShortLib.GetShortFromByteArray(content, i * 2))
+ .ToArray();
+ }
+
+ private object[] ParseFloatData(byte[] content)
+ {
+ int count = content.Length / 4;
+ return Enumerable.Range(0, count)
+ .Select(i => (object)FloatLib.GetFloatFromByteArray(content, i * 4))
+ .ToArray();
+ }
+ }
+
+ public static class ConvertHelper
+ {
+ ///
+ /// 字节数组高低位转换
+ ///
+ ///
+ ///
+ public static byte[] ByteReverse(this byte[] Arrbyte)
+ {
+ byte[] ArrByte = Arrbyte.Select((x, i) => new { x, i }).GroupBy(x => x.i / 2).SelectMany(x => new byte[] { x.Last().x, x.First().x }).ToArray();
+ return ArrByte;
+ }
+ }
+}
diff --git a/IOT.Services/PLCWrapper.cs b/IOT.Services/PLCWrapper.cs
new file mode 100644
index 0000000..f0d3889
--- /dev/null
+++ b/IOT.Services/PLCWrapper.cs
@@ -0,0 +1,104 @@
+using PLCCommunication.PLCType.Omron;
+using System;
+using System.Threading.Tasks;
+
+namespace WindowsService1
+{
+ public class PLCWrapper
+ {
+ private OmronFinsNet _plc;
+ public bool _plcIsConnected;
+ private readonly string _ipAddress;
+ private readonly int _port;
+ private readonly int _maxReconnectAttempts;
+ private readonly int _reconnectDelay;
+ public PLCDataReader _dataReader;
+ public PLCWrapper(string ipAddress, int port, int maxReconnectAttempts = 3, int reconnectDelay = 5000)
+ {
+ _ipAddress = ipAddress;
+ _port = port;
+ _maxReconnectAttempts = maxReconnectAttempts;
+ _reconnectDelay = reconnectDelay;
+ }
+
+ public async Task ConnectAsync()
+ {
+ try
+ {
+ _plc = new OmronFinsNet
+ {
+ IpAddress = _ipAddress,
+ Port = _port
+ };
+ var connectResult = await Task.Run(() => _plc.ConnectServer());
+ _plcIsConnected = connectResult.IsSuccess;
+ if (_plcIsConnected)
+ {
+ _dataReader = new PLCDataReader(GetPLCClient());
+ Logger.WriteInfo("PLC连接成功");
+ }
+ else
+ {
+ Logger.WriteInfo("PLC连接失败");
+ }
+ return _plcIsConnected;
+ }
+ catch (Exception ex)
+ {
+ Logger.WriteError($"PLC连接失败: {ex.Message}");
+ return false;
+ }
+ }
+
+ public void Disconnect()
+ {
+ if (_plcIsConnected)
+ {
+ _plc.ConnectClose();
+ _plcIsConnected = false;
+ Logger.WriteInfo("与PLC断开连接");
+ }
+ }
+
+ public OmronFinsNet GetPLCClient()
+ {
+ if (!_plcIsConnected || _plc == null)
+ {
+ throw new InvalidOperationException("PLC 未连接或未初始化。请先调用 ConnectAsync()。");
+ }
+ return _plc;
+ }
+
+ public async Task ReconnectAsync()
+ {
+ if (_plcIsConnected)
+ {
+ Logger.WriteInfo("PLC已连接,无需重连");
+ return true;
+ }
+
+ for (int attempt = 1; attempt <= _maxReconnectAttempts; attempt++)
+ {
+ Logger.WriteInfo($"尝试重新连接PLC,第 {attempt} 次尝试");
+ if (await ConnectAsync())
+ {
+ Logger.WriteInfo("PLC重新连接成功");
+ return true;
+ }
+
+ if (attempt < _maxReconnectAttempts)
+ {
+ await Task.Delay(_reconnectDelay);
+ }
+ }
+
+ Logger.WriteError($"PLC重新连接失败,已尝试 {_maxReconnectAttempts} 次");
+ return false;
+ }
+
+ public bool IsConnected()
+ {
+ return _plcIsConnected;
+ }
+ }
+}
\ No newline at end of file
diff --git a/IOT.Services/Program.cs b/IOT.Services/Program.cs
new file mode 100644
index 0000000..aeeb614
--- /dev/null
+++ b/IOT.Services/Program.cs
@@ -0,0 +1,20 @@
+using System.ServiceProcess;
+
+namespace WindowsService1
+{
+ internal static class Program
+ {
+ ///
+ /// 应用程序的主入口点。
+ ///
+ static void Main()
+ {
+ ServiceBase[] ServicesToRun;
+ ServicesToRun = new ServiceBase[]
+ {
+ new Service1()
+ };
+ ServiceBase.Run(ServicesToRun);
+ }
+ }
+}
diff --git a/IOT.Services/Properties/AssemblyInfo.cs b/IOT.Services/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..004aa2d
--- /dev/null
+++ b/IOT.Services/Properties/AssemblyInfo.cs
@@ -0,0 +1,35 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// 有关程序集的一般信息由以下
+// 控制。更改这些特性值可修改
+// 与程序集关联的信息。
+[assembly: AssemblyTitle("WindowsService1")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("WindowsService1")]
+[assembly: AssemblyCopyright("Copyright © 2023")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// 将 ComVisible 设置为 false 会使此程序集中的类型
+//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型
+//请将此类型的 ComVisible 特性设置为 true。
+[assembly: ComVisible(false)]
+
+// 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID
+[assembly: Guid("242400f4-9b01-4aee-a6f7-05083757fcdd")]
+
+// 程序集的版本信息由下列四个值组成:
+//
+// 主版本
+// 次版本
+// 生成号
+// 修订号
+//
+//可以指定所有这些值,也可以使用“生成号”和“修订号”的默认值
+//通过使用 "*",如下所示:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/IOT.Services/Service1.Designer.cs b/IOT.Services/Service1.Designer.cs
new file mode 100644
index 0000000..b7b9f85
--- /dev/null
+++ b/IOT.Services/Service1.Designer.cs
@@ -0,0 +1,38 @@
+namespace WindowsService1
+{
+ partial class Service1
+ {
+ ///
+ /// 必需的设计器变量。
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// 清理所有正在使用的资源。
+ ///
+ /// 如果应释放托管资源,为 true;否则为 false。
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ Logger.Shutdown();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region 组件设计器生成的代码
+
+ ///
+ /// 设计器支持所需的方法 - 不要修改
+ /// 使用代码编辑器修改此方法的内容。
+ ///
+ private void InitializeComponent()
+ {
+ components = new System.ComponentModel.Container();
+ this.ServiceName = "Service1";
+ }
+
+ #endregion
+ }
+}
diff --git a/IOT.Services/Service1.cs b/IOT.Services/Service1.cs
new file mode 100644
index 0000000..e347afc
--- /dev/null
+++ b/IOT.Services/Service1.cs
@@ -0,0 +1,1179 @@
+using JinYuan.DataConvertLib;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using PLCCommunication.MQTT;
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+using System.Data;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.ServiceProcess;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml.Linq;
+
+namespace WindowsService1
+{
+ public partial class Service1 : ServiceBase
+ {
+ private readonly List _tasks = new List();
+ private readonly CancellationTokenSource _cts = new CancellationTokenSource();
+
+ private PLCWrapper _plcWrapper;
+ private MqttClientWrapper mqttClient = null;
+ private System.Threading.Timer _periodicTimer;
+
+ private Dictionary _collectionGroups;
+ private Dictionary> _plcAddresses;
+
+ private AcquisitionType[] AcqTypes = null;
+
+ private const int ReconnectionInterval = 5000; // 5 seconds
+ private const int MaxReconnectAttempts = 3;
+
+ public string PlcIpAddress = "127.0.0.1";
+ public int PlcPort = 9600;
+
+
+ public string MQTTServerAddress = "127.0.0.1";
+ public int MQTTServerPort = 1883;
+ public string ClientId = "601UPW01";
+ //PPM主题
+ public string Topic = "EVE/60J/EQP/NEW_PPM/601UPW01";
+ //PPM其他数据
+ public string Topic1 = "eve/60J/EQP/601UPW01";
+ //直角坐标机械臂
+ public string LRobotTopic = "EVE/60J/LRobot/601PWM01_LUR01";
+ //关节机械臂
+ public string JRobotTopic = "EVE/60J/JRobot/601PWM01_STR01";
+
+ public MqttCredential credentials = null;
+
+ public string BU_id = "EVE2BU";
+ public string District_id = "QJ";
+ public string Factory_id = "60J";
+ public string Production_line_id = "MW-601";
+ public string Device_name = "601UPW01";
+
+ public int ThreadInterval = 50;
+ public static bool SwitchLog = false;
+
+ public Service1()
+ {
+ InitializeComponent();
+ ServiceName = "TestMqttService";
+ CanStop = true; // 服务可以被停止
+ CanPauseAndContinue = false;
+ AutoLog = true;
+
+
+ }
+
+ protected override void OnStart(string[] args)
+ {
+ Logger.WriteInfo($"OnStart:服务启动-{DateTime.Now}");
+ InitializeConfig();
+ _plcWrapper = new PLCWrapper(PlcIpAddress, PlcPort);
+ InitializeTask();
+ InitializePLC();
+ InitializeMQTT();
+
+ Task.Run(() => RunChangeDetectionTaskAsync(_cts.Token));// 启动变化检测任务
+
+
+ Logger.WriteInfo($"OnStart:服务启动完毕-{DateTime.Now}");
+ }
+
+ protected override async void OnStop()
+ {
+ _cts.Cancel();
+ _periodicTimer?.Dispose();
+ // 停止 PLC 读取
+ if (_plcWrapper != null)
+ {
+ _plcWrapper.Disconnect();
+ Logger.WriteInfo("PLC 通讯停止.");
+ }
+ if (mqttClient != null)
+ {
+ await mqttClient.Disconnect();
+ Logger.WriteInfo("MQTT 连接已关闭.");
+ }
+ Logger.WriteInfo("OnStop");
+ }
+
+ ///
+ /// 系统配置文件读取
+ ///
+ private void InitializeConfig()
+ {
+ ReadConfigFromXml();
+ PlcIpAddress = ConfigurationManager.AppSettings["PlcIpAddress"] ?? "127.0.0.1";
+ PlcPort = int.Parse(ConfigurationManager.AppSettings["PlcPort"] ?? "9600");
+
+ MQTTServerAddress = ConfigurationManager.AppSettings["MQTTServerAddress"] ?? "127.0.0.1";
+ MQTTServerPort = int.Parse(ConfigurationManager.AppSettings["MQTTServerPort"] ?? "1883");
+ ClientId = ConfigurationManager.AppSettings["ClientId"] ?? "601UPW01";
+
+ Topic = ConfigurationManager.AppSettings["Topic"] ?? "EVE/60J/EQP/NEW_PPM/601UPW01";
+ Topic1 = ConfigurationManager.AppSettings["Topic1"] ?? "eve/60J/EQP/601UPW01";
+ LRobotTopic = ConfigurationManager.AppSettings["LRobotTopic"] ?? "EVE/60J/LRobot/601PWM01_LUR01";
+ JRobotTopic = ConfigurationManager.AppSettings["JRobotTopic"] ?? "EVE/60J/JRobot/601PWM01_STR01";
+
+ BU_id = ConfigurationManager.AppSettings["BU_id"] ?? "601UPW01";
+ District_id = ConfigurationManager.AppSettings["District_id"] ?? "QJ";
+ Factory_id = ConfigurationManager.AppSettings["Factory_id"] ?? "60J";
+ Production_line_id = ConfigurationManager.AppSettings["Production_line_id"] ?? "MW-601";
+ Device_name = ConfigurationManager.AppSettings["Device_name"] ?? "601UPW01";
+ ThreadInterval = int.Parse(ConfigurationManager.AppSettings["ThreadInterval"] ?? "50");
+ SwitchLog = bool.Parse(ConfigurationManager.AppSettings["SwitchLog"] ?? "True");
+ MqttDataService.BU_id = BU_id;
+ MqttDataService.District_id = District_id;
+ MqttDataService.Factory_id = Factory_id;
+ MqttDataService.Production_line_id = Production_line_id;
+ MqttDataService.Device_name = Device_name;
+ MqttDataService.Work_Center_id = "601FGE02";
+
+ Logger.WriteInfo("读取系统配置文件成功!");
+
+
+ }
+
+ private void ReadConfigFromXml()
+ {
+ try
+ {
+ var configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config.xml");
+ var doc = XDocument.Load(configPath);
+ string deviceName = "";
+ _plcAddresses = new Dictionary>();
+ _collectionGroups = new Dictionary();
+
+ foreach (var deviceElement in doc.Descendants("Device"))
+ {
+ var isEnabled = bool.Parse(deviceElement.Attribute("isEnable")?.Value ?? "false");
+
+ if (isEnabled)
+ {
+ deviceName = deviceElement.Attribute("name")?.Value;
+
+ foreach (var groupElement in deviceElement.Elements("CollectionGroup"))
+ {
+ var groupName = groupElement.Attribute("name").Value;
+ Logger.WriteInfo($"添加寄存器组类型: {groupName}");
+
+ var acquisitionType = (AcquisitionType)Enum.Parse(typeof(AcquisitionType), groupName);
+ var groupConfig = new CollectionGroupConfig
+ {
+ Name = groupName,
+ StartAddress = groupElement.Attribute("StartAddress").Value,
+ Length = int.Parse(groupElement.Attribute("length").Value),
+ DefaultDataType = groupElement.Attribute("DataType")?.Value ?? "Short",
+ Types = new Dictionary()
+ };
+
+ var groupAddresses = new Dictionary();
+
+ foreach (var typeElement in groupElement.Elements("Type"))
+ {
+ var typeName = typeElement.Attribute("name").Value;
+ var typeAddress = typeElement.Attribute("address")?.Value ?? typeElement.Value;
+ var dataType = typeElement.Attribute("DataType")?.Value ?? groupConfig.DefaultDataType;
+
+ // 创建TypeConfig对象
+ var typeConfig = new TypeConfig
+ {
+ Name = typeName,
+ Address = typeAddress,
+ DataType = dataType
+ };
+
+ groupConfig.Types[typeName] = typeConfig;
+ groupAddresses[typeName] = typeAddress;
+
+ Logger.WriteInfo($"添加类型: {typeName}, 地址: {typeAddress}, 数据类型: {dataType}");
+ }
+
+ _collectionGroups[acquisitionType] = groupConfig;
+ _plcAddresses[acquisitionType] = groupAddresses;
+ }
+ break; // 只处理第一个启用的Device
+ }
+ }
+
+ Logger.WriteInfo($"读取 【{deviceName}】地址文件成功!!!");
+ }
+ catch (Exception ex)
+ {
+ Logger.WriteError($"读取配置文件错误: {ex.Message}");
+ }
+ }
+
+
+
+
+ private static PLCDataType DetermineDataType(string groupName, string dataTypeAttribute)
+ {
+ if (!string.IsNullOrEmpty(dataTypeAttribute))
+ {
+ return (PLCDataType)Enum.Parse(typeof(PLCDataType), dataTypeAttribute);
+ }
+ if (!string.IsNullOrEmpty(groupName))
+ {
+ AcquisitionType type = (AcquisitionType)Enum.Parse(typeof(AcquisitionType), groupName);
+ // 如果 DataType 属性为空,根据组名推断类型
+ switch (type)
+ {
+ case AcquisitionType.PPM:
+ return PLCDataType.Short;
+ case AcquisitionType.Axis:
+ case AcquisitionType.Cylinder:
+ return PLCDataType.Float;
+ default:
+ throw new ArgumentException($"未知的组名: {groupName}");
+ }
+
+ }
+ else
+ {
+ throw new ArgumentException($"未知的组名: {groupName}");
+ }
+ }
+
+
+ #region 初始化Task线程,PLC,MQTT
+ private void InitializeTask()
+ {
+ if (_collectionGroups != null && _collectionGroups.Count > 0)
+ {
+ List typesList = new List();
+ foreach (var kvp in _collectionGroups)
+ {
+ switch (kvp.Key)
+ {
+ case AcquisitionType.Axis:
+ case AcquisitionType.Cylinder:
+ StartPeriodicTask(); // 启动定时读取任务
+ break;
+ case AcquisitionType.PPM:
+ typesList.Add(kvp.Key);
+ break;
+ default:
+ break;
+ }
+ }
+
+ // 将 List 转换为数组
+ AcqTypes = typesList.ToArray();
+
+ // 如果需要,你可以在这里使用 types 数组
+ if (AcqTypes.Length > 0)
+ {
+ Logger.WriteInfo("添加类型: " + string.Join(", ", AcqTypes));
+ // 这里可以添加其他使用 types 数组的逻辑
+ }
+ }
+ }
+
+
+
+
+ private async void InitializePLC()
+ {
+ try
+ {
+ bool connected = await _plcWrapper.ConnectAsync();
+ if (connected)
+ {
+ Logger.WriteInfo("PLC初始化并连接成功.");
+ }
+ else
+ {
+ Logger.WriteInfo("无法连接 PLC.");
+ throw new Exception("PLC 连接失败");
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.WriteError($"初始化 PLC 错误: {ex.Message}");
+ }
+ }
+
+ private async void InitializeMQTT()
+ {
+ try
+ {
+ mqttClient = new MqttClientWrapper(MQTTServerAddress, MQTTServerPort, ClientId, credentials); //初始化
+ await mqttClient.InitializeAndConnectAsync();//连接MQTT
+ SubscribeTopic(Topic);//订阅主题
+ SubscribeTopic(Topic1);//订阅主题1
+ SubscribeTopic(LRobotTopic);//订阅直角坐标机械臂
+ SubscribeTopic(JRobotTopic);//订阅关机机械臂
+ }
+ catch (Exception ex)
+ {
+ Logger.WriteError($"连接 MQTT 代理时出错: {ex.Message}");
+ }
+ }
+
+ ///
+ /// 订阅主题
+ ///
+ ///
+ private void SubscribeTopic(string topicToSubscribe)
+ {
+ mqttClient.OnMessageReceived += (timestamp, topic, message) =>
+ {
+ //Console.WriteLine($"{timestamp} Topic[{topic}]: {message}");
+ //Logger.WriteMqtt($"{timestamp} Topic[{topic}]");
+ // 在这里更新你的 UI
+ };
+ mqttClient.SubscribeMessage(topicToSubscribe, MqttClientWrapper.MessageFormat.Json, true);
+ }
+
+
+ ///
+ /// 类发布消息
+ ///
+ ///
+ ///
+ ///
+ private async Task SendMqttAsync(string source, string publishTopic, string type, JObject messageObj)
+ {
+ try
+ {
+ if (publishTopic != null)
+ {
+
+ await mqttClient.SendMessageAsync(source, publishTopic, type, messageObj);
+ }
+ }
+ catch (Exception ex)
+ {
+
+ Console.WriteLine($"发布信息: {ex.Message}");
+ }
+
+ }
+
+
+ ///
+ /// 字符串发布消息
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private async Task SendMqttAsync(string source, string publishTopic, string type, string messageObj)
+ {
+ try
+ {
+ if (publishTopic != null)
+ {
+
+
+ await mqttClient.SendMessageAsync(source, publishTopic, type, messageObj);
+ }
+ }
+ catch (Exception ex)
+ {
+
+ Console.WriteLine($"发布信息: {ex.Message}");
+ }
+
+ }
+ #endregion
+
+
+ #region 创建定时线程
+ ///
+ /// 轴扭矩定时器采集线程
+ ///
+ private void StartPeriodicTask()
+ {
+ Logger.WriteInfo("启动定期数据收集任务");
+ _periodicTimer = new System.Threading.Timer(async _ =>
+ {
+ try
+ {
+ Logger.WriteInfo($"定期触发任务: {DateTime.Now}");
+ await CollectAndPublishData("AxisTask", new[] { AcquisitionType.Axis });
+ await CollectAndPublishData("AxisTask", new[] { AcquisitionType.Cylinder });
+ await CollectAndPublishData("LRobotTask", new[] { AcquisitionType.LRobot });
+ Logger.WriteInfo($"定期任务完成: {DateTime.Now}");
+ }
+ catch (Exception ex)
+ {
+ Logger.WriteError($"周期性任务执行错误: {ex.Message}");
+ }
+ }, null, TimeSpan.Zero, TimeSpan.FromSeconds(1)); // 每秒执行一次
+ }
+
+ ///
+ /// 有变化数据采集线程
+ ///
+ ///
+ ///
+ ///
+
+ private async Task RunChangeDetectionTaskAsync(CancellationToken ct)
+ {
+ while (!ct.IsCancellationRequested)
+ {
+ if (_plcWrapper._plcIsConnected)
+ {
+ try
+ {
+ await CollectAndPublishData("ChangeTask", AcqTypes);
+ await Task.Delay(500, ct);
+ }
+ catch (OperationCanceledException)
+ {
+ // 正常取消,无需额外处理
+ }
+ catch (Exception ex)
+ {
+ Logger.WriteError($"变更检测任务错误: {ex.Message}");
+ await Task.Delay(1000, ct);
+ }
+ }
+ }
+ }
+ #endregion
+
+ ///
+ /// 读取数据
+ ///
+ ///
+ ///
+ ///
+ private async Task CollectAndPublishData(string source, AcquisitionType[] types)
+ {
+ const int maxRetries = 3;
+ for (int retry = 0; retry < maxRetries; retry++)
+ {
+ try
+ {
+ var requests = new List();
+ foreach (var type in types)
+ {
+ var config = _collectionGroups[type];
+ requests.Add(new PLCDataReadRequest
+ {
+ StartAddress = config.StartAddress,
+ Length = (ushort)config.Length,
+ StorageName = config.Name
+ });
+ }
+
+ foreach (var request in requests)
+ {
+ var result = await _plcWrapper._dataReader.ReadDataAsync(request);
+ if (result.IsSuccess)
+ {
+ await ProcessPLCData(source, request.StorageName, result.Data);
+ }
+ else
+ {
+ Logger.WriteInfo($"错误读取 {result.StorageName}: {result.ErrorMessage}");
+ throw new Exception($"读取 PLC 数据失败: {result.ErrorMessage}");
+ }
+ }
+ break; // 如果成功,退出重试循环
+ }
+ catch (Exception ex)
+ {
+ Logger.WriteError($"Error {source}: {ex.Message}");
+ if (retry < maxRetries - 1)
+ {
+ Logger.WriteInfo($"尝试重新连接到 PLC,尝试 {retry + 1}");
+ bool reconnected = await AttemptReconnection(_cts.Token);
+ if (!reconnected)
+ {
+ Logger.WriteError("无法重新连接 PLC,放弃进一步尝试");
+ break;
+ }
+ }
+ else
+ {
+ Logger.WriteError($"最大重试次数 ({maxRetries}) 已到达,放弃操作");
+ }
+ }
+ }
+ }
+
+ ///
+ /// 重连PLC
+ ///
+ ///
+ ///
+ private async Task AttemptReconnection(CancellationToken ct)
+ {
+ const int MaxReconnectAttempts = 3;
+ const int ReconnectionInterval = 5000; // 5 seconds
+
+ for (int attempt = 1; attempt <= MaxReconnectAttempts; attempt++)
+ {
+ if (ct.IsCancellationRequested)
+ {
+ return false;
+ }
+
+ Logger.WriteInfo($"尝试重新连接PLC,第 {attempt} 次尝试");
+ bool reconnected = await _plcWrapper.ReconnectAsync();
+ if (reconnected)
+ {
+ Logger.WriteInfo("PLC重新连接成功");
+ return true;
+ }
+
+ if (attempt < MaxReconnectAttempts)
+ {
+ await Task.Delay(ReconnectionInterval, ct);
+ }
+ }
+ Logger.WriteError($"PLC重新连接失败,已尝试 {MaxReconnectAttempts} 次");
+ return false;
+ }
+
+
+ ///
+ /// 读取PLC数据
+ ///
+ ///
+ ///
+ ///
+ ///
+ private async Task ProcessPLCData(string source, string acquisitionTypeName, byte[] data)
+ {
+ try
+ {
+ AcquisitionType type = (AcquisitionType)Enum.Parse(typeof(AcquisitionType), acquisitionTypeName);
+ var typeConfigs = GetTypeConfigs(type).ToList();
+ bool hasChanges = false;
+ List processedValues = null;
+
+ switch (type)
+ {
+ case AcquisitionType.PPM:
+ var ppmResult = ProcessShortData(type, data, typeConfigs);
+ hasChanges = ppmResult.hasChanges;
+ processedValues = ppmResult.values;
+ break;
+
+ case AcquisitionType.Axis:
+ case AcquisitionType.Cylinder:
+ case AcquisitionType.LRobot:
+ var floatResult = ProcessFloatData(type, data, typeConfigs);
+ hasChanges = floatResult.hasChanges;
+ processedValues = floatResult.values;
+ break;
+
+ default:
+ Logger.WriteError($"未知的采集类型: {type}");
+ return;
+ }
+
+ if (hasChanges && processedValues != null)
+ {
+
+ // 上传到MQTT服务器
+ await UpMQTTServerDataNew(source, type, processedValues);
+ }
+ else
+ {
+ Logger.WriteInfo($"{type} 数据未改变,未上传");
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.WriteError($"处理 PLC 数据时出错(源: {source}, 类型: {acquisitionTypeName}): {ex.Message}");
+ }
+ }
+
+ // 相应地,ProcessFloatData方法也需要修改返回类型
+
+ private List _lastFloatStationData = new List();
+ private (bool hasChanges, List values) ProcessFloatData(AcquisitionType type, byte[] rawData, List typeConfigs)
+ {
+ bool hasChanges = false;
+ var currentValues = new DataValue[typeConfigs.Count];
+
+ // 记录实际需要处理的数据点数量
+ Logger.WriteInfo($"开始处理数据: typeConfigs.Count={typeConfigs.Count}, rawData.Length={rawData.Length}");
+ var sw = new Stopwatch();
+ sw.Restart();
+ // 初始化数组为完整的大小
+ currentValues = new DataValue[typeConfigs.Count];
+ var stationDataList = new List();
+
+ try
+ {
+ // 第一步:处理所有Float类型数据(主数据)
+ var floatConfigs = typeConfigs.Where(t => t.DataType.Equals("Float", StringComparison.OrdinalIgnoreCase)).ToList();
+ foreach (var config in floatConfigs)
+ {
+ // 从地址中提取偏移量
+ if (int.TryParse(config.Address.Substring(1), out int address))
+ {
+ int startAddress = int.Parse(typeConfigs[0].Address.Substring(1));
+ int offset = (address - startAddress) * 2;
+
+ if (offset >= 0 && offset + 4 <= rawData.Length)
+ {
+ byte[] valueBytes = new byte[4];
+ for (int j = 0; j < 4; j++)
+ {
+ valueBytes[j] = (byte)(Convert.ToInt32(rawData[offset + j]) & 0xFF);
+ }
+
+ object convertedValue = ConvertFromBytes(valueBytes, "Float");
+ int index = typeConfigs.IndexOf(config);
+ currentValues[index] = new DataValue
+ {
+ Name = config.Name,
+ Value = convertedValue,
+ DataType = config.DataType
+ };
+ }
+ }
+ }
+
+ // 第二步:处理所有Short类型数据
+ var shortConfigs = typeConfigs.Where(t => t.DataType.Equals("Short", StringComparison.OrdinalIgnoreCase)).ToList();
+ foreach (var config in shortConfigs)
+ {
+ if (int.TryParse(config.Address.Substring(1), out int address))
+ {
+ int startAddress = int.Parse(typeConfigs[0].Address.Substring(1));
+ int offset = (address - startAddress) * 2;
+
+ if (offset >= 0 && offset + 2 <= rawData.Length)
+ {
+ byte[] valueBytes = new byte[2];
+ for (int j = 0; j < 2; j++)
+ {
+ valueBytes[j] = (byte)(Convert.ToInt32(rawData[offset + j]) & 0xFF);
+ }
+
+ object convertedValue = ConvertFromBytes(valueBytes, "Short");
+ int index = typeConfigs.IndexOf(config);
+ currentValues[index] = new DataValue
+ {
+ Name = config.Name,
+ Value = convertedValue,
+ DataType = config.DataType
+ };
+ }
+ }
+ }
+
+ // 第三步:组织StationData列表
+
+ foreach (var floatConfig in floatConfigs)
+ {
+ string baseName = floatConfig.Name; // 例如 "S_MagLev"
+ var mainValue = currentValues[typeConfigs.IndexOf(floatConfig)]?.Value;
+ // 先获取MainValue,如果为0则跳过这个工位,暂时不过滤
+ //if (mainValue == null || Convert.ToDouble(mainValue) == 0)
+ //{
+ // Logger.WriteInfo($"跳过工位 {baseName}: MainValue为0或null");
+ // continue;
+ //}
+
+ // 只检查MainValue是否与上一次的值相同,暂时不检查
+ //var lastData = _lastStationData.FirstOrDefault(x => x.Name == baseName);
+ //if (lastData != null)
+ //{
+ // bool valueEqual = Convert.ToDouble(lastData.MainValue) == Convert.ToDouble(mainValue);
+
+ // if (valueEqual)
+ // {
+ // Logger.WriteInfo($"跳过工位 {baseName}: 值未变化 当前值:{mainValue}");
+ // continue;
+ // }
+
+ // Logger.WriteInfo($"工位 {baseName} 数值发生变化: {lastData.MainValue} -> {mainValue}");
+ //}
+
+ // 创建StationData对象
+ var stationData = new StationData
+ {
+ Name = baseName,
+ MainValue = mainValue,
+ Timestamp = DateTime.Now,
+ };
+
+ stationDataList.Add(stationData);
+ Logger.WriteInfo($"添加工位数据: {stationData.Name}, 值: {stationData.MainValue}, 时间戳: {stationData.Timestamp:HH:mm:ss.fff}");
+ }
+
+ // 检查是否有新数据需要更新
+ if (stationDataList.Any())
+ {
+ hasChanges = true; // 如果有数据通过过滤,就表示有变化
+
+ // 创建深拷贝以避免引用问题
+ _lastFloatStationData = stationDataList.Select(s => new StationData
+ {
+ Name = s.Name,
+ MainValue = s.MainValue,
+ Timestamp = s.Timestamp
+ }).ToList();
+
+ _lastFloatStationData = stationDataList;//把当前的新数据赋值到旧的集合中
+
+
+ //// 记录变化信息
+ //foreach (var newData in stationDataList)
+ //{
+ // Logger.WriteInfo($"工位 {newData.Name} 更新: 时间戳: {newData.Timestamp:HH:mm:ss.fff}, 值: {newData.MainValue}");
+ //}
+ }
+ else
+ {
+ Logger.WriteInfo("没有发现时间戳变化的数据");
+ hasChanges = false;
+ }
+
+
+ sw.Stop();
+ Logger.WriteInfo($"处理结束耗时:{sw.ElapsedMilliseconds}ms");
+ return (hasChanges, stationDataList);
+ }
+ catch (Exception ex)
+ {
+ Logger.WriteError($"处理数据时出错: {ex.Message}");
+ return (false, stationDataList);
+ }
+
+ }
+
+
+
+
+ ///
+ /// 检测PPM数据是否有变化
+ ///
+ ///
+ ///
+ private List _lastStationData = new List();
+
+ private readonly StationDataComparer2 _comparer = new StationDataComparer2();
+
+ private (bool hasChanges, List values) ProcessShortData(AcquisitionType type, byte[] rawData, List typeConfigs)
+ {
+ var sw = new Stopwatch();
+ sw.Start();
+ Logger.WriteInfo($"开始新一轮数据处理");
+
+ try
+ {
+ var currentStationData = new Dictionary();
+ bool hasNewData = false;
+
+ // 1. 获取所有主值配置(Float类型)
+ var mainValueConfigs = typeConfigs
+ .Where(t => string.Equals(t.DataType, "Float", StringComparison.OrdinalIgnoreCase))
+ .OrderBy(t => int.Parse(t.Address.Substring(1)))
+ .ToList();
+
+ Logger.WriteInfo($"找到 {mainValueConfigs.Count} 个Float类型配置");
+ foreach (var config in mainValueConfigs)
+ {
+ Logger.WriteInfo($"Float配置: {config.Name}, 地址: {config.Address}");
+ }
+
+ // 2. 获取所有时间配置(Short类型)
+ var timeConfigs = typeConfigs
+ .Where(t => string.Equals(t.DataType, "Short", StringComparison.OrdinalIgnoreCase))
+ .ToList();
+
+ // 3. 首先读取所有工位的当前值和时间戳
+ foreach (var mainConfig in mainValueConfigs)
+ {
+ string stationName = mainConfig.Name;
+
+ // 读取主值
+ double? mainValue = ReadFloatValue(mainConfig, rawData, typeConfigs[0].Address);
+ if (mainValue == null || mainValue <= 0)
+ {
+ Logger.WriteInfo($"工位 {stationName} 主值无效: {mainValue}");
+ continue;
+ }
+
+ // 读取时间组件
+ DateTime timestamp = ReadTimestamp(stationName, timeConfigs, rawData, typeConfigs[0].Address);
+
+ // 添加到当前数据集合
+ currentStationData[stationName] = new StationData
+ {
+ Name = stationName,
+ MainValue = mainValue,
+ Timestamp = timestamp
+ };
+
+ Logger.WriteInfo($"读取工位数据: {stationName}, 值: {mainValue}, 时间: {timestamp:HH:mm:ss.fff}");
+ }
+
+ // 4. 比较新旧数据,确定是否有变化
+ if (currentStationData.Any())
+ {
+ if (_lastStationData == null || !_lastStationData.Any())
+ {
+ hasNewData = true;
+ Logger.WriteInfo("首次数据采集,记录所有数据");
+ }
+ else
+ {
+ // 检查每个工位的数据是否有变化
+ foreach (var current in currentStationData.Values)
+ {
+ var lastData = _lastStationData.FirstOrDefault(x => x.Name == current.Name);
+ if (lastData == null)
+ {
+ hasNewData = true;
+ Logger.WriteInfo($"发现新工位数据: {current.Name}");
+ break;
+ }
+
+ // 检查值和时间戳是否都有变化
+ bool valueChanged = Math.Abs(Convert.ToDouble(lastData.MainValue) - Convert.ToDouble(current.MainValue)) > 0.001;
+ bool timeChanged = Math.Abs((current.Timestamp - lastData.Timestamp).TotalMilliseconds) >= 500;
+
+ if (valueChanged || timeChanged)
+ {
+ hasNewData = true;
+ Logger.WriteInfo($"工位 {current.Name} 数据发生变化 - 值变化: {valueChanged}, 时间变化: {timeChanged}");
+ break;
+ }
+ }
+ }
+ }
+
+ // 5. 如果有新数据,更新最后处理的数据
+ var resultList = currentStationData.Values.ToList();
+ if (hasNewData)
+ {
+ _lastStationData = new List(resultList);
+ Logger.WriteInfo($"更新状态数据,数量: {resultList.Count}");
+ }
+
+ sw.Stop();
+ Logger.WriteInfo($"处理完成,耗时: {sw.ElapsedMilliseconds}ms, 数据点数量: {resultList.Count}, 有变化: {hasNewData}");
+
+ return (hasNewData, resultList);
+ }
+ catch (Exception ex)
+ {
+ Logger.WriteError($"数据处理错误: {ex.Message}\n{ex.StackTrace}");
+ return (false, new List());
+ }
+ }
+
+ private double? ReadFloatValue(TypeConfig config, byte[] rawData, string baseAddress)
+ {
+ try
+ {
+ if (!int.TryParse(config.Address.Substring(1), out int address) ||
+ !int.TryParse(baseAddress.Substring(1), out int startAddress))
+ {
+ return null;
+ }
+
+ int offset = (address - startAddress) * 2;
+ if (offset < 0 || offset + 4 > rawData.Length)
+ {
+ return null;
+ }
+
+ byte[] valueBytes = new byte[4];
+ Array.Copy(rawData, offset, valueBytes, 0, 4);
+ var value = ConvertFromBytes(valueBytes, "Float");
+ return value != null ? Convert.ToDouble(value) : null;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ private DateTime ReadTimestamp(string stationName, List timeConfigs, byte[] rawData, string baseAddress)
+ {
+ // 获取时间组件配置
+ var hourConfig = timeConfigs.FirstOrDefault(t => t.Name == $"{stationName}_Hour");
+ var minConfig = timeConfigs.FirstOrDefault(t => t.Name == $"{stationName}_Min");
+ var secConfig = timeConfigs.FirstOrDefault(t => t.Name == $"{stationName}_Sec");
+ var msecConfig = timeConfigs.FirstOrDefault(t => t.Name == $"{stationName}_MSec");
+
+ // 读取时间值
+ int hour = ReadShortValue(hourConfig, rawData, baseAddress);
+ int minute = ReadShortValue(minConfig, rawData, baseAddress);
+ int second = ReadShortValue(secConfig, rawData, baseAddress);
+ int millisecond = ReadShortValue(msecConfig, rawData, baseAddress);
+
+ // 验证时间值
+ hour = Math.Min(Math.Max(hour, 0), 23);
+ minute = Math.Min(Math.Max(minute, 0), 59);
+ second = Math.Min(Math.Max(second, 0), 59);
+ millisecond = Math.Min(Math.Max(millisecond, 0), 999);
+
+ // 获取当前时间
+ var now = DateTime.Now;
+
+ // 构建读取的时间
+ var readTime = new DateTime(
+ now.Year,
+ now.Month,
+ now.Day,
+ hour,
+ minute,
+ second,
+ millisecond
+ );
+
+ // 如果读取的时间比当前时间大,说明应该是前一天的数据
+ if (readTime > now)
+ {
+ readTime = readTime.AddDays(-1);
+ Logger.WriteInfo($"检测到跨天数据,调整日期为前一天: {readTime:yyyy-MM-dd HH:mm:ss.fff}");
+ }
+
+ return readTime;
+ }
+
+ private int ReadShortValue(TypeConfig config, byte[] rawData, string baseAddress)
+ {
+ try
+ {
+ if (config == null || !int.TryParse(config.Address.Substring(1), out int address) ||
+ !int.TryParse(baseAddress.Substring(1), out int startAddress))
+ {
+ return 0;
+ }
+
+ int offset = (address - startAddress) * 2;
+ if (offset < 0 || offset + 2 > rawData.Length)
+ {
+ return 0;
+ }
+
+ byte[] valueBytes = new byte[2];
+ Array.Copy(rawData, offset, valueBytes, 0, 2);
+ var value = ConvertFromBytes(valueBytes, "Short");
+ return Convert.ToInt32(value);
+ }
+ catch
+ {
+ return 0;
+ }
+ }
+
+
+
+ private object ConvertFromBytes(byte[] bytes, string dataType)
+ {
+ try
+ {
+ if (bytes == null || bytes.Length == 0)
+ return null;
+
+ switch (dataType.ToLower())
+ {
+ case "bool":
+ return BitConverter.ToInt16(bytes, 0) != 0;
+ case "short":
+ return ShortLib.GetShortFromByteArray(bytes, 0, DataFormat.CDAB);
+ case "int":
+ return IntLib.GetIntFromByteArray(bytes, 0, DataFormat.CDAB);
+ case "float":
+ return FloatLib.GetFloatFromByteArray(bytes, 0, DataFormat.CDAB);
+ default:
+ return ShortLib.GetShortFromByteArray(bytes, 0, DataFormat.CDAB);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.WriteError($"转换数据类型失败: {BitConverter.ToString(bytes)} to {dataType}, {ex.Message}");
+ return null;
+ }
+ }
+
+
+
+ ///
+ /// 上传MQTT
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task UpMQTTServerDataNew(string source, AcquisitionType type, List processedValues)
+ {
+ try
+ {
+ if (processedValues == null || processedValues.Count == 0)
+ {
+ return;
+ }
+
+ //var addresses = _plcAddresses[type];
+ //var config = _collectionGroups[type];
+
+ var message = MqttDataService.CreateMqttMessage(source, type, processedValues, _plcAddresses, _collectionGroups);
+
+ if (message != null)
+ {
+ var messageJson = JsonConvert.SerializeObject(message, new JsonSerializerSettings
+ {
+ NullValueHandling = NullValueHandling.Ignore,
+ Formatting = Formatting.None
+ });
+ if (type.ToString() == "PPM")
+ {
+ await SendMqttAsync(source, Topic, type.ToString(), messageJson);
+ }
+ else if (type.ToString() == "Axis" || type.ToString() == "Cylinder")
+ {
+ await SendMqttAsync(source, Topic1, type.ToString(), messageJson);
+ }
+ }
+ if (type.ToString() == "LRobot")
+ {
+ var message1 = MqttDataService.LRobotMessage(source, type, processedValues, _plcAddresses, _collectionGroups);
+ if (message1 != null)
+ {
+ var messageJson1 = JsonConvert.SerializeObject(message1, new JsonSerializerSettings
+ {
+ NullValueHandling = NullValueHandling.Ignore,
+ Formatting = Formatting.None
+ });
+ if (type.ToString() == "LRobot")
+ {
+ await SendMqttAsync(source, LRobotTopic, type.ToString(), messageJson1);
+ }
+ }
+ }
+
+ }
+ catch (Exception ex)
+ {
+ Logger.WriteError($"方法:上传MQTTServer数据,上传失败: {ex.Message}", ex);
+ }
+ }
+
+
+
+
+ private List ConvertToShortList(object[] data)
+ {
+ return data.Select(d => Convert.ToInt16(d)).ToList();
+ }
+
+ private List ConvertToFloatList(object[] data)
+ {
+ return data.Select(d => Convert.ToSingle(d)).ToList();
+ }
+
+ // 获取特定类型的数据类型
+ public string GetDataType(AcquisitionType acquisitionType, string typeName)
+ {
+ if (_collectionGroups.TryGetValue(acquisitionType, out var groupConfig))
+ {
+ if (groupConfig.Types.TryGetValue(typeName, out var typeConfig))
+ {
+ return typeConfig.DataType;
+ }
+ }
+ return "Short"; // 默认返回Short类型
+ }
+
+ // 获取数据类型的字节大小
+ public int GetTypeSize(string dataType)
+ {
+ switch (dataType.ToLower())
+ {
+ case "bool":
+ return 2; // 在PLC中通常用2字节表示
+ case "short":
+ return 2; // 2字节
+ case "int":
+ return 4; // 4字节
+ case "float":
+ return 4; // 4字节
+ default:
+ return 2; // 默认2字节
+ }
+ }
+
+ // 获取组内所有类型配置
+ public IEnumerable GetTypeConfigs(AcquisitionType acquisitionType)
+ {
+ if (_collectionGroups.TryGetValue(acquisitionType, out var groupConfig))
+ {
+ return groupConfig.Types.Values;
+ }
+ return Enumerable.Empty();
+ }
+
+
+ }
+
+
+ ///
+ /// 时间转化
+ ///
+
+ public static class DateTimeExtensions
+ {
+ public static long ToUnixTimeMilliseconds(this DateTime dateTime)
+ {
+ return (long)(dateTime.ToUniversalTime() - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds;
+ }
+
+ public static long GetUnixTimestamp()
+ {
+ return (DateTime.Now.ToUniversalTime().Ticks - 621355968000000000) / 10000000;
+ }
+ }
+
+ // 辅助类
+ public class DataValue
+ {
+ public string Name { get; set; }
+ public object Value { get; set; }
+ public string DataType { get; set; }
+
+ public override bool Equals(object obj)
+ {
+ if (obj is DataValue other)
+ {
+ if (Value == null && other.Value == null) return true;
+ if (Value == null || other.Value == null) return false;
+
+ switch (DataType.ToLower())
+ {
+ case "float":
+ if (Value is float f1 && other.Value is float f2)
+ return Math.Abs(f1 - f2) < 0.000001f;
+ break;
+ case "bool":
+ if (Value is bool b1 && other.Value is bool b2)
+ return b1 == b2;
+ break;
+ }
+
+ return Value.Equals(other.Value);
+ }
+ return false;
+ }
+
+ public override int GetHashCode()
+ {
+ return Value?.GetHashCode() ?? 0;
+ }
+ }
+}
diff --git a/IOT.Services/StationDataComparer.cs b/IOT.Services/StationDataComparer.cs
new file mode 100644
index 0000000..0963233
--- /dev/null
+++ b/IOT.Services/StationDataComparer.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Collections.Generic;
+
+namespace WindowsService1
+{
+ ///
+ /// 判断工位耗时变化
+ ///
+ public class StationDataComparer : IEqualityComparer
+ {
+ private const float FloatEpsilon = 0.000001f; // 可以调整精度
+
+ public bool Equals(StationData x, StationData y)
+ {
+ if (ReferenceEquals(x, y)) return true;
+ if (x is null || y is null) return false;
+
+ // 比较名称
+ if (x.Name != y.Name) return false;
+
+ // 比较主值
+ if (x.MainValue is float xFloat && y.MainValue is float yFloat)
+ {
+ if (Math.Abs(xFloat - yFloat) > FloatEpsilon) return false;
+ }
+ else if (x.MainValue is double xDouble && y.MainValue is double yDouble)
+ {
+ if (Math.Abs(xDouble - yDouble) > FloatEpsilon) return false;
+ }
+ else if (!Equals(x.MainValue, y.MainValue))
+ {
+ return false;
+ }
+
+ // 比较时间戳(可以选择是否忽略毫秒)
+ return Math.Abs((x.Timestamp - y.Timestamp).TotalMilliseconds) < 1;
+ }
+
+ public int GetHashCode(StationData obj)
+ {
+ if (obj is null) return 0;
+
+ unchecked
+ {
+ int hash = 17;
+ hash = hash * 23 + (obj.Name?.GetHashCode() ?? 0);
+
+ // 对于浮点数,可以先转换为整数再计算哈希
+ if (obj.MainValue is float f)
+ {
+ hash = hash * 23 + ((int)(f * 1000000)).GetHashCode();
+ }
+ else
+ {
+ hash = hash * 23 + (obj.MainValue?.GetHashCode() ?? 0);
+ }
+
+ hash = hash * 23 + obj.Timestamp.GetHashCode();
+ return hash;
+ }
+ }
+ }
+
+ ///
+ /// 判断时间戳变化
+ ///
+ public class StationDataComparer2 : IEqualityComparer
+ {
+ public bool Equals(StationData x, StationData y)
+ {
+ if (ReferenceEquals(x, y)) return true;
+ if (x is null || y is null) return false;
+
+ // 只比较名称和时间戳
+ if (x.Name != y.Name) return false;
+
+ // 比较时间戳,检查是否完全相同
+ return x.Timestamp == y.Timestamp;
+ }
+
+ public int GetHashCode(StationData obj)
+ {
+ if (obj is null) return 0;
+
+ unchecked
+ {
+ int hash = 17;
+ hash = hash * 23 + (obj.Name?.GetHashCode() ?? 0);
+ hash = hash * 23 + obj.Timestamp.GetHashCode();
+ return hash;
+ }
+ }
+ }
+}
diff --git a/IOT.Services/XmlConfigReader.cs b/IOT.Services/XmlConfigReader.cs
new file mode 100644
index 0000000..4a95ee2
--- /dev/null
+++ b/IOT.Services/XmlConfigReader.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Collections.Generic;
+using System.Xml.Linq;
+
+namespace WindowsService1
+{
+ public class XmlConfigReader
+ {
+ private Dictionary>> deviceData;
+
+ public XmlConfigReader(string xmlContent)
+ {
+ XDocument doc = XDocument.Parse(xmlContent);
+ deviceData = ReadXmlToNestedDictionary(doc);
+ }
+
+ public XmlConfigReader(XDocument doc)
+ {
+ deviceData = ReadXmlToNestedDictionary(doc);
+ }
+
+ private Dictionary>> ReadXmlToNestedDictionary(XDocument doc)
+ {
+ var result = new Dictionary>>();
+ var mainDevice = doc.Root.Element("Device");
+ string mainDeviceName = mainDevice.Attribute("name").Value;
+ result[mainDeviceName] = new Dictionary>();
+
+ foreach (var subDevice in mainDevice.Elements("Device"))
+ {
+ string subDeviceName = subDevice.Attribute("name").Value;
+ result[mainDeviceName][subDeviceName] = new Dictionary();
+
+ foreach (var type in subDevice.Elements("Type"))
+ {
+ string typeName = type.Attribute("name").Value;
+ string typeValue = type.Value;
+ result[mainDeviceName][subDeviceName][typeName] = typeValue;
+ }
+ }
+
+ return result;
+ }
+
+ public void PrintStructure()
+ {
+ foreach (var mainDevice in deviceData)
+ {
+ Console.WriteLine($"Main Device: {mainDevice.Key}");
+ foreach (var subDevice in mainDevice.Value)
+ {
+ Console.WriteLine($" Sub Device: {subDevice.Key}");
+ foreach (var type in subDevice.Value)
+ {
+ Console.WriteLine($" Type: {type.Key}, Value: {type.Value}");
+ }
+ }
+ }
+ }
+
+ public bool TryGetValue(string mainDeviceName, string subDeviceName, string typeName, out string value)
+ {
+ value = null;
+ return deviceData.TryGetValue(mainDeviceName, out var mainDevice) &&
+ mainDevice.TryGetValue(subDeviceName, out var subDevice) &&
+ subDevice.TryGetValue(typeName, out value);
+ }
+
+ public Dictionary>> GetAllData()
+ {
+ return deviceData;
+ }
+ }
+}
diff --git a/IOT.Services/log4net.config b/IOT.Services/log4net.config
new file mode 100644
index 0000000..b177cf0
--- /dev/null
+++ b/IOT.Services/log4net.config
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/IOT.Services/packages.config b/IOT.Services/packages.config
new file mode 100644
index 0000000..92e2b4b
--- /dev/null
+++ b/IOT.Services/packages.config
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/JinYuan.ControlCenter/ControlCenter.cs b/JinYuan.ControlCenter/ControlCenter.cs
new file mode 100644
index 0000000..f6bdf5b
--- /dev/null
+++ b/JinYuan.ControlCenter/ControlCenter.cs
@@ -0,0 +1,2838 @@
+using JinYuan.Helper;
+using JinYuan.MES.Models;
+using JinYuan.Models;
+using JinYuan.VirtualDataLibrary;
+using Language;
+using MiniExcelLibs;
+using PLC;
+using PLC.PLCAlarm;
+using PLCCommunication;
+using PLCCommunication.Common;
+using PLCCommunication.Common.DataConvert;
+using SqlSugar;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Reflection;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+using static System.Runtime.CompilerServices.RuntimeHelpers;
+using AlarmData = JinYuan.Models.AlarmData;
+using DataType = PLC.DataType;
+
+namespace JinYuan.ControlCenters
+{
+ public partial class ControlCenter : IDisposable
+ {
+ #region 单例
+ private static readonly object uiLock = new object();
+ private static ControlCenter _instance;
+ public static ControlCenter Instance
+ {
+ get
+ {
+ lock (uiLock)
+ {
+ if (_instance == null)
+ _instance = new ControlCenter();
+ return _instance;
+ }
+ }
+ }
+ #endregion
+
+ #region 变量
+ private Dictionary, PlcGroup, Task>> _stationFunctions;
+ private Dictionary _groupThreads;
+ private readonly Dictionary> _plcDeviceMapping = new Dictionary>();
+ private readonly Dictionary _plcAlarmReaders = new Dictionary();
+
+ private CancellationTokenSource _cts;
+ private Task _heartbeatTask;
+ private const int HeartbeatInterval = 1000;
+
+ private readonly object _statusLock = new object();
+ private Dictionary _groupStatus = new Dictionary();
+
+ ///
+ /// 定时器-定时刷新状态和数据
+ ///
+ private System.Timers.Timer _timerRefreshData;
+
+ #region 独立任务队列
+ private readonly ConcurrentQueue _taskQueue = new ConcurrentQueue();
+ private Thread _workerThread;
+ private readonly ManualResetEvent _taskEvent = new ManualResetEvent(false);
+ private bool _isWorkerRunning = true;
+
+ // 队列监控
+ private int _maxQueueSize = 1000;
+ private DateTime _lastQueueWarningTime = DateTime.MinValue;
+ private int _totalProcessedCount = 0;
+ private int _totalRejectedCount = 0;
+
+ // 线程健康监控
+ private DateTime _lastHeartbeat = DateTime.Now;
+ private int _stuckDetectCount = 0;
+ #endregion
+
+ #region 资源管理
+ //private readonly SemaphoreSlim _dbSemaphore = new SemaphoreSlim(10); // 限制数据库并发
+ private readonly SemaphoreSlim _plcWriteSemaphore = new SemaphoreSlim(5); // 限制PLC写入并发
+ private bool _isDisposed = false;
+ private System.Timers.Timer _healthMonitorTimer;
+ #endregion
+
+ #endregion
+
+ #region 2、初始化函数
+ private ControlCenter()
+ {
+ _groupThreads = new Dictionary();
+ _cts = new CancellationTokenSource();
+
+ if (CommonMethods.plcDevices == null)
+ {
+ CommonMethods.plcDevices = new List();
+ }
+
+ // 启动独立任务处理线程
+ StartWorkerThread();
+ InitializeDevicesFromExcel();
+ GetAlarmData();
+ InitFunc();
+
+ _timerRefreshData = new System.Timers.Timer(1000);
+ _timerRefreshData.Elapsed += Timer_RefreshData_Elapsed;
+ _timerRefreshData.Start();
+
+ Thread thread = new Thread(ShowStateEven_Run);
+ thread.IsBackground = true;
+ thread.Start();
+
+ string[] clrPath = new[] { CommonMethods.strAlarmLogspath, CommonMethods.strMesLogspath, CommonMethods.strWattrMeterpath, @"D:\APILog\Logs\" };
+ short[] seltWay = new[] { (short)0, (short)0, (short)0, (short)1 };
+ DeleteLog.Start(clrPath, seltWay);
+
+ if (!CommonMethods.IsRun)
+ {
+ CommonMethods.ResetEventRecv.Reset();
+ }
+
+ // 启动健康监控
+ StartHealthMonitor();
+ }
+
+ ///
+ /// 开始健康监控
+ ///
+ private void StartHealthMonitor()
+ {
+ _healthMonitorTimer = new System.Timers.Timer(30000); // 每30秒检查一次
+ _healthMonitorTimer.Elapsed += HealthMonitorCallback;
+ _healthMonitorTimer.AutoReset = true;
+ _healthMonitorTimer.Start();
+ }
+
+ private void HealthMonitorCallback(object sender, System.Timers.ElapsedEventArgs e)
+ {
+ try
+ {
+ var now = DateTime.Now;
+ var timeSinceLastHeartbeat = now - _lastHeartbeat;
+
+ // 检查工作线程是否卡死
+ if (timeSinceLastHeartbeat.TotalSeconds > 60)
+ {
+ _stuckDetectCount++;
+ LogHelper.Instance.WriteError($"健康监控:工作线程可能卡死,最后心跳:{_lastHeartbeat}, 卡死计数:{_stuckDetectCount}");
+
+ if (_stuckDetectCount >= 3)
+ {
+ LogHelper.Instance.WriteError($"健康监控:工作线程已卡死,尝试恢复...");
+ RecoverWorkerThread();
+ _stuckDetectCount = 0;
+ }
+ }
+ else
+ {
+ _stuckDetectCount = 0;
+ }
+
+ // 记录系统状态
+ int queueSize = _taskQueue.Count;
+ if (queueSize > _maxQueueSize * 0.8)
+ {
+ LogHelper.Instance.WriteError($"健康监控:任务队列接近溢出,当前大小:{queueSize}/{_maxQueueSize}, 已处理:{_totalProcessedCount}, 已拒绝:{_totalRejectedCount}");
+ }
+
+ // 记录线程状态
+ int activeThreads = Process.GetCurrentProcess().Threads.Count;
+ LogHelper.Instance.WriteLog($"健康监控 - 队列:{queueSize}, 线程数:{activeThreads}, 内存:{GC.GetTotalMemory(false) / 1024 / 1024}MB");
+ }
+ catch (Exception ex)
+ {
+ LogHelper.Instance.WriteError($"健康监控异常:{ex.Message}");
+ }
+ }
+
+ ///
+ /// 恢复工作线程
+ ///
+ private void RecoverWorkerThread()
+ {
+ try
+ {
+ _isWorkerRunning = false;
+ _taskEvent.Set();
+
+ if (_workerThread != null && _workerThread.IsAlive)
+ {
+ _workerThread.Join(5000);
+ }
+
+ // 清空队列
+ while (_taskQueue.TryDequeue(out _)) { }
+
+ // 重新启动
+ _isWorkerRunning = true;
+ StartWorkerThread();
+
+ LogHelper.Instance.WriteLog("健康监控:工作线程已恢复");
+ }
+ catch (Exception ex)
+ {
+ LogHelper.Instance.WriteError($"恢复工作线程失败:{ex.Message}");
+ }
+ }
+
+ ///
+ /// 独立工作线程
+ ///
+ private void StartWorkerThread()
+ {
+ _workerThread = new Thread(delegate ()
+ {
+ var lastProgressLog = DateTime.Now;
+
+ while (_isWorkerRunning && !(_cts?.IsCancellationRequested ?? true))
+ {
+ try
+ {
+ // 更新心跳
+ _lastHeartbeat = DateTime.Now;
+
+ // 等待事件,添加超时防止永久阻塞
+ bool hasEvent = _taskEvent.WaitOne(1000);
+
+ if (!hasEvent)
+ {
+ // 超时继续循环,保持心跳
+ continue;
+ }
+
+ // 批量处理任务,提高效率
+ int batchSize = Math.Min(10, _taskQueue.Count);
+ for (int i = 0; i < batchSize; i++)
+ {
+ if (_taskQueue.TryDequeue(out Action task))
+ {
+ try
+ {
+ // 添加超时控制
+ var taskWithTimeout = Task.Run(() => task());
+ if (!taskWithTimeout.Wait(30000)) // 30秒超时
+ {
+ LogHelper.Instance.WriteError($"任务执行超时(30秒),已跳过");
+ }
+ _totalProcessedCount++;
+ }
+ catch (Exception ex)
+ {
+ LogHelper.Instance.WriteError($"业务执行异常:{ex.Message}");
+ }
+ }
+ }
+
+ // 定期记录处理统计
+ if ((DateTime.Now - lastProgressLog).TotalMinutes >= 5)
+ {
+ LogHelper.Instance.WriteLog($"任务处理统计 - 已处理:{_totalProcessedCount}, 已拒绝:{_totalRejectedCount}, 当前队列:{_taskQueue.Count}");
+ lastProgressLog = DateTime.Now;
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.Instance.WriteError($"工作线程异常:{ex.Message}");
+ Thread.Sleep(1000);
+ }
+ }
+ });
+
+ _workerThread.IsBackground = true;
+ _workerThread.Priority = ThreadPriority.AboveNormal;
+ _workerThread.Name = "ControlCenterWorker";
+ _workerThread.Start();
+ }
+
+ ///
+ /// 安全添加任务到队列
+ ///
+ private bool EnqueueTask(Action task, string taskName = "")
+ {
+ try
+ {
+ // 检查队列是否已满
+ if (_taskQueue.Count >= _maxQueueSize)
+ {
+ _totalRejectedCount++;
+
+ // 限制日志频率,避免日志风暴
+ if ((DateTime.Now - _lastQueueWarningTime).TotalSeconds > 30)
+ {
+ LogHelper.Instance.WriteError($"任务队列已满({_taskQueue.Count}/{_maxQueueSize}),拒绝任务:{taskName}");
+ _lastQueueWarningTime = DateTime.Now;
+ }
+ return false;
+ }
+
+ _taskQueue.Enqueue(task);
+ _taskEvent.Set();
+ return true;
+ }
+ catch (Exception ex)
+ {
+ LogHelper.Instance.WriteError($"入队失败:{ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// 初始化工位方法
+ ///
+ private void InitFunc()
+ {
+ _stationFunctions = new Dictionary, PlcGroup, Task>>();
+ _stationFunctions.Add("物流线料盘来料", TrayFeedingStation);
+ _stationFunctions.Add("物流线料盘进站", CheckTrayStation);
+ _stationFunctions.Add("电池进站", FeedingStation);
+ _stationFunctions.Add("电池与拖盘绑定", PackStation);
+ _stationFunctions.Add("强制放行", ForceOutStation);
+ _stationFunctions.Add("料框码解绑", UnBindTrayStation);
+ }
+
+ ///
+ /// 启动心跳及工位读取线程
+ ///
+ public async Task StartAsync()
+ {
+ // 先清理旧的线程
+ StopAndCleanOldThreads();
+
+ // 创建新线程
+ CreatePLCThreads();
+ _heartbeatTask = StartHeartbeatCheck(_cts.Token);
+ await Task.CompletedTask;
+ }
+
+ ///
+ /// 停止并清理旧线程
+ ///
+ private void StopAndCleanOldThreads()
+ {
+ try
+ {
+ foreach (var kvp in _groupThreads)
+ {
+ var thread = kvp.Value;
+ if (thread != null && thread.IsAlive)
+ {
+ try
+ {
+ thread.Join(1000);
+ }
+ catch (ThreadInterruptedException) { }
+ catch { }
+ }
+ }
+ _groupThreads.Clear();
+ }
+ catch (Exception ex)
+ {
+ LogHelper.Instance.WriteError($"清理旧线程异常:{ex.Message}");
+ }
+ }
+
+ ///
+ /// 加载配置文件
+ ///
+ private void InitializeDevicesFromExcel()
+ {
+ string gongwei = CommonMethods.sysConfig.StationNo;
+ CommonMethods.AddDataMonitorLog(1, "开始加载配置参数……".Translated());
+ string deviceConfigPath = CommonMethods.deviceConfigPath;//Path.Combine(configFolderPath, "Device.xlsx");
+ string groupConfigPath = CommonMethods.groupConfigPath;//Path.Combine(configFolderPath, $"Group_{gongwei}.xlsx");
+ string variableConfigPath = CommonMethods.variableConfigPath;//Path.Combine(configFolderPath, $"Variable_{gongwei}.xlsx");
+ //string plcVariableConfigPath = Path.Combine(configFolderPath, "PLCVariable.xlsx");
+
+ if (!File.Exists(deviceConfigPath) || !File.Exists(groupConfigPath) || !File.Exists(variableConfigPath))
+ {
+ LogHelper.Instance.WriteError("配置文件不存在");
+ return;
+ }
+
+ try
+ {
+ var deviceConfigs = MiniExcel.Query(deviceConfigPath).ToList();
+ var groupConfigs = MiniExcel.Query(groupConfigPath).ToList();
+ var variableConfigs = MiniExcel.Query(variableConfigPath).ToList();
+
+ foreach (var deviceConfig in deviceConfigs.Where(d => d.IsActive))
+ {
+ IPEndPoint iep = new IPEndPoint(IPAddress.Parse(deviceConfig.IPAddress), deviceConfig.Port);
+ PlcReadWriteBase plc = PLCFactory.CreatePLC(GetPLCTypeEnum(deviceConfig.PLCType), deviceConfig.PlcNum, $"PLC_{deviceConfig.PlcNum}", iep);
+
+ string GongWei = CommonMethods.sysConfig.GongWei;
+ var stationNo = CommonMethods.sysConfig.StationNo;
+ deviceConfig.Cts = new CancellationTokenSource();
+ deviceConfig.GroupList = groupConfigs
+ .Where(g => g.IsActive && g.PlcNum == deviceConfig.PlcNum && ((g.PlcNum == 1 && g.StationName == stationNo) || (g.PlcNum == 2 && g.StationName == GongWei)) || g.GroupName == "MES心跳")
+ .ToList();
+
+ foreach (var group in deviceConfig.GroupList)
+ {
+ group.VarList = variableConfigs
+ .Where(v => v.GroupName == group.GroupName && v.PlcNum == deviceConfig.PlcNum && v.StationName == group.StationName)
+ .ToList();
+
+ lock (_statusLock)
+ {
+ _groupStatus[group.GroupName] = 0;
+ }
+ }
+
+ _plcDeviceMapping[plc] = new List { deviceConfig };
+ CommonMethods.plcDevices.Add(plc);
+
+ // 初始化 PLCAlarm Reader报警读取
+ var alarmReader = new PLCAlarmReader(
+ Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "AlarmConfig.csv"),
+ Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "AlarmRecords"),
+ RecordType.RecordWhenFinish,
+ false
+ );
+
+ //alarmReader.PLCAlarm_New += AlrmReader_PLCAlarm_New;
+ //alarmReader.PLCAlarm_Remove += AlrmReader_PLCAlarm_Remove;
+ _plcAlarmReaders[plc] = alarmReader;
+
+ CommonMethods.plcStatuses[plc] = new Dictionary
+ {
+ ["OldStatus"] = (short)0,
+ ["StatusCount"] = 0,
+ ["GuidAlarm"] = string.Empty,
+ ["Status"] = new List { false, false, false, false }
+ };
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.Instance.WriteError($"加载配置失败:{ex.Message}");
+ }
+ CommonMethods.AddDataMonitorLog(1, "配置文件加载完成".Translated());
+ }
+
+ #endregion
+
+ #region 3、PLC读写
+
+ #region 创建PLC线程 几个工位就几个线程
+ ///
+ /// 按工位读取线程
+ ///
+ private void CreatePLCThreads()
+ {
+ for (int i = 0; i < CommonMethods.plcDevices.Count; i++)
+ {
+ PlcReadWriteBase plc = CommonMethods.plcDevices[i];
+ int plcIndex = i; // 捕获索引
+
+ if (_plcDeviceMapping.TryGetValue(plc, out var devList))
+ {
+ foreach (PlcDevice dev in devList)
+ {
+ foreach (PlcGroup group in dev.GroupList)
+ {
+ if (group.IsActive)
+ {
+ string key = dev.PlcNum + "-" + group.GroupName;
+
+ // 如果已存在同名线程,先停止
+ if (_groupThreads.ContainsKey(key))
+ {
+ var oldThread = _groupThreads[key];
+ if (oldThread != null && oldThread.IsAlive)
+ {
+ try { oldThread.Join(500); } catch { }
+ }
+ }
+
+ Thread t = new Thread(ReadPLCWorkModel);
+ t.IsBackground = true;
+ t.Priority = ThreadPriority.Highest;
+ t.Name = $"PLC_{key}";
+ _groupThreads[key] = t;
+
+ var param = new Tuple(group, plcIndex);
+ t.Start(param);
+ }
+ }
+ }
+ }
+ }
+
+ LogHelper.Instance.WriteLog($"PLC线程创建完成,共{_groupThreads.Count}个线程");
+ }
+ #endregion
+
+ #region 循环读取PLC工位状态
+ ///
+ /// 读取PLC循环工位
+ ///
+ ///
+ private void ReadPLCWorkModel(object parameter)
+ {
+ Tuple param = parameter as Tuple;
+ if (param == null) return;
+
+ PlcGroup group = param.Item1;
+ int plcIndex = param.Item2;
+
+ // 添加重连计数器
+ int consecutiveErrorCount = 0;
+ const int maxConsecutiveErrors = 10;
+ int lastLogSecond = 0; // 用于限制日志频率
+
+ while (_cts != null && !_cts.Token.IsCancellationRequested)
+ {
+ try
+ {
+ // 检查系统运行状态
+ if (!CommonMethods.IsRun)
+ {
+ Thread.Sleep(100);
+ consecutiveErrorCount = 0;
+ continue;
+ }
+
+ PlcReadWriteBase plc = CommonMethods.plcDevices[plcIndex];
+ if (plc == null || !plc.IsConnected)
+ {
+ consecutiveErrorCount++;
+ if (consecutiveErrorCount > maxConsecutiveErrors && lastLogSecond != DateTime.Now.Second)
+ {
+ lastLogSecond = DateTime.Now.Second;
+ LogHelper.Instance.WriteError($"【诊断-PLC状态】{group.GroupName} | PLC未连接 | 连续失败次数={consecutiveErrorCount}");
+ }
+
+ Thread.Sleep(100);
+ continue;
+ }
+
+ consecutiveErrorCount = 0;
+ if (!CommonMethods.sysConfig.WuLiu && group.GroupName == "物流线料盘来料")
+ {
+ Thread.Sleep(100);
+ continue;
+ }
+
+ // 重点监视物流线料盘进站
+ bool isCriticalStation = group.GroupName == "物流线料盘进站" || group.GroupName == "物流线料盘来料";
+
+ // 读取触发信号
+ var trigResult = ReadWithTimeout(plc, group, 2000);
+ if (trigResult == null || !trigResult.IsSuccess)
+ {
+ if (isCriticalStation && lastLogSecond != DateTime.Now.Second)
+ {
+ lastLogSecond = DateTime.Now.Second;
+ LogHelper.Instance.WriteError($"【诊断-读取失败】{group.GroupName} | 读取触发信号失败");
+ }
+
+ Thread.Sleep(50);
+ continue;
+ }
+
+ short trig = trigResult.Content;
+ int status = 0;
+
+ lock (_statusLock)
+ {
+ if (_groupStatus.ContainsKey(group.GroupName))
+ status = _groupStatus[group.GroupName];
+ }
+
+ // 记录触发信号(仅对关键工站)
+ if (isCriticalStation && trig != 0)
+ {
+ StationDiagnostics.LogTrigger(group.GroupName, trig, status);
+ }
+
+ if (trig == 1 && status == 0)
+ {
+ if (isCriticalStation)
+ {
+ LogHelper.Instance.WriteLog($"【诊断-信号触发】{group.GroupName} | 检测到触发信号 | 开始处理...");
+ }
+
+ JYResult data = plc.ReadValue>(
+ group.VarList[1].VarAddress,
+ DataType.ArrByte,
+ Convert.ToUInt16(group.VarList[1].OffsetOrLength)
+ );
+
+ if (data != null && _stationFunctions.ContainsKey(group.GroupName))
+ {
+ Func, PlcGroup, Task> func = _stationFunctions[group.GroupName];
+
+ lock (_statusLock)
+ {
+ _groupStatus[group.GroupName] = 1;
+ }
+
+ // 使用安全的入队方法
+ string groupName = group.GroupName;
+ DateTime triggerTime = DateTime.Now;
+
+ bool enqueued = EnqueueTask(() =>
+ {
+ try
+ {
+ StationDiagnostics.LogTaskStart(groupName);
+ // 记录入队到开始执行的延迟
+ var queueDelay = DateTime.Now - triggerTime;
+ if (queueDelay.TotalMilliseconds > 500)
+ {
+ LogHelper.Instance.WriteError($"【诊断-队列延迟】{groupName} | 入队到执行延迟={queueDelay.TotalMilliseconds:F0}ms");
+ }
+
+ // 添加超时控制
+ var task = func(data, group);
+ if (!task.Wait(30000))
+ {
+ StationDiagnostics.LogTimeout(groupName, 30);
+ LogHelper.Instance.WriteError($"【诊断-业务超时】{groupName} | 执行超过30秒");
+
+ // 超时后强制完成,避免状态卡死
+ lock (_statusLock)
+ {
+ if (_groupStatus.ContainsKey(groupName) && _groupStatus[groupName] == 1)
+ {
+ WriteFinishToPLC(group, plcIndex);
+ _groupStatus[groupName] = 0;
+ LogHelper.Instance.WriteError($"【诊断-强制恢复】{groupName} | 超时后强制重置状态");
+ }
+ }
+ return;
+ }
+
+
+ StationDiagnostics.LogTaskComplete(groupName, true);
+ LogHelper.Instance.WriteLog($"【业务完成】{groupName} | 总耗时={(DateTime.Now - triggerTime).TotalMilliseconds:F0}ms");
+ }
+ catch (Exception ex)
+ {
+ StationDiagnostics.LogTaskComplete(groupName, false, ex.Message);
+ LogHelper.Instance.WriteError($"【诊断-异常】{groupName} | {ex.Message}");
+
+ // 异常时也要重置状态
+ lock (_statusLock)
+ {
+ if (_groupStatus.ContainsKey(groupName) && _groupStatus[groupName] == 1)
+ {
+ _groupStatus[groupName] = 0;
+ }
+ }
+ }
+ }, groupName);
+
+ if (!enqueued)
+ {
+ LogHelper.Instance.WriteError($"【诊断-入队失败】{groupName} | 队列已满,任务被拒绝");
+ lock (_statusLock)
+ {
+ _groupStatus[group.GroupName] = 0;
+ }
+ }
+ else
+ {
+ StationDiagnostics.LogTaskEnqueued(groupName, _taskQueue.Count);
+ }
+ }
+ }
+ else if (trig == 0 && status == 1)
+ {
+ if (isCriticalStation)
+ {
+ LogHelper.Instance.WriteLog($"【诊断-信号下降】{group.GroupName} | 触发信号已复位 | 清理状态");
+ }
+
+ lock (_statusLock)
+ {
+ _groupStatus[group.GroupName] = 0;
+ }
+
+ ClearFinishFlags(group, plcIndex);
+ }
+
+ Thread.Sleep(50);
+ }
+ catch (Exception ex)
+ {
+ LogHelper.Instance.WriteError($"PLC读取异常:{ex.Message}");
+ Thread.Sleep(100);
+ }
+ }
+ }
+
+ ///
+ /// 带超时的PLC读取(新增)
+ ///
+ private JYResult ReadWithTimeout(PlcReadWriteBase plc, PlcGroup group, int timeoutMs)
+ {
+ try
+ {
+ var task = Task.Run(() => plc.ReadValue>(group.VarList[0].VarAddress, DataType.Short));
+ if (task.Wait(timeoutMs))
+ {
+ return task.Result;
+ }
+ else
+ {
+ LogHelper.Instance.WriteError($"PLC读取超时:{group.GroupName}");
+ return null;
+ }
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ private void WriteFinishToPLC(PlcGroup group, int plcIndex)
+ {
+ try
+ {
+ PlcReadWriteBase plc = CommonMethods.plcDevices[plcIndex];
+ if (plc == null || !plc.IsConnected) return;
+
+ // 使用信号量限制并发写入
+ if (_plcWriteSemaphore.Wait(1000))
+ {
+ try
+ {
+ switch (group.GroupName)
+ {
+ case "物流线料盘来料":
+ case "物流线料盘进站":
+ plc.WriteInt16(group.VarList[5].VarAddress, 1);
+ plc.WriteInt16(group.VarList[4].VarAddress, 1);
+ break;
+ case "电池进站":
+ plc.WriteInt16(group.VarList[5].VarAddress, 1);
+ break;
+ case "电池与拖盘绑定":
+ plc.WriteInt16(group.VarList[8].VarAddress, 1);
+ plc.WriteInt16(group.VarList[7].VarAddress, 1);
+ break;
+ case "料框码解绑":
+ plc.WriteInt16(group.VarList[2].VarAddress, 1);
+ break;
+ case "强制放行":
+ plc.WriteInt16(group.VarList[2].VarAddress, 1);
+ break;
+ }
+ }
+ finally
+ {
+ _plcWriteSemaphore.Release();
+ }
+ }
+ else
+ {
+ LogHelper.Instance.WriteError($"获取PLC写入信号量超时:{group.GroupName}");
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.Instance.WriteError($"写入PLC失败:{ex.Message}");
+ }
+ }
+
+ ///
+ /// 清空PLC响应标志
+ ///
+ private void ClearFinishFlags(PlcGroup group, int plcIndex)
+ {
+ try
+ {
+ PlcReadWriteBase plc = CommonMethods.plcDevices[plcIndex];
+ if (plc == null || !plc.IsConnected) return;
+
+ if (_plcWriteSemaphore.Wait(500))
+ {
+ try
+ {
+ switch (group.GroupName)
+ {
+ case "电池与拖盘绑定":
+ plc.WriteInt16(group.VarList[8].VarAddress, 0);
+ plc.WriteInt16(group.VarList[7].VarAddress, 0);
+ break;
+ case "电池进站":
+ plc.WriteInt16(group.VarList[5].VarAddress, 0);
+ break;
+ case "物流线料盘来料":
+ case "物流线料盘进站":
+ //plc.WriteInt16(group.VarList[4].VarAddress, 0);
+ plc.WriteInt16(group.VarList[5].VarAddress, 0);
+ break;
+ case "料框码解绑":
+ plc.WriteInt16(group.VarList[2].VarAddress, 0);
+ break;
+ }
+ }
+ finally
+ {
+ _plcWriteSemaphore.Release();
+ }
+ }
+ }
+ catch { }
+ }
+
+ #endregion
+
+ #region 开始停止联机事件
+ ///
+ /// 开始联机
+ ///
+ ///
+
+ public async Task ConnectAllPlcsAsync()
+ {
+ bool allConnected = true;
+ if (CommonMethods.plcDevices.Count <= 0)
+ {
+ CommonMethods.AddDataMonitorLog(1, "无PLC配置信息".Translated() + "," + "请检查配置文件".Translated());
+ return false;
+ }
+
+ foreach (var plc in CommonMethods.plcDevices)
+ {
+ if (_plcDeviceMapping.TryGetValue(plc, out var plcDevices))
+ {
+ foreach (var deviceConfig in plcDevices)
+ {
+ string msg = "";
+ try
+ {
+ // 使用 Task.Run 来在后台线程执行连接操作
+ bool isConnected = await Task.Run(delegate ()
+ {
+ // 设置一个连接超时
+ //using var cts = new CancellationTokenSource(10000);
+
+ try
+ {
+ return plc.Connect(ref msg);
+ }
+ catch
+ {
+ msg = "连接超时";
+ return false;
+ }
+ });
+
+ deviceConfig.IsConnected = isConnected;
+ plc.IsConnected = isConnected;
+
+ if (!isConnected)
+ {
+ allConnected = false;
+ LogHelper.Instance.WriteLog($"连接PLC失败: {deviceConfig.PlcNum}, 错误: {msg}");
+ CommonMethods.AddDataMonitorLog(1, "连接PLC失败".Translated() + $": {deviceConfig.PlcNum}, " + "错误".Translated() + $": {msg}");
+ }
+ else
+ {
+ await SendConfigToPLCAsync(plc);
+ await SendUserRightToPLCAsync(plc);
+ CommonMethods.AddDataMonitorLog(0, "成功连接PLC".Translated() + $": {deviceConfig.PlcNum}");
+ }
+ }
+ catch (Exception ex)
+ {
+ allConnected = false;
+ LogHelper.Instance.WriteLog($"连接PLC时发生异常: {deviceConfig.PlcNum}, 错误: {ex.Message}");
+ CommonMethods.AddDataMonitorLog(2, "连接PLC时发生异常".Translated() + $": {deviceConfig.PlcNum}" + "错误".Translated() + $": {ex.Message}");
+ }
+ }
+ }
+ }
+ return allConnected;
+ }
+
+ ///
+ /// 断开链接
+ ///
+ public void DisconnectAllPlcs()
+ {
+ try
+ {
+ if (_cts != null && !_cts.IsCancellationRequested)
+ {
+ _cts.Cancel();
+ }
+
+ foreach (var plc in CommonMethods.plcDevices)
+ {
+ try
+ {
+ if (plc.IsConnected)
+ {
+ plc.Disconnect();
+ plc.IsConnected = false;
+ CommonMethods.AddDataMonitorLog(0, "已断开与PLC".Translated() + $"[{plc.PlcID}]" + "的链接".Translated());
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"断开PLC {plc.PlcID} 异常:{ex.Message}");
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"断开连接异常: {ex.Message}");
+ }
+
+ }
+
+
+ #endregion
+
+ #endregion
+
+ #region MES
+ ///
+ /// Mes心跳
+ ///
+ private void UpdateMesHeartBeat()
+ {
+ Thread.Sleep(100);
+ while (!_cts.Token.IsCancellationRequested)
+ {
+ try
+ {
+ if (CommonMethods.IsRun)//CommonMethods.mesConfig.isUpMes && CommonMethods.mesConfig.IsLoginMesOK &&
+ {
+ bool isOnline = FastPing();
+ CommonMethods.mesConfig.isConnected = isOnline;
+
+ if (!isOnline)
+ {
+ foreach (var plc in CommonMethods.plcDevices)
+ {
+ if (plc.IsConnected && plc.Name == "PLC_1")
+ {
+ plc.WriteInt16("D80", 1);
+ }
+ }
+ }
+
+ //if (Ping() == true)
+ //{
+ // CommonMethods.mesConfig.isConnected = true;
+ // string errStr = $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")},Mes心跳Ping成功,IP:{CommonMethods.mesConfig.MESURLAddressIP},端口号:{CommonMethods.mesConfig.MESURLAddressPort}";
+ // TxtHelper.WriteTxt($@"D:\APILog\Logs\MesLogs\{DateTime.Now.ToString("yyyyMMdd")}\Mes心跳报警\{DateTime.Now.ToString("HH")}.txt", errStr);
+ //}
+ //else//Ping失败,提示检查线路。
+ //{
+ // int count = 0;
+ // bool mesOnLine = false;
+ // do//重复3次
+ // {
+ // Thread.Sleep(1000);
+ // if (Ping() == true) { mesOnLine = true; break; }
+ // count++;
+ // }
+ // while (count < 3);
+
+ // if (!mesOnLine)
+ // {
+ // CommonMethods.mesConfig.isConnected = false;
+ // if (CommonMethods.plcDevices.Count() > 0)
+ // {
+ // for (int i = 0; i < CommonMethods.plcDevices.Count(); i++)
+ // {
+ // if (CommonMethods.plcDevices[i].IsConnected && CommonMethods.plcDevices[i].Name == "PLC_1")
+ // {
+ // CommonMethods.plcDevices[i].WriteInt16("D80", (short)1);
+
+ // string errStr = $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")},Mes心跳Ping失败,IP:{CommonMethods.mesConfig.MESURLAddressIP},端口号:{CommonMethods.mesConfig.MESURLAddressPort}";
+ // TxtHelper.WriteTxt($@"D:\APILog\Logs\MesLogs\{DateTime.Now.ToString("yyyyMMdd")}\Mes心跳报警\{DateTime.Now.ToString("HH")}.txt", errStr);
+ // }
+ // }
+ // }
+ // }
+ //}
+ }
+ }
+ catch (Exception ex)
+ {
+ string errStr = $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")},Mes心跳Ping异常,IP:{CommonMethods.mesConfig.MESURLAddressIP},端口号:{CommonMethods.mesConfig.MESURLAddressPort},原因:{ex.ToString()}";
+ TxtHelper.WriteTxt($@"D:\APILog\Logs\MesLogs\{DateTime.Now.ToString("yyyyMMdd")}\Mes心跳报警\{DateTime.Now.ToString("HH")}.txt", errStr);
+ }
+ Thread.Sleep(1000);
+ }
+ }
+
+ ///
+ /// Ping 检查网络线路问题
+ ///
+ ///
+ public bool FastPing()
+ {
+ try
+ {
+ string ip = CommonMethods.mesConfig.MESURLAddressIP;
+ using var _ping = new Ping();
+ int timeout = 500;
+ var reply = _ping.Send(ip, timeout);
+ return reply.Status == IPStatus.Success;
+ }
+ catch (Exception ex)
+ {
+ string errStr = $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")},Mes心跳Ping异常,IP:{CommonMethods.mesConfig.MESURLAddressIP},端口号:{CommonMethods.mesConfig.MESURLAddressPort},原因:{ex.ToString()}";
+ TxtHelper.WriteTxt($@"D:\APILog\Logs\MesLogs\{DateTime.Now.ToString("yyyyMMdd")}\Mes心跳报警\{DateTime.Now.ToString("HH")}.txt", errStr);
+ return false;
+ }
+
+ }
+ #endregion
+
+ #region 4、心跳和状态检查
+ ///
+ /// 心跳检测
+ ///
+ ///
+ ///
+ private async Task StartHeartbeatCheck(CancellationToken cancellationToken)
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ if (CommonMethods.IsRun)
+ {
+ foreach (var plc in CommonMethods.plcDevices)
+ {
+ if (_plcDeviceMapping.TryGetValue(plc, out var plcDevices))
+ {
+ foreach (var plcDevice in plcDevices)
+ {
+ if (plcDevice.IsActive && plcDevice.IsHeartBeat)
+ {
+ await CheckHeartbeatForDevice(plc, plcDevice);
+ }
+ }
+ }
+ }
+ }
+ await Task.Delay(HeartbeatInterval, cancellationToken);
+ }
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private async Task CheckHeartbeatForDevice(PlcReadWriteBase plc, PlcDevice plcDevice)
+ {
+ if (plcDevice.IsConnected)
+ {
+ try
+ {
+ JYResult result = await Task.Run(() => plc.ReadValue>(plcDevice.HeartBeat, DataType.Short));
+ if (result.IsSuccess)
+ {
+ plcDevice.ErrorTimes = 0;
+ await Task.Run(() => plc.WriteValue(plcDevice.HeartBeat, (short)1, DataType.Short));
+ plcDevice.IsConnected = true;
+ plc.IsConnected = true;
+ return;
+ }
+ }
+ catch { }
+
+ plcDevice.ErrorTimes++;
+ if (plcDevice.ErrorTimes >= plcDevice.AllowErrorTimes)
+ {
+ plcDevice.IsConnected = false;
+ plc.IsConnected = false;
+ LogHelper.Instance.WriteLog($"编号:[{plcDevice.PlcNum}]的PLC链接断开:{plcDevice.ErrorTimes}");
+ }
+ }
+ else
+ {
+ await ReconnectDevice(plc, plcDevice);
+ }
+ }
+
+
+
+ private async Task ReconnectDevice(PlcReadWriteBase plc, PlcDevice plcDevice)
+ {
+ if (!plcDevice.IsFirstConnect)
+ {
+ await Task.Delay(plcDevice.ReConnectTime);
+ plc.Disconnect();
+ }
+
+ string msg = "";
+ var connectResult = await Task.Run(() => plc.Connect(ref msg));
+ plcDevice.IsConnected = connectResult;
+ LogHelper.Instance.WriteLog(connectResult
+ ? $"PLC编号:{plcDevice.PlcNum}{(plcDevice.IsFirstConnect ? "初次" : "重新")}连接成功{msg}!"
+ : $"[{plcDevice.PlcNum}]#PLC{(plcDevice.IsFirstConnect ? "初次" : "重新")}连接失败{msg}!");
+
+ plcDevice.IsFirstConnect = false;
+ }
+
+ #endregion
+
+ #region 定时刷新数据
+
+ private int TotalNum = 0;
+ private int TotalNum1 = 0;
+ private int Regular = 0;
+ private bool _isUploadingEnergy = false;
+ private bool _isGettingEnergy = false;
+
+ private void Timer_RefreshData_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
+ {
+ try
+ {
+ // 防止定时器重入
+ if (Regular >= 300 && !_isUploadingEnergy)
+ {
+ _isUploadingEnergy = true;
+ Task.Run(() =>
+ {
+ try
+ {
+ UploadEquEnergy();
+ }
+ finally
+ {
+ _isUploadingEnergy = false;
+ }
+ });
+ Regular = 0;
+ }
+
+ if (TotalNum > 4)
+ {
+ if (CommonMethods.sysConfig.StationNo == "1" && !_isGettingEnergy)
+ {
+ _isGettingEnergy = true;
+ Task.Run(() =>
+ {
+ try
+ {
+ GetEnergyData();
+ }
+ finally
+ {
+ _isGettingEnergy = false;
+ }
+ });
+ }
+ else if (!_isGettingEnergy)
+ {
+ _isGettingEnergy = true;
+ Task.Run(() =>
+ {
+ try
+ {
+ GetEnergyDataFormPlc();
+ }
+ finally
+ {
+ _isGettingEnergy = false;
+ }
+ });
+ }
+
+ Task.Run(() => Refresh12HourProdQty());
+ GetDefectTypeQty();
+ ReadPLCComm();
+
+ Task.Run(() => GetASqlAlarmData(DateTime.Today, CommonMethods.strClass));
+ TotalNum = 0;
+ }
+ else if (TotalNum == 2)//设备状态读取
+ {
+ SetClass(); //变更班次
+ }
+
+ if (TotalNum1 >= 3600 * 24)
+ {
+ Task.Run(() => DeleteFileOfLocal());
+ TotalNum1 = 0;
+ }
+
+ TotalNum++;
+ TotalNum1++;
+ Regular++;
+ }
+ catch (Exception ex)
+ {
+ LogHelper.Instance.WriteError($"定时器异常:{ex.Message}");
+ }
+ }
+
+ #endregion
+
+ #region 设备综合信息
+ ///
+ /// 读取PLC通用信息
+ ///
+ internal void ReadPLCComm()
+ {
+ try
+ {
+ foreach (var plc in CommonMethods.plcDevices)
+ {
+ if (plc.IsConnected && plc.Name == "PLC_1")
+ {
+ _ = UpTotalCount(null);
+
+ CommonMethods.TotalQty = plc.ReadFloat("D15");//生产总数
+ CommonMethods.ProOKQty = plc.ReadFloat("D17");//良品总数
+ CommonMethods.Ppm = plc.ReadFloat("D13");
+ CommonMethods.JiaDL = Convert.ToDouble(plc.ReadFloat("D11").ToString("F2"));
+
+ #region 开机时间/运行时间
+ CommonMethods.PlcOpenAndRunTime = GetList(6, 0);
+ JYResult resByte = plc.ReadValue>($"D50", DataType.ArrByte, 12);
+ if (resByte.IsSuccess && resByte.Content.Length >= 24)
+ {
+ for (int j = 0; j < CommonMethods.PlcOpenAndRunTimeAdress.Count; j++)
+ {
+ CommonMethods.PlcOpenAndRunTime[j] = IntLib.GetIntFromByteArray(resByte.Content, j * 4, DataFormat.CDAB);
+ }
+ }
+ #endregion
+ }
+ }
+ }
+ catch { }
+ }
+ #endregion
+
+ #region 保存12小时产能数据
+ public int totalNum = 0; // 总数
+ public int okNum = 0; // 良品数
+
+ ///
+ /// 储存数据到数据库
+ ///
+ ///
+ public async Task UpTotalCount(int[] DataCount)
+ {
+ try
+ {
+ DateTime now = DateTime.Now;
+ string date = now.ToString("yyyy-MM-dd");
+ int hour = now.Minute < 30 ? (now.Hour + 23) % 24 : now.Hour;
+
+ if (now.Hour == 0 && now.Minute < 30)
+ date = now.AddDays(-1).ToString("yyyy-MM-dd");
+
+ int inQty = GetToDayHourQty(hour, true);
+ int outQty = GetToDayHourQty(hour, false);
+
+ Hourprod prod = new Hourprod
+ {
+ FDate = date,
+ FHour = hour,
+ ProdIn = inQty,
+ ProdOut = outQty,
+ TestTime = now,
+ Ppm = (int)CommonMethods.Ppm,
+ JiaDL = Convert.ToDecimal(CommonMethods.JiaDL)
+ };
+
+ int update = CommonMethods.db.UpdateSingle(prod,
+ it => new Hourprod { ProdIn = prod.ProdIn, ProdOut = prod.ProdOut, TestTime = prod.TestTime },
+ it => it.FDate == date && it.FHour == hour);
+
+ if (update <= 0)
+ await CommonMethods.db.AddReturnBoolAsync(prod);
+ }
+ catch { }
+
+ //try
+ //{
+ // DateTime dateTime = DateTime.Now;
+ // string strDate = dateTime.ToString("yyyy-MM-dd");
+
+ // // 获取当前时间的小时和分钟
+ // int currentHour = dateTime.Hour;
+ // int currentMinute = dateTime.Minute;
+
+ // // 根据当前分钟确定统计小时
+ // // 如果当前时间在 X:30 之前,则统计的是前一个小时的数据(从前一个小时的30分到这个小时的29分)
+ // // 如果当前时间在 X:30 之后,则统计的是当前小时的数据(从当前小时的30分到下个小时的29分)
+ // int statisticalHour;
+ // if (currentMinute < 30)
+ // {
+ // statisticalHour = (currentHour + 23) % 24; // 前一个小时,处理跨天的情况
+ // // 如果是凌晨0点前的数据,需要使用前一天的日期
+ // if (currentHour == 0)
+ // {
+ // strDate = dateTime.AddDays(-1).ToString("yyyy-MM-dd");
+ // }
+ // }
+ // else
+ // {
+ // statisticalHour = currentHour;
+ // }
+
+ // // 数据有效性验证
+ // if (statisticalHour < 0 || statisticalHour > 23)
+ // {
+ // throw new ArgumentException($"无效的小时值: {statisticalHour}");
+ // }
+
+ // //读取统计小时的PLC投入总数和产出数
+ // int ProdCurrAllQty = GetToDayHourQty(statisticalHour, true);
+ // int ProdCurrOKQty = GetToDayHourQty(statisticalHour, false);
+
+ // // 确保数量为非负数
+ // if (ProdCurrAllQty < 0 || ProdCurrOKQty < 0)
+ // {
+ // LogHelper.Instance.WriteError($"产量不能为负数: 投入={ProdCurrAllQty}, 产出={ProdCurrOKQty}");
+ // //throw new ArgumentException($"产量不能为负数: 投入={ProdCurrAllQty}, 产出={ProdCurrOKQty}");
+ // }
+
+ // Hourprod mh = new Hourprod
+ // {
+ // FDate = strDate,
+ // FHour = statisticalHour,
+ // ProdIn = ProdCurrAllQty,
+ // ProdOut = ProdCurrOKQty,
+ // TestTime = dateTime,
+ // Ppm = (int)CommonMethods.Ppm,
+ // JiaDL = Convert.ToDecimal(CommonMethods.JiaDL)
+ // };
+
+ // // 尝试更新记录
+ // int res = CommonMethods.db.UpdateSingle(
+ // mh,
+ // it => new Hourprod
+ // {
+ // ProdIn = mh.ProdIn,
+ // ProdOut = mh.ProdOut,
+ // TestTime = mh.TestTime,
+ // Ppm = mh.Ppm,
+ // JiaDL = mh.JiaDL
+ // },
+ // it => it.FDate == mh.FDate && it.FHour == mh.FHour
+ // );
+
+ // // 如果更新失败(记录不存在),则新增记录
+ // if (res <= 0)
+ // {
+ // bool addResult = await CommonMethods.db.AddReturnBoolAsync(mh).ConfigureAwait(false);
+ // if (!addResult)
+ // {
+ // LogHelper.Instance.WriteError($"保存小时产能新增记录失败");
+ // }
+ // }
+
+ // // 可以添加日志记录成功信息
+ // // Logger.Log($"成功更新/新增产能数据: 日期={strDate}, 小时={statisticalHour}, 时段={statisticalHour}:30~{(statisticalHour + 1) % 24}:29");
+ //}
+ //catch (Exception ex)
+ //{
+ // // 这里应该添加错误日志记录
+ // LogHelper.Instance.WriteEX($"保存小时产能数据失败: ", ex);
+ //}
+ }
+
+ ///
+ /// 每小时产能读取处理
+ ///
+ ///
+ ///
+ ///
+ private int GetToDayHourQty(int hour, bool flag)
+ {
+ try
+ {
+ string strAddr = flag ? $"D{1040 + hour}" : $"D{1070 + hour}";
+
+ foreach (var plc in CommonMethods.plcDevices)
+ {
+ if (plc.IsConnected && plc.Name == "PLC_1")
+ {
+ int ProdNGQty = plc.ReadInt16(strAddr);
+ return ProdNGQty;
+ }
+
+ }
+
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show(ex.Message, "系统异常", MessageBoxButtons.OK, MessageBoxIcon.Hand);
+ }
+ return 0;
+ }
+
+ #endregion
+
+ #region 5、设备状态
+ ///
+ /// 定时获取设备状态
+ ///
+ ///
+ ///
+ int OldStatues = int.TryParse(IniConfigHelper.ReadIniData("上一次设备状态", "DeviceStatus", "1", CommonMethods.strDeviceStatusPath), out var result) ? result : 0; //退出程序前设备状态
+ string strGuidAlarm = IniConfigHelper.ReadIniData("上一次Guid", "DeviceGuid", Guid.NewGuid().ToString(), CommonMethods.strDeviceStatusPath);//退出程序前Guid
+ DateTime StatuesBeginTime = DateTime.Now; // DateTime.Now.AddMinutes(-6);
+ bool run_AlarmMark = false;//特殊情况1标志(运行状态)
+ bool run_IdleMark = false;//特殊情况2标志(闲置状态)
+ bool isUpIdleAlarm = false;//闲置状态报警信息上传标志
+ List idleAlarmData = new List();//闲置状态的报警信息
+
+ private async void ShowStateEven_Run()
+ {
+ while (!_cts.Token.IsCancellationRequested)
+ {
+
+ try
+ {
+ foreach (var plc in CommonMethods.plcDevices)
+ {
+
+ if (plc.IsConnected && plc.Name == "PLC_1")
+ {
+ DateTime Date = DateTime.Now;
+ bool isUpload = false;
+ string deviceStatus = string.Empty;
+ string deviceStatusDesc = string.Empty;
+ short NewStatus = plc.ReadInt16("D10");
+
+ if (NewStatus != 100)
+ {
+ //特殊情况1:运行->报警->……->运行
+ if (NewStatus == 3 && OldStatues == 1) { run_AlarmMark = true; }//第一次进入报警状态
+ if (NewStatus == 1) { run_AlarmMark = false; }//下一次运行状态,置位
+ if (run_AlarmMark) { NewStatus = 3; }//报警状态后下一次运行状态期间一直是报警状态
+ //特殊情况2:运行->闲置->……->运行
+ if (NewStatus == 4 && OldStatues == 1) { run_IdleMark = true; }//第一次进入闲置状态
+ if (NewStatus == 1) { run_IdleMark = false; }//下一次闲置状态,置位
+ if (run_IdleMark) { NewStatus = 4; }//闲置状态后下一次运行状态期间一直是闲置状态
+
+ //闲置状态结束报警信息上传
+ if (OldStatues == 4 && NewStatus != 4 && isUpIdleAlarm)
+ {
+ _ = UpIdleDataAsync(idleAlarmData, strGuidAlarm);
+ }
+
+ //设备状态信号灯变量
+ CommonMethods.Statues = new List { NewStatus == 1, NewStatus == 2, NewStatus == 3, NewStatus == 4 };
+
+ // 状态切换时生成新的 GUID
+ if (NewStatus != OldStatues)
+ {
+ // 从 Alert 切换到非 Alert 状态时,上传报警信息
+ if (OldStatues == 3 && NewStatus != 3)
+ {
+ ShowAlarmEven_Run(strGuidAlarm, NewStatus);
+ }
+
+ // 只要状态变更,就生成新的 GUID
+ strGuidAlarm = Guid.NewGuid().ToString(); // 强制生成新的 GUID
+
+ isUpload = true;
+ OldStatues = NewStatus;
+ StatuesBeginTime = Convert.ToDateTime(Date);
+
+ IniConfigHelper.WriteIniData("上一次设备状态", "DeviceStatus", NewStatus.ToString(), CommonMethods.strDeviceStatusPath);
+ IniConfigHelper.WriteIniData("上一次Guid", "DeviceGuid", strGuidAlarm, CommonMethods.strDeviceStatusPath);
+ }
+ else
+ {
+ if (Date.Subtract(StatuesBeginTime).TotalSeconds < 5 * 60 && Date.Subtract(StatuesBeginTime).TotalSeconds >= 4 * 60)
+ {
+ isUpload = true;
+ StatuesBeginTime = DateTime.Now;
+ }
+ }
+
+ // 根据状态设置设备状态和描述
+ switch (NewStatus)
+ {
+ case 1:
+ deviceStatus = "Run";
+ deviceStatusDesc = "正常运行";
+ break;
+ case 2://取消
+ deviceStatus = "Maintain";
+ deviceStatusDesc = "停止运行";
+ break;
+ case 3:
+ deviceStatus = "Alert";
+ deviceStatusDesc = "故障调试";
+ // Alert 状态时上传报警信息
+ ShowAlarmEven_Run(strGuidAlarm, NewStatus);
+ break;
+ case 4:
+ deviceStatus = "Idle";
+ deviceStatusDesc = "闲置待机";
+ ShowStuesEven_Run(strGuidAlarm);
+ break;
+ }
+
+ // 上传设备状态信息
+ if (CommonMethods.sysConfig.MesModeSwitching && CommonMethods.mesConfig.isConnected && isUpload && (NewStatus == 1 || NewStatus == 3 || NewStatus == 4))
+ {
+ var (success, message) = await CommonMethods.hbgMes.UploadEquStatusAsync(
+ CommonMethods.mesConfig.DeviceStatus,
+ CommonMethods.mesConfig.siteCode,
+ CommonMethods.mesConfig.lineCode,
+ CommonMethods.mesConfig.equipNum,
+ CommonMethods.mesConfig.MaterialCode,
+ CommonMethods.mesConfig.mesUserName,
+ strGuidAlarm, // RUN 状态时传空
+ deviceStatus,
+ null
+ ).ConfigureAwait(false);
+
+ if (success)
+ {
+ //StatuesCount = 0;
+ }
+ }
+ }
+ }
+
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.Instance.WriteEX(ex);
+ }
+ Thread.Sleep(100);
+ //Task.Delay(100).CountinueWith();
+ }
+ }
+
+
+ #region 触摸屏弹窗
+ private async void ShowStuesEven_Run(string strGuid)
+ {
+ if (CommonMethods.plcDevices.Count() > 0)
+ {
+ for (int i = 0; i < CommonMethods.plcDevices.Count(); i++)
+ {
+ if (CommonMethods.plcDevices[i].IsConnected && CommonMethods.plcDevices[i].Name == "PLC_1")
+ {
+
+ string strCode = "";
+ string alarmName = "";
+ short HMIStatus = CommonMethods.plcDevices[i].ReadInt16("D40");
+ if (HMIStatus != 0)
+ {
+ CommonMethods.AddDataMonitorLog(0, "触发了手动HMI弹窗功能".Translated());
+ switch (HMIStatus) //注意:B1-D12都为闲置代码,上传代码是只需上传闲置代码
+ {
+ case 5:
+ strCode = "D1";
+ alarmName = "设备故障".Translated();
+ CommonMethods.AddDataMonitorLog(0, $"{strCode} {alarmName}");
+ break;
+ case 6:
+ strCode = "D2";
+ alarmName = "设备转型".Translated();
+ CommonMethods.AddDataMonitorLog(0, $"{strCode} {alarmName}");
+ break;
+ case 7:
+ strCode = "D3";
+ alarmName = "工程变更".Translated();
+ CommonMethods.AddDataMonitorLog(0, $"{strCode} {alarmName}");
+ break;
+ case 8:
+ strCode = "D4";
+ alarmName = "MES停机".Translated();
+ CommonMethods.AddDataMonitorLog(0, $"{strCode} {alarmName}");
+ break;
+ case 9:
+ strCode = "D5";
+ alarmName = "品质异常".Translated();
+ CommonMethods.AddDataMonitorLog(0, $"{strCode} {alarmName}");
+ break;
+ case 10:
+ strCode = "D6";
+ alarmName = "配件,物料更换".Translated();
+ CommonMethods.AddDataMonitorLog(0, $"{strCode} {alarmName}");
+ break;
+ case 11:
+ strCode = "D7";
+ alarmName = "来料不良".Translated();
+ CommonMethods.AddDataMonitorLog(0, $"{strCode} {alarmName}");
+ break;
+ case 12:
+ strCode = "D8";
+ alarmName = "待料".Translated();
+ CommonMethods.AddDataMonitorLog(0, $"{strCode} {alarmName}");
+ break;
+ case 13:
+ strCode = "D9";
+ alarmName = "品质检查".Translated();
+ CommonMethods.AddDataMonitorLog(0, $"{strCode} {alarmName}");
+ break;
+ case 14:
+ strCode = "D10";
+ alarmName = "清洁点检".Translated();
+ CommonMethods.AddDataMonitorLog(0, $"{strCode} {alarmName}");
+ break;
+ case 15:
+ strCode = "D11";
+ alarmName = "环境不满足".Translated();
+ CommonMethods.AddDataMonitorLog(0, $"{strCode} {alarmName}");
+ break;
+ case 16:
+ strCode = "D12";
+ alarmName = "其它非计划停机".Translated();
+ CommonMethods.AddDataMonitorLog(0, $"{strCode} {alarmName}");
+ break;
+ default:
+ break;
+ }
+
+ if (CommonMethods.sysConfig.MesModeSwitching && CommonMethods.mesConfig.isConnected)
+ {
+ List alarmObj = new List();
+ AlarmData alarmData = new AlarmData();
+ alarmData.AlarmCode = strCode;
+ alarmData.StartTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
+ alarmData.EndTime = "";
+ alarmData.AlarmType = "非计划损失";
+ alarmData.AlarmContent = alarmName;
+ alarmObj.Add(alarmData);
+
+ //闲置报警上传标志
+ isUpIdleAlarm = true;
+ idleAlarmData.Add(alarmData);
+
+ //上传报警
+ var (success, message) = await CommonMethods.hbgMes.UploadEquAlarmAsync(CommonMethods.mesConfig.DeviceAlarm, CommonMethods.mesConfig.siteCode, CommonMethods.mesConfig.lineCode,
+ CommonMethods.mesConfig.equipNum, CommonMethods.mesConfig.MaterialCode, CommonMethods.mesConfig.mesUserName, alarmObj, strGuid).ConfigureAwait(false);
+ if (success)
+ CommonMethods.AddDataMonitorLog(0, "HMI手动弹窗状态已上传成功,代码".Translated() + $":{strCode}");
+ }
+ bool IsSuccess = CommonMethods.plcDevices[i].WriteInt16("D40", 0);
+ if (IsSuccess)
+ CommonMethods.AddDataMonitorLog(0, $"D40已复位0".Translated());
+
+ }
+ }
+ }
+ }
+ }
+
+ #endregion
+
+ ///
+ /// HMI手动弹窗结束状态上传
+ ///
+ ///
+ ///
+ ///
+ private async Task UpIdleDataAsync(List idleAlarmDate, string strGuidAlarm)
+ {
+ if (idleAlarmDate.Count == 0) { return; }
+ string strCode = "";
+ foreach (var item in idleAlarmDate)
+ {
+ item.EndTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
+ strCode = item.AlarmCode + ",";
+ }
+ if (CommonMethods.sysConfig.MesModeSwitching && CommonMethods.mesConfig.isConnected)
+ {
+ //上传报警
+ var (success, message) = await CommonMethods.hbgMes.UploadEquAlarmAsync(CommonMethods.mesConfig.DeviceAlarm, CommonMethods.mesConfig.siteCode, CommonMethods.mesConfig.lineCode,
+ CommonMethods.mesConfig.equipNum, CommonMethods.mesConfig.MaterialCode, CommonMethods.mesConfig.mesUserName, idleAlarmDate, strGuidAlarm).ConfigureAwait(false);
+ if (success)
+ CommonMethods.AddDataMonitorLog(0, "HMI手动弹窗结束状态已上传成功,代码".Translated() + $":{strCode.TrimEnd(',')}");
+ }
+ idleAlarmData.Clear();
+ isUpIdleAlarm = false;
+ }
+
+ #endregion
+
+ #region 6、设备报警
+ //报警表单数据
+ List listAlarmForm = null;
+ public void GetAlarmData()
+ {
+ //加载PLC报警文件
+ try
+ {
+ string strFileName = Path.Combine(Environment.CurrentDirectory, "欧姆龙报警寄存器对应表.csv");
+ listAlarmForm = CSVHelper.ReadCSV(strFileName);
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show("加载报警文件失败!");
+ }
+
+ }
+
+ ///
+ /// 报警信息读取
+ ///
+ ///
+ ///
+ List listOld = new List();
+ List listNew = new List();
+ private void ShowAlarmEven_Run(string strGuidAlarm,int MachineState)
+ {
+ //if (!CommonMethods.IsRun || !CommonMethods.plcDevices[0].IsConnected)
+ //{
+ // return;
+ //}
+ bool boolIsConnected = false;
+ if (CommonMethods.plcDevices.Count() > 0)
+ {
+ for (int i = 0; i < CommonMethods.plcDevices.Count(); i++)
+ {
+ if (CommonMethods.plcDevices[i].IsConnected && CommonMethods.plcDevices[i].Name == "PLC_1")
+ {
+ boolIsConnected = true;
+ break;
+ }
+ }
+ }
+ if (!CommonMethods.IsRun || !boolIsConnected)
+ {
+ return;
+ }
+
+ try
+ {
+ // 1. 读取 PLC 报警信息
+ List alarmStatusList = ReadAlarmStatusFromPLC();
+ if (alarmStatusList == null || listAlarmForm == null)//true
+ {
+ return;
+ }
+
+ // 2. 解析报警信息并生成新的报警列表
+ List newAlarms = ParseAlarmStatus(alarmStatusList, strGuidAlarm);
+ if (newAlarms.Count > 0)
+ {
+ // 3. 检测新报警并添加到旧报警列表,上传第一次报警信息
+ DetectNewAlarmsAsync(newAlarms);
+
+ // 4. 文字显示新报警信息
+ DisplayAlarmInfo(newAlarms);
+ }
+
+ // 5. 检测已取消的报警并处理
+ List canceledAlarms = DetectCanceledAlarms(newAlarms);
+ if (canceledAlarms.Count > 0)
+ {
+ // 6. 记录已取消的报警信息(保存本地)
+ LogCanceledAlarms(canceledAlarms);
+
+ // 7. 上传已取消的报警信息(报警结束上传一次)
+ UploadCanceledAlarmsAsync(canceledAlarms, strGuidAlarm);
+ }
+
+ if (MachineState != 3 && listOld.Count > 0) //设备状态变为非报警并旧报警仍有报警。旧报警上传并清空
+ {
+ List tempOldAlarmList = new List();
+ foreach (var item in listOld)
+ {
+ item.EndTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); //所有报警全部结束时间赋值
+ tempOldAlarmList.Add(item);
+ }
+
+ // 6. 记录已取消的报警信息(保存本地)
+ LogCanceledAlarms(tempOldAlarmList);
+
+ // 7. 上传已取消的报警信息(报警结束上传一次)
+ UploadCanceledAlarmsAsync(tempOldAlarmList, strGuidAlarm);
+
+ listOld.Clear();
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.Instance.WriteEX(ex);
+ }
+ }
+
+
+ ///
+ /// 从 PLC 读取报警状态
+ ///
+ private List ReadAlarmStatusFromPLC()
+ {
+ string Date = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
+ // 读取足够的字节覆盖所有可能的报警地址
+ int startAddr = 10010;
+ int endAddr = 10519; // 根据实际最大地址调整
+ int wordCount = endAddr - startAddr + 1;
+ int byteCount = wordCount * 2; // 每个字2字节
+
+ if (CommonMethods.plcDevices.Count() > 0)
+ {
+ for (int i = 0; i < CommonMethods.plcDevices.Count(); i++)
+ {
+ if (CommonMethods.plcDevices[i].IsConnected && CommonMethods.plcDevices[i].Name == "PLC_1")
+ {
+
+ JYResult resByte = CommonMethods.plcDevices[i].ReadValue>($"D{startAddr}", DataType.ArrByte, (ushort)byteCount);
+
+ if (resByte.IsSuccess && resByte.Content != null)
+ {
+ return PLCAlarmParseHelper.OmronFinsByte2Status(startAddr, resByte.Content);
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// 解析报警状态并生成新的报警列表
+ ///
+ private List ParseAlarmStatus(List alarmStatusList, string strGuidAlarm)
+ {
+ string Date = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
+
+ var alarmTypeMap = new Dictionary // 定义报警类型映射字典
+ {
+ {'1', "停机报警"},
+ {'2', "非停机报警"},
+ };
+ switch (LanguageManager.LanguageCode)
+ {
+ case "en"://英语报警信息
+ return alarmStatusList
+ .Where(status => status.Status)
+ .Join(
+ listAlarmForm,
+ status => status.PLCAdress,
+ form => form.PLCAdress,
+ (status, form) =>
+ {
+ var alarmContent = string.IsNullOrEmpty(form.EnAlarmContent)
+ ? status.PLCAdress
+ : form.EnAlarmContent;
+ string alarmType = "停机报警"; // 默认值
+ if (form.AlarmCode?.Length >= 3)
+ {
+ char thirdChar = form.AlarmCode[2];
+ // 尝试从字典获取对应的值
+ if (alarmTypeMap.TryGetValue(thirdChar, out string mappedType))
+ {
+ alarmType = mappedType;
+ }
+ }
+ return new AlarmData
+ {
+ AlarmGuid = strGuidAlarm,
+ PLCAdress = status.PLCAdress,
+ AlarmCode = form.AlarmCode,
+ AlarmContent = alarmContent,
+ AlarmType = alarmType,
+ AlarmDesc = alarmContent,
+ AlarmState = "1",
+ StartTime = Date,
+ EndTime = "",
+ Flag = 0
+ };
+ })
+ .ToList();
+ case "hu"://匈牙利语报警信息
+ return alarmStatusList
+ .Where(status => status.Status)
+ .Join(
+ listAlarmForm,
+ status => status.PLCAdress,
+ form => form.PLCAdress,
+ (status, form) =>
+ {
+ var alarmContent = string.IsNullOrEmpty(form.HuAlarmContent)
+ ? status.PLCAdress
+ : form.HuAlarmContent;
+ string alarmType = "停机报警"; // 默认值
+ if (form.AlarmCode?.Length >= 3)
+ {
+ char thirdChar = form.AlarmCode[2];
+ // 尝试从字典获取对应的值
+ if (alarmTypeMap.TryGetValue(thirdChar, out string mappedType))
+ {
+ alarmType = mappedType;
+ }
+ }
+ return new AlarmData
+ {
+ AlarmGuid = strGuidAlarm,
+ PLCAdress = status.PLCAdress,
+ AlarmCode = form.AlarmCode,
+ AlarmContent = alarmContent,
+ AlarmType = alarmType,
+ AlarmDesc = alarmContent,
+ AlarmState = "1",
+ StartTime = Date,
+ EndTime = "",
+ Flag = 0
+ };
+ })
+ .ToList();
+ case "ms"://马来语报警信息
+ return alarmStatusList
+ .Where(status => status.Status)
+ .Join(
+ listAlarmForm,
+ status => status.PLCAdress,
+ form => form.PLCAdress,
+ (status, form) =>
+ {
+ var alarmContent = string.IsNullOrEmpty(form.MsAlarmContent)
+ ? status.PLCAdress
+ : form.MsAlarmContent;
+ string alarmType = "停机报警"; // 默认值
+ if (form.AlarmCode?.Length >= 3)
+ {
+ char thirdChar = form.AlarmCode[2];
+ // 尝试从字典获取对应的值
+ if (alarmTypeMap.TryGetValue(thirdChar, out string mappedType))
+ {
+ alarmType = mappedType;
+ }
+ }
+ return new AlarmData
+ {
+ AlarmGuid = strGuidAlarm,
+ PLCAdress = status.PLCAdress,
+ AlarmCode = form.AlarmCode,
+ AlarmContent = alarmContent,
+ AlarmType = alarmType,
+ AlarmDesc = alarmContent,
+ AlarmState = "1",
+ StartTime = Date,
+ EndTime = "",
+ Flag = 0
+ };
+ })
+ .ToList();
+ case "zh":
+ default:
+ return alarmStatusList
+ .Where(status => status.Status)
+ .Join(
+ listAlarmForm,
+ status => status.PLCAdress,
+ form => form.PLCAdress,
+ (status, form) =>
+ {
+ var alarmContent = string.IsNullOrEmpty(form.AlarmContent)
+ ? status.PLCAdress
+ : form.AlarmContent;
+ string alarmType = "停机报警"; // 默认值
+ if (form.AlarmCode?.Length >= 3)
+ {
+ char thirdChar = form.AlarmCode[2];
+ // 尝试从字典获取对应的值
+ if (alarmTypeMap.TryGetValue(thirdChar, out string mappedType))
+ {
+ alarmType = mappedType;
+ }
+ }
+ return new AlarmData
+ {
+ AlarmGuid = strGuidAlarm,
+ PLCAdress = status.PLCAdress,
+ AlarmCode = form.AlarmCode,
+ AlarmContent = alarmContent,
+ AlarmType = alarmType,
+ AlarmDesc = alarmContent,
+ AlarmState = "1",
+ StartTime = Date,
+ EndTime = "",
+ Flag = 0
+ };
+ })
+ .ToList();
+ }
+ }
+
+ private HashSet _allUploadedAlarmContents = new HashSet();
+ private void DetectNewAlarmsAsync(List newAlarms)
+ {
+ List tempAlarmList = new List();
+ foreach (var alarm in newAlarms)
+ {
+ if (!listOld.Any(o => o.PLCAdress == alarm.PLCAdress))
+ {
+ listOld.Add(alarm);
+ tempAlarmList.Add(alarm); //把新报警保存到临时表中
+ }
+ }
+ if (CommonMethods.sysConfig.MesModeSwitching && CommonMethods.mesConfig.isConnected)
+ {
+ CommonMethods.hbgMes.UploadEquAlarmAsync(
+ CommonMethods.mesConfig.DeviceAlarm,
+ CommonMethods.mesConfig.siteCode,
+ CommonMethods.mesConfig.lineCode,
+ CommonMethods.mesConfig.equipNum,
+ CommonMethods.mesConfig.MaterialCode,
+ CommonMethods.mesConfig.mesUserName,
+ tempAlarmList,
+ strGuidAlarm
+ );
+ }
+ }
+
+ ///
+ /// 显示报警信息
+ ///
+ private void DisplayAlarmInfo(List newAlarms)
+ {
+ StringBuilder sb = new StringBuilder();
+ newAlarms.ForEach(r => sb.Append($"{r.AlarmContent} "));
+
+ if (sb.Length > 0)
+ {
+ CommonMethods.SlideAlarmText = sb.ToString().Substring(0, sb.Length - 10);
+ }
+ }
+
+ ///
+ /// 检测已取消的报警
+ ///
+ private List DetectCanceledAlarms(List newAlarms)
+ {
+ var canceledAlarms = new List();
+ var listOldAlarm = new List(listOld);
+
+ foreach (var item in listOldAlarm)
+ {
+ if (!newAlarms.Any(n => n.PLCAdress == item.PLCAdress))
+ {
+ item.EndTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
+ canceledAlarms.Add(item);
+ listOld.Remove(item);
+ }
+ }
+
+ return canceledAlarms;
+ }
+
+ ///
+ /// 记录已取消的报警信息
+ ///
+ private void LogCanceledAlarms(List canceledAlarms)
+ {
+ var nowDate = DateTime.Now;
+ string basePath = $@"{CommonMethods.strAlarmLogspath}\{nowDate.Year}\{nowDate.Month}";
+ string fileName = $@"{nowDate.Day}.csv";
+ string title = "GUID,报警地址,故障代码,报警内容,开始时间,结束时间,是否停机";
+ string alarmContent = string.Join("\n", canceledAlarms.Select(m1 =>
+ $"{m1.AlarmGuid},{m1.PLCAdress},{m1.AlarmCode},{m1.AlarmContent},{m1.StartTime},{m1.EndTime},{m1.AlarmType}"));
+
+ CSVHelper.WriterCSV(basePath, fileName, title, alarmContent);
+
+ foreach (var m1 in canceledAlarms)
+ {
+ var alarm = new SysLog
+ {
+ InsertTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
+ LogType = "报警日志",
+ Note = m1.AlarmContent == "" ? m1.PLCAdress : m1.AlarmContent,
+ AlarmType = "错误",
+ Operater = CommonMethods.currentAdmin.LoginName,
+ VarName = m1.PLCAdress,
+ AlarmSet = "",
+ AlarmValue = "1",
+ };
+ // 显示报警信息
+ CommonMethods.AddAlarmLog(alarm);//存报警信息
+ // 报警top10保存
+ SaveAlarmCount(canceledAlarms);
+
+ }
+ }
+
+
+ ///
+ /// 报警结束上传
+ ///
+ ///
+ ///
+ private void UploadCanceledAlarmsAsync(List canceledAlarms, string strGuidAlarm)
+ {
+ if (CommonMethods.sysConfig.MesModeSwitching)
+ {
+ CommonMethods.hbgMes.UploadEquAlarmAsync(
+ CommonMethods.mesConfig.DeviceAlarm,
+ CommonMethods.mesConfig.siteCode,
+ CommonMethods.mesConfig.lineCode,
+ CommonMethods.mesConfig.equipNum,
+ CommonMethods.mesConfig.MaterialCode,
+ CommonMethods.mesConfig.mesUserName,
+ canceledAlarms,
+ strGuidAlarm
+ );
+ }
+ }
+
+
+ ///
+ /// 报警Top10保存
+ ///
+ private async Task SaveAlarmCount(List alarmDatas)
+ {
+ bool ISAdd = false;
+ var recordToUpdate = new AlarmCountEntry();
+
+ DateTime today = DateTime.Today;
+ DateTime upTime = DateTime.Now;
+ var listAlarmStatistics = CommonMethods.db.QueryWhereList(it => Convert.ToDateTime(it.AlarmTime.ToString("yyyy-MM-dd")) == today);
+ List fd = new List();
+ if (listAlarmStatistics != null)
+ {
+ alarmDatas.ForEach(async r =>
+ {
+ recordToUpdate = listAlarmStatistics.FirstOrDefault(x => x.PLCAdress.Trim() == r.PLCAdress);
+ if (recordToUpdate != null)
+ {
+ await CommonMethods.db.UpdateAsync(it => it.AlarmCount == it.AlarmCount + 1, it => it.PLCAdress == r.PLCAdress && Convert.ToDateTime(it.AlarmTime.ToString("yyyy-MM-dd")) == today).ConfigureAwait(false);
+ }
+ else
+ {
+ ISAdd = true;
+ AlarmCountEntry alarm = new AlarmCountEntry();
+ alarm.PLCAdress = r.PLCAdress;
+ alarm.AlarmContent = r.AlarmContent;
+ alarm.AlarmCount = 1;
+ alarm.Class = CommonMethods.strClass;
+ alarm.AlarmTime = upTime;
+ fd.Add(alarm);
+ }
+ });
+ if (ISAdd)
+ {
+ bool res = await CommonMethods.db.AddReturnBoolAsync(fd).ConfigureAwait(false);
+ ISAdd = false;
+ }
+ }
+ // 显示报警信息方法已存数据库,报警TOP10就插入新增了
+ //else
+ //{
+ // foreach (var m in listNew)
+ // {
+ // AlarmCountEntry alarm = new AlarmCountEntry();
+ // alarm.PLCAdress = m.PLCAdress;
+ // alarm.AlarmContent = m.AlarmContent;
+ // alarm.AlarmCount = 1;
+ // alarm.Class = CommonMethods.strClass;
+ // alarm.AlarmTime = today;
+ // fd.Add(alarm);
+ // }
+ // await CommonMethods.db.AddReturnBoolAsync(fd);
+ //}
+ await GetASqlAlarmData(DateTime.Today, CommonMethods.strClass).ConfigureAwait(false);
+ }
+
+
+ ///
+ /// 读取数据库报警信息
+ ///
+ ///
+ ///
+ public async Task GetASqlAlarmData(DateTime today, string strClass)
+ {
+ try
+ {
+ List chartAlarms = new List();
+ List AlarmDatas = null;
+ List List_X = new List();
+ List List_Y = new List();
+
+ DateTime baiClassStart = Convert.ToDateTime(DateTime.Now.ToString("yyyy-MM-dd 8:30:00"));
+ DateTime baiClassEnd = Convert.ToDateTime(DateTime.Now.ToString("yyyy-MM-dd 20:29:59"));
+
+ DateTime yeClassStart = Convert.ToDateTime(DateTime.Now.ToString("yyyy-MM-dd 20:30:00"));
+ DateTime yeClassEnd = Convert.ToDateTime(DateTime.Now.AddDays(1).ToString("yyyy-MM-dd 8:29:59"));
+
+
+ if (strClass == "全天".Translated())
+ {
+ AlarmDatas = await CommonMethods.db.QueryListAsync("AlarmCount", 10, it => Convert.ToDateTime(it.AlarmTime.ToString("yyyy-MM-dd")) == today).ConfigureAwait(false);
+ }
+ else if (strClass == "白班".Translated())
+ {
+ AlarmDatas = await CommonMethods.db.QueryListAsync("AlarmCount", 10, it => it.AlarmTime >= baiClassStart && it.AlarmTime <= baiClassEnd && it.Class == strClass).ConfigureAwait(false);
+ }
+ else
+ {
+ AlarmDatas = await CommonMethods.db.QueryListAsync("AlarmCount", 10, it => it.AlarmTime >= yeClassStart && it.AlarmTime <= yeClassEnd && it.Class == strClass).ConfigureAwait(false);
+ }
+
+ if (AlarmDatas != null && AlarmDatas.Count > 0)
+ {
+
+ for (int i = 0; i < AlarmDatas.Count; i++)
+ {
+ chartAlarms.Add(new ChartDataType()
+ {
+ DataType = AlarmDatas[i].AlarmContent,
+ DataCount = (int)AlarmDatas[i].AlarmCount
+ });
+ }
+ CommonMethods.Lst_AlarmTop10_Qty = chartAlarms;
+ }
+ else
+ {
+ CommonMethods.Lst_AlarmTop10_Qty = null;
+ }
+ }
+ catch (Exception)
+ {
+
+ }
+ }
+
+ #endregion
+
+ #region 配置下发给PLC
+ public async Task SendConfigToPLCAsync(PlcReadWriteBase plc)
+ {
+ try
+ {
+ // 给物流线PLC下发 生产型号 和 本机架各垛位的档位
+ if (plc.IsConnected && plc.Name == "PLC_2")
+ {
+ await SendConfigToWuliuPLCAsync(plc);
+ }
+ else if (plc.IsConnected && plc.Name == "PLC_1")
+ {
+ await SendConfigToZuPanPLCAsync(plc);
+ }
+
+ }
+ catch (Exception ex)
+ {
+ CommonMethods.AddLog(true, "PC写入PLC配置异常".Translated() + ":" + ex.Message);
+ LogHelper.Instance.WriteEX(ex);
+ }
+ }
+
+ private async Task SendConfigToWuliuPLCAsync(PlcReadWriteBase plc)
+ {
+ string plc_addr = CommonMethods.mesConfig.GetWuliuModelAddr();
+ if (plc_addr == null || String.IsNullOrEmpty(plc_addr))
+ {
+ CommonMethods.AddDataMonitorLog(2, $"机架号不识别,无对应机架号的PLC生产型号地址({plc_addr})");
+ return;
+ }
+
+ int model = CommonMethods.mesConfig.GetModel(CommonMethods.mesConfig.ModelName);
+ JYResult result = await Task.Run(() => plc.WriteValue(plc_addr, (short)model, DataType.Short)).ConfigureAwait(false);
+ if (result.IsSuccess)
+ {
+ CommonMethods.AddDataMonitorLog(1, $"Write_WuLiu_PLC_model({plc_addr})_{model}_Successful");
+ }
+ else
+ {
+ CommonMethods.AddDataMonitorLog(2, $"Write_Wuliu_PLC_model({plc_addr})_{model}_Failed");
+ }
+
+ Dictionary plc_addrDict = CommonMethods.mesConfig.GetWuliuGradeAddr();
+ if (plc_addrDict == null || plc_addrDict.Count == 0)
+ {
+ CommonMethods.AddDataMonitorLog(2, $"机架号不识别或者垛位号错误,无对应的PLC分档地址({plc_addr})");
+ return;
+ }
+
+ foreach (var kvp in plc_addrDict)
+ {
+ if (kvp.Value == null || String.IsNullOrEmpty(kvp.Value))
+ {
+ continue;
+ }
+
+ int stationNo = kvp.Key;
+ string plcAddress = kvp.Value;
+ string strGrade = MesConfig.GetLocGrade(stationNo, CommonMethods.curFdConfig);
+ int grade = CommonMethods.GetGradeNo(strGrade);
+
+ result = await Task.Run(() => plc.WriteValue(plcAddress, (short)grade, DataType.Short)).ConfigureAwait(false);
+ if (result.IsSuccess)
+ {
+ CommonMethods.AddDataMonitorLog(1, $"Write_PLC_grade({plcAddress})_{strGrade}({grade})_Successful");
+ }
+ else
+ {
+ CommonMethods.AddDataMonitorLog(2, $"Write_PLC_grade({plcAddress})_{strGrade}({grade})_Failed");
+ }
+ }
+ }
+
+ private async Task SendConfigToZuPanPLCAsync(PlcReadWriteBase plc)
+ {
+ var writeTasks = new List();
+ Dictionary loc_plcAddr = CommonMethods.mesConfig.gw_loc_plcAddr[Convert.ToInt16(CommonMethods.sysConfig.StationNo)];
+
+ FDConfig fdconfig = CommonMethods.curFdConfig;
+ foreach (KeyValuePair locKvp in loc_plcAddr)
+ {
+ int stationNo = locKvp.Key;
+ string plcAddr = locKvp.Value;
+
+ string gradeStr = MesConfig.GetLocGrade(stationNo, fdconfig);
+ try
+ {
+ // 转换等级
+ int gradeValue = CommonMethods.GetGradeNo(gradeStr);
+ string logBase = $"LOC{stationNo} Write_Zupan_PLC({plcAddr}:等级{gradeStr},转换PLC值{gradeValue})";
+ JYResult result = await Task.Run(() => plc.WriteValue(plcAddr, (short)gradeValue, DataType.Short)).ConfigureAwait(false);
+ if (result.IsSuccess)
+ {
+ CommonMethods.AddDataMonitorLog(1, $"{logBase} 成功");
+ CommonMethods.AddOPLog(true, $"{logBase} 成功");
+ }
+ else
+ {
+ string failMsg = $"{logBase} 失败,错误:{result.Message ?? "无详细信息"}";
+ CommonMethods.AddDataMonitorLog(2, failMsg);
+ CommonMethods.AddOPLog(false, failMsg);
+ }
+ }
+ catch (Exception ex)
+ {
+ string exMsg = $"LOC{stationNo} Write_Zupan_PLC({plcAddr}) 异常:{ex.Message},堆栈:{ex.StackTrace}";
+ CommonMethods.AddDataMonitorLog(3, exMsg);
+ CommonMethods.AddOPLog(false, exMsg);
+ }
+ }
+ }
+ #endregion
+
+ #region 下发权限给PLC
+ ///
+ /// 下发权限到PLC
+ ///
+ public async Task SendUserRightToPLCAsync(PlcReadWriteBase plc)
+ {
+
+ if (!plc.IsConnected || plc.Name != "PLC_1")
+ {
+ if(plc.Name == "PLC_1")
+ CommonMethods.AddDataMonitorLog(2, $"组盘PLC连接中断,无法给PLC下发用户名和权限");
+ return;
+ }
+
+ ushort quanxian = 1;
+ switch (CommonMethods.currentAdmin.LoginLevel)
+ {
+ case "操作员":
+ quanxian = 1;
+ break;
+ case "工程师":
+ quanxian = 2;
+ break;
+ case "管理员":
+ quanxian = 3;
+ break;
+ default:
+ break;
+ }
+
+ Dictionary plc_addrs = CommonMethods.mesConfig.GetHMIUserRightAddr();
+ if (!string.IsNullOrEmpty(plc_addrs["right"]))
+ {
+ JYResult result = await Task.Run(() => plc.WriteValue(plc_addrs["right"], (short)quanxian, DataType.Short));
+ if (result.IsSuccess)
+ {
+ CommonMethods.AddDataMonitorLog(1, $"Write_PLC_permission_{quanxian}({plc_addrs["right"]})_Successful");
+ }
+ else
+ {
+ CommonMethods.AddDataMonitorLog(2, $"Write_PLC_permission_{quanxian}({plc_addrs["right"]})_Failed");
+ }
+ }
+
+ if (!string.IsNullOrEmpty(plc_addrs["user"]))
+ {
+ const int BARCODE_SIZE = 40;
+ string loginName = string.Concat(CommonMethods.currentAdmin.LoginName + GetString_0(BARCODE_SIZE - CommonMethods.currentAdmin.LoginName.Length));
+ byte[] codeBytes = Encoding.Default.GetBytes(loginName).ByteReverse();
+ JYResult result2 = await Task.Run(() => plc.WriteValue(plc_addrs["user"], codeBytes, DataType.ArrByte));
+ if (result2.IsSuccess)
+ {
+ CommonMethods.AddDataMonitorLog(1, $"Write_PLC_username_{loginName}({plc_addrs["user"]})_Successful\n");
+ }
+ else
+ {
+ CommonMethods.AddDataMonitorLog(2, $"Write_PLC_username_{loginName}({plc_addrs["user"]})_Failed");
+ }
+ }
+ }
+
+ #endregion
+
+ #region 获取白夜班次
+ bool qiehuan = false;
+ private void SetClass()
+ {
+ try
+ {
+ var nowDate = DateTime.Now;
+ string strClass = "";
+ DateTime time1 = Convert.ToDateTime($"{nowDate.ToShortDateString()} {CommonMethods.ChangeClassTime}");
+ DateTime time2 = Convert.ToDateTime($"{nowDate.ToShortDateString()} {Convert.ToDateTime(CommonMethods.ChangeClassTime).AddHours(12).ToString("HH:mm:ss")}");
+ if (nowDate <= time1 && nowDate >= time2)
+ {
+ foreach (var plc in CommonMethods.plcDevices)
+ {
+ if (_plcDeviceMapping.TryGetValue(plc, out var plcDevices))
+ {
+ foreach (var plcDevice in plcDevices)
+ {
+ if (plcDevice.IsConnected)
+ {
+ if (!qiehuan)
+ {
+ if (!SetPlcTime(plc))
+ {
+ CommonMethods.AddLog(true, $"PC写入PLC时间失败".Translated());
+ }
+ else
+ {
+ qiehuan = true;
+ CommonMethods.AddLog(false, "PC写入PLC时间成功".Translated());
+ }
+ }
+ }
+ }
+ strClass = "白班".Translated();
+ }
+ }
+ }
+ else
+ {
+ qiehuan = false;
+ strClass = "夜班".Translated();
+ }
+ CommonMethods.strClass = strClass;
+ }
+ catch (Exception ex)
+ {
+ CommonMethods.AddLog(true, "PC写入PLC时间异常".Translated() + ":" + ex.Message);
+ LogHelper.Instance.WriteEX(ex);
+ }
+ }
+ #endregion
+
+ #region 写入PLC时间
+ ///
+ /// 写当前时间给PLC
+ ///
+ private bool SetPlcTime(PlcReadWriteBase plcDevice)
+ {
+ var nowDate = DateTime.Now;
+ string yearStr = nowDate.Year.ToString();
+
+
+ bool IsSuccess = false;
+
+ // 为防止PLC时间出现偏差,每次自动运行的时候写PLC系统时间
+ short[] writeArr = new short[]
+ {
+ Convert.ToInt16(nowDate.Year),//D70
+ Convert.ToInt16(nowDate.Month),//D71
+ Convert.ToInt16(nowDate.Day),//D72
+ Convert.ToInt16(nowDate.Hour),//D73
+ Convert.ToInt16(nowDate.Minute),//D74
+ Convert.ToInt16(nowDate.Second),//D75
+ (short)1 //D76
+ };
+ if (plcDevice.IsConnected)
+ {
+ JYResult result = plcDevice.WriteValue("D70", writeArr, DataType.ArrShort);
+ if (result.IsSuccess)
+ {
+ IsSuccess = true;
+ }
+ }
+ return IsSuccess;
+
+ }
+ #endregion
+
+ #region 9、IDisposable 实现
+
+
+ // 如果需要在StopAsync中也显示进度,可以添加一个进度回调
+
+ //private Task processTask;
+
+ public async Task StopAsync(IProgress progress = null)
+ {
+ progress?.Report("正在停止控制中心...");
+
+ // 停止健康监控
+ if (_healthMonitorTimer != null)
+ {
+ _healthMonitorTimer.Stop();
+ _healthMonitorTimer.Dispose();
+ }
+
+ // 取消所有操作
+ if (_cts != null)
+ {
+ _cts.Cancel();
+ }
+ _isWorkerRunning = false;
+ _taskEvent.Set();
+
+ // 等待心跳任务完成
+ if (_heartbeatTask != null)
+ {
+ progress?.Report("停止心跳任务...");
+ try
+ {
+ // 使用Wait带超时
+ _heartbeatTask.Wait(5000);
+ }
+ catch (AggregateException) { }
+ catch { }
+ }
+
+ // 停止所有PLC读取线程
+ progress?.Report("停止PLC读取线程...");
+ StopAndCleanOldThreads();
+
+ // 等待工作线程完成
+ if (_workerThread != null && _workerThread.IsAlive)
+ {
+ progress?.Report("停止工作线程...");
+ _workerThread.Join(5000);
+ }
+
+ // 清理队列
+ int remainingTasks = _taskQueue.Count;
+ if (remainingTasks > 0)
+ {
+ progress?.Report($"清理剩余{remainingTasks}个任务...");
+ while (_taskQueue.TryDequeue(out _)) { }
+ }
+
+ progress?.Report("控制中心已停止");
+ }
+
+ public void Dispose()
+ {
+ if (_isDisposed) return;
+ _isDisposed = true;
+
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _isWorkerRunning = false;
+ _taskEvent.Set();
+ _taskEvent.Dispose();
+
+ if (_cts != null)
+ {
+ _cts.Cancel();
+ _cts.Dispose();
+ }
+
+ if (_healthMonitorTimer != null)
+ {
+ _healthMonitorTimer.Stop();
+ _healthMonitorTimer.Dispose();
+ }
+
+ _dbSemaphore?.Dispose();
+ _plcWriteSemaphore?.Dispose();
+
+ if (_timerRefreshData != null)
+ {
+ _timerRefreshData.Stop();
+ _timerRefreshData.Dispose();
+ }
+
+ foreach (var plc in CommonMethods.plcDevices)
+ {
+ try
+ {
+ plc.Disconnect();
+ }
+ catch { }
+ }
+
+ CommonMethods.plcDevices?.Clear();
+ _plcDeviceMapping?.Clear();
+ _plcAlarmReaders?.Clear();
+ CommonMethods.plcStatuses?.Clear();
+
+ _groupStatus?.Clear();
+ _stationFunctions?.Clear();
+ }
+ }
+
+ #endregion
+
+ #region 7、辅助方法
+
+ private static PLCType GetPLCTypeEnum(string plcTypeString)
+ {
+ if (Enum.TryParse(plcTypeString, out PLCType plcType))
+ {
+ return plcType;
+ }
+ throw new ArgumentException($"PLC 类型无效: {plcTypeString}");
+ }
+
+ #endregion
+
+ #region 诊断日志
+
+ private void StartDiagnosticMonitor()
+ {
+ Thread monitorThread = new Thread(() =>
+ {
+ while (!_cts.Token.IsCancellationRequested)
+ {
+ try
+ {
+ Thread.Sleep(60000); // 每分钟输出一次报告
+
+ // 检查关键工站是否卡死
+ CheckStuckStation("物流线料盘进站");
+ CheckStuckStation("物流线料盘来料");
+ CheckStuckStation("电池进站");
+
+ // 输出诊断报告
+ var report = StationDiagnostics.GetDiagnosticReport();
+ LogHelper.Instance.WriteLog(report);
+
+ // 输出系统状态
+ LogHelper.Instance.WriteLog($"【系统状态】队列大小={_taskQueue.Count} | 工作线程运行={_isWorkerRunning} | 总处理={_totalProcessedCount} | 总拒绝={_totalRejectedCount}");
+
+ // 检查PLC连接状态
+ foreach (var plc in CommonMethods.plcDevices)
+ {
+ if (plc != null)
+ {
+ LogHelper.Instance.WriteLog($"【PLC状态】{plc.Name} | IsConnected={plc.IsConnected}");
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.Instance.WriteError($"监控线程异常:{ex.Message}");
+ }
+ }
+ });
+ monitorThread.IsBackground = true;
+ monitorThread.Name = "DiagnosticMonitor";
+ monitorThread.Start();
+ }
+
+ public string GetStationStatus(string stationName)
+ {
+ lock (_statusLock)
+ {
+ var status = _groupStatus.ContainsKey(stationName) ? _groupStatus[stationName] : -1;
+ var queueSize = _taskQueue.Count;
+ var workerAlive = _workerThread?.IsAlive ?? false;
+
+ return $"工站:{stationName} 状态:{status} 队列:{queueSize} 工作线程存活:{workerAlive}";
+ }
+ }
+
+ // 手动重置卡死的工站
+ public void ResetStation(string stationName)
+ {
+ lock (_statusLock)
+ {
+ if (_groupStatus.ContainsKey(stationName))
+ {
+ _groupStatus[stationName] = 0;
+ LogHelper.Instance.WriteLog($"手动重置工站: {stationName}");
+ }
+ }
+ }
+
+ private void CheckStuckStation(string stationName)
+ {
+ // 检查是否有工站卡在处理中状态超过30秒
+ lock (_statusLock)
+ {
+ if (_groupStatus.ContainsKey(stationName) && _groupStatus[stationName] == 1)
+ {
+ LogHelper.Instance.WriteError($"【诊断-卡死检测】{stationName} | 状态为1(处理中)已超过30秒,可能已卡死");
+ }
+ }
+ }
+ public class StationDiagnostics
+ {
+ private static readonly object _lock = new object();
+ private static Dictionary> _triggerHistory = new Dictionary>();
+ private static Dictionary _timeoutCount = new Dictionary();
+ private static Dictionary _lastTriggerTime = new Dictionary();
+ private static Dictionary _lastCompleteTime = new Dictionary();
+
+ ///
+ /// 记录触发信号
+ ///
+ public static void LogTrigger(string stationName, short trigValue, int status)
+ {
+ lock (_lock)
+ {
+ if (!_triggerHistory.ContainsKey(stationName))
+ _triggerHistory[stationName] = new Queue();
+
+ _triggerHistory[stationName].Enqueue(DateTime.Now);
+
+ // 只保留最近100条记录
+ while (_triggerHistory[stationName].Count > 100)
+ _triggerHistory[stationName].Dequeue();
+
+ _lastTriggerTime[stationName] = DateTime.Now;
+
+ LogHelper.Instance.WriteLog($"【诊断-触发】{stationName} | Trig={trigValue} | Status={status} | 队列长度={_triggerHistory[stationName].Count}");
+ }
+ }
+
+ ///
+ /// 记录任务入队
+ ///
+ public static void LogTaskEnqueued(string stationName, int queueSize)
+ {
+ lock (_lock)
+ {
+ LogHelper.Instance.WriteLog($"【诊断-入队】{stationName} | 当前队列大小={queueSize}");
+ }
+ }
+
+ ///
+ /// 记录任务开始执行
+ ///
+ public static void LogTaskStart(string stationName)
+ {
+ lock (_lock)
+ {
+ var now = DateTime.Now;
+ if (_lastTriggerTime.ContainsKey(stationName))
+ {
+ var waitTime = (now - _lastTriggerTime[stationName]).TotalMilliseconds;
+ LogHelper.Instance.WriteLog($"【诊断-执行开始】{stationName} | 等待时间={waitTime:F0}ms");
+ }
+ else
+ {
+ LogHelper.Instance.WriteLog($"【诊断-执行开始】{stationName}");
+ }
+ }
+ }
+
+ ///
+ /// 记录任务完成
+ ///
+ public static void LogTaskComplete(string stationName, bool success, string errorMsg = "")
+ {
+ lock (_lock)
+ {
+ _lastCompleteTime[stationName] = DateTime.Now;
+ LogHelper.Instance.WriteLog($"【诊断-执行完成】{stationName} | 成功={success} | 错误={errorMsg}");
+ }
+ }
+
+ ///
+ /// 记录超时
+ ///
+ public static void LogTimeout(string stationName, int timeoutSeconds)
+ {
+ lock (_lock)
+ {
+ if (!_timeoutCount.ContainsKey(stationName))
+ _timeoutCount[stationName] = 0;
+
+ _timeoutCount[stationName]++;
+
+ LogHelper.Instance.WriteError($"【诊断-超时】{stationName} | 超时时间={timeoutSeconds}秒 | 累计超时次数={_timeoutCount[stationName]}");
+ }
+ }
+
+ ///
+ /// 获取诊断报告
+ ///
+ public static string GetDiagnosticReport()
+ {
+ lock (_lock)
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("=== 工站诊断报告 ===");
+
+ foreach (var station in _triggerHistory.Keys)
+ {
+ sb.AppendLine($"工站: {station}");
+ sb.AppendLine($" 最近触发时间: {(_lastTriggerTime.ContainsKey(station) ? _lastTriggerTime[station].ToString() : "无")}");
+ sb.AppendLine($" 最近完成时间: {(_lastCompleteTime.ContainsKey(station) ? _lastCompleteTime[station].ToString() : "无")}");
+ sb.AppendLine($" 触发次数(最近100): {_triggerHistory[station].Count}");
+ sb.AppendLine($" 超时次数: {(_timeoutCount.ContainsKey(station) ? _timeoutCount[station] : 0)}");
+
+ if (_lastTriggerTime.ContainsKey(station) && _lastCompleteTime.ContainsKey(station))
+ {
+ var processingTime = _lastCompleteTime[station] - _lastTriggerTime[station];
+ sb.AppendLine($" 最近处理耗时: {processingTime.TotalMilliseconds:F0}ms");
+ }
+ sb.AppendLine();
+ }
+
+ return sb.ToString();
+ }
+ }
+ }
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/JinYuan.ControlCenter/ControlCenter_Ext.cs b/JinYuan.ControlCenter/ControlCenter_Ext.cs
new file mode 100644
index 0000000..bd636f1
--- /dev/null
+++ b/JinYuan.ControlCenter/ControlCenter_Ext.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using JinYuan.Helper;
+using JinYuan.Models;
+using JinYuan.VirtualDataLibrary;
+using PLCCommunication.Common;
+using PLCCommunication.Common.DataConvert;
+
+namespace JinYuan.ControlCenters
+{
+ public partial class ControlCenter
+ {
+ public static string GetValueFromByteArray(byte[] bArray, string PLCGroupVariablekey, int offset = 0, Group pf = null)
+ {
+ return GetValueFromByteArray(bArray, getPLCVariable(PLCGroupVariablekey), offset, pf);
+ }
+
+ public static PLCGroupVariable getPLCVariable(string key)
+ {
+ PLCGroupVariable pLCGroupVariable = new PLCGroupVariable();
+ CommonMethods.PLCVariable.TryGetValue(key, out pLCGroupVariable);
+ return pLCGroupVariable;
+ }
+
+ ///
+ /// 获取MES返回结果
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static string GetValueFromByteArray(byte[] bArray, PLCGroupVariable var, int offset = 0, Group pf = null)
+ {
+ string value = "0";
+ try
+ {
+ switch (var.DataType)
+ {
+ case "Float":
+ value = FloatLib.GetFloatFromByteArray(bArray, var.ArrayStart * 2 + offset, DataFormat.CDAB).ToString("0.###");
+ break;
+ case "Short":
+ value = ShortLib.GetShortFromByteArray(bArray, var.ArrayStart * 2 + offset).ToString();
+ break;
+ case "Int":
+ value = IntLib.GetIntFromByteArray(bArray, var.ArrayStart * 2 + offset).ToString();
+ break;
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.Instance.WriteError($"数据解析异常, 原因: {ex}", "SysErrorLog");
+ }
+ return value;
+ }
+ }
+}
diff --git a/JinYuan.ControlCenter/ControlCenter_Plc.cs b/JinYuan.ControlCenter/ControlCenter_Plc.cs
new file mode 100644
index 0000000..85cdc82
--- /dev/null
+++ b/JinYuan.ControlCenter/ControlCenter_Plc.cs
@@ -0,0 +1,2244 @@
+
+using JinYuan.Helper;
+using JinYuan.MES.Enums;
+using JinYuan.MES.Models;
+using JinYuan.Models;
+using JinYuan.VirtualDataLibrary;
+using JinYuan.VirtualDataLibrary.Utlis;
+using Language;
+using PLC;
+using PLCCommunication;
+using PLCCommunication.Common;
+using PLCCommunication.Common.DataConvert;
+using SqlSugar;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Data;
+using System.Diagnostics;
+using System.Diagnostics.Eventing.Reader;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Security.Policy;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using static System.Runtime.CompilerServices.RuntimeHelpers;
+using static System.Windows.Forms.VisualStyles.VisualStyleElement.StartPanel;
+
+
+namespace JinYuan.ControlCenters
+{
+ public partial class ControlCenter
+ {
+ private static readonly SemaphoreSlim _plcSemaphore = new SemaphoreSlim(5, 10); // PLC操作并发限制
+ private static readonly SemaphoreSlim _dbSemaphore = new SemaphoreSlim(10, 20); // 数据库操作并发限制
+ private static readonly object _fileLock = new object(); // 文件锁
+ //private static readonly TimeSpan _plcTimeout = TimeSpan.FromSeconds(1); // PLC操作超时
+ //private static readonly TimeSpan _mesTimeout = TimeSpan.FromSeconds(2); // MES请求超时
+
+ // UI同步上下文:避免跨线程阻塞UI
+ private readonly SynchronizationContext _uiSyncContext = SynchronizationContext.Current;
+
+ #region 保留2位小数
+ private float SaveTwoNum(float TNum)
+ {
+ return (float)Math.Round(TNum, 2, MidpointRounding.AwayFromZero);
+ }
+ #endregion
+
+ #region PLC设备安全访问
+ ///
+ /// 安全获取PLC设备
+ ///
+ /// PLC编号(从1开始)
+ /// PLC设备实例,若不存在或未连接则返回null
+ private PlcReadWriteBase GetPlcDevice(int plcNum)
+ {
+ try
+ {
+ if (CommonMethods.plcDevices != null && plcNum > 0)
+ {
+ // 根据PLC编号查找设备,而不是依赖列表索引
+ var plcDevice = CommonMethods.plcDevices.FirstOrDefault(p =>
+ p != null &&
+ p.Name == $"PLC_{plcNum}" &&
+ p.IsConnected);
+
+ if (plcDevice != null)
+ {
+ return plcDevice;
+ }
+ }
+ }
+ catch { }
+ return null;
+ }
+ #endregion
+
+
+ #region 物流线料盘来料
+ ///
+ /// 物流线料盘来料
+ ///
+ private async Task TrayFeedingStation(JYResult bArrays, object plcModel)
+ {
+ LogHelper.Instance.WriteLog($"Step 1: 进入 TrayFeedingStation - {DateTime.Now:HH:mm:ss.fff}", "WuLiuLog");
+ PlcGroup pf = plcModel as PlcGroup;
+ if (pf == null)
+ {
+ LogHelper.Instance.WriteError("PLC模型转换失败", "WuLiuLog");
+
+ await WritePLCInt16Sync(pf, pf.VarList[4].VarAddress, 2); //反馈结果 1OK 2NG
+ await WritePLCInt16Sync(pf, pf.VarList[5].VarAddress, 1); //反馈处理完成
+ return;
+ }
+
+ using (var timer = new TimedOperation("物流线料盘来料", (elapsedMs, operationName) =>
+ {
+ CommonMethods.AddDataMonitorLog(0, $"物流线料盘来料共耗时:{operationName} 执行耗时:{elapsedMs}ms");
+ }))
+ {
+ if (!bArrays.IsSuccess || bArrays.Content == null)
+ {
+ await HandleFeedingError(pf, "PLC读地址无料框码数据", null);
+ return;
+ }
+
+ try
+ {
+ LogHelper.Instance.WriteLog($"Step 2: ClearPLCRegistersBatch - {DateTime.Now:HH:mm:ss.fff}", "WuLiuLog");
+
+ // 1. 批量清空PLC寄存器
+ await ClearPLCRegistersBatch(pf, new[] { 2, 3, 4 }, 0).ConfigureAwait(false);
+
+ #region 数据解析
+ const int BARCODE_SIZE = 40;
+ byte[] bytes = bArrays.Content.ByteReverse();//高低位转换
+ string trayID = FormatBarcode(StringLib.GetStringFromByteArrayByEncoding(bytes, 0, BARCODE_SIZE, Encoding.ASCII));
+
+ #endregion
+
+ LogHelper.Instance.WriteLog($"Step 3: 数据解析: 料盘码【{trayID}】", "WuLiuLog");
+
+ if (!IsValidTrayID(trayID))
+ {
+ await HandleFeedingError(pf, $"料框码无效:{trayID}", trayID);
+ return;
+ }
+
+ var mesResult = await GetTrayInfoWithTimeout(pf, trayID, 500);
+ LogHelper.Instance.WriteLog($"Step 4: GetTrayInfoWithTimeout: 料盘码【{trayID}】", "WuLiuLog");
+
+ if (!mesResult.Success || mesResult.TrayInfo == null)
+ {
+ await HandleFeedingError(pf, mesResult.ErrorMessage, trayID);
+
+ return;
+ }
+
+ //获取电池型号
+ var modelType = mesResult.TrayInfo.productCode;
+
+ // 获取电池档位信息
+ var gradeResult = await GetBatteryGradesWithTimeout(mesResult.TrayInfo, 500);
+ LogHelper.Instance.WriteLog($"Step 5: 获取电池档位: 料盘码【{trayID}】", "WuLiuLog");
+
+ string finalGrade = gradeResult.Grade;
+ string gradeRemark = gradeResult.IsConsistent ? "" : gradeResult.ErrorMessage;
+
+ LogHelper.Instance.WriteLog($"Step 6:物流线料盘来料: 获取电池型号【{modelType}】,电池档位:【{gradeResult}】{gradeRemark}", "WuLiuLog");
+
+ // 构建并保存实体
+ var mesTrayEntity = BuildTrayEntity(mesResult.TrayInfo, finalGrade, gradeRemark);
+ await SaveTrayDataBatchAsync(new List { mesTrayEntity }).ConfigureAwait(false);
+ LogHelper.Instance.WriteLog($"Step 7: 保存数据: 料盘码【{trayID}】", "WuLiuLog");
+
+ // 反馈PLC
+ await FeedBackToPLC(pf, mesResult.TrayInfo, finalGrade);
+ LogHelper.Instance.WriteLog($"Step 8: 写PLC: 料盘码【{trayID}】", "WuLiuLog");
+ }
+ catch (Exception ex)
+ {
+ LogHelper.Instance.WriteError($"物流线料盘来料异常: {ex}", "WuLiuLog");
+ await WritePLCInt16Sync(pf, pf.VarList[4].VarAddress, 2); //反馈结果 1 OK 2 NG
+ await WritePLCInt16Sync(pf, pf.VarList[5].VarAddress, 1); //反馈处理完成
+ }
+ }
+ }
+
+
+ ///
+ /// 带超时的MES料盘信息获取
+ ///
+ private async Task<(bool Success, TrayInfo TrayInfo, string ErrorMessage)> GetTrayInfoWithTimeout(PlcGroup pf, string trayID, int timeoutMs)
+ {
+ var task = QueryMesTrayInfos(pf, trayID, CancellationToken.None);
+
+ if (await Task.WhenAny(task, Task.Delay(timeoutMs)) != task)
+ {
+ return (false, null, "MES请求超时");
+ }
+
+ var result = await task;
+ return (result.success, result.trayinfo, result.mesMessage);
+ }
+
+ ///
+ /// 等级查询结果
+ ///
+ private class GradeQueryResult
+ {
+ public string Grade { get; set; }
+ public bool IsConsistent { get; set; }
+ public string ErrorMessage { get; set; }
+ }
+
+
+ ///
+ /// 带超时的电池等级查询
+ ///
+ private async Task GetBatteryGradesWithTimeout(TrayInfo trayInfo, int timeoutMs)
+ {
+ var result = new GradeQueryResult { Grade = "", IsConsistent = true, ErrorMessage = "" };
+
+ // 构建电池条码列表
+ var listCode = BuildMaterialLotList(trayInfo);
+
+ if (listCode.Count == 0)
+ {
+ result.ErrorMessage = "料盘中无电池条码";
+ CommonMethods.AddDataMonitorLog(0, result.ErrorMessage);
+ return result;
+ }
+
+ var gradeTask = QueryGrades(listCode, CancellationToken.None);
+
+ if (await Task.WhenAny(gradeTask, Task.Delay(timeoutMs)) != gradeTask)
+ {
+ result.ErrorMessage = "电池等级查询超时";
+ result.IsConsistent = false;
+ CommonMethods.AddDataMonitorLog(0, result.ErrorMessage);
+ return result;
+ }
+
+ var gradeResult = await gradeTask;
+
+ if (!gradeResult.success || gradeResult.batteryInfo == null || gradeResult.batteryInfo.Count == 0)
+ {
+ result.ErrorMessage = "电池等级查询失败或无数据";
+ result.IsConsistent = false;
+ CommonMethods.AddDataMonitorLog(0, result.ErrorMessage);
+ return result;
+ }
+
+ // 检查等级一致性
+ var grades = gradeResult.batteryInfo.Where(g => !string.IsNullOrEmpty(g?.grade)).Select(g => g.grade).Distinct().ToList();
+
+ if (grades.Count == 0)
+ {
+ result.ErrorMessage = "未获取到有效的电池等级";
+ result.IsConsistent = false;
+ CommonMethods.AddDataMonitorLog(0, result.ErrorMessage);
+ return result;
+ }
+
+ if (grades.Count == 1)
+ {
+ result.Grade = grades[0];
+ result.IsConsistent = true;
+ CommonMethods.AddDataMonitorLog(0, $"电池等级查询成功,所有电池等级一致: {result.Grade}");
+ }
+ else
+ {
+ result.ErrorMessage = $"电池等级不一致!检测到多个等级: {string.Join(",", grades)}";
+ result.IsConsistent = false;
+ LogHelper.Instance.WriteError(result.ErrorMessage, "WuLiuLog");
+ CommonMethods.AddDataMonitorLog(0, result.ErrorMessage);
+ }
+
+ return result;
+ }
+
+ ///
+ /// 构建MaterialLot列表
+ ///
+ private List BuildMaterialLotList(TrayInfo trayInfo)
+ {
+ var listCode = new List();
+
+ if (trayInfo?.cellList == null) return listCode;
+
+ foreach (var cell in trayInfo.cellList)
+ {
+ if (!string.IsNullOrEmpty(cell.cellNo))
+ {
+ listCode.Add(new MaterialLot
+ {
+ identification = cell.cellNo,
+ locationBat = cell.channel.ToString()
+ });
+ }
+ }
+
+ return listCode;
+ }
+
+ ///
+ /// 构建TrayEntity实体
+ ///
+ private TrayEntity BuildTrayEntity(TrayInfo trayInfo, string grade, string gradeRemark)
+ {
+ return new TrayEntity
+ {
+ trayID = trayInfo?.trayNo ?? "",
+ model = trayInfo?.productCode ?? CommonMethods.mesConfig.ModelName,
+ grade = grade,
+ moreGrade = "",
+ Count = trayInfo?.cellList?.Count ?? 0,
+ GongWei = CommonMethods.sysConfig.GongWei,
+ Time = DateTime.Now,
+ Result = string.IsNullOrEmpty(gradeRemark) ? "OK" : "NG",
+ Remark = string.IsNullOrEmpty(gradeRemark)
+ ? $"本机架电池型号{CommonMethods.mesConfig.ModelName}, 处理档位{string.Join("|", CommonMethods.GetValidGrades())}"
+ : gradeRemark
+ };
+ }
+
+ ///
+ /// 反馈结果到PLC
+ ///
+ private async Task FeedBackToPLC(PlcGroup pf, TrayInfo trayInfo, string grade)
+ {
+ short model = (short)CommonMethods.mesConfig.GetModel(trayInfo?.productCode ?? CommonMethods.mesConfig.ModelName);
+ if (model == 0) model = 1;
+
+ short gradeCode = 99;
+ if (!string.IsNullOrEmpty(grade))
+ gradeCode = (short)CommonMethods.GetGradeNo(grade);
+
+ await WritePLCInt16Sync(pf, pf.VarList[2].VarAddress, model);
+ await WritePLCInt16Sync(pf, pf.VarList[3].VarAddress, gradeCode);
+ await WritePLCInt16Sync(pf, pf.VarList[4].VarAddress, 1);
+ await WritePLCInt16Sync(pf, pf.VarList[5].VarAddress, 1);
+
+ CommonMethods.AddDataMonitorLog(0, $"物流线料盘来料[{trayInfo.trayNo}]:反馈PLC:[{pf.VarList[2].VarAddress}]写型号{model}(mes获取的原型号:{trayInfo?.productCode}); [{pf.VarList[3].VarAddress}]写挡位{gradeCode}({grade})");
+ LogHelper.Instance.WriteLog($"物流线料盘来料[{trayInfo.trayNo}]:反馈PLC:[{pf.VarList[2].VarAddress}]写型号{model}(mes获取的原型号:{trayInfo?.productCode}); [{pf.VarList[3].VarAddress}]写挡位{gradeCode}({grade})", "WuLiuLog");
+ }
+
+
+ ///
+ /// 处理错误并反馈PLC
+ ///
+ private async Task HandleFeedingError(PlcGroup pf, string errorMessage, string trayID)
+ {
+ var mesTrayEntity = new TrayEntity
+ {
+ trayID = trayID ?? "",
+ model = "",
+ grade = "",
+ moreGrade = "",
+ Count = 0,
+ GongWei = CommonMethods.sysConfig.GongWei,
+ Time = DateTime.Now,
+ Result = "NG",
+ Remark = errorMessage
+ };
+
+ await SaveTrayDataBatchAsync(new List { mesTrayEntity });
+
+
+ await WritePLCInt16Sync(pf, pf.VarList[4].VarAddress, 2); //反馈结果 1OK 2NG
+ await WritePLCInt16Sync(pf, pf.VarList[5].VarAddress, 1); //反馈处理完成
+
+ CommonMethods.AddDataMonitorLog(0, $"料盘码[{trayID}]:" + errorMessage);
+ LogHelper.Instance.WriteLog($"料盘码[{trayID}]:" + errorMessage, "WuLiuLog");
+ }
+
+ // MES查询方法
+ private async Task<(bool success, string mesMessage, MesResType mesResType, TrayInfo trayinfo)> QueryMesTrayInfos(PlcGroup plcGroup, string trayID, CancellationToken token)
+ {
+ token.ThrowIfCancellationRequested();
+
+ try
+ {
+ var (success, mesMessage, mesResType, trayinfo) = await CommonMethods.hbgMes.QueryMesTrayInfosAsync(
+ CommonMethods.mesConfig.queryTrayUrl,
+ CommonMethods.mesConfig.siteCode,
+ CommonMethods.mesConfig.lineCode,
+ CommonMethods.mesConfig.equipNum,
+ DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
+ trayID,
+ CommonMethods.mesConfig.MaterialCode,
+ CommonMethods.mesConfig.mesUserName
+ );
+ return (success, mesMessage, mesResType, trayinfo);
+ }
+ catch (OperationCanceledException)
+ {
+ // 超时, 快速返回,不等待
+ return (false, "MES查询超时", MesResType.B, null);
+ }
+ catch (Exception ex)
+ {
+ // 其他错误
+ LogHelper.Instance.WriteEX(ex);
+ return (false, "MES查询失败", MesResType.B, null);
+ }
+ }
+
+
+ private async Task<(bool success, List batteryInfo)> QueryGrades(List listCode, CancellationToken token)
+ {
+ token.ThrowIfCancellationRequested();
+
+ if (listCode == null || listCode.Count == 0)
+ {
+ return (false, null);
+ }
+
+ try
+ {
+ var (success, batteryInfo) = await CommonMethods.hbgMes.QueryGradeAsync(
+ CommonMethods.mesConfig.queryGradeUrl,
+ CommonMethods.mesConfig.siteCode,
+ CommonMethods.mesConfig.lineCode,
+ CommonMethods.mesConfig.equipNum,
+ DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
+ listCode.Count.ToString(),
+ CommonMethods.mesConfig.MaterialCode,
+ CommonMethods.mesConfig.mesUserName,
+ listCode
+ );
+ return (success, batteryInfo);
+ }
+ catch (OperationCanceledException)
+ {
+ // 超时, 快速返回,不等待
+ LogHelper.Instance.WriteError($"查询电池等级超时", "WuLiuLog");
+ return (false, null);
+ }
+ catch (Exception ex)
+ {
+ LogHelper.Instance.WriteError($"查询电池等级异常: {ex.Message}", "WuLiuLog");
+ return (false, null);
+ }
+ }
+
+
+ #endregion
+
+
+ #region 物流线料盘进站
+ ///
+ /// 物流线料盘进站
+ ///
+ private async Task CheckTrayStation(JYResult bArrays, object plcModel)
+ {
+ LogHelper.Instance.WriteLog($"Step 1: 进入 CheckTrayStation - {DateTime.Now:HH:mm:ss.fff}", "WuLiuLog");
+ PlcGroup pf = plcModel as PlcGroup;
+ if (pf == null)
+ {
+ LogHelper.Instance.WriteError("PLC模型转换失败", "WuLiuLog");
+ await WritePLCInt16Sync(pf, pf.VarList[4].VarAddress, 2); //反馈结果 1OK 2NG
+ await WritePLCInt16Sync(pf, pf.VarList[5].VarAddress, 1); //反馈处理完成
+ return;
+ }
+
+ using (var timer = new TimedOperation("物流线料盘进站", (elapsedMs, operationName) =>
+ {
+ CommonMethods.AddDataMonitorLog(0, $"物流线料盘进站共耗时:{operationName} 执行耗时:{elapsedMs}ms");
+ }))
+ {
+ LogHelper.Instance.WriteLog($"Step 2: 判断bArrays CheckTrayStation - {DateTime.Now:HH:mm:ss.fff}", "WuLiuLog");
+
+ if (!bArrays.IsSuccess || bArrays.Content == null)
+ {
+ await HandleCheckError(pf, "PLC读地址无料框码数据", null);
+ return;
+ }
+
+ try
+ {
+ LogHelper.Instance.WriteLog($"Step 3: ClearPLCRegistersBatch CheckTrayStation - {DateTime.Now:HH:mm:ss.fff}", "WuLiuLog");
+ // 批量清空寄存器
+ await ClearPLCRegistersBatch(pf, new[] { 2, 3, 4 }, 0).ConfigureAwait(false);
+
+ #region 数据解析
+ const int BARCODE_SIZE = 40;
+ LogHelper.Instance.WriteLog($"Step 4: 数据解析 CheckTrayStation - {DateTime.Now:HH:mm:ss.fff}", "WuLiuLog");
+ byte[] bytes = bArrays.Content.ByteReverse();//高低位转换
+ // 读取料盘码
+ string trayID = FormatBarcode(StringLib.GetStringFromByteArrayByEncoding(bytes, 0, BARCODE_SIZE, Encoding.ASCII));
+
+ #endregion
+
+ LogHelper.Instance.WriteLog($"Step 5: 验证料框码ID CheckTrayStation - {DateTime.Now:HH:mm:ss.fff}", "WuLiuLog");
+ // 验证卡板ID
+ if (!IsValidTrayID(trayID))
+ {
+ await HandleCheckError(pf, $"料框码无效:{trayID}", trayID);
+ return;
+ }
+
+ // 获取MES料盘信息
+ var mesResult = await GetTrayInfoWithTimeout(pf, trayID, 500);
+
+ if (!mesResult.Success || mesResult.TrayInfo == null)
+ {
+ await HandleCheckError(pf, mesResult.ErrorMessage, trayID);
+ return;
+ }
+
+ LogHelper.Instance.WriteLog($"Step 8: 组合TrayEntity CheckTrayStation - {DateTime.Now:HH:mm:ss.fff}", "WuLiuLog");
+
+ // 获取电池等级信息
+ var gradeResult = await GetBatteryGradesWithTimeout(mesResult.TrayInfo, 500);
+ string finalGrade = gradeResult.IsConsistent ? gradeResult.Grade : CommonMethods.curFdConfig.Grade1;
+ string add = (gradeResult.Grade == null || string.IsNullOrEmpty(gradeResult.Grade)) ? ",临时使用本机架1垛位的挡位" : "";
+ string gradeRemark = gradeResult.IsConsistent ? "" : gradeResult.ErrorMessage + add;
+
+ // 反馈电池条码到PLC
+ //await WriteBatteryBarcodesToPLC(pf, mesResult.TrayInfo);
+
+
+ // 构建并保存实体
+ var mesTrayEntity = BuildTrayEntity(mesResult.TrayInfo, finalGrade, gradeRemark);
+ await SaveTrayDataBatchAsync(new List { mesTrayEntity }).ConfigureAwait(false);
+
+ // 反馈PLC
+ await FeedBackToPLCCheck(pf, mesResult.TrayInfo, finalGrade);
+ LogHelper.Instance.WriteLog($"物流线料盘进站:料框码:{trayID},往PLC地址[{pf.VarList[4].VarAddress}]写1(OK)", "WuLiuLog");
+ LogHelper.Instance.WriteLog($"Step 11: 结束 CheckTrayStation - {DateTime.Now:HH:mm:ss.fff}", "WuLiuLog");
+ }
+ catch (Exception ex)
+ {
+ LogHelper.Instance.WriteError($"物流线料盘进站异常: {ex}", "WuLiuLog");
+ await WritePLCInt16Sync(pf, pf.VarList[4].VarAddress, 2); //反馈结果 1OK 2NG
+ await WritePLCInt16Sync(pf, pf.VarList[5].VarAddress, 1); //反馈处理完成
+ }
+ }
+ }
+
+ ///
+ /// 进站反馈结果到PLC
+ ///
+ private async Task FeedBackToPLCCheck(PlcGroup pf, TrayInfo trayInfo, string grade)
+ {
+ short model = (short)CommonMethods.mesConfig.GetModel(trayInfo?.productCode ?? CommonMethods.mesConfig.ModelName);
+ if (model == 0) model = 1;
+
+ short gradeCode = 99; // 默认NG档位
+ if (!string.IsNullOrEmpty(grade))
+ gradeCode = (short)CommonMethods.GetGradeNo(grade);
+
+ await WritePLCInt16Sync(pf, pf.VarList[2].VarAddress, model);
+ await WritePLCInt16Sync(pf, pf.VarList[3].VarAddress, gradeCode);
+ await WritePLCInt16Sync(pf, pf.VarList[4].VarAddress, 1);
+ await WritePLCInt16Sync(pf, pf.VarList[5].VarAddress, 1);
+
+ CommonMethods.AddDataMonitorLog(0, $"物流线料盘进站[{trayInfo.trayNo}]:反馈PLC:[{pf.VarList[2].VarAddress}]写型号{model}(mes获取的原型号:{trayInfo?.productCode}); [{pf.VarList[3].VarAddress}]写挡位{gradeCode}({grade})");
+ LogHelper.Instance.WriteError($"物流线料盘进站[{trayInfo.trayNo}]:反馈PLC:[{pf.VarList[2].VarAddress}]写型号{model}(mes获取的原型号:{trayInfo?.productCode}); [{pf.VarList[3].VarAddress}]写挡位{gradeCode}({grade})", "WuLiuLog");
+ LogHelper.Instance.WriteLog($"物流线料盘进站完成", "WuLiuLog");
+ }
+
+ ///
+ /// 处理进站错误
+ ///
+ private async Task HandleCheckError(PlcGroup pf, string errorMessage, string trayID)
+ {
+ var mesTrayEntity = new TrayEntity
+ {
+ trayID = trayID ?? "",
+ model = "",
+ grade = "",
+ moreGrade = "",
+ Count = 0,
+ GongWei = CommonMethods.sysConfig.GongWei,
+ Time = DateTime.Now,
+ Result = "NG",
+ Remark = errorMessage
+ };
+
+ await SaveTrayDataBatchAsync(new List { mesTrayEntity });
+
+ short model = (short)CommonMethods.mesConfig.GetModel(CommonMethods.mesConfig.ModelName);
+ if (model == 0) model = 1;
+
+ short gradeCode = 99; // 默认NG档位
+
+ await WritePLCInt16Sync(pf, pf.VarList[2].VarAddress, model);
+ await WritePLCInt16Sync(pf, pf.VarList[3].VarAddress, gradeCode);//档位NG 99
+ await WritePLCInt16Sync(pf, pf.VarList[4].VarAddress, 2);
+ await WritePLCInt16Sync(pf, pf.VarList[5].VarAddress, 1);
+
+ CommonMethods.AddDataMonitorLog(0, $"物流线料盘进站[{trayID}]:反馈PLC:[{pf.VarList[2].VarAddress}]写型号1(mes获取的原型号:无); [{pf.VarList[3].VarAddress}]写挡位99(NG)");
+ LogHelper.Instance.WriteError($"物流线料盘进站[{trayID}]:反馈PLC:[{pf.VarList[2].VarAddress}]写型号1(mes获取的原型号:无); [{pf.VarList[3].VarAddress}]写挡位99(NG)", "WuLiuLog");
+
+ CommonMethods.AddDataMonitorLog(0, errorMessage);
+ }
+
+ ///
+ /// 保存料盘数据
+ ///
+ ///
+ ///
+ private async Task SaveTrayDataBatchAsync(List list)
+ {
+ if (list == null || list.Count == 0)
+ return;
+
+ try
+ {
+ foreach (var m in list)
+ {
+ if (string.IsNullOrEmpty(m.trayID))
+ {
+ m.Remark += " 料盘ID为空,跳过";
+ continue;
+ }
+ try
+ {
+ int updateCount = await CommonMethods.db.UpdateSingleAsync(m, null, it => it.trayID == m.trayID);
+
+ if (updateCount > 0)
+ {
+ m.Remark += " 更新Yes";
+ }
+ else
+ {
+ // 新增时直接调用底层,减少封装开销
+ bool addSuccess = await Task.Run(() =>
+ CommonMethods.db.AddReturnBoolAsync(m)
+ );
+ m.Remark += addSuccess ? " 新增Yes" : " 新增No";
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.Instance.WriteError($"物流线进站保存{m.trayID}处理异常:{ex}", "WuLiuLog");
+ continue;
+ }
+
+ }
+
+ _uiSyncContext.Post(_ => CommonMethods.ShowTrayDataDelegate?.Invoke(list), null);
+ }
+ catch (Exception ex)
+ {
+ LogHelper.Instance.WriteError($"物流线进站保存处理异常:{ex}", "WuLiuLog");
+ throw;
+ }
+ finally
+ {
+ // _dbSemaphore.Release();
+ }
+ }
+
+
+
+ private bool IsValidTrayID(string trayID)
+ {
+ return !string.IsNullOrWhiteSpace(trayID) &&
+ !trayID.ToUpper().Trim().Equals("ERROR", StringComparison.OrdinalIgnoreCase) &&
+ trayID.Length >= 3; // 增加字母数字校验,提前过滤无效数据
+ }
+
+ #endregion
+
+
+ #region 电池进站
+
+ ///
+ /// 电池进站
+ ///
+ private async Task FeedingStation(JYResult bArrays, object plcModel)
+ {
+ LogHelper.Instance.WriteLog($"Step 1: 进入 FeedingStation - {DateTime.Now:HH:mm:ss.fff}", "ZuPanLog");
+ PlcGroup pf = plcModel as PlcGroup;
+ if (pf == null)
+ {
+ LogHelper.Instance.WriteLog("PLC模型转换失败", "ZuPanLog");
+
+ WritePLCInt16Sync(pf, pf.VarList[5].VarAddress, 2);
+
+ return;
+ }
+
+ Stopwatch sw = Stopwatch.StartNew();
+ try
+ {
+ LogHelper.Instance.WriteLog($"Step 2: 判断bArrays FeedingStation - {DateTime.Now:HH:mm:ss.fff}", "ZuPanLog");
+
+ if (!bArrays.IsSuccess || bArrays.Content == null)
+ {
+ await HandleFeedingStationError(pf, "PLC读地址无料框码数据,不存库", null, null);
+ return;
+ }
+
+ LogHelper.Instance.WriteLog($"Step 3.1: ClearPLCRegistersBatch FeedingStation - {DateTime.Now:HH:mm:ss.fff}", "ZuPanLog");
+
+ #region 清空上一次寄存器数据
+ // 批量清空寄存器
+ await ClearPLCRegistersBatch(pf, new[] { 3, 4, 5 }, 0);
+
+ LogHelper.Instance.WriteLog($"Step 3.2: Clear电池条码 FeedingStation - {DateTime.Now:HH:mm:ss.fff}", "ZuPanLog");
+ // 清空电池条码区域
+ await ClearBatteryBarcodeArea(pf);
+ #endregion
+
+ #region 数据解析
+ const int BARCODE_SIZE = 40;
+ byte[] bytes = bArrays.Content.ByteReverse();
+ string trayID = FormatBarcode(StringLib.GetStringFromByteArrayByEncoding(bytes, 0, BARCODE_SIZE, Encoding.ASCII));
+ #endregion
+
+ if (!IsValidTrayID(trayID))
+ {
+ await HandleFeedingStationError(pf, "料框码无效,不存库", trayID, null);
+ return;
+ }
+
+ CommonMethods.AddDataMonitorLog(0, $"PLC读地址料框码数据,PLC地址[{pf.VarList[1].VarAddress}]:料框码{trayID}");
+
+ // 获取MES料盘信息
+ var mesResult = await GetTrayInfoWithTimeout(pf, trayID, 500);
+ if (!mesResult.Success || mesResult.TrayInfo == null)
+ {
+ await HandleFeedingStationError(pf, mesResult.ErrorMessage, trayID, mesResult.TrayInfo);
+ return;
+ }
+
+ // 获取电池等级信息
+ var gradeResult = await GetBatteryGradesWithTimeout(mesResult.TrayInfo, 500);
+ string finalGrade = gradeResult.IsConsistent ? gradeResult.Grade : CommonMethods.curFdConfig.Grade1;
+ string add = (gradeResult.Grade == null || string.IsNullOrEmpty(gradeResult.Grade) )? ",临时使用本机架1垛位的挡位" : "";
+ string gradeRemark = gradeResult.IsConsistent ? "" : gradeResult.ErrorMessage + add;
+
+ // 反馈电池条码到PLC
+ await WriteBatteryBarcodesToPLC(pf, mesResult.TrayInfo);
+
+ // 构建保存数据
+ var feedingList = BuildFeedingEntityList(mesResult.TrayInfo, finalGrade, gradeRemark);
+ await SaveFeedingDataBatchAsync(feedingList);
+
+ // 反馈结果到PLC
+ await FeedBackToPLCLayer(pf, mesResult.TrayInfo, finalGrade);
+
+ LogHelper.Instance.WriteLog($"Step 12: 结束 FeedingStation - {DateTime.Now:HH:mm:ss.fff}", "ZuPanLog");
+ }
+ catch (Exception ex)
+ {
+ LogHelper.Instance.WriteLog($"电池上料组盘线程处理异常:{ex}", "ZuPanLog");
+ await WritePLCInt16Sync(pf, pf.VarList[5].VarAddress, 2);
+ }
+ finally
+ {
+ sw.Stop();
+ CommonMethods.AddDataMonitorLog(0, "组盘上料共耗时".Translated() + $":{sw.Elapsed.TotalMilliseconds}ms");
+ }
+ }
+
+ ///
+ /// 清空电池条码区域
+ ///
+ private async Task ClearBatteryBarcodeArea(PlcGroup pf)
+ {
+ const int BARCODE_SIZE = 40;
+ int trayCapacity = 40;
+ string strBatteryIDBar = "";
+ for (int i = 0; i < trayCapacity; i++)
+ {
+ strBatteryIDBar += GetString_0(BARCODE_SIZE);
+ }
+
+ byte[] codeBytes = Encoding.Default.GetBytes(strBatteryIDBar).ByteReverse();
+ var plcDevice = GetPlcDevice(pf.PlcNum);
+ if (plcDevice != null)
+ {
+ plcDevice.WriteValue(pf.VarList[2].VarAddress, codeBytes, PLC.DataType.ArrByte);
+ }
+ }
+
+ ///
+ /// 写入电池条码到PLC
+ ///
+ private async Task WriteBatteryBarcodesToPLC(PlcGroup pf, TrayInfo trayInfo)
+ {
+ const int BARCODE_SIZE = 40;
+ string batteryIDBar = string.Concat(trayInfo.cellList.Select(cell =>
+ cell.cellNo + GetString_0(BARCODE_SIZE - cell.cellNo.Length)));
+ byte[] codeBytes = Encoding.Default.GetBytes(batteryIDBar).ByteReverse();
+ await WritePLCByteArray(pf, pf.VarList[2].VarAddress, codeBytes);
+ }
+
+ ///
+ /// 构建上料实体列表
+ ///
+ private List BuildFeedingEntityList(TrayInfo trayInfo, string grade, string gradeRemark)
+ {
+ return trayInfo.cellList.Select((cell, i) => new AGearEntity
+ {
+ Location = cell.channel.ToString(),
+ Time = DateTime.Now,
+ BarCode = cell.cellNo,
+ TrayID = trayInfo.trayNo,
+ Result = string.IsNullOrEmpty(gradeRemark) ? "OK" : "NG",
+ Remark = gradeRemark,
+ Grade = grade,
+ Model = trayInfo.productCode ?? CommonMethods.mesConfig.ModelName,
+ GongWei = CommonMethods.sysConfig.GongWei,
+ }).ToList();
+ }
+
+ ///
+ /// 反馈上料结果到PLC
+ ///
+ private async Task FeedBackToPLCLayer(PlcGroup pf, TrayInfo trayInfo, string grade)
+ {
+ short model = (short)CommonMethods.mesConfig.GetModel(trayInfo?.productCode ?? CommonMethods.mesConfig.ModelName);
+ if (model == 0) model = 1;
+
+ short gradeCode = 99;
+ if (!string.IsNullOrEmpty(grade))
+ gradeCode = (short)CommonMethods.GetGradeNo(grade);
+
+ await WritePLCInt16Sync(pf, pf.VarList[3].VarAddress, model);
+ await WritePLCInt16Sync(pf, pf.VarList[4].VarAddress, gradeCode);
+ await WritePLCInt16Sync(pf, pf.VarList[5].VarAddress, 1);
+
+ CommonMethods.AddDataMonitorLog(0, $"电池进站[{trayInfo.trayNo}]:反馈PLC:[{pf.VarList[2].VarAddress}]写型号{model}(mes获取的原型号:{trayInfo?.productCode}); [{pf.VarList[3].VarAddress}]写挡位{gradeCode}({grade})");
+ LogHelper.Instance.WriteError($"电池进站[{trayInfo.trayNo}]:反馈PLC:[{pf.VarList[2].VarAddress}]写型号{model}(mes获取的原型号:{trayInfo?.productCode}); [{pf.VarList[3].VarAddress}]写挡位{gradeCode}({grade})", "ZuPanLog");
+ }
+
+ ///
+ /// 处理上料错误
+ ///
+ private async Task HandleFeedingStationError(PlcGroup pf, string errorMessage, string trayID, TrayInfo trayInfo)
+ {
+ var entity = new AGearEntity
+ {
+ Time = DateTime.Now,
+ BarCode = "",
+ TrayID = trayID ?? "",
+ Result = "NG",
+ Remark = errorMessage,
+ Grade = "",
+ Model = trayInfo?.productCode ?? "",
+ GongWei = CommonMethods.sysConfig.GongWei,
+ };
+
+ _uiSyncContext.Post(_ => CommonMethods.ShowFeedingDelegate?.Invoke(new List { entity }), null);
+ await WritePLCInt16Sync(pf, pf.VarList[5].VarAddress, 2);
+ }
+
+ #region 保存上料数据
+ ///
+ /// 保存上料数据
+ ///
+ ///
+ private async Task SaveFeedingDataBatchAsync(List list)
+ {
+ if (list == null || list.Count == 0)
+ return;
+
+ try
+ {
+ var barcodes = list.Select(x => x.BarCode).ToList();
+
+ var existing = CommonMethods.db.QueryWhereList()
+ .Where(x => barcodes.Contains(x.BarCode))
+ .Select(x => x.BarCode)
+ .ToList();
+
+ var toUpdate = list.Where(x => existing.Contains(x.BarCode)).ToList();
+ var toInsert = list.Where(x => !existing.Contains(x.BarCode)).ToList();
+
+
+ // 批量更新、批量插入
+ if (toUpdate.Any())
+ await CommonMethods.db.UpdateAsync(toUpdate);
+ if (toInsert.Any())
+ await CommonMethods.db.AddReturnBoolAsync(toInsert);
+
+ // UI更新(主线程)
+ _uiSyncContext.Post(_ => CommonMethods.ShowFeedingDelegate?.Invoke(list), null);
+ }
+ catch (Exception ex)
+ {
+ LogHelper.Instance.WriteError($"组盘工位保存处理异常:{ex}", "SysErrorLog");
+ throw; // 重新抛出异常,让上层知道保存失败
+ }
+ finally
+ {
+ //_dbSemaphore.Release();
+ }
+ }
+ #endregion
+
+ #endregion
+
+ #region 满箱上传
+ ///
+ /// 组盘
+ ///
+ ///
+ ///
+ ///
+ private async Task PackStation(JYResult bArrays, object plcModel)
+ {
+ LogHelper.Instance.WriteLog($"Step 1: 进入 PackStation - {DateTime.Now:HH:mm:ss.fff}");
+ PlcGroup pf = plcModel as PlcGroup;
+ if (pf == null)
+ {
+ LogHelper.Instance.WriteError("PLC模型转换失败", "ZuPanLog");
+
+ UpdatePLCStatus(pf, 999, 2, 2, 2, 999);
+ return;
+ }
+
+ using (var timer = new TimedOperation("电池满层绑定", (elapsedMs, operationName) =>
+ {
+ LogHelper.Instance.WriteLog($"电池满层绑定共耗时:{elapsedMs}ms", "ZuPanLog");
+ CommonMethods.AddDataMonitorLog(0, $"电池满层绑定共耗时:{operationName} 执行耗时:{elapsedMs}ms");
+ }))
+ {
+ if (bArrays.IsSuccess && bArrays.Content != null)
+ {
+ await ProcessPackStation(bArrays, pf).ConfigureAwait(false);
+ }
+ }
+ }
+
+ private async Task ProcessPackStation(JYResult bArrays, PlcGroup pf)
+ {
+ try
+ {
+ LogHelper.Instance.WriteLog($"Step 2: PackStation ParseAndValidateData - {DateTime.Now:HH:mm:ss.fff}", "ZuPanLog");
+
+ var parseTask = ParseAndValidateData(bArrays, pf);
+ int timeout = 200;
+ if (await Task.WhenAny(parseTask, Task.Delay(timeout)) == parseTask)
+ {
+ LogHelper.Instance.WriteLog($"Step 3: PackStation ParseAndValidateData完成 - {DateTime.Now:HH:mm:ss.fff}", "ZuPanLog");
+ // 正常返回
+ var parsedData = await parseTask;
+ if (!parsedData.IsValid)
+ {
+ LogHelper.Instance.WriteLog($"Step 4: PackStation HandleInvalidData - {DateTime.Now:HH:mm:ss.fff}", "ZuPanLog");
+ await HandleInvalidData(parsedData, pf);
+ return;
+ }
+
+ LogHelper.Instance.WriteLog($"Step 4: PackStation ProcessBinding - {DateTime.Now:HH:mm:ss.fff}", "ZuPanLog");
+ // 处理绑定逻辑
+ await ProcessBinding(parsedData, pf);
+ }
+ else
+ {
+ LogHelper.Instance.WriteLog($"Step 3: PackStation ParseAndValidateData超时 - {DateTime.Now:HH:mm:ss.fff}", "ZuPanLog");
+ // 超时,直接返回,
+ LogHelper.Instance.WriteError($"PLC解析超时({timeout}ms)", "ZuPanLog");
+ UpdatePLCStatus(pf, 999, 2, 2, 2, 999);
+ return;
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.Instance.WriteError($"电池满层绑定线程处理异常:{ex}", "ZuPanLog");
+ // 异常时反馈NG状态
+ UpdatePLCStatus(pf, 999, 2, 2, 2, 999);
+ }
+ }
+ #endregion
+
+ #region 保存叠盘数据
+ ///
+ /// 保存叠盘工位数据
+ ///
+ ///
+ private async Task SaveBlankingDataBatchAsync(List list)
+ {
+ if (list == null || list.Count == 0) return;
+
+ await _dbSemaphore.WaitAsync().ConfigureAwait(false);
+ //数据的保存
+ try
+ {
+ if (list != null && list.Count > 0)
+ {
+ //数据显示
+ foreach (BGearEntity m in list)
+ {
+ string strRes = "";
+ if (m.VassoioID != "NG")
+ {
+ bool b = await CommonMethods.db.AddReturnBoolAsync(m);
+
+ }
+ else
+ {
+ strRes = ",不存储";
+ }
+ m.Remark = m.Remark + strRes;
+ }
+
+ //数据显示
+ //CommonMethods.ShowBlankingDelegate.Invoke(list);
+ // UI更新
+ _uiSyncContext.Post(_ => CommonMethods.ShowBlankingDelegate?.Invoke(list), null);
+ }
+ else
+ {
+ LogHelper.Instance.WriteError($"组盘工位数据List为空", "SysErrorLog");
+ }
+
+ //// 过滤有效数据
+ //var validList = list.Where(m => m.VassoioID != "NG").ToList();
+ //if (validList.Any())
+ //{
+ // await CommonMethods.db.Insertable(validList).ExecuteCommandAsync().ConfigureAwait(false);
+ //}
+
+
+ }
+ catch (Exception ex)
+ {
+ LogHelper.Instance.WriteError($"组盘数据保存异常:{ex}", "SysErrorLog");
+ }
+ finally
+ {
+ //_dbSemaphore.Release();
+ }
+ }
+ #endregion
+
+ #region 初始化方法
+ // 初始化PLC状态
+ private void InitializePLCStatus(PlcGroup pf)
+ {
+ ////电池条码读取结果
+ //CommonMethods.plcDevices[pf.PlcNum - 1].WriteInt16(pf.VarList[4].VarAddress, 0);
+ ////卡板码读取结果反馈
+ //CommonMethods.plcDevices[pf.PlcNum - 1].WriteInt16(pf.VarList[5].VarAddress, 0);
+ ////绑定结果反馈
+ //CommonMethods.plcDevices[pf.PlcNum - 1].WriteInt16(pf.VarList[6].VarAddress, 0);
+ ////绑定处理完成反馈
+ //CommonMethods.plcDevices[pf.PlcNum - 1].WriteInt16(pf.VarList[7].VarAddress, 0);
+ _ = ClearPLCRegistersBatch(pf, new[] { 4, 5, 6, 7 }, 0);
+ }
+ #endregion
+
+ #region 数据解析和验证
+ private async Task ParseAndValidateData(JYResult bArrays, PlcGroup pf)
+ {
+ var parsedData = new ParsedData();
+
+ const int BARCODE_SIZE = 40;
+ string containerCode = "";
+ var plcDevice = GetPlcDevice(pf.PlcNum);
+
+ // 读取跺号
+ int stationNo = 0;
+ if (plcDevice != null)
+ {
+ stationNo = plcDevice.ReadInt16(pf.VarList[4].VarAddress);
+ }
+
+ if (stationNo != 0 )
+ {
+ JYResult kbArrays = plcDevice.ReadValue>(
+ $"{pf.VarList[3].VarAddress}",
+ PLC.DataType.ArrByte,
+ Convert.ToUInt16(pf.VarList[3].OffsetOrLength));
+
+ // 从3个可能的地址读取卡板码
+ //for (int i = 0; i < 3; i++)
+ {
+ string code = FormatBarcode(StringLib.GetStringFromByteArrayByEncoding(
+ kbArrays.Content.ByteReverse(),
+ (stationNo - 1) * BARCODE_SIZE,
+ BARCODE_SIZE,
+ Encoding.ASCII));
+
+ if (!string.IsNullOrEmpty(code) && code.ToUpper() != "ERROR" && code.Length >= 8)
+ {
+ containerCode = code;
+ //stationNo = i + 1;
+ //break;
+ }
+ }
+ }
+
+ // 读取层数
+ short layerNum = 0;
+ if (plcDevice != null)
+ {
+ layerNum = plcDevice.ReadInt16(pf.VarList[2].VarAddress);
+ }
+
+ parsedData.ContainerCode = (string)containerCode;
+ parsedData.StationNo = stationNo;
+ parsedData.LayerNum = layerNum;
+ // 验证基础数据
+ var (ret, errType, errMessage) = ValidateBasicData(containerCode, stationNo, layerNum);
+ if (!ret)
+ {
+ parsedData.ErrorType = errType;
+ parsedData.ErrorMessage = errMessage;
+ parsedData.IsValid = ret;
+ return parsedData;
+ }
+
+ // 解析电池条码
+ var batteryInfos = ParseBatteryBarcodes(bArrays, layerNum);
+ var validBarcodeCount = batteryInfos.Count(b => !string.IsNullOrEmpty(b.identification) && b.identification.ToUpper() != "ERROR");
+ parsedData.BatteryInfos = batteryInfos;
+ parsedData.ValidBarcodeCount = validBarcodeCount;
+ // 验证电池数据
+ var (batteryRet, batErrType, batErrMessage) = ValidateBatteryData(validBarcodeCount);
+ if (!batteryRet)
+ {
+ parsedData.ErrorType = batErrType;
+ parsedData.ErrorMessage = batErrMessage;
+ parsedData.IsValid = batteryRet;
+ return parsedData;
+ }
+ string grade = "";
+
+ // 读取电池等级(从第一个有效条码获取)
+ if (validBarcodeCount > 0)
+ {
+ grade = await GetBatteryGrade(batteryInfos.First(b => !string.IsNullOrEmpty(b.identification) && b.identification.ToUpper() != "ERROR").identification).ConfigureAwait(false);
+ parsedData.Grade = grade;
+ }
+ if(parsedData.Grade == "")
+ {
+ parsedData.Grade = MesConfig.GetLocGrade(stationNo, CommonMethods.curFdConfig);//CommonMethods.mesConfig.GetGradeNo("A").ToString();
+ }
+ parsedData.IsValid = true;
+
+ return parsedData;
+ }
+
+ private async Task<(string, int)> ParseContainerCode(PlcGroup pf)
+ {
+ //await _plcSemaphore.WaitAsync().ConfigureAwait(false);
+ try
+ {
+ // 将PLC读取和解析放到后台线程执行
+ //var cts = new CancellationTokenSource(_plcTimeout);
+ return await Task.Run(() =>
+ {
+ const int BARCODE_SIZE = 40;
+ const int BARCODE_ACTUAL_LENGTH = 39;
+
+ string containerCode = "";
+ int stationNo = 0;
+
+ var plcDevice = GetPlcDevice(pf.PlcNum);
+ if (plcDevice != null)
+ {
+ stationNo = plcDevice.ReadInt16(pf.VarList[4].VarAddress);
+ JYResult kbArrays = plcDevice.ReadValue>(
+ $"{pf.VarList[3].VarAddress}",
+ PLC.DataType.ArrByte,
+ Convert.ToUInt16(pf.VarList[3].OffsetOrLength));
+
+ // 从3个可能的地址读取卡板码
+ if(stationNo != 0)
+ {
+ string code = FormatBarcode(StringLib.GetStringFromByteArrayByEncoding(
+ kbArrays.Content.ByteReverse(),
+ (stationNo - 1) * BARCODE_SIZE,
+ BARCODE_ACTUAL_LENGTH,
+ Encoding.ASCII));
+
+ if (!string.IsNullOrEmpty(code) && code.ToUpper() != "ERROR" && code.Length >= 8)
+ {
+ containerCode = code;
+ //stationNo = i + 1;
+ //break;
+ }
+ }
+ }
+
+ return (containerCode, stationNo);
+ });
+ }
+ catch (TaskCanceledException)
+ {
+ LogHelper.Instance.WriteError($"PLC读取容器码超时", "SysErrorLog");
+ return ("", 0);
+ }
+ finally
+ {
+ _plcSemaphore.Release();
+ }
+ }
+
+ private List ParseBatteryBarcodes(JYResult bArrays, short layerNum)
+ {
+ const int BARCODE_SIZE = 40;
+
+ int maxBatteryNum = CommonMethods.mesConfig.rowsPerLayer * CommonMethods.mesConfig.colsPerLayer;
+ var batteryInfos = new List(maxBatteryNum);
+
+ for (int i = 0; i < maxBatteryNum; i++)
+ {
+ string location = ParseLocation(layerNum, i);
+ string barcode = FormatBarcode(StringLib.GetStringFromByteArrayByEncoding(
+ bArrays.Content.ByteReverse(),
+ i * BARCODE_SIZE,
+ BARCODE_SIZE,
+ Encoding.ASCII));
+
+ LogHelper.Instance.WriteLog($"PLC读取绑盘条码{i}-{barcode}-{location}", "绑盘条码");
+
+ if (string.IsNullOrEmpty(barcode) || barcode.Length < 8)
+ {
+ LogHelper.Instance.WriteLog($"PLC读取绑盘条码{i}-{barcode}-{location}异常!!!", "绑盘条码");
+ continue;
+ }
+
+ batteryInfos.Add(new BatteryInfo
+ {
+ identification = barcode,
+ locationRow = location
+ });
+
+ if (!string.IsNullOrEmpty(barcode) && barcode.ToUpper() != "ERROR")
+ {
+ LogHelper.Instance.WriteLog($"电池条码:{barcode},序号:{i + 1}, 位置:{location},层数:{layerNum}");
+ }
+ }
+
+ return batteryInfos;
+ }
+
+ private (bool, ErrorType, string) ValidateBasicData(string containerCode, int stationNo, int layerNum)
+ {
+ string errorMessage = "";
+ ErrorType errorType = ErrorType.None;
+ // 验证卡板码
+ if (string.IsNullOrEmpty(containerCode) ||
+ containerCode.ToUpper().Trim() == "ERROR" ||
+ containerCode.ToUpper().Trim() == "NG" || containerCode.Length < 8)
+ {
+ errorType = ErrorType.ContainerCodeEmpty;
+ errorMessage = "箱唛码NG";
+ return (false, errorType, errorMessage);
+ }
+
+ // 验证 机架号
+ if (stationNo < 1 || stationNo > 3)
+ {
+ errorType = ErrorType.InvalidStation;
+ errorMessage = $"机架号NG{stationNo}";
+ return (false, errorType, errorMessage);
+ }
+
+ // 验证层数范围
+ if (layerNum < 1 || layerNum > CommonMethods.mesConfig.packLayers)
+ {
+ errorType = ErrorType.InvalidLayer;
+ errorMessage = $"层数NG{layerNum}";
+ return (false, errorType, errorMessage);
+ }
+
+ return (true, errorType, errorMessage);
+ }
+
+ private (bool, ErrorType, string) ValidateBatteryData(int validBarcodeCount)
+ {
+ string errMessage = "";
+ ErrorType errorType = ErrorType.None;
+ int maxBatteryNum = CommonMethods.mesConfig.rowsPerLayer * CommonMethods.mesConfig.colsPerLayer;
+
+ // 验证是否有有效条码
+ if (validBarcodeCount == 0)
+ {
+ errorType = ErrorType.NoValidBarcodes;
+ errMessage = "电池信息列表为空";
+ return (false, errorType, errMessage);
+ }
+
+ // 验证是否满盘
+ if (validBarcodeCount != maxBatteryNum)
+ {
+ errorType = ErrorType.NotFullPack;
+ errMessage = $"电池未满盘,有效条码:{validBarcodeCount},应满盘:{maxBatteryNum}";
+ return (false, errorType, errMessage);
+ }
+
+ return (true, errorType, errMessage);
+ }
+
+ private async Task GetBatteryGrade(string barcode)
+ {
+ try
+ {
+ await _dbSemaphore.WaitAsync();
+ var entities = await Task.Run(() => CommonMethods.db.QueryWhereList(it => it.BarCode == barcode)).ConfigureAwait(false);
+ if (entities == null || entities.Count == 0)
+ {
+ LogHelper.Instance.WriteError($"本地数据库无该电池条码{barcode}", "SysErrorLog");
+ return string.Empty;
+ }
+
+ return entities[0].Grade;
+ }
+ catch (Exception ex)
+ {
+ LogHelper.Instance.WriteError($"本地数据库无该电池条码{barcode}:{ex}", "SysErrorLog");
+ return string.Empty;
+ }
+ finally
+ {
+ _dbSemaphore.Release();
+ }
+ }
+
+ #endregion
+
+ #region 数据处理方法
+ private async Task HandleInvalidData(ParsedData data, PlcGroup pf)
+ {
+ // 创建基础实体
+ var bGearEntity = CreateBaseBGearEntity(data);
+ short batteryReadResult = 0;
+ short containerReadResult = 0;
+ short bindingResult = 0;
+ short processCompleteLayer = 999;
+
+ switch (data.ErrorType)
+ {
+ case ErrorType.ContainerCodeEmpty:
+ bGearEntity.Result = "NG";
+ bGearEntity.Remark = data.ErrorMessage;
+ batteryReadResult = 2;
+ containerReadResult = 2;
+ bindingResult = 2;
+ processCompleteLayer = data.LayerNum;
+ break;
+
+ case ErrorType.InvalidLayer:
+ bGearEntity.Result = "NG";
+ bGearEntity.Remark = data.ErrorMessage;
+ batteryReadResult = 2;
+ containerReadResult = 2;
+ bindingResult = 2;
+ processCompleteLayer = data.LayerNum;
+ break;
+
+ case ErrorType.NoValidBarcodes:
+ bGearEntity.Result = "NG";
+ bGearEntity.Remark = data.ErrorMessage;
+ batteryReadResult = 2;
+ containerReadResult = 1;
+ bindingResult = 2;
+ processCompleteLayer = data.LayerNum;
+ break;
+
+ case ErrorType.NotFullPack:
+ bGearEntity.Result = "NG";
+ bGearEntity.Remark = data.ErrorMessage ?? "电池未满盘,不绑定";
+ batteryReadResult = 1;
+ containerReadResult = 1;
+ bindingResult = 2;
+ processCompleteLayer = data.LayerNum;
+ break;
+ }
+
+ // 显示结果
+ var bGearlist = new List { bGearEntity };
+ CommonMethods.ShowBlankingDelegate?.Invoke(bGearlist);
+
+ UpdatePLCStatus(pf, (short)(data.LayerNum == 0? 999: data.LayerNum),
+ batteryReadResult,
+ containerReadResult,
+ bindingResult,
+ processCompleteLayer);
+
+ // 记录日志
+ CommonMethods.AddDataMonitorLog(1, data.ErrorMessage);
+ }
+
+ private async Task ProcessBinding(ParsedData data, PlcGroup pf)
+ {
+ // 创建基础实体
+ var bGearEntity = CreateBaseBGearEntity(data);
+ bGearEntity.Total = data.ValidBarcodeCount;
+ bGearEntity.Qty = data.Grade;
+
+ LogHelper.Instance.WriteLog($"Step 5: PackStation ProcessMESBinding - {DateTime.Now:HH:mm:ss.fff}");
+ await ProcessMESBinding(data, pf, bGearEntity);
+ }
+
+ private async Task ProcessMESBinding(ParsedData data, PlcGroup pf, BGearEntity bGearEntity)
+ {
+ try
+ {
+ // 合并电池信息
+ var totalBatteryInfos = new List();
+ totalBatteryInfos.AddRange(data.BatteryInfos);
+
+ if (data.LayerNum == CommonMethods.mesConfig.packLayers)
+ {
+ LogHelper.Instance.WriteLog($"Step 6: PackStation QueryWhereList - {DateTime.Now:HH:mm:ss.fff}");
+
+ // 查询数据库中已存在的绑定记录
+ var existingBatteries = CommonMethods.db.QueryWhereList(
+ it => it.containerCode == data.ContainerCode)
+ .Select(a => new BatteryInfo
+ {
+ identification = a.identification,
+ locationRow = a.location //(a.location).Replace("-", "")
+ })
+ .GroupBy(b => new { b.identification, b.locationRow })
+ .Select(g => g.First()) // 保留每组的第一条记录
+ .ToList();
+
+ totalBatteryInfos.AddRange(existingBatteries);
+
+ totalBatteryInfos = totalBatteryInfos
+ .GroupBy(b => new { b.identification, b.locationRow })
+ .Select(g => g.First())
+ .ToList();
+
+ bGearEntity.Total = totalBatteryInfos.Count;
+
+ LogHelper.Instance.WriteLog($"Step 7: PackStation CallMESPackLoadAsync - {DateTime.Now:HH:mm:ss.fff}");
+
+ // 调用MES接口
+ var mesResult = await CallMESPackLoadAsync(data, totalBatteryInfos);
+
+ if (!mesResult.Success)
+ {
+ bGearEntity.OutSuccTotal = 0;
+ LogHelper.Instance.WriteLog($"Step 8: PackStation HandleMESRequestFailed - {DateTime.Now:HH:mm:ss.fff}");
+ await HandleMESRequestFailed(data, pf, bGearEntity, mesResult.ErrorMessage);
+ return;
+ }
+
+ LogHelper.Instance.WriteLog($"Step 8: PackStation ProcessMESResult - {DateTime.Now:HH:mm:ss.fff}");
+ // 处理MES返回结果
+ await ProcessMESResult(data, pf, bGearEntity, mesResult).ConfigureAwait(false);
+
+ //string palletId = "";
+ //var upParamResult = await CallMESUpResultParamAsync(data.ContainerCode, palletId, totalBatteryInfos);
+ }
+ else
+ {
+ LogHelper.Instance.WriteLog($"Step 6: PackStation ProcessTempResult - {DateTime.Now:HH:mm:ss.fff}");
+ // 处理MES返回结果
+ await ProcessTempResult(data, pf, bGearEntity).ConfigureAwait(false);
+ }
+
+ }
+ catch (Exception ex)
+ {
+ LogHelper.Instance.WriteError($"MES绑定处理异常:{ex}", "SysErrorLog");
+ UpdatePLCStatus(pf, data.LayerNum, 1, 1, 2, data.LayerNum);
+ }
+ }
+
+ private async Task CallMESPackLoadAsync(ParsedData data, List totalBatteryInfos)
+ {
+ if (!CommonMethods.sysConfig.FinishSwitching)
+ {
+ var result = await CommonMethods.hbgMes.PackLoadAsync(
+ CommonMethods.mesConfig.packLoadUrl,
+ CommonMethods.mesConfig.siteCode,
+ CommonMethods.mesConfig.lineCode,
+ CommonMethods.mesConfig.equipNum,
+ data.ContainerCode,
+ totalBatteryInfos.Count,
+ data.Grade,
+ CommonMethods.mesConfig.MaterialCode,
+ totalBatteryInfos
+ );
+ return new MESPackLoadResult
+ {
+ Success = result.Item1,
+ ErrorMessage = result.Item2,
+ ResType = (short)result.Item3,
+ MaterialPackInfos = result.Item4
+ };
+
+ }
+ else
+ {
+ var result = await CommonMethods.hbgMes.PackLoadFinishAsync(
+ CommonMethods.mesConfig.finishedPackUrl,
+ CommonMethods.mesConfig.siteCode,
+ CommonMethods.mesConfig.lineCode,
+ CommonMethods.mesConfig.equipNum,
+ data.ContainerCode,
+ totalBatteryInfos.Count,
+ data.Grade,
+ CommonMethods.mesConfig.MaterialCode,
+ totalBatteryInfos
+ );
+
+ return new MESPackLoadResult
+ {
+ Success = result.Item1,
+ ErrorMessage = result.Item2,
+ ResType = (short)result.Item3,
+ MaterialPackInfos = result.Item4
+ };
+ }
+ }
+
+ private async Task ProcessMESResult(ParsedData data, PlcGroup pf, BGearEntity bGearEntity, MESPackLoadResult mesResult)
+ {
+ var batteryEntities = new List();
+ bool allSuccess = true;
+
+ LogHelper.Instance.WriteLog($"Step 9: PackStation 整合 - {DateTime.Now:HH:mm:ss.fff}");
+ // 处理每个电池的绑定结果
+ var matpackDict = mesResult.MaterialPackInfos.ToDictionary(m => m.identification, StringComparer.OrdinalIgnoreCase);
+
+ foreach (var batteryInfo in data.BatteryInfos)
+ {
+ if (matpackDict.TryGetValue(batteryInfo.identification, out var matpackInfo))
+ {
+ batteryEntities.Add(new BatteryEntity
+ {
+ identification = matpackInfo.identification,
+ location = $"{matpackInfo.locationRow[0]}-{matpackInfo.locationRow[1]}-{matpackInfo.locationRow.Substring(2)}",
+ packResult = mesResult.Success,
+ packFalseReason = mesResult.ErrorMessage,
+ containerCode = data.ContainerCode,
+ grade = data.Grade,
+ outStationType = 1
+ });
+
+ if (!matpackInfo.packResult)
+ {
+ allSuccess = false;
+ }
+ }
+ }
+
+ LogHelper.Instance.WriteLog($"Step 10: PackStation SaveBatteryEntitiesBatchAsync 开始 - {DateTime.Now:HH:mm:ss.fff}");
+ // 保存到数据库
+ await SaveBatteryEntitiesBatchAsync(batteryEntities);
+
+ LogHelper.Instance.WriteLog($"Step 11: PackStation SaveBatteryEntitiesBatchAsync 结束 - {DateTime.Now:HH:mm:ss.fff}");
+
+ // 更新UI
+ //CommonMethods.ShowBatteryNGRsnDelegate?.Invoke(batteryEntities);
+ //CommonMethods.ShowBatteryStatusDelegate?.Invoke(data.StationNo, batteryEntities);
+ //CommonMethods.ShowBatteryQRCoderDelegate?.Invoke(batteryEntities);
+
+ // 设置结果
+ if (allSuccess)
+ {
+ bGearEntity.Result = "OK";
+ bGearEntity.Remark = "电芯绑定成功";
+ UpdatePLCStatus(pf, data.LayerNum, 1, 1, 1, data.LayerNum);
+ }
+ else
+ {
+ bGearEntity.Result = "绑定_NG";
+ bGearEntity.Remark = "部分电芯绑定失败";
+ UpdatePLCStatus(pf, data.LayerNum, 1, 1, 2, data.LayerNum);
+ }
+
+ bGearEntity.OutSuccTotal = batteryEntities.Where(m => m.packResult).Count();
+
+ LogHelper.Instance.WriteLog($"Step 12: PackStation ShowBlankingDelegate 开始 - {DateTime.Now:HH:mm:ss.fff}");
+
+ // 显示结果
+ var bGearlist = new List { bGearEntity };
+ CommonMethods.ShowBlankingDelegate?.Invoke(bGearlist);
+ LogHelper.Instance.WriteLog($"Step 13: PackStation ShowBlankingDelegate 结束 - {DateTime.Now:HH:mm:ss.fff}");
+
+ // 保存到本地数据库
+ if (bGearEntity.Result == "OK")
+ {
+ LogHelper.Instance.WriteLog($"Step 14: PackStation AddReturnBoolAsync 开始 - {DateTime.Now:HH:mm:ss.fff}");
+ bool saveSuccess = await CommonMethods.db.AddReturnBoolAsync(bGearlist);
+ CommonMethods.AddDataMonitorLog(1, $"电池满层绑定本地存储{(saveSuccess ? "成功" : "失败")}");
+
+ LogHelper.Instance.WriteLog($"Step 15: PackStation AddReturnBoolAsync 结束 - {DateTime.Now:HH:mm:ss.fff}");
+ }
+ else
+ {
+ LogHelper.Instance.WriteLog($"Step 14: PackStation ng 结果不存库 - {DateTime.Now:HH:mm:ss.fff}");
+ }
+ }
+
+ private async Task CallMESUpResultParamAsync(string containerCode, string palletId, List totalBatteryInfos)
+ {
+ var result = await CommonMethods.hbgMes.ProducResultParamAsync(
+ CommonMethods.mesConfig.upResultParamUrl,
+ CommonMethods.mesConfig.siteCode,
+ CommonMethods.mesConfig.lineCode,
+ CommonMethods.mesConfig.equipNum,
+ CommonMethods.mesConfig.MaterialCode,
+ CommonMethods.mesConfig.mesUserName,
+ containerCode,
+ palletId,
+ totalBatteryInfos,
+ MesUploadType.DZ
+ );
+
+ return result;
+
+ }
+
+ private async Task ProcessTempResult(ParsedData data, PlcGroup pf, BGearEntity bGearEntity)
+ {
+ var batteryEntities = new List();
+ bool allSuccess = true;
+
+ foreach (var batteryInfo in data.BatteryInfos)
+ {
+ batteryEntities.Add(new BatteryEntity
+ {
+ identification = batteryInfo.identification,
+ location = batteryInfo.locationRow, //$"{batteryInfo.locationRow[0]}-{batteryInfo.locationRow[1]}-{batteryInfo.locationRow.Substring(2)}",
+ packResult = true,
+ packFalseReason = "",
+ containerCode = data.ContainerCode,
+ grade = data.Grade,
+ outStationType = 1
+ });
+ }
+
+ LogHelper.Instance.WriteLog($"Step 7: PackStation SaveBatteryEntitiesBatchAsync 开始 - {DateTime.Now:HH:mm:ss.fff}");
+ // 保存到数据库
+ await SaveBatteryEntitiesBatchAsync(batteryEntities);
+
+ // 更新UI
+ //CommonMethods.ShowBatteryNGRsnDelegate?.Invoke(batteryEntities);
+ //CommonMethods.ShowBatteryStatusDelegate?.Invoke(data.StationNo, batteryEntities);
+ //CommonMethods.ShowBatteryQRCoderDelegate?.Invoke(batteryEntities);
+
+ LogHelper.Instance.WriteLog($"Step 8: PackStation SaveBatteryEntitiesBatchAsync 完成 - {DateTime.Now:HH:mm:ss.fff}");
+
+ // 设置结果
+ //if (allSuccess)
+ {
+ bGearEntity.Result = "OK";
+ bGearEntity.Remark = "待整垛电芯一起绑定";
+ UpdatePLCStatus(pf, data.LayerNum, 1, 1, 1, data.LayerNum);
+ }
+ //else
+ //{
+ // bGearEntity.Result = "绑定_NG";
+ // bGearEntity.Remark = "待整垛电芯绑定";
+ // UpdatePLCStatus(pf, data.LayerNum, 1, 1, 2, data.LayerNum);
+ //}
+
+ bGearEntity.OutSuccTotal = batteryEntities.Where(m => !m.packResult).Count();
+
+ LogHelper.Instance.WriteLog($"Step 9: PackStation ShowBlankingDelegate - {DateTime.Now:HH:mm:ss.fff}");
+
+ // 显示结果
+ var bGearlist = new List { bGearEntity };
+ CommonMethods.ShowBlankingDelegate?.Invoke(bGearlist);
+
+ // 保存到本地数据库
+ if (bGearEntity.Result == "OK")
+ {
+ LogHelper.Instance.WriteLog($"Step 10: PackStation AddReturnBoolAsync 开始 - {DateTime.Now:HH:mm:ss.fff}");
+ bool saveSuccess = await CommonMethods.db.AddReturnBoolAsync(bGearlist).ConfigureAwait(false);
+ CommonMethods.AddDataMonitorLog(1, $"电池满层绑定本地存储{(saveSuccess ? "成功" : "失败")}");
+ LogHelper.Instance.WriteLog($"Step 11: PackStation AddReturnBoolAsync 结束 - {DateTime.Now:HH:mm:ss.fff}");
+ }
+ else
+ {
+ LogHelper.Instance.WriteLog($"Step 10: PackStation ng结果不保存 - {DateTime.Now:HH:mm:ss.fff}");
+ }
+ }
+
+ private async Task ProcessDebugBinding(ParsedData data, PlcGroup pf, BGearEntity bGearEntity)
+ {
+ // 创建模拟的绑定结果
+ var batteryEntities = new List();
+
+ for (int i = 0; i < data.BatteryInfos.Count; i++)
+ {
+ var batteryInfo = data.BatteryInfos[i];
+ bool packResult = true; //(i % 3) != 1; // 模拟部分失败
+ string packFalseReason = packResult ? null : "模拟失败原因";
+
+ batteryEntities.Add(new BatteryEntity
+ {
+ identification = string.IsNullOrEmpty(batteryInfo.identification) ? $"DC_{i}" : batteryInfo.identification,
+ location = batteryInfo.locationRow,
+ packResult = packResult,
+ packFalseReason = packFalseReason,
+ containerCode = data.ContainerCode,
+ grade = data.Grade,
+ outStationType = 1
+ });
+ }
+
+ // 保存到数据库
+ await SaveBatteryEntitiesBatchAsync(batteryEntities).ConfigureAwait(false);
+
+ // 更新UI
+ //CommonMethods.ShowBatteryNGRsnDelegate?.Invoke(batteryEntities);
+ //CommonMethods.ShowBatteryStatusDelegate?.Invoke(data.StationNo, batteryEntities);
+ //CommonMethods.ShowBatteryQRCoderDelegate?.Invoke(batteryEntities);
+
+ // 设置结果
+ bGearEntity.Result = "OK";
+ bGearEntity.Remark = "调试模式绑定成功";
+ bGearEntity.OutSuccTotal = batteryEntities.Count;
+
+ UpdatePLCStatus(pf, data.LayerNum, 0, 0, 1, data.LayerNum);
+
+ // 显示结果
+ var bGearlist = new List { bGearEntity };
+ CommonMethods.ShowBlankingDelegate?.Invoke(bGearlist);
+ }
+
+ private async Task ProcessDebugMode(ParsedData data, PlcGroup pf, BGearEntity bGearEntity)
+ {
+ // 调试模式模拟数据
+ data.StationNo = data.StationNo == 0 ? 1 : data.StationNo;
+ data.LayerNum = data.LayerNum == (short)0 ? (short)1 : data.LayerNum;
+ data.Grade = data.Grade ?? MesConfig.GetLocGrade(data.StationNo, CommonMethods.curFdConfig);
+
+ // 生成模拟电池信息
+ int maxBatteryNum = CommonMethods.mesConfig.rowsPerLayer * CommonMethods.mesConfig.colsPerLayer;
+ var batteryEntities = new List(maxBatteryNum);
+
+ for (int i = 0; i < maxBatteryNum; i++)
+ {
+ bool packResult = (i % 5) != 1; // 模拟部分失败
+ string packFalseReason = packResult ? null : $"错误NG{(i % 5)}";
+
+ batteryEntities.Add(new BatteryEntity
+ {
+ identification = $"DC_{i}",
+ location = ParseLocation(data.LayerNum, i),
+ packResult = packResult,
+ packFalseReason = packFalseReason,
+ containerCode = "DEBUG_XM_CODE123",
+ grade = data.Grade,
+ outStationType = 1
+ });
+ }
+
+ // 保存到数据库
+ await SaveBatteryEntitiesBatchAsync(batteryEntities).ConfigureAwait(false);
+
+ // 更新UI
+ //CommonMethods.ShowBatteryNGRsnDelegate?.Invoke(batteryEntities);
+ //CommonMethods.ShowBatteryStatusDelegate?.Invoke(data.StationNo, batteryEntities);
+
+ //CommonMethods.ShowBatteryQRCoderDelegate?.Invoke(batteryEntities);
+
+ // 设置结果
+ bGearEntity.Result = "NG";
+ bGearEntity.Remark = "电芯绑定失败(调试模式)";
+ bGearEntity.OutSuccTotal = batteryEntities.Where(x => x.packResult == true).Count();
+ bGearEntity.Qty = data.Grade;
+ bGearEntity.Total = batteryEntities.Count;
+
+ // 显示结果
+ var bGearlist = new List { bGearEntity };
+ CommonMethods.ShowBlankingDelegate?.Invoke(bGearlist);
+
+ UpdatePLCStatus(pf, data.LayerNum, 0, 0, 2, data.LayerNum);
+ }
+
+ #endregion
+
+ #region 辅助方法
+
+ private BGearEntity CreateBaseBGearEntity(ParsedData data)
+ {
+ return new BGearEntity
+ {
+ TestTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
+ VassoioID = data.ContainerCode ?? string.Empty,
+ GongWei = CommonMethods.sysConfig.GongWei,
+ StackingLayer = data.LayerNum
+ };
+ }
+
+ private async Task SaveBatteryEntitiesBatchAsync(List batteryEntities)
+ {
+ if (batteryEntities == null || !batteryEntities.Any()) return;
+
+ try
+ {
+ var containerCodes = batteryEntities.Select(m => m.containerCode).Distinct().ToList();
+ bool b = await CommonMethods.db.AddReturnBoolAsync(batteryEntities);
+ CommonMethods.AddDataMonitorLog(0, $"电池绑定数据批量保存成功");
+ }
+ catch (Exception ex)
+ {
+ LogHelper.Instance.WriteError($"电池绑定保存异常:{ex}", "SysErrorLog");
+ }
+ finally
+ {
+ }
+ }
+
+ private void UpdatePLCStatus(PlcGroup pf, short layerNum,
+ short batteryReadResult, short containerReadResult,
+ short bindingResult, short processCompleteLayer)
+ {
+
+ var dict = new Dictionary();
+ if (batteryReadResult != 0) dict[pf.VarList[5].VarAddress] = batteryReadResult; // 电池条码读取结果反馈
+ if (containerReadResult != 0) dict[pf.VarList[6].VarAddress] = containerReadResult; // 卡板码读取结果反馈
+ if (bindingResult != 0) dict[pf.VarList[7].VarAddress] = bindingResult; // 绑定结果反馈
+ if (processCompleteLayer != 0) dict[pf.VarList[8].VarAddress] = processCompleteLayer; // 绑定处理完成反馈
+
+ WritePLCInt16Sync(pf, pf.VarList[5].VarAddress, batteryReadResult);
+ WritePLCInt16Sync(pf, pf.VarList[6].VarAddress, containerReadResult);
+ WritePLCInt16Sync(pf, pf.VarList[7].VarAddress, bindingResult);
+ WritePLCInt16Sync(pf, pf.VarList[8].VarAddress, processCompleteLayer);
+ }
+
+ private async Task HandleMESDisconnected(ParsedData data, PlcGroup pf, BGearEntity bGearEntity)
+ {
+ await Task.Run(() =>
+ {
+ bGearEntity.Result = "MES_NG";
+ bGearEntity.Remark = "MES连接中断";
+
+ //var bGearlist = new List { bGearEntity };
+ //CommonMethods.ShowBlankingDelegate?.Invoke(bGearlist);
+
+ //CommonMethods.AddDataMonitorLog(1, "MES连接中断,停止上传");
+ //CommonMethods.plcDevices[pf.PlcNum - 1].WriteInt16("D80", 1);
+
+ //UpdatePLCStatus(pf, data.LayerNum, 1, 1, 2, data.LayerNum);
+
+ var bGearlist = new List { bGearEntity };
+ _uiSyncContext.Post(_ => CommonMethods.ShowBlankingDelegate?.Invoke(bGearlist), null);
+
+ CommonMethods.AddDataMonitorLog(1, "MES连接中断,停止上传");
+ _ = WritePLCInt16Sync(pf, "D80", 1);
+
+ UpdatePLCStatus(pf, data.LayerNum, 1, 1, 2, data.LayerNum);
+ });
+ }
+
+ private async Task HandleMESRequestFailed(ParsedData data, PlcGroup pf, BGearEntity bGearEntity, string errorMessage)
+ {
+ await Task.Run(() =>
+ {
+ bGearEntity.Result = "MES_NG";
+ bGearEntity.Remark = $"MES请求失败:{errorMessage}";
+
+ var bGearlist = new List { bGearEntity };
+
+ _uiSyncContext.Post(_ => CommonMethods.ShowBlankingDelegate?.Invoke(bGearlist), null);
+
+ CommonMethods.AddDataMonitorLog(1, $"MES请求失败:{errorMessage}");
+ _ = WritePLCInt16Sync(pf, "D80", 1);
+
+ UpdatePLCStatus(pf, data.LayerNum, 1, 1, 2, data.LayerNum);
+ });
+ }
+
+ private async Task HandleMESDataMismatch(ParsedData data, PlcGroup pf, BGearEntity bGearEntity, MESPackLoadResult mesResult)
+ {
+ await Task.Run(() =>
+ {
+ CommonMethods.AddDataMonitorLog(1,
+ $"返回数据数量不匹配({mesResult.MaterialPackInfos?.Count ?? 0} != {data.BatteryInfos.Count})");
+
+ bGearEntity.Result = "绑定_NG";
+ bGearEntity.Remark = "部分电芯没有返回绑定结果";
+ bGearEntity.OutSuccTotal = mesResult.MaterialPackInfos?.Where(x => x.packResult == true).Count() ?? 0;
+
+ //var bGearlist = new List { bGearEntity };
+ //CommonMethods.ShowBlankingDelegate?.Invoke(bGearlist);
+
+ //UpdatePLCStatus(pf, data.LayerNum, 1, 1, 2, data.LayerNum);
+ var bGearlist = new List { bGearEntity };
+ _uiSyncContext.Post(_ => CommonMethods.ShowBlankingDelegate?.Invoke(bGearlist), null);
+
+ UpdatePLCStatus(pf, data.LayerNum, 1, 1, 2, data.LayerNum);
+ });
+ }
+
+ #endregion
+
+ #region 数据模型
+
+ private class ParsedData
+ {
+ public bool IsValid { get; set; }
+ public string ContainerCode { get; set; }
+ public short LayerNum { get; set; }
+ public List BatteryInfos { get; set; }
+ public int ValidBarcodeCount { get; set; }
+ public string Grade { get; set; }
+ public string ErrorMessage { get; set; }
+ public ErrorType ErrorType { get; set; }
+ public int StationNo { get; set; }
+ }
+
+ private class MESPackLoadResult
+ {
+ public bool Success { get; set; }
+ public string ErrorMessage { get; set; }
+ public short ResType { get; set; }
+ public List MaterialPackInfos { get; set; }
+ }
+
+ private enum ErrorType
+ {
+ None,
+ ContainerCodeEmpty,
+ InvalidStation,
+ InvalidLayer,
+ NoValidBarcodes,
+ NotFullPack
+ }
+
+ #endregion
+
+ #region 强制出站
+ private Task ForceOutStation(JYResult bArrays, object plcModel)
+ {
+
+ PlcGroup pf = plcModel as PlcGroup;
+ if (pf == null)
+ {
+ LogHelper.Instance.WriteError("PLC模型转换失败", "SysErrorLog");
+ return Task.CompletedTask;
+ }
+
+ using (var timer = new TimedOperation("强制出站", (elapsedMs, operationName) =>
+ {
+ CommonMethods.AddDataMonitorLog(0, $"强制出站共耗时:{operationName} 执行耗时:{elapsedMs}ms");
+ }))
+ {
+ // 空方法直接返回已完成任务,避免异步开销
+ return Task.CompletedTask;
+ }
+ }
+
+ ///
+ /// 解析位置编码
+ ///
+ /// 位置编码(0-99999)
+ /// 包含层号和位置信息的元组
+ public string ParseLocation(int layerNum, int number)
+ {
+ if (CommonMethods.mesConfig.colsPerLayer <= 0)
+ throw new InvalidOperationException("列数配置无效");
+
+ Dictionary dictRow = new Dictionary() { { 1, "A" }, { 2, "B" }, { 3, "C" }, { 4, "D" }, { 5, "E" }, };
+ if (CommonMethods.mesConfig.ModelName == "LF350S")
+ {
+ int row = number / CommonMethods.mesConfig.colsPerLayer + 1;
+ string rowStr = dictRow[row];
+ int col = (number % CommonMethods.mesConfig.colsPerLayer) + 1;
+ return $"{rowStr}{col}";
+ }
+ else
+ {
+ int row = number / CommonMethods.mesConfig.colsPerLayer + 1;
+ int col = (number % CommonMethods.mesConfig.colsPerLayer) + 1;
+ return $"{layerNum}{row}{col}";
+ }
+ }
+
+ #endregion
+
+
+ #region 料框码解绑
+ ///
+ /// 料框码解绑
+ ///
+ ///
+ ///
+ ///
+ private async Task UnBindTrayStation(JYResult bArrays, object plcModel)
+ {
+ PlcGroup pf = plcModel as PlcGroup;
+ if (pf == null)
+ {
+ LogHelper.Instance.WriteError("PLC模型转换失败", "SysErrorLog");
+ return;
+ }
+
+ CommonMethods.AddDataMonitorLog(0, $"料框码解绑开始");
+ int _mesTimeout = 1000;
+ using (var cts = new CancellationTokenSource(_mesTimeout))
+ {
+ if (bArrays.IsSuccess && bArrays.Content != null)
+ {
+
+ byte[] bytes = bArrays.Content.ByteReverse();
+
+ //料框码
+ string trayCode = FormatBarcode(StringLib.GetStringFromByteArrayByEncoding(bytes, 0, 40, System.Text.Encoding.ASCII));
+ CommonMethods.AddDataMonitorLog(0, $"料框码解绑料况码{trayCode}");
+ if (string.IsNullOrEmpty(trayCode) || trayCode.ToUpper().Contains("ERROR") || trayCode.ToUpper().Contains("NG"))
+ {
+ await WritePLCInt16Sync(pf, pf.VarList[2].VarAddress, 2);
+ return;
+ }
+
+
+ try
+ {
+ var (success, mesMessage) = await UnbindTrayInfo(trayCode);
+
+ {
+
+ await WritePLCInt16Sync(pf, pf.VarList[2].VarAddress, success ? (short)1 : (short)2);
+ LogHelper.Instance.WriteError($"料框码解绑完成:{trayCode},解绑结果:{success},{mesMessage}", "ZuPanLog");
+ CommonMethods.AddDataMonitorLog(0, $"料框码解绑完成,料框码:{trayCode},解绑结果:{success},{mesMessage}");
+ }
+
+ }
+ catch (OperationCanceledException)
+ {
+ await WritePLCInt16Sync(pf, pf.VarList[2].VarAddress, 2);
+ LogHelper.Instance.WriteError($"料框码解绑被取消(超时):{trayCode}", "ZuPanLog");
+ CommonMethods.AddDataMonitorLog(0, $"料框码解绑被取消(超时):{trayCode}");
+ }
+ catch (Exception ex)
+ {
+ await WritePLCInt16Sync(pf, pf.VarList[2].VarAddress, 2);
+ LogHelper.Instance.WriteError($"料框码解绑异常:{ex.Message},料框码:{trayCode}", "ZuPanLog");
+ CommonMethods.AddDataMonitorLog(0, $"料框码解绑异常:{ex.Message},料框码:{trayCode}");
+ }
+ }
+ }
+ }
+
+ // MES查询方法
+ private async Task<(bool success, string mesMessage)> UnbindTrayInfo(string trayID)
+ {
+ //if (!CommonMethods.mesConfig.isConnected)
+ //{
+ // //CommonMethods.AddDataMonitorLog(1, "MES连接中断,停止上传");
+
+ // return (false, "MES连接中断", MesResType.B, null);
+ //}
+ try
+ {
+ var (success, mesMessage) = await CommonMethods.hbgMes.UnbindTrayInfosAsync(
+ CommonMethods.mesConfig.unbindTrayUrl,
+ CommonMethods.mesConfig.equipNum,
+ trayID
+ ).ConfigureAwait(false);
+
+ return (success, mesMessage);
+ }
+ catch (Exception ex)
+ {
+ return (false, $"MES解绑失败{ex.Message}");
+ }
+ }
+ #endregion
+
+
+
+
+
+ #region 创建空的数组List
+
+ private List GetList(int count, T defaultValue)
+ {
+ if (count <= 0)
+ {
+ return new List();
+ }
+ // Enumerable.Repeat比循环Add更高效,尤其是大数据量
+ return Enumerable.Repeat(defaultValue, count).ToList();
+ }
+
+ public List GetMesType(int Num)
+ {
+ List mesResTypes = new List();
+ for (int i = 0; i < Num; i++)
+ {
+ mesResTypes.Add(MesResType.UnKnow);
+ }
+ return mesResTypes;
+ }
+
+ ///
+ /// PLC内批量写\0
+ ///
+ ///
+ ///
+ public string GetString_0(int Num)
+ {
+ string ff = "";
+ for (int i = 0; i < Num; i++)
+ {
+ ff = ff.Insert(ff.Length, "\0");
+ }
+ return ff;
+ }
+
+
+ #endregion
+
+
+ #region 辅助方法
+
+ ///
+ /// 批量清空PLC寄存器
+ ///
+ private async Task ClearPLCRegistersBatch(PlcGroup pf, int[] varIndexes, short value)
+ {
+ var dict = varIndexes.ToDictionary(i => pf.VarList[i].VarAddress, _ => value);
+ await WritePLCInt16Batch(pf, dict);
+ }
+
+ private async Task WritePLCInt16BatchWithTimeout(PlcGroup pf, Dictionary addressValues)
+ {
+ try
+ {
+ // 给PLC写入加超时,避免卡慢
+ var writeTask = WritePLCInt16Batch(pf, addressValues);
+ //var timeoutTask = Task.Delay(_plcTimeout);
+
+ //var completedTask = await Task.WhenAny(writeTask, timeoutTask).ConfigureAwait(false);
+ //if (completedTask == timeoutTask)
+ //{
+ // //LogHelper.Instance.WriteError($"PLC批量写入超时({_plcTimeout.TotalSeconds}s)", "WuLiuLog");
+ // throw new TimeoutException("PLC写入超时");
+ //}
+
+ //await writeTask.ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ LogHelper.Instance.WriteError($"PLC批量写入异常:{ex.Message}", "WuLiuLog");
+ // 可考虑重试1次(可选)
+ // await Task.Delay(100);
+ // await WritePLCInt16Batch(pf, addressValues).ConfigureAwait(false);
+ }
+ }
+
+ ///
+ /// 批量写入Int16到PLC
+ ///
+ private async Task WritePLCInt16Batch(PlcGroup pf, Dictionary addressValueDict)
+ {
+ //if (addressValueDict.Count == 0) return;
+
+ //var tasks = addressValueDict.Select(kv => WritePLCInt16Sync(pf, kv.Key, kv.Value));
+ //await Task.WhenAll(tasks);
+
+ if (addressValueDict == null || addressValueDict.Count == 0) return;
+
+ var plc = GetPlcDevice(pf.PlcNum);
+ if (plc == null) return;
+
+ //await _plcSemaphore.WaitAsync().ConfigureAwait(false);
+ try
+ {
+ // 【关键优化】批量写入,一次通讯完成
+ foreach (var kv in addressValueDict)
+ {
+ plc.WriteInt16(kv.Key, kv.Value);
+ }
+ }
+ finally
+ {
+ //_plcSemaphore.Release();
+ }
+ }
+
+ ///
+ /// 写入Int16到PLC
+ ///
+ private async Task WritePLCInt16Sync(PlcGroup pf, string address, short value)
+ {
+
+ try
+ {
+ var plc = GetPlcDevice(pf.PlcNum);
+ if (plc == null) return;
+
+ //// 加信号量 + 超时保护
+ //await _plcSemaphore.WaitAsync(100);
+ //try
+ {
+ plc.WriteInt16(address, value);
+ }
+ //finally
+ //{
+ // _plcSemaphore.Release();
+ //}
+ }
+ catch (Exception ex)
+ {
+ LogHelper.Instance.WriteError($"PLC写入失败(地址:{address}):{ex.Message}", "SysErrorLog");
+ }
+ }
+
+
+ ///
+ /// 写入字节数组到PLC(带超时)
+ ///
+ private async Task WritePLCByteArray(PlcGroup pf, string address, byte[] data)
+ {
+ await _plcSemaphore.WaitAsync();
+ try
+ {
+ //var cts = new CancellationTokenSource(_plcTimeout);
+ //await Task.Run(() =>
+ // CommonMethods.plcDevices[pf.PlcNum - 1].WriteValue(address.ToString(), data, PLC.DataType.ArrByte), cts.Token);
+ await Task.Run(() => GetPlcDevice(pf.PlcNum).WriteValue(address.ToString(), data, PLC.DataType.ArrByte));
+ }
+ finally
+ {
+ _plcSemaphore.Release();
+ }
+ }
+
+
+
+ ///
+ /// 检测数据是否只包含字母加数字
+ ///
+ ///
+ ///
+ private static readonly Regex _alphaNumericRegex = new Regex("^[a-zA-Z0-9]+$", RegexOptions.Compiled);
+ public bool IsAlphaNumeric(string input)
+ {
+ return !string.IsNullOrEmpty(input) && _alphaNumericRegex.IsMatch(input);
+ }
+
+ ///
+ /// 格式化条码
+ ///
+ private string FormatBarcode(string barcode)
+ {
+ if (string.IsNullOrEmpty(barcode)) return string.Empty;
+ int index = barcode.IndexOf('\0');
+ if (index >= 0)
+ barcode = barcode.Substring(0, index);
+ return barcode.Replace("\0", "").Replace("\r", "").Replace(" ", "").Trim();
+ }
+
+
+ #endregion
+
+ }
+}
+
+//http://10.22.160.3/core/api/public/product/process/param/new/result
+
+//{
+// "equipNum": "60PLR03",
+// "type": "DZ",
+// "payload": "{\"siteCode\":\"60J\",\"lineCode\":\"KW-602\",\"userName\":\"admin\",\"materialCode\":\"81044802\",\"carCode\":\"\",\"recordDate\":\"2026/06/24 12:01:00\",\"qty\":2,\"containerCode\":\"ETE\",\"identificationList\":[{\"identification\":\"ML-10085826-001\",\"qualityStatus\":\"\",\"tagDataVOList\":[{\"tagCode\":\"TRAY_NUMBER_DBJJG\",\"tagValue\":\"\",\"tagTime\":\"2026/06/24 12:01:00\",\"tagCalculateResult\":\"\",\"tagRemark\":\"托盘号\"},{\"tagCode\":\"PALLET_NUMBER_DBJJG\",\"tagValue\":\"\",\"tagTime\":\"2026/06/24 12:01:00\",\"tagCalculateResult\":\"\",\"tagRemark\":\"栈板号\"},{\"tagCode\":\"BATTERY_LOCATION_DBJJG\",\"tagValue\":\"111\",\"tagTime\":\"2026/06/24 12:01:00\",\"tagCalculateResult\":\"\",\"tagRemark\":\"电池位置\"},{\"tagCode\":\"TOP_COVER_CODE_DBJJG\",\"tagValue\":\"\",\"tagTime\":\"2026/06/24 12:01:00\",\"tagCalculateResult\":\"\",\"tagRemark\":\"电芯顶盖码\"}]},{\"identification\":\"ML-10085826-002\",\"qualityStatus\":\"\",\"tagDataVOList\":[{\"tagCode\":\"TRAY_NUMBER_DBJJG\",\"tagValue\":\"\",\"tagTime\":\"2026/06/2412:01:00\",\"tagCalculateResult\":\"\",\"tagRemark\":\"托盘号\"},{\"tagCode\":\"PALLET_NUMBER_DBJJG\",\"tagValue\":\"\",\"tagTime\":\"2026/06/24 12:01:00\",\"tagCalculateResult\":\"\",\"tagRemark\":\"栈板号\"},{\"tagCode\":\"BATTERY_LOCATION_DBJJG\",\"tagValue\":\"4512\",\"tagTime\":\"2026/06/24 12:01:00\",\"tagCalculateResult\":\"\",\"tagRemark\":\"电池位置\"},{\"tagCode\":\"TOP_COVER_CODE_DBJJG\",\"tagValue\":\"\",\"tagTime\":\"2026/06/24 12:01:00\",\"tagCalculateResult\":\"\",\"tagRemark\":\"电芯顶盖码\"}]}]}"
+//}
\ No newline at end of file
diff --git a/JinYuan.ControlCenter/ControlCenter_Total.cs b/JinYuan.ControlCenter/ControlCenter_Total.cs
new file mode 100644
index 0000000..4f34ae1
--- /dev/null
+++ b/JinYuan.ControlCenter/ControlCenter_Total.cs
@@ -0,0 +1,744 @@
+
+using JinYuan.Helper;
+using JinYuan.MES.Models;
+using JinYuan.Models;
+using JinYuan.VirtualDataLibrary;
+using Language;
+using PLCCommunication;
+using PLCCommunication.Common;
+using PLCCommunication.Modbus;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using static System.Windows.Forms.LinkLabel;
+
+
+namespace JinYuan.ControlCenters
+{
+ public partial class ControlCenter
+ {
+ #region 刷新12小时产能
+
+ public int GetHour()
+ {
+ DateTime now = DateTime.Now;
+ int nowHour = Convert.ToInt32($"{now.Hour}30");
+ int hour = 0;
+ if (CommonMethods.LastHour != nowHour)
+ {
+ hour = Convert.ToInt32($"{now.Hour - 1}30");
+ if (now.Hour == 0)
+ {
+ hour = 2330;
+ }
+ }
+ else
+ {
+ hour = Convert.ToInt32($"{now.Hour}30");
+ }
+ return hour;
+ }
+
+
+ public void Refresh12HourProdQty()
+ {
+ List chart12Hours = new List();
+ DateTime dtTime = DateTime.Now;
+ string strDate = dtTime.ToString("yyyy-MM-dd");
+ string nextDate = dtTime.AddDays(1).ToString("yyyy-MM-dd");
+ int Hour = GetHour();
+
+ var listTotal = CommonMethods.db.QueryWhereList(t =>
+ t.FDate == strDate || t.FDate == nextDate);// 查询两天的数据
+
+ if (listTotal != null && listTotal.Count > 0)
+ {
+ for (int i = 0; i < 24; i++) // 改为24小时
+ {
+ int currentHour = (i + 8) % 24; // 从8点开始
+ int nextHour = (currentHour + 1) % 24;
+ string currentDate = currentHour >= 8 ? strDate : nextDate;
+
+ var hour1 = listTotal.Where(p =>
+ p.FDate == currentDate &&
+ p.FHour == currentHour).FirstOrDefault();
+
+ int ProdALLQty = 0;
+ int ProdOKQty = 0;
+ int ProdNGQty = 0;
+ string strOkRatio = "0.00";
+ if (hour1 != null)
+ {
+ ProdALLQty = hour1.ProdIn;
+ ProdOKQty = hour1.ProdOut;
+ }
+ ProdNGQty = ProdALLQty - ProdOKQty;
+ if (ProdOKQty >= 0 && ProdALLQty > 0)
+ {
+ strOkRatio = (Math.Round(decimal.Parse(ProdOKQty.ToString()) / decimal.Parse(ProdALLQty.ToString()), 4)).ToString();
+ }
+ string displayTime = $"{currentHour:D2}:30~{nextHour:D2}:29";
+
+ chart12Hours.Add(new Chart12HourData()
+ {
+ DisplayTime = displayTime,
+ ProdIn = ProdALLQty,
+ ProdOut = ProdOKQty,
+ ProdNg = ProdNGQty,
+ OKRatio = strOkRatio
+ });
+ }
+ }
+ else
+ {
+ for (int i = 0; i < 24; i++)
+ {
+ int currentHour = (i + 8) % 24;
+ int nextHour = (currentHour + 1) % 24;
+
+ Random rand = new Random();
+ int ranOK = rand.Next(1000, 3000);
+ int ranNG = rand.Next(400, 1200);
+ int total = ranOK + ranNG;
+ string displayTime = $"{currentHour:D2}:30~{nextHour:D2}:29";
+ string strOkRatio = (Math.Round(decimal.Parse(ranOK.ToString()) / decimal.Parse(total.ToString()), 4)).ToString();
+ chart12Hours.Add(new Chart12HourData()
+ {
+ DisplayTime = displayTime,
+ ProdIn = ranOK + ranNG,
+ ProdOut = ranOK,
+ ProdNg = ranNG,
+ OKRatio = strOkRatio // 保留2位小数,添加%
+ });
+ }
+ }
+ if (chart12Hours != null)
+ {
+ CommonMethods.Lst_Prod12HourData = chart12Hours;
+ }
+ }
+ #endregion
+
+ #region 检测项不良
+ ///
+ /// 读取检测项不良
+ ///
+ private void GetDefectTypeQty()
+ {
+
+ try
+ {
+ if (CommonMethods.Lst_DefectTypeQty.Count <= 0)
+ {
+
+ //CommonMethods.Lst_DefectTypeQty.Add(new ChartDataType { DataType = "极性检测不良".Translated(), DataCount = 0 });
+ //CommonMethods.Lst_DefectTypeQty.Add(new ChartDataType { DataType = "称重不良".Translated(), DataCount = 0 });
+ //CommonMethods.Lst_DefectTypeQty.Add(new ChartDataType { DataType = "铜极耳剪切不良".Translated(), DataCount = 0 });
+ //CommonMethods.Lst_DefectTypeQty.Add(new ChartDataType { DataType = "铝极耳剪切不良".Translated(), DataCount = 0 });
+ //CommonMethods.Lst_DefectTypeQty.Add(new ChartDataType { DataType = "厚度不良", DataCount = 0 });
+ //CommonMethods.Lst_DefectTypeQty.Add(new ChartDataType { DataType = "焊前CCD正极耳顶面不良", DataCount = 0 });
+ CommonMethods.Lst_DefectTypeQty.Add(new ChartDataType { DataType = "扫码不良".Translated(), DataCount = 0 });
+ CommonMethods.Lst_DefectTypeQty.Add(new ChartDataType { DataType = "MES不良".Translated(), DataCount = 0 });
+ CommonMethods.Lst_DefectTypeQty.Add(new ChartDataType { DataType = "其他不良".Translated(), DataCount = 0 });
+ }
+ else //读取PLC数据
+ {
+ var plcDevicesCopy = CommonMethods.plcDevices.ToList();
+ if (plcDevicesCopy.Count() > 0)
+ {
+ for (int i = 0; i < plcDevicesCopy.Count(); i++)
+ {
+ var plcDevice = plcDevicesCopy[i];
+ if (plcDevice != null && plcDevice.IsConnected && plcDevice.Name == "PLC_1")
+ {
+
+ Task.Run(() =>
+ {
+ try
+ {
+ int ProdDRFIDNgQty = plcDevice.ReadInt16("D1000");//扫码不良
+ int ProdDXNgQty = plcDevice.ReadInt16("D1001");//MES不良
+ int ProdGRFIDNgQty = plcDevice.ReadInt16("D1002");//其他不良
+
+ CommonMethods.Lst_DefectTypeQty.Where(p => p.DataType == "扫码不良".Translated()).FirstOrDefault().DataCount = ProdDRFIDNgQty;
+ CommonMethods.Lst_DefectTypeQty.Where(p => p.DataType == "MES不良".Translated()).FirstOrDefault().DataCount = ProdDXNgQty;
+ CommonMethods.Lst_DefectTypeQty.Where(p => p.DataType == "其他不良".Translated()).FirstOrDefault().DataCount = ProdGRFIDNgQty;
+ //CommonMethods.Lst_DefectTypeQty.Where(p => p.DataType == "铝极耳剪切不良".Translated()).FirstOrDefault().DataCount = ProdYLZNgQty;
+ }
+ catch (Exception ex)
+ {
+ LogHelper.Instance.WriteEX(ex);
+ }
+ });
+ }
+ }
+
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.Instance.WriteEX(ex);
+ }
+ }
+ #endregion
+
+ #region 智能电表 ---默认D1300开始
+ public void GetEnergyData()
+ {
+ ModbusRtu busRtuClient = new ModbusRtu();
+ if (CommonMethods.plcDevices.Count() > 0)
+ {
+ for (int i = 0; i < CommonMethods.plcDevices.Count(); i++)
+ {
+ if (CommonMethods.plcDevices[i].IsConnected && CommonMethods.plcDevices[i].Name == "PLC_1")
+ {
+ List listM = new List();
+ List ListF = new List();
+ List listAddre = new List()
+ {
+ "D1100","D1102","D1104","D1106","D1108","D1110","D1112","D1114","D1116","D1118","D1120","D1122","D1124","D1126"
+ };
+ try
+ {
+
+ ElectricEnergy m = new ElectricEnergy();
+ //for (int i = 0; i < listAddre.Count; i++)
+ //{
+ // ListF.Add(CommonMethods.plcDevices[0].ReadFloat(listAddre[i]));
+ //}
+
+ //m.AccumulatedElectricity = (Decimal)ListF[0]; //有功电能(累计电能)D1100
+ //m.PhaseVoltageA = (Decimal)ListF[1]; ; //A相电压 D1102
+ //m.PhaseVoltageB = (Decimal)ListF[2]; ;//B相电压 D1104
+ //m.PhaseVoltageC = (Decimal)ListF[3]; ;//C相电压 D1106
+ //m.PhaseCurrentA = (Decimal)ListF[4]; ; //电流IA D1108
+ //m.PhaseCurrentB = (Decimal)ListF[5]; ;//电流IB D1110
+ //m.PhaseCurrentC = (Decimal)ListF[6]; ;//电流IC D1112
+ //m.PhasePowerA = (Decimal)ListF[7]; ; //A相有功功率 D1114
+ //m.PhasePowerB = (Decimal)ListF[8]; ; //B相有功功率 D1116
+ //m.PhasePowerC = (Decimal)ListF[9]; ;//C相有功功率 D1118
+ //m.ActivePower = (Decimal)ListF[10]; ; //总有功功率 D1120
+ //m.PowerFactor = (Decimal)ListF[11]; ; //总功率因数 D1122
+ //m.pt = (Decimal)ListF[12]; ; //电压变比 D1124
+ //m.ct = (Decimal)ListF[13]; ; //电流变比 D1126
+
+ //listM.Add(m);
+ //CommonMethods.listElectricEnergy = listM;
+ //return;
+
+ busRtuClient.SerialPortInni(sp =>
+ {
+ sp.PortName = "COM1";
+ sp.BaudRate = 9600;
+ sp.DataBits = 8;
+ sp.StopBits = System.IO.Ports.StopBits.One;
+ sp.Parity = System.IO.Ports.Parity.None;
+ });
+ busRtuClient.DataFormat = (PLCCommunication.Common.DataFormat)DataFormat.CDAB;
+ busRtuClient.Open(); // 打开
+ if (busRtuClient.IsOpen())
+ {
+ // 读取数据
+ JYResult readResult = busRtuClient.Read("35", 30);
+ JYResult readResult1 = busRtuClient.Read("3", 2);
+ if (readResult.IsSuccess && readResult1.IsSuccess)
+ {
+ //DPT
+ Byte DptH = busRtuClient.ByteTransform.TransByte(readResult.Content, 0);//高8位
+ Byte DptL = busRtuClient.ByteTransform.TransByte(readResult.Content, 1);//低8位
+ //DPQ
+ Byte DpqH = busRtuClient.ByteTransform.TransByte(readResult.Content, 2);//高8位
+ Byte DpqL = busRtuClient.ByteTransform.TransByte(readResult.Content, 3);//低8位
+
+ //m = new ElectricEnergy();
+ Decimal q = busRtuClient.ByteTransform.TransUInt16(readResult.Content, 56);
+ Decimal w = busRtuClient.ByteTransform.TransUInt16(readResult.Content, 58);
+
+ m.pt = Convert.ToDecimal(busRtuClient.ByteTransform.TransInt16(readResult1.Content, 0)); //电压变比 D1124
+ m.ct = Convert.ToDecimal(busRtuClient.ByteTransform.TransInt16(readResult1.Content, 2)); //电流变比 D1126
+ Decimal Power = (q * 65536 + w) / 1000;
+ m.AccumulatedElectricity = Convert.ToDecimal(Power * m.pt * m.ct); //有功电能(累计电能)D1100
+ ListF.Add((float)m.AccumulatedElectricity.Value);
+ //电压
+ m.PhaseVoltageA = Convert.ToDecimal(busRtuClient.ByteTransform.TransUInt16(readResult.Content, 4) * Math.Pow(10, DptH - 4)); //A相电压 D1102
+ m.PhaseVoltageB = Convert.ToDecimal(busRtuClient.ByteTransform.TransUInt16(readResult.Content, 6) * Math.Pow(10, DptH - 4));//B相电压 D1104
+ m.PhaseVoltageC = Convert.ToDecimal(busRtuClient.ByteTransform.TransUInt16(readResult.Content, 8) * Math.Pow(10, DptH - 4));//C相电压 D1106
+ ListF.Add((float)m.PhaseVoltageA.Value);
+ ListF.Add((float)m.PhaseVoltageB.Value);
+ ListF.Add((float)m.PhaseVoltageC.Value);
+
+ //电流
+ m.PhaseCurrentA = Convert.ToDecimal(busRtuClient.ByteTransform.TransUInt16(readResult.Content, 16) * Math.Pow(10, DptL - 4)); //电流IA D1108
+ m.PhaseCurrentB = Convert.ToDecimal(busRtuClient.ByteTransform.TransUInt16(readResult.Content, 18) * Math.Pow(10, DptL - 4));//电流IB D1110
+ m.PhaseCurrentC = Convert.ToDecimal(busRtuClient.ByteTransform.TransUInt16(readResult.Content, 20) * Math.Pow(10, DptL - 4));//电流IC D1112
+ ListF.Add((float)m.PhaseCurrentA.Value);
+ ListF.Add((float)m.PhaseCurrentB.Value);
+ ListF.Add((float)m.PhaseCurrentC.Value);
+
+ //有功功率
+ m.PhasePowerA = Convert.ToDecimal(busRtuClient.ByteTransform.TransInt16(readResult.Content, 22) * Math.Pow(10, DpqH - 4)); //A相有功功率 D1114
+ m.PhasePowerB = Convert.ToDecimal(busRtuClient.ByteTransform.TransInt16(readResult.Content, 24) * Math.Pow(10, DpqH - 4)); //B相有功功率 D1116
+ m.PhasePowerC = Convert.ToDecimal(busRtuClient.ByteTransform.TransInt16(readResult.Content, 26) * Math.Pow(10, DpqH - 4));//C相有功功率 D1118
+ m.ActivePower = Convert.ToDecimal(busRtuClient.ByteTransform.TransInt16(readResult.Content, 28) * Math.Pow(10, DpqH - 4)); //总有功功率 D1120
+ ListF.Add((float)m.PhasePowerA.Value);
+ ListF.Add((float)m.PhasePowerB.Value);
+ ListF.Add((float)m.PhasePowerC.Value);
+ ListF.Add((float)m.ActivePower.Value);
+ m.PowerFactor = Convert.ToDecimal(busRtuClient.ByteTransform.TransInt16(readResult.Content, 44)) / 1000; //总功率因数 D1122
+ ListF.Add((float)m.PowerFactor.Value);
+ ListF.Add((float)m.pt.Value);
+ ListF.Add((float)m.ct.Value);
+
+ for (int j = 0; j < ListF.Count; j++)
+ {
+ CommonMethods.plcDevices[i].WriteFloat(listAddre[j], ListF[j]);
+ }
+ listM.Add(m);
+ CommonMethods.listElectricEnergy = listM;
+
+ }
+ else
+ {
+ TxtHelper.WriteTxt($@"D:\APILog\Logs\智能电表信息\{DateTime.Now.ToString("yyyy-MM-dd")}.txt", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "智能电表:读取数据失败");
+ }
+ }
+ else
+ {
+ TxtHelper.WriteTxt($@"D:\APILog\Logs\智能电表信息\{DateTime.Now.ToString("yyyy-MM-dd")}.txt", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "智能电表:串口打开失败");
+ }
+ }
+ catch (Exception ex)
+ {
+ TxtHelper.WriteTxt($@"D:\APILog\Logs\智能电表信息\{DateTime.Now.ToString("yyyy-MM-dd")}.txt", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + $"智能电表:{ex}");
+ }
+ finally
+ {
+ if (busRtuClient.IsOpen())
+ {
+ busRtuClient.Close();
+ }
+ }
+ }
+ }
+ }
+ }
+
+ public void GetEnergyDataFormPlc()
+ {
+ ModbusRtu busRtuClient = new ModbusRtu();
+ if (CommonMethods.plcDevices.Count() > 0)
+ {
+ for (int i = 0; i < CommonMethods.plcDevices.Count(); i++)
+ {
+ if (CommonMethods.plcDevices[i].IsConnected && CommonMethods.plcDevices[i].Name == "PLC_1")
+ {
+
+ List listM = new List();
+ List ListF = new List();
+ List listAddre = new List()
+ {
+ "D1100","D1102","D1104","D1106","D1108","D1110","D1112","D1114","D1116","D1118","D1120","D1122","D1124","D1126"
+ };
+ try
+ {
+
+ ElectricEnergy m = new ElectricEnergy();
+ for (int j = 0; j < listAddre.Count; j++)
+ {
+ ListF.Add(CommonMethods.plcDevices[i].ReadFloat(listAddre[j]));
+ }
+
+ m.AccumulatedElectricity = (Decimal)ListF[0]; //有功电能(累计电能)D1100
+ m.PhaseVoltageA = (Decimal)ListF[1]; //A相电压 D1102
+ m.PhaseVoltageB = (Decimal)ListF[2]; //B相电压 D1104
+ m.PhaseVoltageC = (Decimal)ListF[3]; //C相电压 D1106
+ m.PhaseCurrentA = (Decimal)ListF[4]; //电流IA D1108
+ m.PhaseCurrentB = (Decimal)ListF[5]; //电流IB D1110
+ m.PhaseCurrentC = (Decimal)ListF[6]; //电流IC D1112
+ m.PhasePowerA = (Decimal)ListF[7]; //A相有功功率 D1114
+ m.PhasePowerB = (Decimal)ListF[8]; //B相有功功率 D1116
+ m.PhasePowerC = (Decimal)ListF[9]; //C相有功功率 D1118
+ m.ActivePower = (Decimal)ListF[10]; //总有功功率 D1120
+ m.PowerFactor = (Decimal)ListF[11]; //总功率因数 D1122
+ m.pt = (Decimal)ListF[12]; //电压变比 D1124
+ m.ct = (Decimal)ListF[13]; //电流变比 D1126
+
+ listM.Add(m);
+ CommonMethods.listElectricEnergy = listM;
+ return;
+ }
+ catch (Exception ex)
+ {
+ TxtHelper.WriteTxt($@"D:\APILog\Logs\智能电表信息\{DateTime.Now.ToString("yyyy-MM-dd")}.txt", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + $"智能电表:{ex}");
+ }
+ finally
+ {
+ if (busRtuClient.IsOpen())
+ {
+ busRtuClient.Close();
+ }
+ }
+ //if (CommonMethods.plcDevices.Count <= 0) { return; }
+ //if (CommonMethods.plcDevices[0] != null && CommonMethods.plcDevices[0].IsConnected && CommonMethods.energyMeter != null && CommonMethods.energyMeter.Connected)
+ //{
+
+ // List