配方是给机器设定的一组运行参数,当机器要生产不同规格的产品时,可以给机器设定不同的参数。
配方的设计要点之一是如何保存配方。制药行业要求对生产数据一般保存5年,生产数据包括生产时所用的配方,当打印报表时需要把对应的配方打印出来。这就要求已生产过的配方要能长期保存,不能被修改或删除。
以下内容介绍如何基于WinCC的用户归档存储配方。用户归档是对SQL数据库的一种封装,与直接读写数据库相比,用户归档可以满足双机热备的要求。
用户归档的读写要求
用户归档实质是存储在SQL Server中的数据库,优点是基于WinCC软件实现了两台电脑里数据库的双机热备。SQL Server也有一些热备方法,例如“数据库复制:发布-订阅”,但是因为WinCC的限制不能使用,用户归档的热备在此更合适。
要实现用户归档的热备冗余,不能直接写数据库,脚本中只能通过UA API函数、控制变量对用户归档写入,对用户归档读取可以直接读数据库。UA API函数只在C脚本中支持,用C脚本读写配方过于复杂,所以在VB脚本中使用控制变量操作配方。
配方的数据流向
用控制变量操作配方,只能将配方从变量写入到用户归档的数据库,或者将特定配方从用户归档的数据库写入到变量。实际使用中,需要先查看配方,然后再将配方下载到PLC,因此需要两套不同的变量,其中一组变量作为中间变量(内部变量)用于查看、编辑,另一组变量是PLC中的实际变量(外部变量),将中间变量的值写入到这组变量就意味着下发了配方。数据流图如下。
用户归档数据库中的特殊字段
红框中的字段是每个配方数据库表中都统一的字段,以下是这些字段的说明。
字段名称 | 类型 | 描述 |
Recipe_ID | 字符串 | 通过Recipe_ID唯一标识一个配方,该字段由Recipe_No和Recipe_Edition拼接而成。 |
Recipe_No | 字符串 | 新建配方时指定唯一配方编号,修改配方将生产新的配方版本,不会覆盖旧配方,新旧配方的配方编号相同。 |
Recipe_Edition | 字符串 | 配方的版本,新建的配方的版本为1,修改并保存后配方版本自动加1,再与配方编号拼接成新的配方ID,在数据库中存储一条新的数据。 |
Recipe_Name | 字符串 | 配方名称。 |
Recipe_Description | 字符串 | 配方描述,同一配方的不同版本的描述可以不同。 |
IsProduced | 数字(整型) | 已生产标志位,当配方已下发并执行了生产,将这个配方的标志位置1,否则这个标志位为0。 |
IsDeleted | 数字(整型) | 当删除一个配方时,如果IsProduced为1,则把IsDeleted标志位置1,表示已删除,查看配方时不再显示这条配方,但配方依然存储在数据库中,打印报表时依然可以调出这个配方的数据;如果IsProduced为0,说明这个配方没有生产过,将会从数据库中删除。 |
使用控制变量操作用户归档,则必须给每个字段值绑定参数变量,然后将参数变量的值写入到用户归档。
绑定的参数变量如下,“XX_”代表设备前缀,不同的设备指定不同的设备前缀。设备前缀之后的变量名在所有设备中都是统一的,之后的代码根据这些变量名传递配方信息。“M_”代表这是中间变量(内部变量),还有一组不带“M_”中缀的同名变量,那些是外部变量,下载配方时通过中间变量向外部变量写值完成。
变量名 | 类型 |
XX_M_RecipeID | 文本变量8位字符集 |
XX_M_RecipeNo | 文本变量8位字符集 |
XX_M_RecipeEdition | 文本变量8位字符集 |
XX_M_RecipeName | 文本变量8位字符集 |
XX_M_RecipeDescription | 文本变量8位字符集 |
XX_M_IsProduced | 二进制变量 |
XX_M_IsDeleted | 二进制变量 |
如何用控制变量操作用户归档
用户归档需要绑定四个变量对其进行操作,这四个变量分别为ID、Job、Field、Value,控制变量的说明如下:
控制变量 |
功能 |
数据类型 |
---|---|---|
ID |
用户归档的数据记录编号 |
有符号 32 位数 |
Job |
可能存在下列作业: “6”= 读取变量写入到用户归档中的数据记录 “7”= 将数据记录从用户归档写入变量 “8”= 删除用户归档中的数据记录 执行作业后,“作业”变量将变为以下数值: “0”= 无错误 “-1”= 有错误 |
有符号 32 位数 |
Field |
用户归档的特定字段 |
文本变量,8 位 |
Value |
特定用户归档字段的值 |
文本变量,8 位 |
控制变量“ID”和“作业”的组合:
ID |
作业 =“6” |
作业 =“7” |
作业 =“8” |
-1 |
读取变量向用户归档中新增数据记录 |
- |
删除最低 ID 的数据记录 |
-6 |
读取变量写入到最低 ID 的数据记录 |
读取最低 ID 的数据记录写入到变量 |
删除最低 ID 的数据记录 |
-9 |
读取变量写入到最高 ID 的数据记录 |
读取最高 ID 的数据记录写入到变量 |
删除最高 ID 的数据记录 |
>0 |
读取变量写入到ID变量指定的数据记录 |
读取ID变量指定的数据记录写入到变量 |
删除ID变量指定的数据记录 |
0 |
读取变量写入到字段变量和值变量指定的数据记录 | 读取字段变量和值变量指定的数据记录写入到变量 | 删除字段变量和值变量指定的数据记录 |
操作用户归档的全局函数
用控制变量操作用户归档的方式不太明晰,写成全局变量可以使该过程更加简便,常用的方式是用Field变量和Value变量确定一条记录,以下是写在全局脚本中操作用户归档的函数。用控制变量操作用户归档是一个异步的过程,函数中添加了检查Job变量返回值的代码,变成了同步过程,函数返回0表示执行成功,返回-1表示执行失败。
注意:虽然是全局函数,但是在设定了前缀的画面窗口中调用时,全局函数中使用的变量也会被加上前缀。
Function Delete_UA(strUAName,strField,strValue)'删除一条信息 HMIRuntime.Tags(strUAName&"_ID").Write 0 HMIRuntime.Tags(strUAName&"_Field").Write strField HMIRuntime.Tags(strUAName&"_Value").Write strValue HMIRuntime.Tags(strUAName&"_Job").Write 8 Do Delete_UA = HMIRuntime.Tags(strUAName&"_Job").Read Loop Until Delete_UA=0 Or Delete_UA=-1 End Function Function Select_UA(strUAName,strField,strValue) '下载一条信息 HMIRuntime.Tags(strUAName&"_ID").Write 0 HMIRuntime.Tags(strUAName&"_Field").Write strField HMIRuntime.Tags(strUAName&"_Value").Write strValue HMIRuntime.Tags(strUAName&"_Job").Write 7 Do Select_UA = HMIRuntime.Tags(strUAName&"_Job").Read Loop Until Select_UA=0 Or Select_UA=-1 End Function Function Insert_UA(strUAName)'添加一条信息 HMIRuntime.Tags(strUAName&"_ID").Write -1 HMIRuntime.Tags(strUAName&"_Field").Write "" HMIRuntime.Tags(strUAName&"_Value").Write "" HMIRuntime.Tags(strUAName&"_Job").Write 6 Do Insert_UA = HMIRuntime.Tags(strUAName&"_Job").Read Loop Until Insert_UA=0 Or Insert_UA=-1 End Function Function Update_UA(strUAName,strField,strValue)'修改一条信息 HMIRuntime.Tags(strUAName&"_ID").Write 0 HMIRuntime.Tags(strUAName&"_Field").Write strField HMIRuntime.Tags(strUAName&"_Value").Write strValue HMIRuntime.Tags(strUAName&"_Job").Write 6 Do Update_UA = HMIRuntime.Tags(strUAName&"_Job").Read Loop Until Update_UA=0 Or Update_UA=-1 End Function
配方界面概览
操作配方的全局函数
上图的新建、保存、下载、上载和删除按钮中会用到许多脚本,这些脚本并不直接写在控件中,而是以函数形式定义在VBS全局项目模块中,然后在控件中调用函数。一个WinCC程序可能要管理许多台设备的配方,也就有许多张配方画面,假如直接将脚本写在控件中,当需要修改脚本时,会要修改很多遍。所以将脚本集中定义在全局项目模块,便于维护代码。
这些函数中通过控件名称调用控件,必须在配方画面中才能使用,并且控件的命名必须依照给定的名称。
调用函数会用到三个参数:
- tagPrefix:变量前缀。不同设备的变量都带有不同的变量前缀,避免变量名冲突。
- DSN:数据源名称。配方数据表的名称,建议用户归档以“变量前缀+Recipe”命名,实际在数据库中还会加上“UA#”前缀。例如,假设变量前缀是“XX_”,则DSN为“UA#XX_Recipe”。
- objNameList:这是一个二维数组,第一列是配方数据表字段名和画面控件的名称,要求配方数据表字段名和被编辑的控件的名称一致;第二列是变量名。
全局项目模块中的代码如下(建议文件命名为_Recipe.bmo):
'该文件中的函数仅用于当前项目的recipe,目的是为了方便编辑,不具有移植性。 '新建配方 Sub NewRecipe(tagPrefix, DSN, objNameList) '----------------------------------------------------- ' 新建配方 ' 输入配方编号、配方名和配方描述, ' 检查配方编号是否与已有的配方重复, ' 将配方信息写入到界面控件中显示。 '----------------------------------------------------- '询问是否新建配方 Dim RecipeNo,RecipeName,RecipeDescription If ScreenItems("BT_Save").Enabled=True Then '检查是否有配方正在编辑 If Msgbox (TranslateText("配方正在编辑,确定不保存该配方,继续新建配方吗?", 2052),vbOKCancel+vbQuestion,"Note") = vbCancel Then Exit Sub End If Else If Msgbox (TranslateText("新建配方吗?",2052),vbOKCancel+vbQuestion,"Note") = vbCancel Then Exit Sub End If End If '创建数据库对象 Dim cnn, rs Set cnn = CreateObject("ADODB.Connection") Set rs = CreateObject("ADODB.Recordset") '输入配方编号 Do While True RecipeNo = Trim(InputBox(TranslateText("请输入新配方编号:", 2052),"Note")) '检查输入的配方编号是否符合要求 If RecipeNo="" Or Check_LawlessChar(RecipeNo)=True Then If MsgBox (TranslateText("配方编号只能包含字母和数字,请重新输入!", 2052),vbRetryCancel + vbInformation,"Note") = vbCancel Then Exit Sub Else '如果选择重试则继续执行循环 End If else '检查配方编号是否重复 Dim SQL SQL = "select Recipe_No from "& DSN &" where Recipe_No='" & RecipeNo & "' AND IsDeleted = 0" ConnectDatabase cnn,rs,SQL If rs.recordcount>0 Then If MsgBox (TranslateText("配方编号重复!",2052),vbRetryCancel + vbInformation,"Note") = vbCancel Then rs.Close cnn.Close Exit Sub else '如果选择重试则继续执行循环 End If Else rs.Close cnn.Close Exit Do End If End If Loop Set rs = Nothing Set cnn = Nothing '输入配方名称 Do While True RecipeName = Trim(InputBox(TranslateText("请输入新配方名称:",2052),"Note")) If RecipeName="" Then If MsgBox (TranslateText("配方名称不能为空!",2052),vbRetryCancel + vbInformation,"Note") = vbCancel Then Exit Sub else '如果选择重试则继续执行循环 End If Else Exit Do End If Loop '输入配方描述 RecipeDescription = Trim(InputBox(TranslateText("请输入新配方描述(可以为空):",2052),"Note")) '清空控件内容 Dim objName For Each objName In objNameList ScreenItems(objName(0)).text = "" Next '向控件填入配方信息 ScreenItems("ComboRecipeList").text = RecipeNo ScreenItems("ComboEditionList").text = "0" ScreenItems("Recipe_Name").text = RecipeName ScreenItems("Recipe_Description").text = RecipeDescription '设置按钮状态 ScreenItems("BT_New").Enabled = False ScreenItems("BT_Download").Enabled = False ScreenItems("BT_Save").Enabled = True ScreenItems("BT_Delete").Enabled = False ScreenItems("ComboRecipeList").Enabled = True End Sub '******************************************************************************************************************************************* '读取数据库中的配方编号写入到ComboBox控件 Sub WriteRecipeNoToComboBox(DSN, objName) '创建数据库对象 Dim cnn, rs Set cnn = CreateObject("ADODB.Connection") Set rs = CreateObject("ADODB.Recordset") '查询不为空并且未删除的配方编号 Dim SQL SQL = "Select distinct Recipe_No FROM "& DSN &" where Recipe_No<>'' AND Recipe_No IS NOT NULL AND IsDeleted = 0" ConnectDatabase cnn,rs,SQL '将配方编号写入到控件 ScreenItems(objName).clear If rs.RecordCount>0 Then rs.MoveFirst Dim i For i = 1 To rs.RecordCount ScreenItems(objName).AddItem rs("Recipe_No") rs.MoveNext Next End If rs.Close Set rs = Nothing cnn.Close Set cnn = Nothing End Sub '******************************************************************************************************************************************* '保存配方 Sub SaveRecipe(tagPrefix, DSN, objNameList) '-------------------------- ' 保存配方: ' 查找配方编号的最大版本号, ' 将最大版本号递增置为下一版本号, ' 将配方信息写入到中间变量, ' 将界面上的配方参数写入到中间变量, ' 将中间变量的值写入到用户归档中新增一条记录, ' 保存配方并不会修改已存在的配方,始终是增加一个新版本的配方。 '-------------------------- '检查是否选择配方 If ScreenItems("ComboRecipeList").text="" Or ScreenItems("ComboEditionList").text="" Then MsgBOX TranslateText("没有选择正确的配方,不能保存",2052) Exit Sub End If '电子签名 Dim sComments If EsigDialog(sComments,False,"") <> 1 Then Exit Sub End If '创建数据库对象 Dim cnn, rs Set cnn = CreateObject("ADODB.Connection") Set rs = CreateObject("ADODB.Recordset") '查找数据库中配方的最大版本号 Dim intMaxID,i intMaxID=0 Dim SQL SQL = "select Recipe_Edition from "& DSN &" where Recipe_No='" & RecipeNo & "'" ConnectDatabase cnn,rs,SQL If rs.recordcount>0 Then rs.MoveFirst For i = 1 To rs.recordcount If intMaxID<CInt(Mid(rs("Recipe_Edition"),1)) Then ' 查找最大配方版本号 intMaxID=CInt(Mid(rs("Recipe_Edition"),1)) End If rs.MoveNext Next End If '关闭数据库连接 rs.Close cnn.Close Set rs = Nothing Set cnn = Nothing '计算配方下一版本号 NextRecipeEdition = intMaxID + 1 '清空中间变量的已删除和已生产标志 HMIRuntime.Tags("@NOP::" &tagPrefix& "M_IsProduced").Write 0 HMIRuntime.Tags("@NOP::" &tagPrefix& "M_IsDeleted").Write 0 '获取配方ID Dim recipeID recipeID = ScreenItems("ComboRecipeList").text &"_"& NextRecipeEdition '将界面显示的配方信息写入内部变量 HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeNo").Write ScreenItems("ComboRecipeList").text HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeEdition").Write NextRecipeEdition HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeName").Write ScreenItems("Recipe_Name").text HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeDescription").Write ScreenItems("Recipe_Description").text HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeID").Write recipeID '将界面显示的配方参数写入内部变量 Dim objName For Each objName In objNameList If Trim(ScreenItems(objName(0)).text) = "" Then HMIRuntime.Tags("@NOP::" &tagPrefix& "M_"& objName(1)).Write 0 Else HMIRuntime.Tags("@NOP::" &tagPrefix& "M_"& objName(1)).Write ScreenItems(objName(0)).text End If Next '保存配方,把变量值写入到数据库 If Insert_UA("@NOP::" &tagPrefix& "UA_Recipe") <> 0 Then Msgbox TranslateText("保存失败!",2052) Exit Sub End If '记录Audit CreateOpMsg PrefixToName(tagPrefix), TranslateText("保存配方",2052)&recipeID , "", recipeID, sComments '更新配方编号控件中的内容 Call WriteRecipeNoToComboBox(DSN, "ComboRecipeList") '向界面控件添加配方版本 ScreenItems("ComboEditionList").AddItem NextRecipeEdition ScreenItems("ComboEditionList").text = NextRecipeEdition Msgbox TranslateText("保存升级成功",2052) '设置按钮状态 ScreenItems("BT_New").Enabled=True ScreenItems("BT_Download").Enabled=True ScreenItems("BT_Delete").Enabled=True ScreenItems("BT_Save").Enabled=False ScreenItems("ComboRecipeList").Enabled=True End Sub '******************************************************************************************************************************************* '下载配方 Sub DownloadRecipe(tagPrefix, DSN, objNameList) '--------------------------------------- ' 下载配方: ' 从用户归档中读取配方到中间变量, ' 再把配方从中间变量写入到外部变量。 '--------------------------------------- ' If HMIRuntime.Tags("A_CommunicationFail").Read=1 Then ' MsgBOX "通讯失败,不能下载" ' Exit Sub ' End If '检查是否选择配方 If ScreenItems("ComboRecipeList").text="" Or ScreenItems("ComboEditionList").text="" Then MsgBOX TranslateText("没有选择正确的配方,不能下载!",2052) Exit Sub End If '电子签名 Dim sComments If EsigDialog(sComments,False,"") <> 1 Then Exit Sub End If '计算配方ID Dim RecipeID RecipeID = ScreenItems("ComboRecipeList").text&"_"&ScreenItems("ComboEditionList").text '更新中间变量 If Select_UA( "@NOP::" &tagPrefix& "UA_Recipe","Recipe_ID",RecipeID) <> 0 Then MsgBOX TranslateText("获取配方失败!",2052) Exit Sub end if '将配方从中间变量写入到外部变量 Dim objName For Each objName In objNameList HMIRuntime.Tags("@NOP::" &tagPrefix& objName(1)).Write HMIRuntime.Tags("@NOP::" &tagPrefix& "M_"&objName(1)).Read Next HMIRuntime.Tags("@NOP::" &tagPrefix& "RecipeNo").Write HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeNo").Read HMIRuntime.Tags("@NOP::" &tagPrefix& "RecipeEdition").Write HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeEdition").Read HMIRuntime.Tags("@NOP::" &tagPrefix& "RecipeName").Write HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeName").Read HMIRuntime.Tags("@NOP::" &tagPrefix& "RecipeDescription").Write HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeDescription").Read HMIRuntime.Tags("@NOP::" &tagPrefix& "RecipeID").Write HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeID").Read '记录Audit CreateOpMsg PrefixToName(tagPrefix), TranslateText("下载配方",2052)&recipeID , "", recipeID, sComments '设置按钮状态 ScreenItems("BT_New").Enabled=True ScreenItems("BT_Download").Enabled=True ScreenItems("BT_Delete").Enabled=True ScreenItems("ComboRecipeList").Enabled=True Msgbox TranslateText("下载成功",2052) End Sub '******************************************************************************************************************************************* '上载配方 Sub Upload(tagPrefix, DSN, objNameList) '检查是否有配方正在编辑 If ScreenItems("BT_Save").Enabled=True Then If Msgbox (TranslateText("配方正在编辑,确定不保存该配方,继续上载配方吗?",2052),vbQuestion+vbOKCancel,"Note") = vbCancel Then Exit Sub End If End If '检查是否选择了配方编号,询问是否上传到当前配方编号并新建配方版本。 Dim NewRecipeNoFlag If Trim(ScreenItems("ComboRecipeList").text) <> "" And _ Trim(ScreenItems("Recipe_Name").text) <> "" Then ' 判断是否沿用当前选择的配方编号 Select Case MsgBOX( TranslateText("是否上传到当前配方编号并更新配方版本?",2052), vbYesNoCancel + vbQuestion) Case vbYes NewRecipeNoFlag = False Case vbno NewRecipeNoFlag = True Case Else Exit Sub End Select Else NewRecipeNoFlag = True End If '电子签名 Dim sComments If EsigDialog(sComments,False,"") <> 1 Then Exit Sub End If '创建数据库对象 Dim cnn, rs, SQL Set cnn = CreateObject("ADODB.Connection") Set rs = CreateObject("ADODB.Recordset") Dim RecipeNo, RecipeName, RecipeDescription If NewRecipeNoFlag = True Then ' 输入新配方编号和配方名称 Do While True RecipeNo = Trim(InputBox(TranslateText("请输入新配方编号:",2052),"Note")) '检查输入的配方编号是否符合要求 If RecipeNo="" Or Check_LawlessChar(RecipeNo)=True Then If MsgBox (TranslateText("配方编号只能包含字母和数字,请重新输入!",2052),vbRetryCancel + vbInformation,"Note") = vbCancel Then Exit Sub Else '如果选择重试则继续执行循环 End If else '检查配方编号是否重复 SQL = "select Recipe_No from "& DSN &" where Recipe_No='" & RecipeNo & "' AND IsDeleted = 0" ConnectDatabase cnn,rs,SQL If rs.recordcount>0 Then If MsgBox (TranslateText("配方编号重复!",2052),vbRetryCancel + vbInformation,"Note") = vbCancel Then rs.Close cnn.Close Exit Sub else '如果选择重试则继续执行循环 End If Else rs.Close cnn.Close Exit Do End If End If Loop '输入配方名称 Do While True RecipeName= Trim(InputBox(TranslateText("请输入新配方名称:",2052),"Note")) If RecipeName="" Then If MsgBox (TranslateText("配方名称不能为空!",2052),vbRetryCancel + vbInformation,"Note") = vbCancel Then Exit Sub else '如果选择重试则继续执行循环 End If Else Exit Do End If Loop Else '使用当前配方编号和配方名称 RecipeNo = Trim(ScreenItems("ComboRecipeList").text) RecipeName = Trim(ScreenItems("Recipe_Name").text) End If '输入配方描述 RecipeDescription = Trim(InputBox(TranslateText("请输入新配方描述(可以为空):",2052),"Note")) '查找特定配方编号下最大版本号 Dim intMaxID,i intMaxID = 0 SQL = "select Recipe_Edition from "& DSN &" where Recipe_No='" & RecipeNo & "'" ConnectDatabase cnn,rs,SQL If rs.recordcount>0 Then rs.MoveFirst For i = 1 To rs.recordcount If intMaxID<CInt(Mid(rs("Recipe_Edition"),1)) Then ' 查找最大配方版本号 intMaxID=CInt(Mid(rs("Recipe_Edition"),1)) End If rs.MoveNext Next End If '设置下一版本号 NextRecipeEdition = intMaxID + 1 '计算配方ID Dim recipeID recipeID = RecipeNo &"_"& NextRecipeEdition '将配方参数从外部变量写入到中间变量 Dim objName For Each objName In objNameList HMIRuntime.Tags("@NOP::" &tagPrefix& "M_"&objName(1)).Write HMIRuntime.Tags("@NOP::" &tagPrefix& objName(1)).Read Next '清空中间变量已删除和已生产标志 HMIRuntime.Tags("@NOP::" &tagPrefix& "M_IsProduced").Write 0 HMIRuntime.Tags("@NOP::" &tagPrefix& "M_IsDeleted").Write 0 '将输入的配方信息写入内部变量 HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeNo").Write RecipeNo HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeEdition").Write NextRecipeEdition HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeName").Write RecipeName HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeDescription").Write RecipeDescription HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeID").Write recipeID '从中间变量写入到用户归档 If Insert_UA("@NOP::" &tagPrefix& "UA_Recipe") <> 0 Then Msgbox TranslateText("上载失败!",2052) Exit Sub End If '记录Audit CreateOpMsg PrefixToName(tagPrefix), TranslateText("上载配方",2052)&recipeID , "", recipeID, sComments '更新控件 Call WriteRecipeNoToComboBox(DSN, "ComboRecipeList") ScreenItems("ComboRecipeList").text = RecipeNo ScreenItems("ComboEditionList").text = "" Msgbox TranslateText("上载成功并已保存",2052) ScreenItems("BT_New").Enabled=True ScreenItems("BT_Download").Enabled=True ScreenItems("BT_Delete").Enabled=True ScreenItems("ComboRecipeList").Enabled=True ScreenItems("BT_Save").Enabled=False End Sub '******************************************************************************************************************************************* '删除配方 Sub DeleteRecipe(tagPrefix, DSN, objNameList) '-------------------------------------- ' 删除配方: ' 检查需删除的配方是否生产过, ' 生产过的配方将其IsDeleted字段置1, ' 之后查询时不再显示该配方, ' 打印报表时依然可以打印该配方; ' 未生产过的配方则被直接删除。 '------------------------------------- '检查是否选择配方 If ScreenItems("ComboRecipeList").text="" Or ScreenItems("ComboEditionList").text="" Then MsgBOX TranslateText("没有选择正确的配方!",2052) Exit Sub End If '电子签名 Dim sComments If EsigDialog(sComments,False,"") <> 1 Then Exit Sub End If '计算配方ID Dim RecipeID RecipeID = ScreenItems("ComboRecipeList").text&"_"&ScreenItems("ComboEditionList").text '更新中间变量 If Select_UA ("@NOP::" &tagPrefix& "UA_Recipe","Recipe_ID", RecipeID ) <> 0 Then MsgBOX TranslateText("读取配方失败!",2052) exit Sub end if '删除配方 If HMIRuntime.Tags("@NOP::" &tagPrefix& "M_IsProduced").Read = 1 Then HMIRuntime.Tags("@NOP::" &tagPrefix& "M_IsDeleted").Write 1 If Update_UA ("@NOP::" &tagPrefix& "UA_Recipe", "Recipe_ID", RecipeID ) <> 0 Then Msgbox TranslateText("删除配方失败!",2052) exit sub end if Else If Delete_UA ("@NOP::" &tagPrefix& "UA_Recipe","Recipe_ID",RecipeID ) <> 0 Then Msgbox TranslateText("删除配方失败!",2052) exit sub end if End If '记录Audit CreateOpMsg PrefixToName(tagPrefix), TranslateText("删除配方",2052)&recipeID , "", recipeID, sComments '清空配方控件内容 Dim objName For Each objName In objNameList ScreenItems(objName(0)).text = "" Next ScreenItems("Recipe_Name").text = "" ScreenItems("Recipe_Description").text = "" ScreenItems("ComboRecipeList").text = "" ScreenItems("ComboEditionList").clear ScreenItems("ComboEditionList").text = "" '更新控件中的配方编号 Call WriteRecipeNoToComboBox(DSN, "ComboRecipeList") '设置按钮状态 ScreenItems("BT_New").Enabled=True ScreenItems("BT_Download").Enabled=False ScreenItems("BT_Delete").Enabled=False Msgbox TranslateText("删除成功",2052) End Sub '******************************************************************************************************************************************* '获取配方版本 Sub GetRecipeEdition(tagPrefix, DSN, objNameList) '------------------------------------------------ ' 查询数据库中特定配方编号的版本,写入到下拉控件。 '------------------------------------------------ '创建数据库对象 Dim cnn,rs,cnnStr, SQL,i Set cnn = CreateObject("ADODB.Connection") Set rs = CreateObject("ADODB.Recordset") '查询特定配方编号的数据 SQL = "select Recipe_Edition from "& DSN &" where Recipe_No='" & ScreenItems("ComboRecipeList").text & "' AND IsDeleted = 0" ConnectDatabase cnn,rs,SQL '将数据库中存在的配方版本填入控件 ScreenItems("ComboEditionList").clear If rs.RecordCount>0 Then rs.MoveFirst For i = 1 To rs.RecordCount ScreenItems("ComboEditionList").AddItem rs("Recipe_Edition") rs.MoveNext Next End If '关闭数据库连接 rs.Close Set rs = Nothing cnn.Close Set cnn = Nothing '清空参数控件内容 For Each objName In objNameList ScreenItems(objName(0)).text = "" Next '清空配方信息控件内容 ScreenItems("Recipe_Name").text = "" ScreenItems("Recipe_Description").text = "" '设置按钮状态 ScreenItems("BT_New").Enabled=True ScreenItems("BT_Delete").Enabled=False ScreenItems("BT_Save").Enabled=False ScreenItems("BT_Download").Enabled=False End Sub '******************************************************************************************************************************************* '获取配方 Sub GetRecipe(tagPrefix, DSN, objNameList) '---------------------------------------------- ' 用配方编号和配方版本拼接得到配方ID, ' 用配方ID指定配方写入到中间变量, ' 再将中间变量的内容写入到控件去显示。 '---------------------------------------------- '获取配方ID Dim RecipeID RecipeID = ScreenItems("ComboRecipeList").text &"_"& ScreenItems("ComboEditionList").text '将配方从数据库写入到中间变量 If Select_UA("@NOP::" &tagPrefix& "UA_Recipe","Recipe_ID",RecipeID) <> 0 Then Msgbox TranslateText("读取配方失败!",2052) Exit Sub End If '将配方从中间变量写入到控件 Dim objName For Each objName In objNameList ScreenItems(objName(0)).text = HMIRuntime.Tags("@NOP::" &tagPrefix& "M_"&objName(1)).Read Next '将配方信息从中间变量写入到控件 ScreenItems("Recipe_Name").text = HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeName").Read ScreenItems("Recipe_Description").text = HMIRuntime.Tags("@NOP::" &tagPrefix& "M_RecipeDescription").Read '设置按钮状态 ScreenItems("BT_New").Enabled=True ScreenItems("BT_Delete").Enabled=True ScreenItems("BT_Save").Enabled=False ScreenItems("BT_Download").Enabled=True End Sub
依赖函数
上述代码中依赖一些其他的函数。
TranslateText()
TranslateText()函数用于脚本内文本多语言翻译,函数的设计和使用详见:WinCC脚本内文本多语言化的一种方法
Check_LawlessChar()
输入配方编号要求为字母和数据,Check_LawlessChar()函数通过正则表达式判断输入的字符是否符合要求,符合则返回1,不符合返回0。代码如下:
Function Check_LawlessChar(strName) Dim regEx Set regEx = New RegExp regEx.Pattern = "^[a-z0-9A-Z]+$" regEx.IgnoreCase = True regEx.Global = True Validate = regEx.test(strName) Set regEx = Nothing Check_LawlessChar = Not Validate End Function
ConnectDatabase()
ConnectDatabase()函数用于简化数据库连接,代码如下:
Function ConnectDatabase(cnn,rs,SQL) Dim cnnStr cnnStr = "Provider=SQLOLEDB.1;Integrated Security=SSPI;Persist Security Info=False;Initial Catalog="& HMIRuntime.Tags("@NOTP::@DatasourceNameRT").Read &";Data Source=" & HMIRuntime.Tags("@NOTP::@ServerName").Read & "\WINCC" cnn.ConnectionString = cnnStr cnn.CursorLocation = 3 cnn.Open rs.Open SQL,cnn,1,3 End Function
EsigDialog()和CreateOpMsg()
EsigDialog()和CreateOpMsg()分别用于生产电子签名和记录审计追踪,详见:WinCC的电子签名与审计追踪 2.0
PrefixToName()
审计追踪需要记录设备名称,已知设备的变量前缀,可以得到对应的变量名称,PrefixToName()就是为了实现这个作用,但要根据项目修改和添加一下代码:
Function PrefixToName(Prefix) Select Case Trim(Prefix) Case "XX_" PrefixToName = TranslateText("设备1", 2052) Case "XX1_" PrefixToName = TranslateText("设备2", 2052) Case "" PrefixToName = "System" Case Else PrefixToName = Prefix End Select End Function
画面全局声明区定义
将函数所需要的三个参数定义在画面的全局声明区中,在控件的脚本中就可以直接使用。
打开全局声明区只需打开画面中任意一个VBS编辑器,点击下图中红色方框的按钮,就可显示全局声明区。
全局声明区代码示例如下:
'数据库名称 Dim DSN DSN = "UA#XX_Recipe" '控件名称\数据库字段名,变量名称 Dim objNameList objNameList = Array(_ Array("i16_Set_VfFaninfeed", "i16_Set_VfFaninfeed"),_ Array("i16_Set_VfFanPreheat", "i16_Set_VfFanPre-heat"),_ Array("i16_Set_VfFan1Heat", "i16_Set_VfFan1#Heat"),_ Array("i16_Set_VfFan1Cool", "i16_Set_VfFan1Cool"),_ Array("i16_Set_VfFan2Cool", "i16_Set_VfFan2Cool"),_ Array("Recipe_Setbeltspeed", "Recipe_Setbeltspeed"),_ Array("Recipe_Setheat2", "Recipe_Setheat2"),_ Array("Sv1Height_Upperlimit", "r32_Set_Sv1Height_Upperlimit"),_ Array("Sv2Height_Upperlimit", "r32_Set_Sv2Height_Upperlimit"),_ Array("Sv3Height_Upperlimit", "r32_Set_Sv3Height_Upperlimit") _ ) '变量前缀 Dim TagPrefix TagPrefix = "XX_"
操作批次的函数执行过程
点击新建、保存、下载、上载、删除按钮以及查询配方,执行流程如下:
新建配方
新建按钮中的脚本如下:
Sub OnClick(Byval Item) NewRecipe TagPrefix, DSN, objNameList End Sub
点击“新建”按钮,调用NewRecipe()函数,将会执行以下操作:
- 检查“BT_Save”按钮是否使能,判断当前是否有正在编辑的配方没保存,弹出提示框询问用户是否继续操作;
- 弹出对话框要求输入配方编号、配方名称和配方描述,检查输入的配方编号是否符合要求,和是否与现有的配方编号重复;
- 清空界面中参数控件的值,向配方信息控件写入用户输入的值;
- 更新按钮的使能状态。
保存配方
保存按钮中的脚本如下:
Sub OnClick(Byval Item) SaveRecipe TagPrefix, DSN, objNameList End Sub
点击“保存”按钮,调用SaveRecipe()函数,将会执行以下操作:
- 检查是否有配方编号和配方版本;
- 验证电子签名;
- 查找数据库中该配方编号的最大版本,计算得到下一配方版本;
- 清空中间变量;
- 向中间变量写入界面上显示的值;
- 将中间变量的值保存到用户归档;
- 记录审计追踪;
- 更新界面上配方版本控件的值;
- 更新按钮的使能状态。
下载配方
下载按钮中的脚本如下:
Sub OnClick(Byval Item) DownloadRecipe TagPrefix, DSN, objNameList End Sub
点击“下载”按钮,调用DownloadRecipe()函数,将会执行以下操作:
- 检查是否选择了一个配方;
- 验证电子签名;
- 通过配方编号和配方版本计算得到配方ID;
- 用配方ID从用户归档查询配方值写入到中间变量;
- 将中间变量的写入到外部变量;
- 记录审计追踪;
- 更新按钮的使能状态。
上载配方
上载按钮中的脚本如下:
Sub OnClick(Byval Item) Upload TagPrefix, DSN, objNameList End Sub
点击“上载”按钮,调用Upload()函数,将会执行以下操作:
- 检查“BT_Save”按钮是否使能,判断当前是否有正在编辑的配方没保存,弹出提示框询问用户是否继续操作;
- 如果当前界面已选择了配方,询问用户是否上载到当前配方编号;
- 验证电子签名;
- 如果需新建配方编号,弹出对话框要求输入配方编号、配方名称和配方描述,检查输入的配方编号是否符合要求,和是否与现有的配方编号重复;
- 如果使用当前配方编号,则只需输入新的配方描述;
- 查找数据库中该配方编号的最大版本,计算得到下一配方版本;
- 把外部变量的值写入到中间变量;
- 把控件中的配方信息写入到中间变量;
- 把中间变量的值保存到用户归档;
- 记录审计追踪;
- 更新配方编号和配方版本控件的值;
- 更新按钮的使能状态。
删除配方
删除按钮中的脚本如下:
Sub OnClick(Byval Item) DeleteRecipe TagPrefix, DSN, objNameList End Sub
点击“删除”按钮,调用DeleteRecipe()函数,将会执行以下操作:
- 检查是否选择了一个配方;
- 验证电子签名;
- 通过配方编号和配方版本计算得到配方ID;
- 用配方ID从用户归档查询配方值写入到中间变量;
- 检查“M_IsProduced”是否为1,判断该配方是否被生产过,以确定是否要删除该配方;
- 如果“M_IsProduced”是否为1,则把“M_IsDeleted”变量置1,更新用户归档中的值;
- 如果“M_IsProduced”是否为0;则直接在用户归档中删除该配方;
- 记录审计追踪;
- 更新配方编号控件的值。
- 更新按钮的使能状态。
编辑配方
编辑配方有以下2点要求:
- 能告知被编辑的参数的最大值和最小值,输入的值不能超过最大值和最小值;
- 编辑在线配方值时要求有电子签名和审计追踪。
要满足上述要求,就不能直接在Wincc的输入输出域控件里输入值,需要设计一个输入界面显示参数的最大值和最小值,用脚本检查输入的是否在范围之内,编辑在线配方值时还要进行电子签名。输入界面如下,点击数值控件时以对话框形式弹出,类似于屏幕键盘(以下称作屏幕键盘界面)。
屏幕键盘界面是个通过画面窗口调用的画面,这个画面窗口平常隐藏,需要输入配方值时通过调用函数显示画面窗口,编辑数据库配方值和在线配方值用的是不同的调用函数。
编辑数据库配方值时调用KeyboardDispScreen()函数显示屏幕键盘界面,示例如下:
Sub OnClick(ByVal Item) KeyboardDispScreen AccessPath, Item.ObjectName, 0, 50, "配方变量:进瓶风机频率设定(Hz)" End Sub
编辑在线配方值时调用KeyboardDispTag()函数显示屏幕键盘界面,示例如下:
Sub OnClick(Byval Item) KeyboardDispTag parent.TagPrefix, "i16_Set_VfFaninfeed", 0, 50, Item.TooltipText End Sub
屏幕键盘界面通过位于根画面的画面窗口控件引用,画面窗口控件名为“Keyboard”。屏幕键盘界面中有四个隐藏的文本控件,分别用于存储变量前缀、变量名、画面路径、控件名称,然后屏幕键盘界面里的代码就可以使用这几个值。KeyboardDispScreen()函数只传递画面路径和控件名称,KeyboardDispTag()函数只传递变量前缀和变量名。
KeyboardDispScreen()函数代码如下:
'--------------------------------------------------------------------------------- ' ScreenPath:调用函数的控件所在的画面路径 ' ItemName :调用控件的控件名 ' MinVal :最小值 ' MaxVal :最大值 ' TagNote :被编辑的参数的描述 '--------------------------------------------------------------------------------- Function KeyboardDispScreen(ScreenPath,ItemName,MinVal,MaxVal,TagNote) Dim objScrKeyboard,objScrWindow Set objScrKeyboard=HMIRuntime.Screens(BaseScreenName).ScreenItems("Keyboard") objScrKeyboard.CaptionText=TagNote'TagName objScrKeyboard.Visible=True Set objScrWindow = objScrKeyboard.Screen objScrWindow.ScreenItems("ScreenPath").Text = ScreenPath objScrWindow.ScreenItems("ItemName").Text = ItemName objScrWindow.ScreenItems("ET_MinValue").OutputValue = MinVal objScrWindow.ScreenItems("ET_MaxValue").OutputValue = MaxVal End Function
KeyboardDispTag()函数代码如下:
'--------------------------------------------------------------------------------- ' Prefix :变量的前缀 ' ItemName :变量名称 ' MinVal :最小值 ' MaxVal :最大值 ' TagNote :被编辑的变量的描述 '--------------------------------------------------------------------------------- Function KeyboardDispTag(Prefix,TagName,MinVal,MaxVal,TagNote) Dim objScrKeyboard,objScrWindow Set objScrKeyboard=HMIRuntime.Screens(BaseScreenName).ScreenItems("Keyboard") objScrKeyboard.CaptionText=TagNote'TagName objScrKeyboard.Visible=True Set objScrWindow = objScrKeyboard.Screen objScrWindow.ScreenItems("Prefix").Text = Prefix objScrWindow.ScreenItems("TagName").Text = TagName objScrWindow.ScreenItems("ET_MinValue").OutputValue = MinVal objScrWindow.ScreenItems("ET_MaxValue").OutputValue = MaxVal End Function
存储变量前缀、变量名、画面路径、控件名称的四个控件获得值后,就会脚本写入到全局声明的变量,便于之后使用。
“确认”按钮中的代码如下,如果控件名称(ItemName)变量不为空,就执行写入到界面控件的脚本,其中第36行至53行代码会将调用控件同一个画面的某些按钮设为启用或禁用。
如果变量名称(TagName)变量不为空,验证电子签名后就会执行写入到变量的代码。
Sub OnClick(Byval Item) Dim objScrKeyboard, InputValue, MaxValue, MinValue, OutputValue ' 获取画面窗口控件 Set objScrKeyboard = Parent '检查输入内容是否为空 If Trim(ScreenItems("Txt_Value").Text)="" Then objScrKeyboard.Visible=False Exit Sub End If '获取数据值和限制值 InputValue=CSng(ScreenItems("Txt_Value").Text) MaxValue=CSng(ScreenItems("ET_MaxValue").OutputValue) MinValue=CSng(ScreenItems("ET_MinValue").OutputValue) '检查值范围 If InputValue>MaxValue Then OutputValue=MaxValue Elseif InputValue<MinValue Then OutputValue=MinValue Else OutputValue=InputValue End If If ItemName <> "" Then Dim objScrEdit '获取画面对象 Set objScrEdit=HMIRuntime.Screens(ScreenPath) '写入值 objScrEdit.ScreenItems(ItemName).text = CStr(OutputValue) '更新按钮状态 Dim EnableButtuns, DisableButtuns, ButttunName EnableButtuns = Array("BT_Save") DisableButtuns = Array("BT_Download", "BT_Delete") Dim ItemNameTemp For Each ItemNameTemp In objScrEdit.ScreenItems If ItemNameTemp.Type = "HMIButton" Then For Each ButttunName In EnableButtuns If ItemNameTemp.ObjectName = ButttunName Then ItemNameTemp.Enabled = True End If Next For Each ButttunName In DisableButtuns If ItemNameTemp.ObjectName = ButttunName Then ItemNameTemp.Enabled = False End If Next End If Next End If If TagName <> "" Then '电子签名,写入值 Dim TagNote TagNote = parent.CaptionText TagNewValueES PrefixToName(Prefix), TagNote, Prefix & TagName, OutputValue 'HMIRuntime.Tags(Prefix & TagName).Write OutputValue End If objScrKeyboard.Visible=False End Sub