# HG changeset patch # User Edouard Tisserant # Date 1678436009 -3600 # Node ID 65969628e9204e10a1512836612790b4d3b037f0 # Parent ac0e6de439b5d2561c1c6037d9c6026dd541f5f6# Parent 5450dd9e9370f600d3eeb758d95b1d978b4a96a3 merged diff -r 5450dd9e9370 -r 65969628e920 Beremiz_service.py --- a/Beremiz_service.py Tue Mar 07 09:00:33 2023 +0000 +++ b/Beremiz_service.py Fri Mar 10 09:13:29 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 5450dd9e9370 -r 65969628e920 PLCControler.py --- a/PLCControler.py Tue Mar 07 09:00:33 2023 +0000 +++ b/PLCControler.py Fri Mar 10 09:13:29 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 5450dd9e9370 -r 65969628e920 PLCGenerator.py --- a/PLCGenerator.py Tue Mar 07 09:00:33 2023 +0000 +++ b/PLCGenerator.py Fri Mar 10 09:13:29 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 5450dd9e9370 -r 65969628e920 POULibrary.py --- a/POULibrary.py Tue Mar 07 09:00:33 2023 +0000 +++ b/POULibrary.py Fri Mar 10 09:13:29 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 5450dd9e9370 -r 65969628e920 controls/PouInstanceVariablesPanel.py --- a/controls/PouInstanceVariablesPanel.py Tue Mar 07 09:00:33 2023 +0000 +++ b/controls/PouInstanceVariablesPanel.py Fri Mar 10 09:13:29 2023 +0100 @@ -174,6 +174,19 @@ self.DebugInstanceImage: _ButtonCallbacks( self.DebugButtonCallback, self.DebugButtonDClickCallback)} + self.FilterCtrl = wx.SearchCtrl(self) + self.FilterCtrl.ShowCancelButton(True) + self.FilterCtrl.Bind(wx.EVT_TEXT, self.OnFilterUpdate) + self.FilterCtrl.Bind(wx.EVT_SEARCHCTRL_CANCEL_BTN, self.OnFilterCancel) + + searchMenu = wx.Menu() + item = searchMenu.AppendCheckItem(-1, _("Match Case")) + self.Bind(wx.EVT_MENU, self.OnSearchMenu, item) + item = searchMenu.AppendCheckItem(-1, _("Whole Words")) + self.Bind(wx.EVT_MENU, self.OnSearchMenu, item) + self.FilterCtrl.SetMenu(searchMenu) + + buttons_sizer = wx.FlexGridSizer(cols=3, hgap=0, rows=1, vgap=0) buttons_sizer.AddWindow(self.ParentButton) buttons_sizer.AddWindow(self.InstanceChoice, flag=wx.GROW) @@ -181,9 +194,10 @@ buttons_sizer.AddGrowableCol(1) buttons_sizer.AddGrowableRow(0) - main_sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=2, vgap=0) + main_sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=3, vgap=0) main_sizer.AddSizer(buttons_sizer, flag=wx.GROW) main_sizer.AddWindow(self.VariablesList, flag=wx.GROW) + main_sizer.AddWindow(self.FilterCtrl, flag=wx.GROW) main_sizer.AddGrowableCol(0) main_sizer.AddGrowableRow(1) @@ -199,6 +213,11 @@ self.PouInfos = None self.PouInstance = None + self.Filter = None + self.FilterCaseSensitive = False + self.FilterWholeWord = False + + def __del__(self): self.Controller = None @@ -236,6 +255,21 @@ self.RefreshView() + def OnSearchMenu(self, event): + searchMenu = self.FilterCtrl.GetMenu().GetMenuItems() + self.FilterCaseSensitive = searchMenu[0].IsChecked() + self.FilterWholeWord = searchMenu[1].IsChecked() + self.RefreshView() + + def OnFilterUpdate(self, event): + self.Filter = self.FilterCtrl.GetValue() + self.RefreshView() + event.Skip() + + def OnFilterCancel(self, event): + self.FilterCtrl.SetValue('') + event.Skip() + def RefreshView(self): self.Freeze() self.VariablesList.DeleteAllItems() @@ -252,6 +286,15 @@ if self.PouInfos is not None: root = self.VariablesList.AddRoot("", data=self.PouInfos) for var_infos in self.PouInfos.variables: + if self.Filter: + pattern = self.Filter + varname = var_infos.name + if not self.FilterCaseSensitive: + pattern = pattern.upper() + varname = varname.upper() + if ((pattern != varname) if self.FilterWholeWord else + (pattern not in varname)): + continue if var_infos.type is not None: text = "%s (%s)" % (var_infos.name, var_infos.type) else: diff -r 5450dd9e9370 -r 65969628e920 editors/TextViewer.py --- a/editors/TextViewer.py Tue Mar 07 09:00:33 2023 +0000 +++ b/editors/TextViewer.py Fri Mar 10 09:13:29 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 5450dd9e9370 -r 65969628e920 exemples/csv_read/beremiz.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/exemples/csv_read/beremiz.xml Fri Mar 10 09:13:29 2023 +0100 @@ -0,0 +1,5 @@ + + + + + diff -r 5450dd9e9370 -r 65969628e920 exemples/csv_read/plc.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/exemples/csv_read/plc.xml Fri Mar 10 09:13:29 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 5450dd9e9370 -r 65969628e920 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 Fri Mar 10 09:13:29 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 5450dd9e9370 -r 65969628e920 exemples/csv_read/project_files/my_int_csv.ods Binary file exemples/csv_read/project_files/my_int_csv.ods has changed diff -r 5450dd9e9370 -r 65969628e920 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 Fri Mar 10 09:13:29 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 5450dd9e9370 -r 65969628e920 exemples/csv_read/project_files/my_str_csv.ods Binary file exemples/csv_read/project_files/my_str_csv.ods has changed diff -r 5450dd9e9370 -r 65969628e920 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 Fri Mar 10 09:13:29 2023 +0100 @@ -0,0 +1,2 @@ + + diff -r 5450dd9e9370 -r 65969628e920 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 Fri Mar 10 09:13:29 2023 +0100 @@ -0,0 +1,2 @@ + + diff -r 5450dd9e9370 -r 65969628e920 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 Fri Mar 10 09:13:29 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 5450dd9e9370 -r 65969628e920 exemples/svghmi_jumps/svghmi_0@svghmi/svghmi.svg --- a/exemples/svghmi_jumps/svghmi_0@svghmi/svghmi.svg Tue Mar 07 09:00:33 2023 +0000 +++ b/exemples/svghmi_jumps/svghmi_0@svghmi/svghmi.svg Fri Mar 10 09:13:29 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 5450dd9e9370 -r 65969628e920 exemples/svghmi_references/plc.xml --- a/exemples/svghmi_references/plc.xml Tue Mar 07 09:00:33 2023 +0000 +++ b/exemples/svghmi_references/plc.xml Fri Mar 10 09:13:29 2023 +0100 @@ -1,7 +1,7 @@ - + @@ -22,12 +22,12 @@ - + - + - + @@ -39,7 +39,7 @@ - LocalVar0 + PLCHMIVAR @@ -50,7 +50,7 @@ - LocalVar1 + LocalVar0 diff -r 5450dd9e9370 -r 65969628e920 exemples/svghmi_references/svghmi_0@svghmi/svghmi.svg --- a/exemples/svghmi_references/svghmi_0@svghmi/svghmi.svg Tue Mar 07 09:00:33 2023 +0000 +++ b/exemples/svghmi_references/svghmi_0@svghmi/svghmi.svg Fri Mar 10 09:13:29 2023 +0100 @@ -25,7 +25,7 @@ image/svg+xml - + @@ -90,9 +90,9 @@ inkscape:window-height="836" id="namedview4" showgrid="false" - inkscape:zoom="0.92709556" - inkscape:cx="1883.1062" - inkscape:cy="712.84763" + inkscape:zoom="0.23177389" + inkscape:cx="1502.9251" + inkscape:cy="-465.32787" inkscape:window-x="0" inkscape:window-y="27" inkscape:window-maximized="1" @@ -103,7 +103,8 @@ fit-margin-top="0" fit-margin-left="0" fit-margin-right="0" - fit-margin-bottom="0" /> + fit-margin-bottom="0" + inkscape:pagecheckerboard="true" /> + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.99999988;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:4, 4;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> Switch widget - - - - - Home - - - - - Swith - - - - - Buttons - - - - declaration of "position" HMI local variable - - - - - Show popup 1 - - - - Show popup 2 - - - + x="76.354057" + y="466.02609" /> + x="496.35406" + y="466.02609" /> + height="190.44576" + x="254.41968" + y="382.29605" /> Page + x="141.59323" + y="402.47458">Page (inkscape) final position in page offset positionoffset positionfor "B" offset positionfor "C" HMI:Switch@... (group) |-. "A" (group) | |- reference (rect) | |- ... |-. "B" (group) | |- frame (rect) | |- ... |-. "C" (group) | |- frame (rect) | |- ... @@ -774,74 +592,280 @@ sodipodi:role="line" x="317.2059" y="99.850906" - id="tspan1259" - style="text-align:start;text-anchor:start;stroke-width:1px">groups that represent the possible states of the widget.groups that represent the possible states of the widget. Since all groups need to appear in the same place, they overlap and Since all groups need to appear in the same place, they overlap the drawing becomes hard to understand and maintain.and the drawing becomes hard to understand and maintain.Using specially labelled "reference" and "frame" rectangles, Using specially labelled "reference" and "frame" rectangles, groups can be spread out. Theses rectangles can be used groups can be spread out. + style="text-align:start;text-anchor:start;stroke-width:1px" + id="tspan474">in widget or anywhere in the drawing, and do not appear in final result. reference + x="137.59323" + y="560.47461">reference frame frame + x="557.59326" + y="560.47461">frame + Button widgets + HMI:Switch@... (group) |- reference (rect) |-. "A" (group) | |- ... |-. "B" (group) | |- frame (rect) | |- ... |-. "C" (group) | |- frame (rect) | |- ... + or + + + simple + + + + with widgets + + + user choice : %s + + + selected dialog : %s + + Switch and Assign widgets can be used together to simulate behavior modal dialog or "popup" with user feedback."selection" and "userChoice" local HMI are used to respectivelyselect dialog to be shown and store user choice.Here, "reference" and "frame" rectangles are necessary toto spread out dialogs and page, otherwise overlapping. + inkscape:label=""simple""> - - - - Close - - A MODAL DIALOG + sodipodi:role="line">A SIMPLE MODAL DIALOG + + + OK + + + + Cancel + + + + + X + + + inkscape:label=""withWidgets""> - - - - Close - - A MODAL DIALOGA MODAL DIALOGwith widgets + y="1068.5115">with widgets + + + + X + + + + :dialog="None" +:return="Applied" +:plcvar=uservar +@dialog=selection +@return=userChoice +@uservar=.position +@plcvar=/PLCHMIVAR + + Apply + + + + In this example, 3 types of button ar connected to the sameHMI local variable. Here, "reference" and "frame" rectangles are used toseparate active and inactive state of buttons + A + B + C + + + Page (final result) + + + A + + + + B + + + + C + + + + + + + Home + + + + + Swith + + + + + Buttons - + + declaration of user_level HMI local variabledeclaration of "position" HMI local variable + + + declaration of 'selection' local variable + + + declaration of 'userChoice' local variable + + + (not a PLC variable) - - - declaration of "range" HMI local variable + + + declaration of "range" HMI local variable - - - declaration of "size" HMI local variable + + + declaration of "size" HMI local variable - - Button widgets - - declaration of "position" HMI local variable + id="text193">declaration of "position" HMI local variable + diff -r 5450dd9e9370 -r 65969628e920 py_ext/pous.xml --- a/py_ext/pous.xml Tue Mar 07 09:00:33 2023 +0000 +++ b/py_ext/pous.xml Fri Mar 10 09:13:29 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 5450dd9e9370 -r 65969628e920 py_ext/py_ext.py --- a/py_ext/py_ext.py Tue Mar 07 09:00:33 2023 +0000 +++ b/py_ext/py_ext.py Fri Mar 10 09:13:29 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 5450dd9e9370 -r 65969628e920 runtime/NevowServer.py --- a/runtime/NevowServer.py Tue Mar 07 09:00:33 2023 +0000 +++ b/runtime/NevowServer.py Fri Mar 10 09:13:29 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 5450dd9e9370 -r 65969628e920 svghmi/analyse_widget.xslt --- a/svghmi/analyse_widget.xslt Tue Mar 07 09:00:33 2023 +0000 +++ b/svghmi/analyse_widget.xslt Fri Mar 10 09:13:29 2023 +0100 @@ -262,6 +262,42 @@ speed + + + + + + + + Arguments are either: + + + + - 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:Assign:notify=1@notify=/PLCVAR + + HMI:Assign:ack=2:notify=1@ack=.local_var@notify=/PLCVAR + + + + + + Assign variables on click + + @@ -818,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 5450dd9e9370 -r 65969628e920 svghmi/detachable_pages.ysl2 --- a/svghmi/detachable_pages.ysl2 Tue Mar 07 09:00:33 2023 +0000 +++ b/svghmi/detachable_pages.ysl2 Fri Mar 10 09:13:29 2023 +0100 @@ -25,6 +25,17 @@ emit "preamble:default-page" { | | var default_page = "«$default_page»"; + const "screensaverpage", "$hmi_pages_descs[arg[1]/@value = 'ScreenSaver']"; + const "delay" choose { + when "$screensaverpage" { + const "delaystr", "$screensaverpage/arg[2]/@value"; + if "not(regexp:test($delaystr,'^[0-9]+$'))" + error > ScreenSaver page has missing or malformed delay argument. + value "$delaystr"; + } + otherwise > null + } + | var screensaver_delay = «$delay»; } const "keypads_descs", "$parsed_widgets/widget[@type = 'Keypad']"; @@ -126,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 = { @@ -154,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 @@ -167,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 5450dd9e9370 -r 65969628e920 svghmi/gen_index_xhtml.xslt --- a/svghmi/gen_index_xhtml.xslt Tue Mar 07 09:00:33 2023 +0000 +++ b/svghmi/gen_index_xhtml.xslt Fri Mar 10 09:13:29 2023 +0100 @@ -555,6 +555,23 @@ + + + + + + + + + + + + + + + + + @@ -620,6 +637,27 @@ "; + + + + + + + + ScreenSaver page has missing or malformed delay argument. + + + + + + null + + + + var screensaver_delay = + + ; + @@ -657,7 +695,8 @@ - + + @@ -695,6 +734,21 @@ + + + + + /* + + */ + + + + class PageWidget extends Widget{} + + + + @@ -748,7 +802,15 @@ - + + + + Page id=" + + " : only one root path can be declared + + + " @@ -765,31 +827,35 @@ ], - - + + Page id=" " : No match for path " - + " in HMI tree page_index: - + , page_class: " - + ", widgets: [ + [hmi_widgets[" + + "], []], + - + , @@ -890,6 +956,14 @@ + DISCARDABLES: + + + + + + + In Foreach: @@ -945,6 +1019,21 @@ + + + + + + + + + + + + + + + @@ -1128,6 +1217,8 @@ + const xmlns = "http://www.w3.org/2000/svg"; + let id = document.getElementById.bind(document); var svg_root = id(" @@ -1378,7 +1469,7 @@ ,{ - assignments: [], + enable_assignments: [], compute_enable: function(value, oldval, varnum) { @@ -1394,13 +1485,13 @@ if(varnum == - ) this.assignments[ + ) this.enable_assignments[ ] = value; let - = this.assignments[ + = this.enable_assignments[ ]; @@ -1516,8 +1607,6 @@ var cache = hmitree_types.map(_ignored => undefined); - var updates = new Map(); - function page_local_index(varname, pagename){ @@ -1530,7 +1619,7 @@ new_index = next_available_index++; - hmi_locals[pagename] = {[varname]:new_index} + hmi_locals[pagename] = {[varname]:new_index}; } else { @@ -1556,8 +1645,6 @@ cache[new_index] = defaultval; - updates.set(new_index, defaultval); - if(persistent_locals.has(varname)) persistent_indexes.set(new_index, varname); @@ -2073,7 +2160,7 @@ } - + undeafen(index){ @@ -2083,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); } @@ -2330,7 +2421,9 @@ + + @@ -2344,7 +2437,7 @@ var hmi_widgets = { - + } @@ -2656,6 +2749,199 @@ } + + + + + + + + Arguments are either: + + + + - 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:Assign:notify=1@notify=/PLCVAR + + HMI:Assign:ack=2:notify=1@ack=.local_var@notify=/PLCVAR + + + + + + Assign variables on click + + + + class + AssignWidget + extends Widget{ + + frequency = 2; + + + + onmouseup(evt) { + + svg_root.removeEventListener("pointerup", this.bound_onmouseup, true); + + if(this.enable_state) { + + this.activity_state = false + + this.request_animate(); + + this.assign(); + + } + + } + + + + onmousedown(){ + + if(this.enable_state) { + + svg_root.addEventListener("pointerup", this.bound_onmouseup, true); + + this.activity_state = true; + + this.request_animate(); + + } + + } + + + + } + + + + + + + + + /disabled + + + + + + + activable_sub:{ + + + + + + /active /inactive + + + no + + + + + + }, + + has_activity: + + , + + init: function() { + + this.bound_onmouseup = this.onmouseup.bind(this); + + this.element.addEventListener("pointerdown", this.onmousedown.bind(this)); + + }, + + 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( + + , + + ); + + + + + }, + + @@ -2677,9 +2963,17 @@ if(jump_history.length > 1){ - jump_history.pop(); - - let [page_name, index] = jump_history.pop(); + let page_name, index; + + do { + + jump_history.pop(); // forget current page + + if(jump_history.length == 0) return; + + [page_name, index] = jump_history[jump_history.length-1]; + + } while(page_name == "ScreenSaver") // never go back to ScreenSaver switch_page(page_name, index); @@ -2689,7 +2983,7 @@ init() { - this.element.setAttribute("onclick", "hmi_widgets['"+this.element_id+"'].on_click(evt)"); + this.element.onclick = this.on_click.bind(this); } @@ -5298,7 +5592,7 @@ animate: function(){ - this.value_elt.textContent = String(this.display); + multiline_to_svg_text(this.value_elt, String(this.display)); }, @@ -5336,7 +5630,7 @@ - this.value_elt.textContent = ""; + multiline_to_svg_text(this.value_elt, ""); }, @@ -5923,39 +6217,31 @@ frequency = 2; - - - make_on_click() { - - let that = this; - - const name = this.args[0]; - - return function(evt){ - - /* TODO: in order to allow jumps to page selected through - - for exemple a dropdown, support path pointing to local - - variable whom value would be an HMI_TREE index and then - - jump to a relative page not hard-coded in advance - - */ - - if(that.enable_state) { - - const index = - - (that.is_relative && that.indexes.length > 0) ? - - that.indexes[0] + that.offset : undefined; - - fading_page_switch(name, index); - - that.notify(); - - } + target_page_is_current_page = false; + + button_beeing_pressed = false; + + + + onmouseup(evt) { + + svg_root.removeEventListener("pointerup", this.bound_onmouseup, true); + + if(this.enable_state) { + + const index = + + (this.is_relative && this.indexes.length > 0) ? + + this.indexes[0] + this.offset : undefined; + + this.button_beeing_pressed = false; + + this.activity_state = this.target_page_is_current_page || this.button_beeing_pressed; + + fading_page_switch(this.args[0], index); + + this.notify(); } @@ -5963,6 +6249,24 @@ + onmousedown(){ + + if(this.enable_state) { + + svg_root.addEventListener("pointerup", this.bound_onmouseup, true); + + this.button_beeing_pressed = true; + + this.activity_state = true; + + this.request_animate(); + + } + + } + + + notify_page_change(page_name, index) { // called from animate() @@ -5973,7 +6277,9 @@ const ref_name = this.args[0]; - this.activity_state = ((ref_name == undefined || ref_name == page_name) && index == ref_index); + this.target_page_is_current_page = ((ref_name == undefined || ref_name == page_name) && index == ref_index); + + this.activity_state = this.target_page_is_current_page || this.button_beeing_pressed; // Since called from animate, update activity directly @@ -6031,7 +6337,9 @@ init: function() { - this.element.onclick = this.make_on_click(); + this.bound_onmouseup = this.onmouseup.bind(this); + + this.element.addEventListener("pointerdown", this.onmousedown.bind(this)); this.activable = true; @@ -6094,10 +6402,10 @@ - + - + @@ -6922,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( + + , + + ); + + + + + }, + + @@ -8721,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; + + } } @@ -8743,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; } @@ -8841,6 +9273,8 @@ + console.log("dispatch(",value,oldval, index, time); + // naive local buffer impl. // data is updated only when graph is visible @@ -11045,7 +11479,1265 @@ - var ws_url = + const dvgetters = { + + INT: (dv,offset) => [dv.getInt16(offset, true), 2], + + BOOL: (dv,offset) => [dv.getInt8(offset, true), 1], + + NODE: (dv,offset) => [dv.getInt8(offset, true), 1], + + REAL: (dv,offset) => [dv.getFloat32(offset, true), 4], + + STRING: (dv, offset) => { + + const size = dv.getInt8(offset); + + return [ + + String.fromCharCode.apply(null, new Uint8Array( + + dv.buffer, /* original buffer */ + + offset + 1, /* string starts after size*/ + + size /* size of string */ + + )), size + 1]; /* total increment */ + + } + + }; + + + + // Called on requestAnimationFrame, modifies DOM + + var requestAnimationFrameID = null; + + function animate() { + + let rearm = true; + + do{ + + if(page_fading == "pending" || page_fading == "forced"){ + + if(page_fading == "pending") + + svg_root.classList.add("fade-out-page"); + + page_fading = "in_progress"; + + if(page_fading_args.length) + + setTimeout(function(){ + + switch_page(...page_fading_args); + + },1); + + break; + + } + + + + // Do the page swith if pending + + if(page_switch_in_progress){ + + if(current_subscribed_page != current_visible_page){ + + switch_visible_page(current_subscribed_page); + + } + + + + page_switch_in_progress = false; + + + + if(page_fading == "in_progress"){ + + svg_root.classList.remove("fade-out-page"); + + page_fading = "off"; + + } + + } + + + + if(jumps_need_update) update_jumps(); + + + + + + pending_widget_animates.forEach(widget => widget._animate()); + + pending_widget_animates = []; + + rearm = false; + + } while(0); + + + + requestAnimationFrameID = null; + + + + if(rearm) requestHMIAnimation(); + + } + + + + function requestHMIAnimation() { + + if(requestAnimationFrameID == null){ + + requestAnimationFrameID = window.requestAnimationFrame(animate); + + } + + } + + + + // Message reception handler + + // Hash is verified and HMI values updates resulting from binary parsing + + // are stored until browser can compute next frame, DOM is left untouched + + function ws_onmessage(evt) { + + + + let data = evt.data; + + let dv = new DataView(data); + + let i = 0; + + try { + + for(let hash_int of hmi_hash) { + + if(hash_int != dv.getUint8(i)){ + + throw new Error("Hash doesn't match"); + + }; + + i++; + + }; + + + + while(i < data.byteLength){ + + let index = dv.getUint32(i, true); + + i += 4; + + let iectype = hmitree_types[index]; + + if(iectype != undefined){ + + let dvgetter = dvgetters[iectype]; + + let [value, bytesize] = dvgetter(dv,i); + + dispatch_value(index, value); + + i += bytesize; + + } else { + + throw new Error("Unknown index "+index); + + } + + }; + + + + // register for rendering on next frame, since there are updates + + } catch(err) { + + // 1003 is for "Unsupported Data" + + // ws.close(1003, err.message); + + + + // TODO : remove debug alert ? + + alert("Error : "+err.message+"\nHMI will be reloaded."); + + + + // force reload ignoring cache + + location.reload(true); + + } + + }; + + + + hmi_hash_u8 = new Uint8Array(hmi_hash); + + + + var ws = null; + + + + function send_blob(data) { + + if(ws && data.length > 0) { + + ws.send(new Blob([hmi_hash_u8].concat(data))); + + }; + + }; + + + + const typedarray_types = { + + INT: (number) => new Int16Array([number]), + + BOOL: (truth) => new Int8Array([truth]), + + NODE: (truth) => new Int8Array([truth]), + + REAL: (number) => new Float32Array([number]), + + STRING: (str) => { + + // beremiz default string max size is 128 + + str = str.slice(0,128); + + binary = new Uint8Array(str.length + 1); + + binary[0] = str.length; + + for(let i = 0; i < str.length; i++){ + + binary[i+1] = str.charCodeAt(i); + + } + + return binary; + + } + + /* TODO */ + + }; + + + + function send_reset() { + + send_blob(new Uint8Array([1])); /* reset = 1 */ + + }; + + + + var subscriptions = []; + + + + function subscribers(index) { + + let entry = subscriptions[index]; + + let res; + + if(entry == undefined){ + + res = new Set(); + + subscriptions[index] = [res,0]; + + }else{ + + [res, _ign] = entry; + + } + + return res + + } + + + + function get_subscription_period(index) { + + let entry = subscriptions[index]; + + if(entry == undefined) + + return 0; + + let [_ign, period] = entry; + + return period; + + } + + + + function set_subscription_period(index, period) { + + let entry = subscriptions[index]; + + if(entry == undefined){ + + subscriptions[index] = [new Set(), period]; + + } else { + + entry[1] = period; + + } + + } + + + + function reset_subscription_periods() { + + for(let index in subscriptions) + + subscriptions[index][1] = 0; + + } + + + + if(has_watchdog){ + + // artificially subscribe the watchdog widget to "/heartbeat" hmi variable + + // Since dispatch directly calls change_hmi_value, + + // PLC will periodically send variable at given frequency + + subscribers(heartbeat_index).add({ + + /* type: "Watchdog", */ + + frequency: 1, + + indexes: [heartbeat_index], + + new_hmi_value: function(index, value, oldval) { + + apply_hmi_value(heartbeat_index, value+1); + + } + + }); + + } + + + + + + var page_fading = "off"; + + var page_fading_args = "off"; + + function fading_page_switch(...args){ + + if(page_fading == "in_progress") + + page_fading = "forced"; + + else + + page_fading = "pending"; + + page_fading_args = args; + + + + requestHMIAnimation(); + + + + } + + document.body.style.backgroundColor = "black"; + + + + // subscribe to per instance current page hmi variable + + // PLC must prefix page name with "!" for page switch to happen + + subscribers(current_page_var_index).add({ + + frequency: 1, + + indexes: [current_page_var_index], + + new_hmi_value: function(index, value, oldval) { + + if(value.startsWith("!")) + + fading_page_switch(value.slice(1)); + + } + + }); + + + + function svg_text_to_multiline(elt) { + + return(Array.prototype.map.call(elt.children, x=>x.textContent).join("\n")); + + } + + + + function multiline_to_svg_text(elt, str, blank) { + + str.split('\n').map((line,i) => {elt.children[i].textContent = blank?"":line;}); + + } + + + + function switch_langnum(langnum) { + + langnum = Math.max(0, Math.min(langs.length - 1, langnum)); + + + + for (let translation of translations) { + + let [objs, msgs] = translation; + + let msg = msgs[langnum]; + + for (let obj of objs) { + + multiline_to_svg_text(obj, msg); + + obj.setAttribute("lang",langnum); + + } + + } + + return langnum; + + } + + + + // backup original texts + + for (let translation of translations) { + + let [objs, msgs] = translation; + + msgs.unshift(svg_text_to_multiline(objs[0])); + + } + + + + var lang_local_index = hmi_local_index("lang"); + + var langcode_local_index = hmi_local_index("lang_code"); + + var langname_local_index = hmi_local_index("lang_name"); + + subscribers(lang_local_index).add({ + + indexes: [lang_local_index], + + new_hmi_value: function(index, value, oldval) { + + let current_lang = switch_langnum(value); + + let [langname,langcode] = langs[current_lang]; + + apply_hmi_value(langcode_local_index, langcode); + + apply_hmi_value(langname_local_index, langname); + + switch_page(); + + } + + }); + + + + // returns en_US, fr_FR or en_UK depending on selected language + + function get_current_lang_code(){ + + return cache[langcode_local_index]; + + } + + + + function setup_lang(){ + + let current_lang = cache[lang_local_index]; + + let new_lang = switch_langnum(current_lang); + + if(current_lang != new_lang){ + + apply_hmi_value(lang_local_index, new_lang); + + } + + } + + + + setup_lang(); + + + + function update_subscriptions() { + + let delta = []; + + if(!ws) + + // dont' change subscriptions if not connected + + return; + + + + for(let index in subscriptions){ + + let widgets = subscribers(index); + + + + // periods are in ms + + let previous_period = get_subscription_period(index); + + + + // subscribing with a zero period is unsubscribing + + let new_period = 0; + + if(widgets.size > 0) { + + let maxfreq = 0; + + for(let widget of widgets){ + + let wf = widget.frequency; + + if(wf != undefined && maxfreq < wf) + + maxfreq = wf; + + } + + + + if(maxfreq != 0) + + new_period = 1000/maxfreq; + + } + + + + if(previous_period != new_period) { + + set_subscription_period(index, new_period); + + if(index <= last_remote_index){ + + delta.push( + + new Uint8Array([2]), /* subscribe = 2 */ + + new Uint32Array([index]), + + new Uint16Array([new_period])); + + } + + } + + } + + send_blob(delta); + + }; + + + + function send_hmi_value(index, value) { + + if(index > last_remote_index){ + + dispatch_value(index, value); + + + + if(persistent_indexes.has(index)){ + + let varname = persistent_indexes.get(index); + + document.cookie = varname+"="+value+"; max-age=3153600000"; + + } + + + + return; + + } + + + + let iectype = hmitree_types[index]; + + let tobinary = typedarray_types[iectype]; + + send_blob([ + + new Uint8Array([0]), /* setval = 0 */ + + new Uint32Array([index]), + + tobinary(value)]); + + + + // DON'T DO THAT unless read_iterator in svghmi.c modifies wbuf as well, not only rbuf + + // cache[index] = value; + + }; + + + + function apply_hmi_value(index, new_val) { + + // Similarly to previous comment, taking decision to update based + + // on cache content is bad and can lead to inconsistency + + /*let old_val = cache[index];*/ + + if(new_val != undefined /*&& old_val != new_val*/) + + send_hmi_value(index, new_val); + + return new_val; + + } + + + + const quotes = {"'":null, '"':null}; + + + + function eval_operation_string(old_val, opstr) { + + let op = opstr[0]; + + let given_val; + + if(opstr.length < 2) + + return undefined; + + if(opstr[1] in quotes){ + + if(opstr.length < 3) + + return undefined; + + if(opstr[opstr.length-1] == opstr[1]){ + + given_val = opstr.slice(2,opstr.length-1); + + } + + } else { + + given_val = Number(opstr.slice(1)); + + } + + let new_val; + + switch(op){ + + case "=": + + new_val = given_val; + + break; + + case "+": + + new_val = old_val + given_val; + + break; + + case "-": + + new_val = old_val - given_val; + + break; + + case "*": + + new_val = old_val * given_val; + + break; + + case "/": + + new_val = old_val / given_val; + + break; + + } + + return new_val; + + } + + + + var current_visible_page; + + var current_subscribed_page; + + var current_page_index; + + var page_node_local_index = hmi_local_index("page_node"); + + var page_switch_in_progress = false; + + + + function toggleFullscreen() { + + let elem = document.documentElement; + + + + if (!document.fullscreenElement) { + + elem.requestFullscreen().catch(err => { + + console.log("Error attempting to enable full-screen mode: "+err.message+" ("+err.name+")"); + + }); + + } else { + + document.exitFullscreen(); + + } + + } + + + + // prevents context menu from appearing on right click and long touch + + document.body.addEventListener('contextmenu', e => { + + toggleFullscreen(); + + e.preventDefault(); + + }); + + + + 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); + + } + + document.body.addEventListener('pointerdown', reset_screensaver_timer); + + // initialize screensaver + + reset_screensaver_timer(); + + } + + + + + + function detach_detachables() { + + + + for(let eltid in detachable_elements){ + + let [element,parent] = detachable_elements[eltid]; + + parent.removeChild(element); + + } + + }; + + + + function switch_page(page_name, page_index) { + + if(page_switch_in_progress){ + + /* page switch already going */ + + /* TODO LOG ERROR */ + + return false; + + } + + page_switch_in_progress = true; + + + + if(page_name == undefined) + + page_name = current_subscribed_page; + + else if(page_index == undefined){ + + [page_name, page_index] = page_name.split('@') + + } + + + + let old_desc = page_desc[current_subscribed_page]; + + let new_desc = page_desc[page_name]; + + + + if(new_desc == undefined){ + + /* TODO LOG ERROR */ + + return false; + + } + + + + if(page_index == undefined) + + page_index = new_desc.page_index; + + else if(typeof(page_index) == "string") { + + let hmitree_node = hmitree_nodes[page_index]; + + if(hmitree_node !== undefined){ + + let [int_index, hmiclass] = hmitree_node; + + if(hmiclass == new_desc.page_class) + + page_index = int_index; + + else + + page_index = new_desc.page_index; + + } else { + + page_index = new_desc.page_index; + + } + + } + + + + if(old_desc){ + + old_desc.widgets.map(([widget,relativeness])=>widget.unsub()); + + } + + const new_offset = page_index == undefined ? 0 : page_index - new_desc.page_index; + + + + const container_id = page_name + (page_index != undefined ? page_index : ""); + + + + new_desc.widgets.map(([widget,relativeness])=>widget.sub(new_offset,relativeness,container_id)); + + + + update_subscriptions(); + + + + current_subscribed_page = page_name; + + current_page_index = page_index; + + let page_node; + + if(page_index != undefined){ + + page_node = hmitree_paths[page_index]; + + }else{ + + page_node = ""; + + } + + apply_hmi_value(page_node_local_index, page_node); + + + + jumps_need_update = true; + + + + requestHMIAnimation(); + + let [last_page_name, last_page_index] = jump_history[jump_history.length-1]; + + if(last_page_name != page_name || last_page_index != page_index){ + + jump_history.push([page_name, page_index]); + + if(jump_history.length > 42) + + jump_history.shift(); + + } + + + + apply_hmi_value(current_page_var_index, page_index == undefined + + ? page_name + + : page_name + "@" + hmitree_paths[page_index]); + + + + // when entering a page, assignments are evaluated + + new_desc.widgets[0][0].assign(); + + + + return true; + + }; + + + + function switch_visible_page(page_name) { + + + + let old_desc = page_desc[current_visible_page]; + + let new_desc = page_desc[page_name]; + + + + if(old_desc){ + + for(let eltid in old_desc.required_detachables){ + + if(!(eltid in new_desc.required_detachables)){ + + let [element, parent] = old_desc.required_detachables[eltid]; + + parent.removeChild(element); + + } + + } + + for(let eltid in new_desc.required_detachables){ + + if(!(eltid in old_desc.required_detachables)){ + + let [element, parent] = new_desc.required_detachables[eltid]; + + parent.appendChild(element); + + } + + } + + }else{ + + for(let eltid in new_desc.required_detachables){ + + let [element, parent] = new_desc.required_detachables[eltid]; + + parent.appendChild(element); + + } + + } + + + + svg_root.setAttribute('viewBox',new_desc.bbox.join(" ")); + + current_visible_page = page_name; + + }; + + + + /* From https://jsfiddle.net/ibowankenobi/1mmh7rs6/6/ */ + + function getAbsoluteCTM(element){ + + var height = svg_root.height.baseVal.value, + + width = svg_root.width.baseVal.value, + + viewBoxRect = svg_root.viewBox.baseVal, + + vHeight = viewBoxRect.height, + + vWidth = viewBoxRect.width; + + if(!vWidth || !vHeight){ + + return element.getCTM(); + + } + + var sH = height/vHeight, + + sW = width/vWidth, + + matrix = svg_root.createSVGMatrix(); + + matrix.a = sW; + + matrix.d = sH + + var realCTM = element.getCTM().multiply(matrix.inverse()); + + realCTM.e = realCTM.e/sW + viewBoxRect.x; + + realCTM.f = realCTM.f/sH + viewBoxRect.y; + + return realCTM; + + } + + + + function apply_reference_frames(){ + + const matches = svg_root.querySelectorAll("g[svghmi_x_offset]"); + + matches.forEach((group) => { + + let [x,y] = ["x", "y"].map((axis) => Number(group.getAttribute("svghmi_"+axis+"_offset"))); + + let ctm = getAbsoluteCTM(group); + + // zero translation part of CTM + + // to only apply rotation/skewing to offset vector + + ctm.e = 0; + + ctm.f = 0; + + let invctm = ctm.inverse(); + + let vect = new DOMPoint(x, y); + + let newvect = vect.matrixTransform(invctm); + + let transform = svg_root.createSVGTransform(); + + transform.setTranslate(newvect.x, newvect.y); + + group.transform.baseVal.appendItem(transform); + + ["x", "y"].forEach((axis) => group.removeAttribute("svghmi_"+axis+"_offset")); + + }); + + } + + + + // prepare SVG + + apply_reference_frames(); + + init_widgets(); + + detach_detachables(); + + + + // show main page + + switch_page(default_page); + + + + var reconnect_delay = 0; + + var periodic_reconnect_timer; + + var force_reconnect = false; + + + + // Once connection established + + function ws_onopen(evt) { + + // Work around memory leak with websocket on QtWebEngine + + // reconnect every hour to force deallocate websocket garbage + + if(window.navigator.userAgent.includes("QtWebEngine")){ + + if(periodic_reconnect_timer){ + + window.clearTimeout(periodic_reconnect_timer); + + } + + periodic_reconnect_timer = window.setTimeout(() => { + + force_reconnect = true; + + ws.close(); + + periodic_reconnect_timer = null; + + }, 3600000); + + } + + + + // forget earlier subscriptions locally + + reset_subscription_periods(); + + + + // update PLC about subscriptions and current page + + switch_page(); + + + + // at first try reconnect immediately + + reconnect_delay = 1; + + }; + + + + function ws_onclose(evt) { + + console.log("Connection closed. code:"+evt.code+" reason:"+evt.reason+" wasClean:"+evt.wasClean+" Reload in "+reconnect_delay+"ms."); + + ws = null; + + // 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 = window.location.href.replace(/^http(s?:\/\/[^\/]*)\/.*$/, 'ws$1/ws') @@ -11053,1071 +12745,25 @@ - var ws = new WebSocket(ws_url); - - ws.binaryType = 'arraybuffer'; - - - - const dvgetters = { - - INT: (dv,offset) => [dv.getInt16(offset, true), 2], - - BOOL: (dv,offset) => [dv.getInt8(offset, true), 1], - - NODE: (dv,offset) => [dv.getInt8(offset, true), 1], - - REAL: (dv,offset) => [dv.getFloat32(offset, true), 4], - - STRING: (dv, offset) => { - - const size = dv.getInt8(offset); - - return [ - - String.fromCharCode.apply(null, new Uint8Array( - - dv.buffer, /* original buffer */ - - offset + 1, /* string starts after size*/ - - size /* size of string */ - - )), size + 1]; /* total increment */ - - } - - }; - - - - // Apply updates recieved through ws.onmessage to subscribed widgets - - function apply_updates() { - - updates.forEach((value, index) => { - - dispatch_value(index, value); - - }); - - updates.clear(); + function create_ws(){ + + ws = new WebSocket(ws_url); + + ws.binaryType = 'arraybuffer'; + + ws.onmessage = ws_onmessage; + + ws.onclose = ws_onclose; + + ws.onopen = ws_onopen; } - // Called on requestAnimationFrame, modifies DOM - - var requestAnimationFrameID = null; - - function animate() { - - let rearm = true; - - do{ - - if(page_fading == "pending" || page_fading == "forced"){ - - if(page_fading == "pending") - - svg_root.classList.add("fade-out-page"); - - page_fading = "in_progress"; - - if(page_fading_args.length) - - setTimeout(function(){ - - switch_page(...page_fading_args); - - },1); - - break; - - } - - - - // Do the page swith if pending - - if(page_switch_in_progress){ - - if(current_subscribed_page != current_visible_page){ - - switch_visible_page(current_subscribed_page); - - } - - - - page_switch_in_progress = false; - - - - if(page_fading == "in_progress"){ - - svg_root.classList.remove("fade-out-page"); - - page_fading = "off"; - - } - - } - - - - if(jumps_need_update) update_jumps(); - - - - - - pending_widget_animates.forEach(widget => widget._animate()); - - pending_widget_animates = []; - - rearm = false; - - } while(0); - - - - requestAnimationFrameID = null; - - - - if(rearm) requestHMIAnimation(); - - } - - - - function requestHMIAnimation() { - - if(requestAnimationFrameID == null){ - - requestAnimationFrameID = window.requestAnimationFrame(animate); - - } - - } - - - - // Message reception handler - - // Hash is verified and HMI values updates resulting from binary parsing - - // are stored until browser can compute next frame, DOM is left untouched - - ws.onmessage = function (evt) { - - - - let data = evt.data; - - let dv = new DataView(data); - - let i = 0; - - try { - - for(let hash_int of hmi_hash) { - - if(hash_int != dv.getUint8(i)){ - - throw new Error("Hash doesn't match"); - - }; - - i++; - - }; - - - - while(i < data.byteLength){ - - let index = dv.getUint32(i, true); - - i += 4; - - let iectype = hmitree_types[index]; - - if(iectype != undefined){ - - let dvgetter = dvgetters[iectype]; - - let [value, bytesize] = dvgetter(dv,i); - - updates.set(index, value); - - i += bytesize; - - } else { - - throw new Error("Unknown index "+index); - - } - - }; - - - - apply_updates(); - - // register for rendering on next frame, since there are updates - - } catch(err) { - - // 1003 is for "Unsupported Data" - - // ws.close(1003, err.message); - - - - // TODO : remove debug alert ? - - alert("Error : "+err.message+"\nHMI will be reloaded."); - - - - // force reload ignoring cache - - location.reload(true); - - } - - }; - - - - hmi_hash_u8 = new Uint8Array(hmi_hash); - - - - function send_blob(data) { - - if(data.length > 0) { - - ws.send(new Blob([hmi_hash_u8].concat(data))); - - }; - - }; - - - - const typedarray_types = { - - INT: (number) => new Int16Array([number]), - - BOOL: (truth) => new Int16Array([truth]), - - NODE: (truth) => new Int16Array([truth]), - - REAL: (number) => new Float32Array([number]), - - STRING: (str) => { - - // beremiz default string max size is 128 - - str = str.slice(0,128); - - binary = new Uint8Array(str.length + 1); - - binary[0] = str.length; - - for(let i = 0; i < str.length; i++){ - - binary[i+1] = str.charCodeAt(i); - - } - - return binary; - - } - - /* TODO */ - - }; - - - - function send_reset() { - - send_blob(new Uint8Array([1])); /* reset = 1 */ - - }; - - - - var subscriptions = []; - - - - function subscribers(index) { - - let entry = subscriptions[index]; - - let res; - - if(entry == undefined){ - - res = new Set(); - - subscriptions[index] = [res,0]; - - }else{ - - [res, _ign] = entry; - - } - - return res - - } - - - - function get_subscription_period(index) { - - let entry = subscriptions[index]; - - if(entry == undefined) - - return 0; - - let [_ign, period] = entry; - - return period; - - } - - - - function set_subscription_period(index, period) { - - let entry = subscriptions[index]; - - if(entry == undefined){ - - subscriptions[index] = [new Set(), period]; - - } else { - - entry[1] = period; - - } - - } - - - - if(has_watchdog){ - - // artificially subscribe the watchdog widget to "/heartbeat" hmi variable - - // Since dispatch directly calls change_hmi_value, - - // PLC will periodically send variable at given frequency - - subscribers(heartbeat_index).add({ - - /* type: "Watchdog", */ - - frequency: 1, - - indexes: [heartbeat_index], - - new_hmi_value: function(index, value, oldval) { - - apply_hmi_value(heartbeat_index, value+1); - - } - - }); - - } - - - - - - var page_fading = "off"; - - var page_fading_args = "off"; - - function fading_page_switch(...args){ - - if(page_fading == "in_progress") - - page_fading = "forced"; - - else - - page_fading = "pending"; - - page_fading_args = args; - - - - requestHMIAnimation(); - - - - } - - document.body.style.backgroundColor = "black"; - - - - // subscribe to per instance current page hmi variable - - // PLC must prefix page name with "!" for page switch to happen - - subscribers(current_page_var_index).add({ - - frequency: 1, - - indexes: [current_page_var_index], - - new_hmi_value: function(index, value, oldval) { - - if(value.startsWith("!")) - - fading_page_switch(value.slice(1)); - - } - - }); - - - - function svg_text_to_multiline(elt) { - - return(Array.prototype.map.call(elt.children, x=>x.textContent).join("\n")); - - } - - - - function multiline_to_svg_text(elt, str, blank) { - - str.split('\n').map((line,i) => {elt.children[i].textContent = blank?"":line;}); - - } - - - - function switch_langnum(langnum) { - - langnum = Math.max(0, Math.min(langs.length - 1, langnum)); - - - - for (let translation of translations) { - - let [objs, msgs] = translation; - - let msg = msgs[langnum]; - - for (let obj of objs) { - - multiline_to_svg_text(obj, msg); - - obj.setAttribute("lang",langnum); - - } - - } - - return langnum; - - } - - - - // backup original texts - - for (let translation of translations) { - - let [objs, msgs] = translation; - - msgs.unshift(svg_text_to_multiline(objs[0])); - - } - - - - var lang_local_index = hmi_local_index("lang"); - - var langcode_local_index = hmi_local_index("lang_code"); - - var langname_local_index = hmi_local_index("lang_name"); - - subscribers(lang_local_index).add({ - - indexes: [lang_local_index], - - new_hmi_value: function(index, value, oldval) { - - let current_lang = switch_langnum(value); - - let [langname,langcode] = langs[current_lang]; - - apply_hmi_value(langcode_local_index, langcode); - - apply_hmi_value(langname_local_index, langname); - - switch_page(); - - } - - }); - - - - // returns en_US, fr_FR or en_UK depending on selected language - - function get_current_lang_code(){ - - return cache[langcode_local_index]; - - } - - - - function setup_lang(){ - - let current_lang = cache[lang_local_index]; - - let new_lang = switch_langnum(current_lang); - - if(current_lang != new_lang){ - - apply_hmi_value(lang_local_index, new_lang); - - } - - } - - - - setup_lang(); - - - - function update_subscriptions() { - - let delta = []; - - for(let index in subscriptions){ - - let widgets = subscribers(index); - - - - // periods are in ms - - let previous_period = get_subscription_period(index); - - - - // subscribing with a zero period is unsubscribing - - let new_period = 0; - - if(widgets.size > 0) { - - let maxfreq = 0; - - for(let widget of widgets){ - - let wf = widget.frequency; - - if(wf != undefined && maxfreq < wf) - - maxfreq = wf; - - } - - - - if(maxfreq != 0) - - new_period = 1000/maxfreq; - - } - - - - if(previous_period != new_period) { - - set_subscription_period(index, new_period); - - if(index <= last_remote_index){ - - delta.push( - - new Uint8Array([2]), /* subscribe = 2 */ - - new Uint32Array([index]), - - new Uint16Array([new_period])); - - } - - } - - } - - send_blob(delta); - - }; - - - - function send_hmi_value(index, value) { - - if(index > last_remote_index){ - - dispatch_value(index, value); - - - - if(persistent_indexes.has(index)){ - - let varname = persistent_indexes.get(index); - - document.cookie = varname+"="+value+"; max-age=3153600000"; - - } - - - - return; - - } - - - - let iectype = hmitree_types[index]; - - let tobinary = typedarray_types[iectype]; - - send_blob([ - - new Uint8Array([0]), /* setval = 0 */ - - new Uint32Array([index]), - - tobinary(value)]); - - - - // DON'T DO THAT unless read_iterator in svghmi.c modifies wbuf as well, not only rbuf - - // cache[index] = value; - - }; - - - - function apply_hmi_value(index, new_val) { - - // Similarly to previous comment, taking decision to update based - - // on cache content is bad and can lead to inconsistency - - /*let old_val = cache[index];*/ - - if(new_val != undefined /*&& old_val != new_val*/) - - send_hmi_value(index, new_val); - - return new_val; - - } - - - - const quotes = {"'":null, '"':null}; - - - - function eval_operation_string(old_val, opstr) { - - let op = opstr[0]; - - let given_val; - - if(opstr.length < 2) - - return undefined; - - if(opstr[1] in quotes){ - - if(opstr.length < 3) - - return undefined; - - if(opstr[opstr.length-1] == opstr[1]){ - - given_val = opstr.slice(2,opstr.length-1); - - } - - } else { - - given_val = Number(opstr.slice(1)); - - } - - let new_val; - - switch(op){ - - case "=": - - new_val = given_val; - - break; - - case "+": - - new_val = old_val + given_val; - - break; - - case "-": - - new_val = old_val - given_val; - - break; - - case "*": - - new_val = old_val * given_val; - - break; - - case "/": - - new_val = old_val / given_val; - - break; - - } - - return new_val; - - } - - - - var current_visible_page; - - var current_subscribed_page; - - var current_page_index; - - var page_node_local_index = hmi_local_index("page_node"); - - var page_switch_in_progress = false; - - - - function toggleFullscreen() { - - let elem = document.documentElement; - - - - if (!document.fullscreenElement) { - - elem.requestFullscreen().catch(err => { - - console.log("Error attempting to enable full-screen mode: "+err.message+" ("+err.name+")"); - - }); - - } else { - - document.exitFullscreen(); - - } - - } - - - - function prepare_svg() { - - // prevents context menu from appearing on right click and long touch - - document.body.addEventListener('contextmenu', e => { - - toggleFullscreen(); - - e.preventDefault(); - - }); - - - - for(let eltid in detachable_elements){ - - let [element,parent] = detachable_elements[eltid]; - - parent.removeChild(element); - - } - - }; - - - - function switch_page(page_name, page_index) { - - if(page_switch_in_progress){ - - /* page switch already going */ - - /* TODO LOG ERROR */ - - return false; - - } - - page_switch_in_progress = true; - - - - if(page_name == undefined) - - page_name = current_subscribed_page; - - else if(page_index == undefined){ - - [page_name, page_index] = page_name.split('@') - - } - - - - let old_desc = page_desc[current_subscribed_page]; - - let new_desc = page_desc[page_name]; - - - - if(new_desc == undefined){ - - /* TODO LOG ERROR */ - - return false; - - } - - - - if(page_index == undefined) - - page_index = new_desc.page_index; - - else if(typeof(page_index) == "string") { - - let hmitree_node = hmitree_nodes[page_index]; - - if(hmitree_node !== undefined){ - - let [int_index, hmiclass] = hmitree_node; - - if(hmiclass == new_desc.page_class) - - page_index = int_index; - - else - - page_index = new_desc.page_index; - - } else { - - page_index = new_desc.page_index; - - } - - } - - - - if(old_desc){ - - old_desc.widgets.map(([widget,relativeness])=>widget.unsub()); - - } - - const new_offset = page_index == undefined ? 0 : page_index - new_desc.page_index; - - - - const container_id = page_name + (page_index != undefined ? page_index : ""); - - - - new_desc.widgets.map(([widget,relativeness])=>widget.sub(new_offset,relativeness,container_id)); - - - - update_subscriptions(); - - - - current_subscribed_page = page_name; - - current_page_index = page_index; - - let page_node; - - if(page_index != undefined){ - - page_node = hmitree_paths[page_index]; - - }else{ - - page_node = ""; - - } - - apply_hmi_value(page_node_local_index, page_node); - - - - jumps_need_update = true; - - - - requestHMIAnimation(); - - jump_history.push([page_name, page_index]); - - if(jump_history.length > 42) - - jump_history.shift(); - - - - apply_hmi_value(current_page_var_index, page_index == undefined - - ? page_name - - : page_name + "@" + hmitree_paths[page_index]); - - - - return true; - - }; - - - - function switch_visible_page(page_name) { - - - - let old_desc = page_desc[current_visible_page]; - - let new_desc = page_desc[page_name]; - - - - if(old_desc){ - - for(let eltid in old_desc.required_detachables){ - - if(!(eltid in new_desc.required_detachables)){ - - let [element, parent] = old_desc.required_detachables[eltid]; - - parent.removeChild(element); - - } - - } - - for(let eltid in new_desc.required_detachables){ - - if(!(eltid in old_desc.required_detachables)){ - - let [element, parent] = new_desc.required_detachables[eltid]; - - parent.appendChild(element); - - } - - } - - }else{ - - for(let eltid in new_desc.required_detachables){ - - let [element, parent] = new_desc.required_detachables[eltid]; - - parent.appendChild(element); - - } - - } - - - - svg_root.setAttribute('viewBox',new_desc.bbox.join(" ")); - - current_visible_page = page_name; - - }; - - - - // Once connection established - - ws.onopen = function (evt) { - - init_widgets(); - - send_reset(); - - // show main page - - prepare_svg(); - - switch_page(default_page); - - }; - - - - ws.onclose = function (evt) { - - // TODO : add visible notification while waiting for reload - - console.log("Connection closed. code:"+evt.code+" reason:"+evt.reason+" wasClean:"+evt.wasClean+" Reload in 10s."); - - // TODO : re-enable auto reload when not in debug - - //window.setTimeout(() => location.reload(true), 10000); - - alert("Connection closed. code:"+evt.code+" reason:"+evt.reason+" wasClean:"+evt.wasClean+"."); - - - - }; - - - - const xmlns = "http://www.w3.org/2000/svg"; + create_ws() + + var edit_callback; diff -r 5450dd9e9370 -r 65969628e920 svghmi/inline_svg.ysl2 --- a/svghmi/inline_svg.ysl2 Tue Mar 07 09:00:33 2023 +0000 +++ b/svghmi/inline_svg.ysl2 Fri Mar 10 09:13:29 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 5450dd9e9370 -r 65969628e920 svghmi/svghmi.js --- a/svghmi/svghmi.js Tue Mar 07 09:00:33 2023 +0000 +++ b/svghmi/svghmi.js Fri Mar 10 09:13:29 2023 +0100 @@ -23,13 +23,6 @@ // Open WebSocket to relative "/ws" address var has_watchdog = window.location.hash == "#watchdog"; -var ws_url = - window.location.href.replace(/^http(s?:\/\/[^\/]*)\/.*$/, 'ws$1/ws') - + '?mode=' + (has_watchdog ? "watchdog" : "multiclient"); - -var ws = new WebSocket(ws_url); -ws.binaryType = 'arraybuffer'; - const dvgetters = { INT: (dv,offset) => [dv.getInt16(offset, true), 2], BOOL: (dv,offset) => [dv.getInt8(offset, true), 1], @@ -98,7 +91,7 @@ // Message reception handler // Hash is verified and HMI values updates resulting from binary parsing // are stored until browser can compute next frame, DOM is left untouched -ws.onmessage = function (evt) { +function ws_onmessage(evt) { let data = evt.data; let dv = new DataView(data); @@ -140,16 +133,18 @@ hmi_hash_u8 = new Uint8Array(hmi_hash); +var ws = null; + function send_blob(data) { - if(data.length > 0) { + if(ws && data.length > 0) { ws.send(new Blob([hmi_hash_u8].concat(data))); }; }; const typedarray_types = { INT: (number) => new Int16Array([number]), - BOOL: (truth) => new Int16Array([truth]), - NODE: (truth) => new Int16Array([truth]), + BOOL: (truth) => new Int8Array([truth]), + NODE: (truth) => new Int8Array([truth]), REAL: (number) => new Float32Array([number]), STRING: (str) => { // beremiz default string max size is 128 @@ -199,6 +194,11 @@ } } +function reset_subscription_periods() { + for(let index in subscriptions) + subscriptions[index][1] = 0; +} + if(has_watchdog){ // artificially subscribe the watchdog widget to "/heartbeat" hmi variable // Since dispatch directly calls change_hmi_value, @@ -298,6 +298,10 @@ function update_subscriptions() { let delta = []; + if(!ws) + // dont' change subscriptions if not connected + return; + for(let index in subscriptions){ let widgets = subscribers(index); @@ -418,12 +422,30 @@ } } -function prepare_svg() { - // prevents context menu from appearing on right click and long touch - document.body.addEventListener('contextmenu', e => { - toggleFullscreen(); - e.preventDefault(); - }); +// prevents context menu from appearing on right click and long touch +document.body.addEventListener('contextmenu', e => { + toggleFullscreen(); + e.preventDefault(); +}); + +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); + } + document.body.addEventListener('pointerdown', reset_screensaver_timer); + // initialize screensaver + reset_screensaver_timer(); +} + + +function detach_detachables() { for(let eltid in detachable_elements){ let [element,parent] = detachable_elements[eltid]; @@ -492,14 +514,20 @@ jumps_need_update = true; requestHMIAnimation(); - jump_history.push([page_name, page_index]); - if(jump_history.length > 42) - jump_history.shift(); + let [last_page_name, last_page_index] = jump_history[jump_history.length-1]; + if(last_page_name != page_name || last_page_index != page_index){ + jump_history.push([page_name, page_index]); + if(jump_history.length > 42) + jump_history.shift(); + } apply_hmi_value(current_page_var_index, page_index == undefined ? page_name : page_name + "@" + hmitree_paths[page_index]); + // when entering a page, assignments are evaluated + new_desc.widgets[0][0].assign(); + return true; }; @@ -572,26 +600,72 @@ }); } +// prepare SVG +apply_reference_frames(); +init_widgets(); +detach_detachables(); + +// show main page +switch_page(default_page); + +var reconnect_delay = 0; +var periodic_reconnect_timer; +var force_reconnect = false; + // Once connection established -ws.onopen = function (evt) { - apply_reference_frames(); - init_widgets(); - send_reset(); - // show main page - prepare_svg(); - switch_page(default_page); -}; - -ws.onclose = function (evt) { - // TODO : add visible notification while waiting for reload - console.log("Connection closed. code:"+evt.code+" reason:"+evt.reason+" wasClean:"+evt.wasClean+" Reload in 10s."); - // TODO : re-enable auto reload when not in debug - //window.setTimeout(() => location.reload(true), 10000); - alert("Connection closed. code:"+evt.code+" reason:"+evt.reason+" wasClean:"+evt.wasClean+"."); - -}; - -const xmlns = "http://www.w3.org/2000/svg"; +function ws_onopen(evt) { + // Work around memory leak with websocket on QtWebEngine + // reconnect every hour to force deallocate websocket garbage + if(window.navigator.userAgent.includes("QtWebEngine")){ + if(periodic_reconnect_timer){ + window.clearTimeout(periodic_reconnect_timer); + } + periodic_reconnect_timer = window.setTimeout(() => { + force_reconnect = true; + ws.close(); + periodic_reconnect_timer = null; + }, 3600000); + } + + // forget earlier subscriptions locally + reset_subscription_periods(); + + // update PLC about subscriptions and current page + switch_page(); + + // at first try reconnect immediately + reconnect_delay = 1; +}; + +function ws_onclose(evt) { + console.log("Connection closed. code:"+evt.code+" reason:"+evt.reason+" wasClean:"+evt.wasClean+" Reload in "+reconnect_delay+"ms."); + ws = null; + // 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 = + window.location.href.replace(/^http(s?:\/\/[^\/]*)\/.*$/, 'ws$1/ws') + + '?mode=' + (has_watchdog ? "watchdog" : "multiclient"); + +function create_ws(){ + ws = new WebSocket(ws_url); + ws.binaryType = 'arraybuffer'; + ws.onmessage = ws_onmessage; + ws.onclose = ws_onclose; + ws.onopen = ws_onopen; +} + +create_ws() + var edit_callback; const localtypes = {"PAGE_LOCAL":null, "HMI_LOCAL":null} function edit_value(path, valuetype, callback, initial) { diff -r 5450dd9e9370 -r 65969628e920 svghmi/svghmi.py --- a/svghmi/svghmi.py Tue Mar 07 09:00:33 2023 +0000 +++ b/svghmi/svghmi.py Fri Mar 10 09:13:29 2023 +0100 @@ -636,19 +636,31 @@ svghmi_cmds[thing] = ( "Popen(" + repr(shlex.split(given_command.format(**svghmi_options))) + - ")") if given_command else "pass # no command given" + ")") if given_command else "None # no command given" runtimefile_path = os.path.join(buildpath, "runtime_%s_svghmi_.py" % location_str) runtimefile = open(runtimefile_path, 'w') runtimefile.write(""" -# TODO : multiple watchdog (one for each svghmi instance) +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# generated by beremiz/svghmi/svghmi.py + +browser_proc = None + def svghmi_{location}_watchdog_trigger(): - {svghmi_cmds[Watchdog]} + global browser_proc + 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") + browser_proc = {svghmi_cmds[Start]} max_svghmi_sessions = {maxConnections_total} def _runtime_{location}_svghmi_start(): - global svghmi_watchdog, svghmi_servers + global svghmi_watchdog, svghmi_servers, browser_proc srv = svghmi_servers.get("{interface}:{port}", None) if srv is not None: @@ -673,7 +685,7 @@ path_list.append("{path}") - {svghmi_cmds[Start]} + browser_proc = {svghmi_cmds[Start]} if {enable_watchdog}: if svghmi_watchdog is None: @@ -686,7 +698,7 @@ def _runtime_{location}_svghmi_stop(): - global svghmi_watchdog, svghmi_servers + global svghmi_watchdog, svghmi_servers, browser_proc if svghmi_watchdog is not None: svghmi_watchdog.cancel() @@ -702,7 +714,10 @@ svghmi_listener.stopListening() svghmi_servers.pop("{interface}:{port}") - {svghmi_cmds[Stop]} + stop_proc = {svghmi_cmds[Stop]} + waitpid_timeout(stop_proc, "SVGHMI stop command") + waitpid_timeout(browser_proc, "SVGHMI browser process") + browser_proc = None """.format(location=location_str, xhtml=target_fname, diff -r 5450dd9e9370 -r 65969628e920 svghmi/svghmi_server.py --- a/svghmi/svghmi_server.py Tue Mar 07 09:00:33 2023 +0000 +++ b/svghmi/svghmi_server.py Fri Mar 10 09:13:29 2023 +0100 @@ -8,6 +8,7 @@ from __future__ import absolute_import import errno from threading import RLock, Timer +import os, time try: from runtime.spawn_subprocess import Popen @@ -23,6 +24,9 @@ from autobahn.websocket.protocol import WebSocketProtocol from autobahn.twisted.resource import WebSocketResource +from runtime.loglevels import LogLevelsDict +from runtime import GetPLCObjectSingleton + max_svghmi_sessions = None svghmi_watchdog = None @@ -219,6 +223,7 @@ _hmi_session = HMISession(self) registered = svghmi_session_manager.register(_hmi_session) self._hmi_session = _hmi_session + self._hmi_session.reset() def onClose(self, wasClean, code, reason): global svghmi_session_manager @@ -299,3 +304,21 @@ render_HEAD = render_GET +def waitpid_timeout(proc, helpstr="", timeout = 3): + if proc is None: + return + def waitpid_timeout_loop(pid=proc.pid, timeout = timeout): + try: + while os.waitpid(pid,os.WNOHANG) == (0,0): + time.sleep(1) + timeout = timeout - 1 + if not timeout: + GetPLCObjectSingleton().LogMessage( + LogLevelsDict["WARNING"], + "Timeout waiting for {} PID: {}".format(helpstr, str(pid))) + break + except OSError: + # workaround exception "OSError: [Errno 10] No child processes" + pass + Thread(target=waitpid_timeout_loop, name="Zombie hunter").start() + diff -r 5450dd9e9370 -r 65969628e920 svghmi/widget_assign.ysl2 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/svghmi/widget_assign.ysl2 Fri Mar 10 09:13:29 2023 +0100 @@ -0,0 +1,88 @@ +// widget_assign.ysl2 + +widget_desc("Assign") { + longdesc + || + + Arguments are either: + + - 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:Assign:notify=1@notify=/PLCVAR + HMI:Assign:ack=2:notify=1@ack=.local_var@notify=/PLCVAR + + || + + shortdesc > Assign variables on click + +} + +widget_class("Assign") { +|| + frequency = 2; + + onmouseup(evt) { + svg_root.removeEventListener("pointerup", this.bound_onmouseup, true); + if(this.enable_state) { + this.activity_state = false + this.request_animate(); + this.assign(); + } + } + + onmousedown(){ + if(this.enable_state) { + svg_root.addEventListener("pointerup", this.bound_onmouseup, true); + this.activity_state = true; + this.request_animate(); + } + } + +|| +} + +widget_defs("Assign") { + optional_activable(); + + | init: function() { + | this.bound_onmouseup = this.onmouseup.bind(this); + | this.element.addEventListener("pointerdown", this.onmousedown.bind(this)); + | }, + + | 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 5450dd9e9370 -r 65969628e920 svghmi/widget_back.ysl2 --- a/svghmi/widget_back.ysl2 Tue Mar 07 09:00:33 2023 +0000 +++ b/svghmi/widget_back.ysl2 Fri Mar 10 09:13:29 2023 +0100 @@ -9,17 +9,20 @@ shortdesc > Jump to previous page } -// TODO: use es6 widget_class("Back") || on_click(evt) { if(jump_history.length > 1){ - jump_history.pop(); - let [page_name, index] = jump_history.pop(); + let page_name, index; + do { + jump_history.pop(); // forget current page + if(jump_history.length == 0) return; + [page_name, index] = jump_history[jump_history.length-1]; + } while(page_name == "ScreenSaver") // never go back to ScreenSaver switch_page(page_name, index); } } init() { - this.element.setAttribute("onclick", "hmi_widgets['"+this.element_id+"'].on_click(evt)"); + this.element.onclick = this.on_click.bind(this); } || diff -r 5450dd9e9370 -r 65969628e920 svghmi/widget_input.ysl2 --- a/svghmi/widget_input.ysl2 Tue Mar 07 09:00:33 2023 +0000 +++ b/svghmi/widget_input.ysl2 Fri Mar 10 09:13:29 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 5450dd9e9370 -r 65969628e920 svghmi/widget_jump.ysl2 --- a/svghmi/widget_jump.ysl2 Tue Mar 07 09:00:33 2023 +0000 +++ b/svghmi/widget_jump.ysl2 Fri Mar 10 09:13:29 2023 +0100 @@ -52,23 +52,28 @@ || activable = false; frequency = 2; + target_page_is_current_page = false; + button_beeing_pressed = false; - make_on_click() { - let that = this; - const name = this.args[0]; - return function(evt){ - /* TODO: in order to allow jumps to page selected through - for exemple a dropdown, support path pointing to local - variable whom value would be an HMI_TREE index and then - jump to a relative page not hard-coded in advance - */ - if(that.enable_state) { - const index = - (that.is_relative && that.indexes.length > 0) ? - that.indexes[0] + that.offset : undefined; - fading_page_switch(name, index); - that.notify(); - } + onmouseup(evt) { + svg_root.removeEventListener("pointerup", this.bound_onmouseup, true); + if(this.enable_state) { + const index = + (this.is_relative && this.indexes.length > 0) ? + this.indexes[0] + this.offset : undefined; + this.button_beeing_pressed = false; + this.activity_state = this.target_page_is_current_page || this.button_beeing_pressed; + fading_page_switch(this.args[0], index); + this.notify(); + } + } + + onmousedown(){ + if(this.enable_state) { + svg_root.addEventListener("pointerup", this.bound_onmouseup, true); + this.button_beeing_pressed = true; + this.activity_state = true; + this.request_animate(); } } @@ -77,7 +82,8 @@ if(this.activable) { const ref_index = this.indexes.length > 0 ? this.indexes[0] + this.offset : undefined; const ref_name = this.args[0]; - this.activity_state = ((ref_name == undefined || ref_name == page_name) && index == ref_index); + this.target_page_is_current_page = ((ref_name == undefined || ref_name == page_name) && index == ref_index); + this.activity_state = this.target_page_is_current_page || this.button_beeing_pressed; // Since called from animate, update activity directly if(this.enable_displayed_state && this.has_activity) { this.animate_activity(); @@ -98,7 +104,8 @@ const "jump_disability","$has_activity and $has_disability"; | init: function() { - | this.element.onclick = this.make_on_click(); + | this.bound_onmouseup = this.onmouseup.bind(this); + | this.element.addEventListener("pointerdown", this.onmousedown.bind(this)); if "$has_activity" { | this.activable = true; } @@ -136,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 5450dd9e9370 -r 65969628e920 svghmi/widget_page.ysl2 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/svghmi/widget_page.ysl2 Fri Mar 10 09:13:29 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 5450dd9e9370 -r 65969628e920 svghmi/widget_xygraph.ysl2 --- a/svghmi/widget_xygraph.ysl2 Tue Mar 07 09:00:33 2023 +0000 +++ b/svghmi/widget_xygraph.ysl2 Fri Mar 10 09:13:29 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 5450dd9e9370 -r 65969628e920 svghmi/widgetlib/bool_indicator.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/svghmi/widgetlib/bool_indicator.svg Fri Mar 10 09:13:29 2023 +0100 @@ -0,0 +1,82 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + diff -r 5450dd9e9370 -r 65969628e920 svghmi/widgetlib/simple_text_display.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/svghmi/widgetlib/simple_text_display.svg Fri Mar 10 09:13:29 2023 +0100 @@ -0,0 +1,64 @@ + + + + + + + + + + image/svg+xml + + + + + + value + diff -r 5450dd9e9370 -r 65969628e920 svghmi/widgets_common.ysl2 --- a/svghmi/widgets_common.ysl2 Tue Mar 07 09:00:33 2023 +0000 +++ b/svghmi/widgets_common.ysl2 Fri Mar 10 09:13:29 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 5450dd9e9370 -r 65969628e920 targets/Linux/XSD --- a/targets/Linux/XSD Tue Mar 07 09:00:33 2023 +0000 +++ b/targets/Linux/XSD Fri Mar 10 09:13:29 2023 +0100 @@ -1,6 +1,7 @@ + %(toolchain_gcc)s - \ No newline at end of file + diff -r 5450dd9e9370 -r 65969628e920 targets/Linux/__init__.py --- a/targets/Linux/__init__.py Tue Mar 07 09:00:33 2023 +0000 +++ b/targets/Linux/__init__.py Fri Mar 10 09:13:29 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 5450dd9e9370 -r 65969628e920 targets/Linux/plc_Linux_main.c --- a/targets/Linux/plc_Linux_main.c Tue Mar 07 09:00:33 2023 +0000 +++ b/targets/Linux/plc_Linux_main.c Fri Mar 10 09:13:29 2023 +0100 @@ -7,11 +7,33 @@ #include #include #include +#include #include #include #include - -static sem_t Run_PLC; +#ifdef REALTIME_LINUX +#include +#endif + +#define _Log(level,text,...) \ + {\ + char mstr[256];\ + snprintf(mstr, 255, text, ##__VA_ARGS__);\ + LogMessage(LOG_CRITICAL, mstr, strlen(mstr));\ + } + +#define _LogError(text,...) _Log(LOG_CRITICAL, text, ##__VA_ARGS__) +#define _LogWarning(text,...) _Log(LOG_WARNING, text, ##__VA_ARGS__) + +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 +52,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_cycle_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_cycle_time); + inc_timespec(&next_cycle_time, next); + // interrupt clock_nanpsleep + pthread_kill(PLC_thread, SIGUSR1); +} + void catch_signal(int sig) { // signal(SIGTERM, catch_signal); @@ -71,48 +88,139 @@ 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; } +#define MAX_JITTER period_ns/10 +#define MIN_IDLE_TIME_NS 1000000 /* 1ms */ +/* Macro to compare timespec, evaluate to True if a is past b */ +#define timespec_gt(a,b) (a.tv_sec > b.tv_sec || (a.tv_sec == b.tv_sec && a.tv_nsec > b.tv_nsec)) void PLC_thread_proc(void *arg) { + /* initialize next occurence and period */ + period_ns = common_ticktime__; + clock_gettime(CLOCK_MONOTONIC, &next_cycle_time); + while (!PLC_shutdown) { - sem_wait(&Run_PLC); + int res; + struct timespec plc_end_time; + int periods = 0; +#ifdef REALTIME_LINUX + struct timespec deadline_time; + struct timespec plc_start_time; +#endif + + // Sleep until next PLC run + res = clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next_cycle_time, NULL); + if(res==EINTR){ + continue; + } + if(res!=0){ + _LogError("PLC thread timer returned error %d \n", res); + return; + } + +#ifdef REALTIME_LINUX + // timer overrun detection + clock_gettime(CLOCK_MONOTONIC, &plc_start_time); + deadline_time=next_cycle_time; + inc_timespec(&deadline_time, MAX_JITTER); + if(timespec_gt(plc_start_time, deadline_time)){ + _LogWarning("PLC thread woken up too late. PLC cyclic task interval is too small.\n"); + } +#endif + + PLC_GetTime(&__CURRENT_TIME); __run(); - } + + // ensure next PLC cycle occurence is in the future + clock_gettime(CLOCK_MONOTONIC, &plc_end_time); + while(timespec_gt(plc_end_time, next_cycle_time)){ + periods += 1; + inc_timespec(&next_cycle_time, period_ns); + } + + // plc execution time overrun detection + if(periods > 1) { + // Mitigate CPU hogging, in case of too small cyclic task interval: + // - since cycle deadline already missed, better keep system responsive + // - test if next cycle occurs after minimal idle + // - enforce minimum idle time if not + + struct timespec earliest_possible_time = plc_end_time; + inc_timespec(&earliest_possible_time, MIN_IDLE_TIME_NS); + while(timespec_gt(earliest_possible_time, next_cycle_time)){ + periods += 1; + inc_timespec(&next_cycle_time, period_ns); + } + + // increment tick count anyhow, so that task scheduling keeps consistent + __tick+=periods-1; + + _LogWarning("PLC execution time is longer than requested PLC cyclic task interval. %d cycles skipped\n", periods); + } + } + pthread_exit(0); } #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 +229,22 @@ 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); + + 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 +270,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 +310,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 +351,7 @@ } struct RT_to_nRT_signal_s { + int used; pthread_cond_t WakeCond; pthread_mutex_t WakeCondLock; }; @@ -245,7 +360,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 +369,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 +382,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 +393,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 5450dd9e9370 -r 65969628e920 targets/Win32/plc_Win32_main.c --- a/targets/Win32/plc_Win32_main.c Tue Mar 07 09:00:33 2023 +0000 +++ b/targets/Win32/plc_Win32_main.c Fri Mar 10 09:13:29 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 5450dd9e9370 -r 65969628e920 targets/beremiz.h --- a/targets/beremiz.h Tue Mar 07 09:00:33 2023 +0000 +++ b/targets/beremiz.h Fri Mar 10 09:13:29 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 5450dd9e9370 -r 65969628e920 targets/plc_debug.c --- a/targets/plc_debug.c Tue Mar 07 09:00:33 2023 +0000 +++ b/targets/plc_debug.c Fri Mar 10 09:13:29 2023 +0100 @@ -154,8 +154,6 @@ UnpackVar(dsc, &value_p, NULL, &size); - printf("Reminding %%d %%ld \n", retain_list_collect_cursor, size); - /* if buffer not full */ Remind(retain_offset, size, value_p); /* increment cursor according size*/ @@ -211,8 +209,6 @@ retain_list_collect_cursor++; } - printf("Retain size %%d \n", retain_size); - return retain_size; } @@ -274,7 +270,7 @@ default: break; } - force_list_apply_cursor++; \ + force_list_apply_cursor++; } /* Reset buffer cursor */ diff -r 5450dd9e9370 -r 65969628e920 tests/projects/svghmi/svghmi_0@svghmi/svghmi.svg --- a/tests/projects/svghmi/svghmi_0@svghmi/svghmi.svg Tue Mar 07 09:00:33 2023 +0000 +++ b/tests/projects/svghmi/svghmi_0@svghmi/svghmi.svg Fri Mar 10 09:13:29 2023 +0100 @@ -136,11 +136,11 @@ inkscape:current-layer="hmi0" showgrid="false" units="px" - inkscape:zoom="0.40092403" - inkscape:cx="323.58553" - inkscape:cy="-56.756946" - 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) + Home + + + Back + + user Home + + + + + + + + + + + + Leave ScreenSaver + + + + + + + Home + + + + + + + + Settings + + + + + + + + Pump 0 + + + + + + + + Pump 1 + + + + + + + + Pump 2 + + + + + + + + Pump 3 + + + user + user + + + login +