# HG changeset patch # User Edouard Tisserant # Date 1677573591 -3600 # Node ID 0c06de0a39b53dcebcc5d24319df4f08171af8f6 # Parent 57bcc9545ca1896e618ae4cbf69b6425168aea64# Parent ed1ec3136c2b4ee899384d9228cdf9050326ef15 Merge. diff -r ed1ec3136c2b -r 0c06de0a39b5 Beremiz_service.py --- a/Beremiz_service.py Mon Feb 27 13:47:36 2023 +0100 +++ b/Beremiz_service.py Tue Feb 28 09:39:51 2023 +0100 @@ -185,6 +185,12 @@ return os.path.join(beremiz_dir, *args) +import locale +# Matiec's standard library relies on libC's locale-dependent +# string to/from number convertions, but IEC-61131 counts +# on '.' for decimal point. Therefore locale is reset to "C" */ +locale.setlocale(locale.LC_NUMERIC, "C") + def SetupI18n(): # Get folder containing translation files localedir = os.path.join(beremiz_dir, "locale") @@ -205,7 +211,6 @@ # Define locale domain loc.AddCatalog(domain) - import locale global default_locale default_locale = locale.getdefaultlocale()[1] diff -r ed1ec3136c2b -r 0c06de0a39b5 PLCControler.py --- a/PLCControler.py Mon Feb 27 13:47:36 2023 +0100 +++ b/PLCControler.py Tue Feb 28 09:39:51 2023 +0100 @@ -448,12 +448,12 @@ return len(self.GetInstanceList(pou_infos, name, debug)) > 0 return False - def GenerateProgram(self, filepath=None): + def GenerateProgram(self, filepath=None, **kwargs): errors = [] warnings = [] if self.Project is not None: try: - self.ProgramChunks = GenerateCurrentProgram(self, self.Project, errors, warnings) + self.ProgramChunks = GenerateCurrentProgram(self, self.Project, errors, warnings,**kwargs) self.NextCompiledProject = self.Copy(self.Project) program_text = "".join([item[0] for item in self.ProgramChunks]) if filepath is not None: @@ -1147,28 +1147,35 @@ def GetConfigurationExtraVariables(self): global_vars = [] - for var_name, var_type, var_initial in self.GetConfNodeGlobalInstances(): - tempvar = PLCOpenParser.CreateElement("variable", "globalVars") - tempvar.setname(var_name) - - tempvartype = PLCOpenParser.CreateElement("type", "variable") - if var_type in self.GetBaseTypes(): - tempvartype.setcontent(PLCOpenParser.CreateElement( - var_type.lower() - if var_type in ["STRING", "WSTRING"] - else var_type, "dataType")) + for global_instance in self.GetConfNodeGlobalInstances(): + if type(global_instance)==tuple: + # usual global without modifier from a CTN or a library + var_name, var_type, var_initial = global_instance + tempvar = PLCOpenParser.CreateElement("variable", "globalVars") + tempvar.setname(var_name) + + tempvartype = PLCOpenParser.CreateElement("type", "variable") + if var_type in self.GetBaseTypes(): + tempvartype.setcontent(PLCOpenParser.CreateElement( + var_type.lower() + if var_type in ["STRING", "WSTRING"] + else var_type, "dataType")) + else: + tempderivedtype = PLCOpenParser.CreateElement("derived", "dataType") + tempderivedtype.setname(var_type) + tempvartype.setcontent(tempderivedtype) + tempvar.settype(tempvartype) + + if var_initial != "": + value = PLCOpenParser.CreateElement("initialValue", "variable") + value.setvalue(var_initial) + tempvar.setinitialValue(value) + + global_vars.append(tempvar) else: - tempderivedtype = PLCOpenParser.CreateElement("derived", "dataType") - tempderivedtype.setname(var_type) - tempvartype.setcontent(tempderivedtype) - tempvar.settype(tempvartype) - - if var_initial != "": - value = PLCOpenParser.CreateElement("initialValue", "variable") - value.setvalue(var_initial) - tempvar.setinitialValue(value) - - global_vars.append(tempvar) + # case of varlists from a TC6 library + global_vars.append(global_instance) + return global_vars # Function that returns the block definition associated to the block type given diff -r ed1ec3136c2b -r 0c06de0a39b5 PLCGenerator.py --- a/PLCGenerator.py Mon Feb 27 13:47:36 2023 +0100 +++ b/PLCGenerator.py Tue Feb 28 09:39:51 2023 +0100 @@ -277,21 +277,27 @@ ("\n", ())] var_number = 0 - varlists = [(varlist, varlist.getvariable()[:]) for varlist in configuration.getglobalVars()] + varlists = configuration.getglobalVars()[:] extra_variables = self.Controler.GetConfigurationExtraVariables() - extra_global_vars = None - if len(extra_variables) > 0 and len(varlists) == 0: - extra_global_vars = PLCOpenParser.CreateElement("globalVars", "interface") - configuration.setglobalVars([extra_global_vars]) - varlists = [(extra_global_vars, [])] - - for variable in extra_variables: - varlists[-1][0].appendvariable(variable) - varlists[-1][1].append(variable) + extra_CTN_globals = [] + + for item in extra_variables: + if item.getLocalTag() == "globalVars": + varlists.append(item) + else: + extra_CTN_globals.append(item) + + if len(extra_CTN_globals) > 0: + extra_varlist = PLCOpenParser.CreateElement("globalVars", "interface") + + for variable in extra_CTN_globals: + extra_varlist.appendvariable(variable) + + varlists.append(extra_varlist) # Generate any global variable in configuration - for varlist, varlist_variables in varlists: + for varlist in varlists: variable_type = errorVarTypes.get("VAR_GLOBAL", "var_local") # Generate variable block with modifier config += [(" VAR_GLOBAL", ())] @@ -303,7 +309,7 @@ config += [(" NON_RETAIN", (tagname, variable_type, (var_number, var_number + len(varlist.getvariable())), "non_retain"))] config += [("\n", ())] # Generate any variable of this block - for var in varlist_variables: + for var in varlist.getvariable(): vartype_content = var.gettype().getcontent() if vartype_content.getLocalTag() == "derived": var_type = vartype_content.getname() @@ -331,12 +337,6 @@ var_number += 1 config += [(" END_VAR\n", ())] - if extra_global_vars is not None: - configuration.remove(extra_global_vars) - else: - for variable in extra_variables: - varlists[-1][0].remove(variable) - # Generate any resource in the configuration for resource in configuration.getresource(): config += self.GenerateResource(resource, configuration.getname()) @@ -458,7 +458,7 @@ return resrce # Generate the entire program for current project - def GenerateProgram(self, log): + def GenerateProgram(self, log, noconfig=False): log("Collecting data types") # Find all data types defined for datatype in self.Project.getdataTypes(): @@ -480,6 +480,8 @@ for pou_name in self.PouComputed.keys(): log("Generate POU %s"%pou_name) self.GeneratePouProgram(pou_name) + if noconfig: + return # Generate every configurations defined log("Generate Config(s)") for config in self.Project.getconfigurations(): @@ -1765,7 +1767,7 @@ return program -def GenerateCurrentProgram(controler, project, errors, warnings): +def GenerateCurrentProgram(controler, project, errors, warnings, **kwargs): generator = ProgramGenerator(controler, project, errors, warnings) if hasattr(controler, "logger"): def log(txt): @@ -1774,5 +1776,5 @@ def log(txt): pass - generator.GenerateProgram(log) + generator.GenerateProgram(log,**kwargs) return generator.GetGeneratedProgram() diff -r ed1ec3136c2b -r 0c06de0a39b5 POULibrary.py --- a/POULibrary.py Mon Feb 27 13:47:36 2023 +0100 +++ b/POULibrary.py Tue Feb 28 09:39:51 2023 +0100 @@ -44,7 +44,7 @@ def GetSTCode(self): if not self.program: - self.program = self.LibraryControler.GenerateProgram()[0]+"\n" + self.program = self.LibraryControler.GenerateProgram(noconfig=True)[0]+"\n" return self.program def GetName(self): @@ -65,9 +65,14 @@ def GlobalInstances(self): """ - @return: [(instance_name, instance_type),...] + @return: [varlist_object, ...] """ - return [] + varlists = [] + for configuration in self.LibraryControler.Project.getconfigurations(): + varlist = configuration.getglobalVars() + if len(varlist)>0 : + varlists += varlist + return varlists def FatalError(self, message): """ Raise an exception that will trigger error message intended to diff -r ed1ec3136c2b -r 0c06de0a39b5 editors/TextViewer.py --- a/editors/TextViewer.py Mon Feb 27 13:47:36 2023 +0100 +++ b/editors/TextViewer.py Tue Feb 28 09:39:51 2023 +0100 @@ -130,8 +130,7 @@ self.Editor.SetUseTabs(0) self.Editor.SetModEventMask(wx.stc.STC_MOD_BEFOREINSERT | - wx.stc.STC_MOD_BEFOREDELETE | - wx.stc.STC_PERFORMED_USER) + wx.stc.STC_MOD_BEFOREDELETE) self.Bind(wx.stc.EVT_STC_STYLENEEDED, self.OnStyleNeeded, self.Editor) self.Editor.Bind(wx.stc.EVT_STC_MARGINCLICK, self.OnMarginClick) @@ -213,25 +212,24 @@ self.SearchResults = None self.CurrentFindHighlight = None + Buffering = "Off" def OnModification(self, event): if not self.DisableEvents: mod_type = event.GetModificationType() if mod_type & wx.stc.STC_MOD_BEFOREINSERT: if self.CurrentAction is None: - self.StartBuffering() + self.Buffering = "ShouldStart" elif self.CurrentAction[0] != "Add" or self.CurrentAction[1] != event.GetPosition() - 1: - self.Controler.EndBuffering() - self.StartBuffering() + self.Buffering = "ShouldRestart" self.CurrentAction = ("Add", event.GetPosition()) - wx.CallAfter(self.RefreshModel) + self.RefreshModelAfter() elif mod_type & wx.stc.STC_MOD_BEFOREDELETE: if self.CurrentAction is None: - self.StartBuffering() + self.Buffering = "ShouldStart" elif self.CurrentAction[0] != "Delete" or self.CurrentAction[1] != event.GetPosition() + 1: - self.Controler.EndBuffering() - self.StartBuffering() + self.Buffering = "ShouldRestart" self.CurrentAction = ("Delete", event.GetPosition()) - wx.CallAfter(self.RefreshModel) + self.RefreshModelAfter() event.Skip() def OnDoDrop(self, event): @@ -379,7 +377,7 @@ elif values[3] == self.TagName: self.ResetBuffer() event.SetDragText(values[0]) - wx.CallAfter(self.RefreshModel) + self.RefreshModelAfter() else: message = _("Variable don't belong to this POU!") if message is not None: @@ -429,10 +427,14 @@ self.ParentWindow.RefreshFileMenu() self.ParentWindow.RefreshEditMenu() + def EndBuffering(self): + self.Controler.EndBuffering() + def ResetBuffer(self): if self.CurrentAction is not None: - self.Controler.EndBuffering() + self.EndBuffering() self.CurrentAction = None + self.Buffering == "Off" def GetBufferState(self): if not self.Debug and self.TextSyntax != "ALL": @@ -834,12 +836,29 @@ self.RemoveHighlight(*self.CurrentFindHighlight) self.CurrentFindHighlight = None + pending_model_refresh=False def RefreshModel(self): + self.pending_model_refresh=False self.RefreshJumpList() self.Colourise(0, -1) + + if self.Buffering == "ShouldStart": + self.StartBuffering() + self.Buffering == "On" + elif self.Buffering == "ShouldRestart": + self.EndBuffering() + self.StartBuffering() + self.Buffering == "On" + self.Controler.SetEditedElementText(self.TagName, self.GetText()) self.ResetSearchResults() + def RefreshModelAfter(self): + if self.pending_model_refresh: + return + self.pending_model_refresh=True + wx.CallAfter(self.RefreshModel) + def OnKeyDown(self, event): key = event.GetKeyCode() if self.Controler is not None: diff -r ed1ec3136c2b -r 0c06de0a39b5 exemples/csv_read/beremiz.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/exemples/csv_read/beremiz.xml Tue Feb 28 09:39:51 2023 +0100 @@ -0,0 +1,5 @@ + + + + + diff -r ed1ec3136c2b -r 0c06de0a39b5 exemples/csv_read/plc.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/exemples/csv_read/plc.xml Tue Feb 28 09:39:51 2023 +0100 @@ -0,0 +1,341 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CSV_NAME_0 + + + + + + + CSV_ROWIDX + + + + + + + CSV_COLIDX + + + + + + + CSV_NAME_1 + + + + + + + CSV_ROWSTR + + + + + + + CSV_COLSTR + + + + + + + + + + + CSV_RES_0 + + + + + + + + + + + CSV_ACK_0 + + + + + + + + + + + CSV_ACK_1 + + + + + + + + + + + CSV_RES_1 + + + + + + + CSV_RELOAD_BTN + + + + + + + + + + + + + + + + + + diff -r ed1ec3136c2b -r 0c06de0a39b5 exemples/csv_read/project_files/my_int_csv.csv --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/exemples/csv_read/project_files/my_int_csv.csv Tue Feb 28 09:39:51 2023 +0100 @@ -0,0 +1,15 @@ +1,1.01,1.02,1.03,1.04,1.05,1.06 +1.1,1.11,1.12,1.13,1.14,1.15,1.16 +1.2,1.21,1.22,1.23,1.24,1.25,1.26 +1.3,1.31,1.32,1.33,1.34,1.35,1.36 +1.4,1.41,1.42,1.43,1.44,1.45,1.46 +1.5,1.51,1.52,1.53,1.54,1.55,1.56 +1.6,1.61,1.62,1.63,1.64,1.65,1.66 +1.7,1.71,1.72,1.73,1.74,1.75,1.76 +1.8,1.81,1.82,1.83,1.84,1.85,1.86 +1.9,1.91,1.92,1.93,1.94,1.95,1.96 +2,2.01,2.02,2.03,2.04,2.05,2.06 +2.1,2.11,2.12,2.13,2.14,2.15,2.16 +2.2,2.21,2.22,2.23,2.24,2.25,2.26 +2.3,2.31,2.32,2.33,2.34,2.35,2.36 +2.4,2.41,2.42,2.43,2.44,2.45,2.46 diff -r ed1ec3136c2b -r 0c06de0a39b5 exemples/csv_read/project_files/my_int_csv.ods Binary file exemples/csv_read/project_files/my_int_csv.ods has changed diff -r ed1ec3136c2b -r 0c06de0a39b5 exemples/csv_read/project_files/my_str_csv.csv --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/exemples/csv_read/project_files/my_str_csv.csv Tue Feb 28 09:39:51 2023 +0100 @@ -0,0 +1,8 @@ +Title,Ingredient A,Ingredient B,Ingredient C,Ingredient D,Ingredient E,Ingredient F,Ingredient G +Recipe 1,1,2,3,4,5,6,7 +Recipe 2,2,3,4,5,6,7,8 +Recipe 3,3,4,5,6,7,8,9 +Recipe 4,4,5,6,7,8,9,10 +Recipe 5,5,6,7,8,9,10,11 +Recipe 6,6,7,8,9,10,11,12 +Recipe 7,7,8,9,10,11,12,13 diff -r ed1ec3136c2b -r 0c06de0a39b5 exemples/csv_read/project_files/my_str_csv.ods Binary file exemples/csv_read/project_files/my_str_csv.ods has changed diff -r ed1ec3136c2b -r 0c06de0a39b5 exemples/csv_read/svghmi_0@svghmi/baseconfnode.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/exemples/csv_read/svghmi_0@svghmi/baseconfnode.xml Tue Feb 28 09:39:51 2023 +0100 @@ -0,0 +1,2 @@ + + diff -r ed1ec3136c2b -r 0c06de0a39b5 exemples/csv_read/svghmi_0@svghmi/confnode.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/exemples/csv_read/svghmi_0@svghmi/confnode.xml Tue Feb 28 09:39:51 2023 +0100 @@ -0,0 +1,2 @@ + + diff -r ed1ec3136c2b -r 0c06de0a39b5 exemples/csv_read/svghmi_0@svghmi/svghmi.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/exemples/csv_read/svghmi_0@svghmi/svghmi.svg Tue Feb 28 09:39:51 2023 +0100 @@ -0,0 +1,1787 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + 8 + + + + + + + 8 + + + + + + 8 + + + + + Row# + Column# + + file.csv + + + File name + + + + + + + Q + + + + W + + + + E + + + + R + + + + T + + + + Y + + + + U + + + + I + + + + O + + + + P + + + + A + + + + S + + + + D + + + + F + + + + G + + + + H + + + + J + + + + K + + + + L + + + + Z + + + + X + + + + C + + + + V + + + + B + + + + N + + + + M + + + + . + : + + + + ; + , + + + + 1 + + + + 2 + + + + 3 + + + + 4 + + + + 5 + + + + 6 + + + + 7 + + + + 8 + + + + 9 + - + + + + 0 + + + + + + + Esc + + + + + + + + + + + + + Caps + Lock + + + + Caps + Lock + + + + text + + + + Shift + + Shift + + + + Shift + + Shift + + + information + + + + + number + + + + + + + + 7 + + + + 4 + + + + 1 + + + + 8 + + + + 5 + + + + 2 + + + + 9 + + + + 6 + + + + 3 + + + + 0 + + + + + Esc + + + + + + + + +/- + + information + + + . + + + Result: + + Ingredient A + + + + Recipe 1 + + + Row name + Column name + + file.csv + + + File name + + 8 + + + + + Result: + + + + + + + Reload CSVs + + + + Upload csv at http://localhost:8009/settings/ + diff -r ed1ec3136c2b -r 0c06de0a39b5 exemples/svghmi_jumps/svghmi_0@svghmi/svghmi.svg --- a/exemples/svghmi_jumps/svghmi_0@svghmi/svghmi.svg Mon Feb 27 13:47:36 2023 +0100 +++ b/exemples/svghmi_jumps/svghmi_0@svghmi/svghmi.svg Tue Feb 28 09:39:51 2023 +0100 @@ -25,7 +25,7 @@ image/svg+xml - + @@ -40,17 +40,17 @@ guidetolerance="10" inkscape:pageopacity="0" inkscape:pageshadow="2" - inkscape:window-width="1600" - inkscape:window-height="836" + inkscape:window-width="1850" + inkscape:window-height="1036" id="namedview4" showgrid="false" - inkscape:zoom="0.23177389" - inkscape:cx="1999.5317" - inkscape:cy="-682.74047" + inkscape:zoom="0.46354778" + inkscape:cx="-544.27948" + inkscape:cy="655.56978" inkscape:window-x="0" inkscape:window-y="27" inkscape:window-maximized="1" - inkscape:current-layer="hmi0" + inkscape:current-layer="g2496" showguides="true" inkscape:guide-bbox="true" borderlayer="true" @@ -67,7 +67,7 @@ transform="translate(1320,1520)" width="100%" height="100%" - inkscape:label="HMI:Page:RelativePage@/FB_ZERO" /> + inkscape:label="HMI:Page:RelativePage:p=6@p=page_number@/FB_ZERO" /> + inkscape:label="HMI:Page:Relative:p=5@p=page_number" /> HMI:Jump:RelativePage + 0 + transform="translate(620.54487,-11.353461)"> + transform="translate(4.2410198,-11.353461)"> - Press Ctrl+X to edit SVG elements directly with XML editor + + declaration of user_level HMI local variable(not a PLC variable) + diff -r ed1ec3136c2b -r 0c06de0a39b5 py_ext/pous.xml --- a/py_ext/pous.xml Mon Feb 27 13:47:36 2023 +0100 +++ b/py_ext/pous.xml Tue Feb 28 09:39:51 2023 +0100 @@ -1,10 +1,10 @@ - + - + @@ -17,6 +17,1646 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 'CSVRdStr("' + + + + + + + FILE_NAME + + + + + + + '","' + + + + + + + ROW + + + + + + + '","' + + + + + + + COLUMN + + + + + + + '")' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OLDCODE + + + + + + + OLDCODE + + + + + + + OLDCODE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pyext_csv_update + + + + + + + + + + + ACK + + + + + + + + + + + + + RESULT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + '#' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TRIG + + + + + + + + + + + ACK + + + + + + + + + + + RESULT + + + + + + + 'pyext_csv_reload()' + + + + + + + + + + + pyext_csv_update + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 'CSVRdInt("' + + + + + + + FILE_NAME + + + + + + + '",' + + + + + + + ROW + + + + + + + ',' + + + + + + + COLUMN + + + + + + + ')' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OLDCODE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OLDCODE + + + + + + + OLDCODE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pyext_csv_update + + + + + + + + + + + + + ACK + + + + + + + + + + + + + RESULT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + '#' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -445,6 +2085,19 @@ - + + + + + + + + + + + + + + diff -r ed1ec3136c2b -r 0c06de0a39b5 py_ext/py_ext.py --- a/py_ext/py_ext.py Mon Feb 27 13:47:36 2023 +0100 +++ b/py_ext/py_ext.py Tue Feb 28 09:39:51 2023 +0100 @@ -30,6 +30,98 @@ from py_ext.PythonFileCTNMixin import PythonFileCTNMixin import util.paths as paths +pyext_python_lib_code = """ + +import csv +from collections import OrderedDict + +csv_int_files = {} +def CSVRdInt(fname, rowidx, colidx): + \"\"\" + Return value at row/column pointed by integer indexes + Assumes data starts at first row and first column, no headers. + \"\"\" + global csv_int_files + data = csv_int_files.get(fname, None) + if data is None: + data = list() + try: + csvfile = open(fname, 'rb') + except IOError: + return "#FILE_NOT_FOUND" + try: + dialect = csv.Sniffer().sniff(csvfile.read(1024)) + csvfile.seek(0) + reader = csv.reader(csvfile, dialect) + for row in reader: + data.append(row) + except csv.Error: + return "#CSV_ERROR" + finally: + csvfile.close() + csv_int_files[fname] = data + + try: + row = data[rowidx] + except IndexError: + return "#ROW_NOT_FOUND" + + try: + return row[colidx] + except IndexError: + return "#COL_NOT_FOUND" + + +csv_str_files = {} +def CSVRdStr(fname, rowname, colname): + \"\"\" + Return value at row/column pointed by a pair of names as string + Assumes first row is column headers and first column is row name. + \"\"\" + global csv_str_files + entry = csv_str_files.get(fname, None) + if entry is None: + data = dict() + try: + csvfile = open(fname, 'rb') + except IOError: + return "#FILE_NOT_FOUND" + try: + dialect = csv.Sniffer().sniff(csvfile.read(1024)) + csvfile.seek(0) + reader = csv.reader(csvfile, dialect) + headers = dict([(name, index) for index, name in enumerate(reader.next()[1:])]) + for row in reader: + data[row[0]] = row[1:] + except csv.Error: + return "#CSV_ERROR" + finally: + csvfile.close() + csv_str_files[fname] = (headers, data) + else: + headers, data = entry + + try: + row = data[rowname] + except KeyError: + return "#ROW_NOT_FOUND" + + try: + colidx = headers[colname] + except KeyError: + return "#COL_NOT_FOUND" + + try: + return row[colidx] + except IndexError: + return "#COL_NOT_FOUND" + +def pyext_csv_reload(): + global csv_int_files, csv_str_files + csv_int_files.clear() + csv_str_files.clear() + +""" class PythonLibrary(POULibrary): def GetLibraryPath(self): @@ -57,7 +149,13 @@ pythonfile.write(plc_python_code) pythonfile.close() - return (["py_ext"], [(Gen_Pythonfile_path, IECCFLAGS)], True), "" + runtimefile_path = os.path.join(buildpath, "runtime_00_pyext.py") + runtimefile = open(runtimefile_path, 'w') + runtimefile.write(pyext_python_lib_code) + runtimefile.close() + return ((["py_ext"], [(Gen_Pythonfile_path, IECCFLAGS)], True), "", + ("runtime_00_pyext.py", open(runtimefile_path, "rb"))) + class PythonFile(PythonFileCTNMixin): diff -r ed1ec3136c2b -r 0c06de0a39b5 runtime/NevowServer.py --- a/runtime/NevowServer.py Mon Feb 27 13:47:36 2023 +0100 +++ b/runtime/NevowServer.py Tue Feb 28 09:39:51 2023 +0100 @@ -27,6 +27,7 @@ from __future__ import print_function import os import collections +import shutil import platform as platform_module from zope.interface import implements from nevow import appserver, inevow, tags, loaders, athena, url, rend @@ -230,6 +231,18 @@ "Restart or Repair"), action=_("Do")) + # pylint: disable=no-self-argument + def uploadFile( + ctx=annotate.Context(), + uploadedfile=annotate.FileUpload(required=True, + label=_("File to upload"))): + pass + + uploadFile = annotate.autocallable(uploadFile, + label=_( + "Upload a file to PLC working directory"), + action=_("Upload")) + customSettingsURLs = { } @@ -304,8 +317,14 @@ GetPLCObjectSingleton().RepairPLC() else: MainWorker.quit() - - + + def uploadFile(self, uploadedfile, **kwargs): + if uploadedfile is not None: + fobj = getattr(uploadedfile, "file", None) + if fobj is not None: + with open(uploadedfile.filename, 'w') as destfd: + fobj.seek(0) + shutil.copyfileobj(fobj,destfd) def locateChild(self, ctx, segments): if segments[0] in customSettingsURLs: diff -r ed1ec3136c2b -r 0c06de0a39b5 svghmi/analyse_widget.xslt --- a/svghmi/analyse_widget.xslt Mon Feb 27 13:47:36 2023 +0100 +++ b/svghmi/analyse_widget.xslt Tue Feb 28 09:39:51 2023 +0100 @@ -854,6 +854,46 @@ Value to display + + + + + + + + Arguments are either: + + + + - XXX reference path TODO + + + + - name=value: setting variable with literal value. + + - name=other_name: copy variable content into another + + + + "active"+"inactive" labeled elements can be provided to show feedback when pressed + + + + Exemples: + + + + HMI:Page:notify=1@notify=/PLCVAR + + HMI:Page:ack=2:notify=1@ack=.local_var@notify=/PLCVAR + + + + + + Page + + diff -r ed1ec3136c2b -r 0c06de0a39b5 svghmi/detachable_pages.ysl2 --- a/svghmi/detachable_pages.ysl2 Mon Feb 27 13:47:36 2023 +0100 +++ b/svghmi/detachable_pages.ysl2 Tue Feb 28 09:39:51 2023 +0100 @@ -137,6 +137,10 @@ const "_detachable_elements", "func:detachable_elements($hmi_pages | $keypads)"; const "detachable_elements", "$_detachable_elements[not(ancestor::*/@id = $_detachable_elements/@id)]"; +emit "declarations:page-class" { + | class PageWidget extends Widget{} +} + emit "declarations:detachable-elements" { | | var detachable_elements = { @@ -165,8 +169,13 @@ const "all_page_widgets","$hmi_widgets[@id = $page_all_elements/@id and @id != $page/@id]"; const "page_managed_widgets","$all_page_widgets[not(@id=$in_forEach_widget_ids)]"; + + const "page_root_path", "$desc/path[not(@assign)]"; + if "count($page_root_path)>1" + error > Page id="«$page/@id»" : only one root path can be declared + const "page_relative_widgets", - "$page_managed_widgets[func:is_descendant_path(func:widget(@id)/path/@value, $desc/path/@value)]"; + "$page_managed_widgets[func:is_descendant_path(func:widget(@id)/path/@value, $page_root_path/@value)]"; // Take closest ancestor in detachable_elements // since nested detachable elements are filtered out @@ -178,19 +187,19 @@ ancestor-or-self::*[@id = $detachable_elements/@id]"""; | "«$pagename»": { - //| widget: hmi_widgets["«@id»"], | bbox: [«$p/@x», «$p/@y», «$p/@w», «$p/@h»], - if "$desc/path/@value" { - if "count($desc/path/@index)=0" - warning > Page id="«$page/@id»" : No match for path "«$desc/path/@value»" in HMI tree - | page_index: «$desc/path/@index», - | page_class: "«$indexed_hmitree/*[@hmipath = $desc/path/@value]/@class»", + if "count($page_root_path)=1"{ + if "count($page_root_path/@index)=0" + warning > Page id="«$page/@id»" : No match for path "«$page_root_path/@value»" in HMI tree + | page_index: «$page_root_path/@index», + | page_class: "«$indexed_hmitree/*[@hmipath = $page_root_path/@value]/@class»", } | widgets: [ + | [hmi_widgets["«$page/@id»"], []], foreach "$page_managed_widgets" { const "widget_paths_relativeness" foreach "func:widget(@id)/path" { - value "func:is_descendant_path(@value, $desc/path/@value)"; + value "func:is_descendant_path(@value, $page_root_path/@value)"; if "position()!=last()" > , } | [hmi_widgets["«@id»"], [«$widget_paths_relativeness»]]`if "position()!=last()" > ,` diff -r ed1ec3136c2b -r 0c06de0a39b5 svghmi/gen_index_xhtml.xslt --- a/svghmi/gen_index_xhtml.xslt Mon Feb 27 13:47:36 2023 +0100 +++ b/svghmi/gen_index_xhtml.xslt Tue Feb 28 09:39:51 2023 +0100 @@ -734,6 +734,21 @@ + + + + + /* + + */ + + + + class PageWidget extends Widget{} + + + + @@ -787,7 +802,15 @@ - + + + + Page id=" + + " : only one root path can be declared + + + " @@ -804,31 +827,35 @@ ], - - + + Page id=" " : No match for path " - + " in HMI tree page_index: - + , page_class: " - + ", widgets: [ + [hmi_widgets[" + + "], []], + - + , @@ -1190,6 +1217,8 @@ + const xmlns = "http://www.w3.org/2000/svg"; + let id = document.getElementById.bind(document); var svg_root = id(" @@ -1440,7 +1469,7 @@ ,{ - assignments: [], + enable_assignments: [], compute_enable: function(value, oldval, varnum) { @@ -1456,13 +1485,13 @@ if(varnum == - ) this.assignments[ + ) this.enable_assignments[ ] = value; let - = this.assignments[ + = this.enable_assignments[ ]; @@ -2131,7 +2160,7 @@ } - + undeafen(index){ @@ -2141,6 +2170,10 @@ this.incoming[index] = undefined; + // TODO: add timestamp argument to dispatch, so that defered data do not appear wrong on graphs + + this.lastdispatch[index] = Date.now(); + this.do_dispatch(new_val, old_val, index); } @@ -2388,7 +2421,9 @@ + + @@ -2402,7 +2437,7 @@ var hmi_widgets = { - + } @@ -5557,7 +5592,7 @@ animate: function(){ - this.value_elt.textContent = String(this.display); + multiline_to_svg_text(this.value_elt, String(this.display)); }, @@ -5595,7 +5630,7 @@ - this.value_elt.textContent = ""; + multiline_to_svg_text(this.value_elt, ""); }, @@ -6367,10 +6402,10 @@ - + - + @@ -7195,6 +7230,128 @@ ], + + + + + + + + Arguments are either: + + + + - XXX reference path TODO + + + + - name=value: setting variable with literal value. + + - name=other_name: copy variable content into another + + + + "active"+"inactive" labeled elements can be provided to show feedback when pressed + + + + Exemples: + + + + HMI:Page:notify=1@notify=/PLCVAR + + HMI:Page:ack=2:notify=1@ack=.local_var@notify=/PLCVAR + + + + + + Page + + + + + + + + + /disabled + + + + + + + assignments: {}, + + dispatch: function(value, oldval, varnum) { + + + + + + + + + if(varnum == + + ) this.assignments[" + + "] = value; + + + + + + }, + + assign: function() { + + + + + + + + + + + + + + + + const + + = this.assignments[" + + "]; + + if( + + != undefined) + + this.apply_hmi_value( + + , + + ); + + + + this.apply_hmi_value( + + , + + ); + + + + + }, + + @@ -8994,21 +9151,23 @@ // Compute visible Y range by merging fixed curves Y ranges - for(let minmax of this.minmaxes){ - - if(minmax){ - - let [min,max] = minmax; - - if(min < y_min) - - y_min = min; - - if(max > y_max) - - y_max = max; - - } + for(let varopts of this.variables_options){ + + let minmax = varopts.minmax + + if(minmax){ + + let [min,max] = minmax; + + if(min < y_min) + + y_min = min; + + if(max > y_max) + + y_max = max; + + } } @@ -9016,11 +9175,11 @@ if(y_min !== Infinity && y_max !== -Infinity){ - this.fixed_y_range = true; + this.fixed_y_range = true; } else { - this.fixed_y_range = false; + this.fixed_y_range = false; } @@ -9114,6 +9273,8 @@ + console.log("dispatch(",value,oldval, index, time); + // naive local buffer impl. // data is updated only when graph is visible @@ -12128,29 +12289,37 @@ - var screensaver_timer = null; - - function reset_screensaver_timer() { - - if(screensaver_timer){ - - window.clearTimeout(screensaver_timer); + if(screensaver_delay){ + + var screensaver_timer = null; + + function reset_screensaver_timer() { + + if(screensaver_timer){ + + window.clearTimeout(screensaver_timer); + + } + + screensaver_timer = window.setTimeout(() => { + + switch_page("ScreenSaver"); + + screensaver_timer = null; + + }, screensaver_delay*1000); } - screensaver_timer = window.setTimeout(() => { - - switch_page("ScreenSaver"); - - screensaver_timer = null; - - }, screensaver_delay*1000); + document.body.addEventListener('pointerdown', reset_screensaver_timer); + + // initialize screensaver + + reset_screensaver_timer(); } - if(screensaver_delay) - - document.body.addEventListener('pointerdown', reset_screensaver_timer); + @@ -12314,6 +12483,12 @@ + // when entering a page, assignments are evaluated + + new_desc.widgets[0][0].assign(); + + + return true; }; @@ -12474,16 +12649,12 @@ - // initialize screensaver - - reset_screensaver_timer(); - - - var reconnect_delay = 0; var periodic_reconnect_timer; + var force_reconnect = false; + // Once connection established @@ -12504,6 +12675,8 @@ periodic_reconnect_timer = window.setTimeout(() => { + force_reconnect = true; + ws.close(); periodic_reconnect_timer = null; @@ -12540,14 +12713,26 @@ ws = null; - // reconect - - // TODO : add visible notification while waiting for reload + // Do not attempt to reconnect immediately in case: + + // - connection was closed by server (PLC stop) + + // - connection was closed locally with an intention to reconnect + + if(evt.code=1000 && !force_reconnect){ + + window.alert("Connection closed by server"); + + location.reload(); + + } window.setTimeout(create_ws, reconnect_delay); reconnect_delay += 500; + force_reconnect = false; + }; @@ -12580,8 +12765,6 @@ - const xmlns = "http://www.w3.org/2000/svg"; - var edit_callback; const localtypes = {"PAGE_LOCAL":null, "HMI_LOCAL":null} diff -r ed1ec3136c2b -r 0c06de0a39b5 svghmi/inline_svg.ysl2 --- a/svghmi/inline_svg.ysl2 Mon Feb 27 13:47:36 2023 +0100 +++ b/svghmi/inline_svg.ysl2 Tue Feb 28 09:39:51 2023 +0100 @@ -209,6 +209,7 @@ const "result_svg_ns", "exsl:node-set($result_svg)"; emit "preamble:inline-svg" { + | const xmlns = "http://www.w3.org/2000/svg"; | let id = document.getElementById.bind(document); | var svg_root = id("«$svg/@id»"); } diff -r ed1ec3136c2b -r 0c06de0a39b5 svghmi/svghmi.js --- a/svghmi/svghmi.js Mon Feb 27 13:47:36 2023 +0100 +++ b/svghmi/svghmi.js Tue Feb 28 09:39:51 2023 +0100 @@ -525,6 +525,9 @@ ? page_name : page_name + "@" + hmitree_paths[page_index]); + // when entering a page, assignments are evaluated + new_desc.widgets[0][0].assign(); + return true; }; @@ -607,6 +610,7 @@ var reconnect_delay = 0; var periodic_reconnect_timer; +var force_reconnect = false; // Once connection established function ws_onopen(evt) { @@ -617,6 +621,7 @@ window.clearTimeout(periodic_reconnect_timer); } periodic_reconnect_timer = window.setTimeout(() => { + force_reconnect = true; ws.close(); periodic_reconnect_timer = null; }, 3600000); @@ -635,10 +640,16 @@ function ws_onclose(evt) { console.log("Connection closed. code:"+evt.code+" reason:"+evt.reason+" wasClean:"+evt.wasClean+" Reload in "+reconnect_delay+"ms."); ws = null; - // reconect - // TODO : add visible notification while waiting for reload + // Do not attempt to reconnect immediately in case: + // - connection was closed by server (PLC stop) + // - connection was closed locally with an intention to reconnect + if(evt.code=1000 && !force_reconnect){ + window.alert("Connection closed by server"); + location.reload(); + } window.setTimeout(create_ws, reconnect_delay); reconnect_delay += 500; + force_reconnect = false; }; var ws_url = @@ -655,7 +666,6 @@ create_ws() -const xmlns = "http://www.w3.org/2000/svg"; var edit_callback; const localtypes = {"PAGE_LOCAL":null, "HMI_LOCAL":null} function edit_value(path, valuetype, callback, initial) { diff -r ed1ec3136c2b -r 0c06de0a39b5 svghmi/svghmi.py --- a/svghmi/svghmi.py Mon Feb 27 13:47:36 2023 +0100 +++ b/svghmi/svghmi.py Tue Feb 28 09:39:51 2023 +0100 @@ -650,8 +650,8 @@ def svghmi_{location}_watchdog_trigger(): global browser_proc - restart_proc = {svghmi_cmds[Watchdog]} - waitpid_timeout(restart_proc, "SVGHMI watchdog triggered command") + watchdog_proc = {svghmi_cmds[Watchdog]} + waitpid_timeout(watchdog_proc, "SVGHMI watchdog triggered command") stop_proc = {svghmi_cmds[Stop]} waitpid_timeout(stop_proc, "SVGHMI stop command") waitpid_timeout(browser_proc, "SVGHMI browser process") diff -r ed1ec3136c2b -r 0c06de0a39b5 svghmi/widget_input.ysl2 --- a/svghmi/widget_input.ysl2 Mon Feb 27 13:47:36 2023 +0100 +++ b/svghmi/widget_input.ysl2 Tue Feb 28 09:39:51 2023 +0100 @@ -92,7 +92,7 @@ if "$have_value" { | animate: function(){ - | this.value_elt.textContent = String(this.display); + | multiline_to_svg_text(this.value_elt, String(this.display)); | }, } @@ -114,7 +114,7 @@ } if "$have_value" { - | this.value_elt.textContent = ""; + | multiline_to_svg_text(this.value_elt, ""); } | }, } diff -r ed1ec3136c2b -r 0c06de0a39b5 svghmi/widget_jump.ysl2 --- a/svghmi/widget_jump.ysl2 Mon Feb 27 13:47:36 2023 +0100 +++ b/svghmi/widget_jump.ysl2 Tue Feb 28 09:39:51 2023 +0100 @@ -143,8 +143,8 @@ otherwise value "$page_desc/arg[1]/@value"; } const "target_page_path" choose { - when "arg" value "$hmi_pages_descs[arg[1]/@value = $target_page_name]/path[1]/@value"; - otherwise value "$page_desc/path[1]/@value"; + when "arg" value "$hmi_pages_descs[arg[1]/@value = $target_page_name]/path[not(@assign)]/@value"; + otherwise value "$page_desc/path[not(@assign)]/@value"; } if "not(func:same_class_paths($target_page_path, path[1]/@value))" diff -r ed1ec3136c2b -r 0c06de0a39b5 svghmi/widget_page.ysl2 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/svghmi/widget_page.ysl2 Tue Feb 28 09:39:51 2023 +0100 @@ -0,0 +1,60 @@ +// widget_page.ysl2 + +widget_desc("Page") { + longdesc + || + + Arguments are either: + + - XXX reference path TODO + + - name=value: setting variable with literal value. + - name=other_name: copy variable content into another + + "active"+"inactive" labeled elements can be provided to show feedback when pressed + + Exemples: + + HMI:Page:notify=1@notify=/PLCVAR + HMI:Page:ack=2:notify=1@ack=.local_var@notify=/PLCVAR + + || + + shortdesc > Page + +} + +widget_defs("Page") { + + | assignments: {}, + | dispatch: function(value, oldval, varnum) { + const "widget", "."; + foreach "path" { + const "varid","generate-id()"; + const "varnum","position()-1"; + if "@assign" foreach "$widget/path[@assign]" if "$varid = generate-id()" { + | if(varnum == «$varnum») this.assignments["«@assign»"] = value; + } + } + | }, + | assign: function() { + const "paths","path"; + foreach "arg[contains(@value,'=')]"{ + const "name","substring-before(@value,'=')"; + const "value","substring-after(@value,'=')"; + const "index" foreach "$paths" if "@assign = $name" value "position()-1"; + const "isVarName", "regexp:test($value,'^[a-zA-Z_][a-zA-Z0-9_]+$')"; + choose { + when "$isVarName"{ + | const «$value» = this.assignments["«$value»"]; + | if(«$value» != undefined) + | this.apply_hmi_value(«$index», «$value»); + } + otherwise { + | this.apply_hmi_value(«$index», «$value»); + } + } + } + | }, +} + diff -r ed1ec3136c2b -r 0c06de0a39b5 svghmi/widget_xygraph.ysl2 --- a/svghmi/widget_xygraph.ysl2 Mon Feb 27 13:47:36 2023 +0100 +++ b/svghmi/widget_xygraph.ysl2 Tue Feb 28 09:39:51 2023 +0100 @@ -58,20 +58,21 @@ let y_min = Infinity, y_max = -Infinity; // Compute visible Y range by merging fixed curves Y ranges - for(let minmax of this.minmaxes){ - if(minmax){ - let [min,max] = minmax; - if(min < y_min) - y_min = min; - if(max > y_max) - y_max = max; - } + for(let varopts of this.variables_options){ + let minmax = varopts.minmax + if(minmax){ + let [min,max] = minmax; + if(min < y_min) + y_min = min; + if(max > y_max) + y_max = max; + } } if(y_min !== Infinity && y_max !== -Infinity){ - this.fixed_y_range = true; + this.fixed_y_range = true; } else { - this.fixed_y_range = false; + this.fixed_y_range = false; } this.ymin = y_min; diff -r ed1ec3136c2b -r 0c06de0a39b5 svghmi/widgetlib/bool_indicator.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/svghmi/widgetlib/bool_indicator.svg Tue Feb 28 09:39:51 2023 +0100 @@ -0,0 +1,82 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + diff -r ed1ec3136c2b -r 0c06de0a39b5 svghmi/widgetlib/simple_text_display.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/svghmi/widgetlib/simple_text_display.svg Tue Feb 28 09:39:51 2023 +0100 @@ -0,0 +1,64 @@ + + + + + + + + + + image/svg+xml + + + + + + value + diff -r ed1ec3136c2b -r 0c06de0a39b5 svghmi/widgets_common.ysl2 --- a/svghmi/widgets_common.ysl2 Mon Feb 27 13:47:36 2023 +0100 +++ b/svghmi/widgets_common.ysl2 Tue Feb 28 09:39:51 2023 +0100 @@ -134,7 +134,7 @@ | "«@id»": new «$widget/@type»Widget ("«@id»",«$freq»,[«$args»],[«$variables»],«$enable_expr»,{ if "$widget/@enable_expr" { - | assignments: [], + | enable_assignments: [], | compute_enable: function(value, oldval, varnum) { | let result = false; | do { @@ -142,8 +142,8 @@ const "varid","generate-id()"; const "varnum","position()-1"; if "@assign" foreach "$widget/path[@assign]" if "$varid = generate-id()" { - | if(varnum == «$varnum») this.assignments[«position()-1»] = value; - | let «@assign» = this.assignments[«position()-1»]; + | if(varnum == «$varnum») this.enable_assignments[«position()-1»] = value; + | let «@assign» = this.enable_assignments[«position()-1»]; | if(«@assign» == undefined) break; } } @@ -464,11 +464,12 @@ } } } - + undeafen(index){ this.deafen[index] = undefined; let [new_val, old_val] = this.incoming[index]; this.incoming[index] = undefined; + this.lastdispatch[index] = Date.now(); this.do_dispatch(new_val, old_val, index); } @@ -600,12 +601,14 @@ } const "included_ids","$parsed_widgets/widget[not(@type = $excluded_types) and not(@id = $discardable_elements/@id)]/@id"; +const "page_ids","$parsed_widgets/widget[@type = 'Page']/@id"; const "hmi_widgets","$hmi_elements[@id = $included_ids]"; +const "page_widgets","$hmi_elements[@id = $page_ids]"; const "result_widgets","$result_svg_ns//*[@id = $hmi_widgets/@id]"; emit "declarations:hmi-elements" { | var hmi_widgets = { - apply "$hmi_widgets", mode="hmi_widgets"; + apply "$hmi_widgets | $page_widgets", mode="hmi_widgets"; | } | } diff -r ed1ec3136c2b -r 0c06de0a39b5 targets/Linux/XSD --- a/targets/Linux/XSD Mon Feb 27 13:47:36 2023 +0100 +++ b/targets/Linux/XSD Tue Feb 28 09:39:51 2023 +0100 @@ -1,6 +1,7 @@ + %(toolchain_gcc)s - \ No newline at end of file + diff -r ed1ec3136c2b -r 0c06de0a39b5 targets/Linux/__init__.py --- a/targets/Linux/__init__.py Mon Feb 27 13:47:36 2023 +0100 +++ b/targets/Linux/__init__.py Tue Feb 28 09:39:51 2023 +0100 @@ -32,7 +32,11 @@ extension = ".so" def getBuilderCFLAGS(self): - return toolchain_gcc.getBuilderCFLAGS(self) + ["-fPIC"] + additional_cflags = ["-fPIC"] + build_for_realtime = self.CTRInstance.GetTarget().getcontent().getRealTime() + if build_for_realtime: + additional_cflags.append("-DREALTIME_LINUX") + return toolchain_gcc.getBuilderCFLAGS(self) + additional_cflags def getBuilderLDFLAGS(self): return toolchain_gcc.getBuilderLDFLAGS(self) + ["-shared", "-lrt"] diff -r ed1ec3136c2b -r 0c06de0a39b5 targets/Linux/plc_Linux_main.c --- a/targets/Linux/plc_Linux_main.c Mon Feb 27 13:47:36 2023 +0100 +++ b/targets/Linux/plc_Linux_main.c Tue Feb 28 09:39:51 2023 +0100 @@ -7,11 +7,23 @@ #include #include #include +#include #include #include #include - -static sem_t Run_PLC; +#ifdef REALTIME_LINUX +#include +#endif + +static unsigned long __debug_tick; + +static pthread_t PLC_thread; +static pthread_mutex_t python_wait_mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_mutex_t python_mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_mutex_t debug_wait_mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_mutex_t debug_mutex = PTHREAD_MUTEX_INITIALIZER; + +static int PLC_shutdown = 0; long AtomicCompareExchange(long* atomicvar,long compared, long exchange) { @@ -30,39 +42,34 @@ CURRENT_TIME->tv_nsec = tmp.tv_nsec; } -void PLC_timer_notify(sigval_t val) -{ - PLC_GetTime(&__CURRENT_TIME); - sem_post(&Run_PLC); -} - -timer_t PLC_timer; +static long long period_ns = 0; +struct timespec next_abs_time; + +static void inc_timespec(struct timespec *ts, unsigned long long value_ns) +{ + long long next_ns = ((long long) ts->tv_sec * 1000000000) + ts->tv_nsec + value_ns; +#ifdef __lldiv_t_defined + lldiv_t next_div = lldiv(next_ns, 1000000000); + ts->tv_sec = next_div.quot; + ts->tv_nsec = next_div.rem; +#else + ts->tv_sec = next_ns / 1000000000; + ts->tv_nsec = next_ns % 1000000000; +#endif +} void PLC_SetTimer(unsigned long long next, unsigned long long period) { - struct itimerspec timerValues; - /* - printf("SetTimer(%lld,%lld)\n",next, period); - */ - memset (&timerValues, 0, sizeof (struct itimerspec)); - { -#ifdef __lldiv_t_defined - lldiv_t nxt_div = lldiv(next, 1000000000); - lldiv_t period_div = lldiv(period, 1000000000); - timerValues.it_value.tv_sec = nxt_div.quot; - timerValues.it_value.tv_nsec = nxt_div.rem; - timerValues.it_interval.tv_sec = period_div.quot; - timerValues.it_interval.tv_nsec = period_div.rem; -#else - timerValues.it_value.tv_sec = next / 1000000000; - timerValues.it_value.tv_nsec = next % 1000000000; - timerValues.it_interval.tv_sec = period / 1000000000; - timerValues.it_interval.tv_nsec = period % 1000000000; -#endif - } - timer_settime (PLC_timer, 0, &timerValues, NULL); -} -// + /* + printf("SetTimer(%lld,%lld)\n",next, period); + */ + period_ns = period; + clock_gettime(CLOCK_MONOTONIC, &next_abs_time); + inc_timespec(&next_abs_time, next); + // interrupt clock_nanpsleep + pthread_kill(PLC_thread, SIGUSR1); +} + void catch_signal(int sig) { // signal(SIGTERM, catch_signal); @@ -71,16 +78,11 @@ exit(0); } - -static unsigned long __debug_tick; - -pthread_t PLC_thread; -static pthread_mutex_t python_wait_mutex = PTHREAD_MUTEX_INITIALIZER; -static pthread_mutex_t python_mutex = PTHREAD_MUTEX_INITIALIZER; -static pthread_mutex_t debug_wait_mutex = PTHREAD_MUTEX_INITIALIZER; -static pthread_mutex_t debug_mutex = PTHREAD_MUTEX_INITIALIZER; - -int PLC_shutdown = 0; +void PLCThreadSignalHandler(int sig) +{ + if (sig == SIGUSR2) + pthread_exit(NULL); +} int ForceSaveRetainReq(void) { return PLC_shutdown; @@ -89,30 +91,79 @@ void PLC_thread_proc(void *arg) { while (!PLC_shutdown) { - sem_wait(&Run_PLC); + // Sleep until next PLC run + // TODO check result of clock_nanosleep and wait again or exit eventually + int res = clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next_abs_time, NULL); + if(res==EINTR){ + continue; + } + if(res!=0){ + printf("PLC thread died with error %d \n", res); + return; + } + PLC_GetTime(&__CURRENT_TIME); __run(); + inc_timespec(&next_abs_time, period_ns); } pthread_exit(0); } +#define _LogError(text,...) \ + {\ + char mstr[256];\ + snprintf(mstr, 255, text, ##__VA_ARGS__);\ + LogMessage(LOG_CRITICAL, mstr, strlen(mstr));\ + } #define maxval(a,b) ((a>b)?a:b) int startPLC(int argc,char **argv) { - struct sigevent sigev; - setlocale(LC_NUMERIC, "C"); + + int ret; + pthread_attr_t *pattr = NULL; + +#ifdef REALTIME_LINUX + struct sched_param param; + pthread_attr_t attr; + + /* Lock memory */ + ret = mlockall(MCL_CURRENT|MCL_FUTURE); + if(ret == -1) { + _LogError("mlockall failed: %m\n"); + return ret; + } + + /* Initialize pthread attributes (default values) */ + ret = pthread_attr_init(&attr); + if (ret) { + _LogError("init pthread attributes failed\n"); + return ret; + } + + /* Set scheduler policy and priority of pthread */ + ret = pthread_attr_setschedpolicy(&attr, SCHED_FIFO); + if (ret) { + _LogError("pthread setschedpolicy failed\n"); + return ret; + } + param.sched_priority = PLC_THREAD_PRIORITY; + ret = pthread_attr_setschedparam(&attr, ¶m); + if (ret) { + _LogError("pthread setschedparam failed\n"); + return ret; + } + + /* Use scheduling parameters of attr */ + ret = pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED); + if (ret) { + _LogError("pthread setinheritsched failed\n"); + return ret; + } + + pattr = &attr; +#endif PLC_shutdown = 0; - sem_init(&Run_PLC, 0, 0); - - pthread_create(&PLC_thread, NULL, (void*) &PLC_thread_proc, NULL); - - memset (&sigev, 0, sizeof (struct sigevent)); - sigev.sigev_value.sival_int = 0; - sigev.sigev_notify = SIGEV_THREAD; - sigev.sigev_notify_attributes = NULL; - sigev.sigev_notify_function = PLC_timer_notify; - pthread_mutex_init(&debug_wait_mutex, NULL); pthread_mutex_init(&debug_mutex, NULL); pthread_mutex_init(&python_wait_mutex, NULL); @@ -121,14 +172,26 @@ pthread_mutex_lock(&debug_wait_mutex); pthread_mutex_lock(&python_wait_mutex); - timer_create (CLOCK_MONOTONIC, &sigev, &PLC_timer); - if( __init(argc,argv) == 0 ){ - PLC_SetTimer(common_ticktime__,common_ticktime__); - + if((ret = __init(argc,argv)) == 0 ){ + + /* Signal to wakeup PLC thread when period changes */ + signal(SIGUSR1, PLCThreadSignalHandler); + /* Signal to end PLC thread */ + signal(SIGUSR2, PLCThreadSignalHandler); /* install signal handler for manual break */ signal(SIGINT, catch_signal); + + /* initialize next occurence and period */ + period_ns = common_ticktime__; + clock_gettime(CLOCK_MONOTONIC, &next_abs_time); + + ret = pthread_create(&PLC_thread, pattr, (void*) &PLC_thread_proc, NULL); + if (ret) { + _LogError("create pthread failed\n"); + return ret; + } }else{ - return 1; + return ret; } return 0; } @@ -154,11 +217,9 @@ { /* Stop the PLC */ PLC_shutdown = 1; - sem_post(&Run_PLC); - PLC_SetTimer(0,0); - pthread_join(PLC_thread, NULL); - sem_destroy(&Run_PLC); - timer_delete (PLC_timer); + /* Order PLCThread to exit */ + pthread_kill(PLC_thread, SIGUSR2); + pthread_join(PLC_thread, NULL); __cleanup(); pthread_mutex_destroy(&debug_wait_mutex); pthread_mutex_destroy(&debug_mutex); @@ -196,7 +257,7 @@ /*__DEBUG is protected by this mutex */ __DEBUG = !disable; if (disable) - pthread_mutex_unlock(&debug_mutex); + pthread_mutex_unlock(&debug_mutex); return 0; } @@ -237,6 +298,7 @@ } struct RT_to_nRT_signal_s { + int used; pthread_cond_t WakeCond; pthread_mutex_t WakeCondLock; }; @@ -245,7 +307,7 @@ #define _LogAndReturnNull(text) \ {\ - char mstr[256] = text " for ";\ + char mstr[256] = text " for ";\ strncat(mstr, name, 255);\ LogMessage(LOG_CRITICAL, mstr, strlen(mstr));\ return NULL;\ @@ -254,9 +316,10 @@ void *create_RT_to_nRT_signal(char* name){ RT_to_nRT_signal_t *sig = (RT_to_nRT_signal_t*)malloc(sizeof(RT_to_nRT_signal_t)); - if(!sig) - _LogAndReturnNull("Failed allocating memory for RT_to_nRT signal"); - + if(!sig) + _LogAndReturnNull("Failed allocating memory for RT_to_nRT signal"); + + sig->used = 1; pthread_cond_init(&sig->WakeCond, NULL); pthread_mutex_init(&sig->WakeCondLock, NULL); @@ -266,10 +329,10 @@ void delete_RT_to_nRT_signal(void* handle){ RT_to_nRT_signal_t *sig = (RT_to_nRT_signal_t*)handle; - pthread_cond_destroy(&sig->WakeCond); - pthread_mutex_destroy(&sig->WakeCondLock); - - free(sig); + pthread_mutex_lock(&sig->WakeCondLock); + sig->used = 0; + pthread_cond_signal(&sig->WakeCond); + pthread_mutex_unlock(&sig->WakeCondLock); } int wait_RT_to_nRT_signal(void* handle){ @@ -277,7 +340,14 @@ RT_to_nRT_signal_t *sig = (RT_to_nRT_signal_t*)handle; pthread_mutex_lock(&sig->WakeCondLock); ret = pthread_cond_wait(&sig->WakeCond, &sig->WakeCondLock); + if(!sig->used) ret = -EINVAL; pthread_mutex_unlock(&sig->WakeCondLock); + + if(!sig->used){ + pthread_cond_destroy(&sig->WakeCond); + pthread_mutex_destroy(&sig->WakeCondLock); + free(sig); + } return ret; } diff -r ed1ec3136c2b -r 0c06de0a39b5 targets/Win32/plc_Win32_main.c --- a/targets/Win32/plc_Win32_main.c Mon Feb 27 13:47:36 2023 +0100 +++ b/targets/Win32/plc_Win32_main.c Tue Feb 28 09:39:51 2023 +0100 @@ -82,7 +82,6 @@ { unsigned long thread_id = 0; BOOL tmp; - setlocale(LC_NUMERIC, "C"); debug_sem = CreateSemaphore( NULL, // default security attributes diff -r ed1ec3136c2b -r 0c06de0a39b5 targets/beremiz.h --- a/targets/beremiz.h Mon Feb 27 13:47:36 2023 +0100 +++ b/targets/beremiz.h Tue Feb 28 09:39:51 2023 +0100 @@ -32,4 +32,13 @@ int unblock_RT_to_nRT_signal(void* handle); void nRT_reschedule(void); + +#ifdef REALTIME_LINUX + +#ifndef PLC_THREAD_PRIORITY +#define PLC_THREAD_PRIORITY 80 #endif + +#endif + +#endif diff -r ed1ec3136c2b -r 0c06de0a39b5 targets/plc_debug.c --- a/targets/plc_debug.c Mon Feb 27 13:47:36 2023 +0100 +++ b/targets/plc_debug.c Tue Feb 28 09:39:51 2023 +0100 @@ -270,7 +270,7 @@ default: break; } - force_list_apply_cursor++; \ + force_list_apply_cursor++; } /* Reset buffer cursor */ diff -r ed1ec3136c2b -r 0c06de0a39b5 tests/projects/svghmi/svghmi_0@svghmi/svghmi.svg --- a/tests/projects/svghmi/svghmi_0@svghmi/svghmi.svg Mon Feb 27 13:47:36 2023 +0100 +++ b/tests/projects/svghmi/svghmi_0@svghmi/svghmi.svg Tue Feb 28 09:39:51 2023 +0100 @@ -136,11 +136,11 @@ inkscape:current-layer="hmi0" showgrid="false" units="px" - inkscape:zoom="0.40092403" - inkscape:cx="2333.0807" - inkscape:cy="1015.6842" - inkscape:window-width="1600" - inkscape:window-height="836" + inkscape:zoom="2.2679688" + inkscape:cx="1135.0439" + inkscape:cy="130.37028" + inkscape:window-width="1850" + inkscape:window-height="1036" inkscape:window-x="0" inkscape:window-y="27" inkscape:window-maximized="1" @@ -149,6 +149,33 @@ inkscape:snap-global="true" inkscape:snap-bbox="true" inkscape:bbox-nodes="true" /> + + + declaration of user_level HMI local variable(not a PLC variable) + Back + user Home - + user + user + + + login +