merge MQTT grafted from python2 branch - untested
--- a/.gitignore Thu Aug 22 12:16:45 2024 +0200
+++ b/.gitignore Sat Sep 07 12:50:57 2024 +0200
@@ -14,4 +14,6 @@
\ No newline at end of file
--- a/C_runtime/PLCObject.cpp Thu Aug 22 12:16:45 2024 +0200
+++ b/C_runtime/PLCObject.cpp Sat Sep 07 12:50:57 2024 +0200
@@ -344,6 +344,9 @@
uint32_t PLCObject::LoadPLC(void)
+ // TODO use PLCLibMutex // Load the last transferred PLC md5 hex digest
--- a/C_runtime/PLCObject.hpp Thu Aug 22 12:16:45 2024 +0200
+++ b/C_runtime/PLCObject.hpp Sat Sep 07 12:50:57 2024 +0200
@@ -71,7 +71,10 @@
uint32_t SetTraceVariablesList(const list_trace_order_1_t * orders, int32_t * debugtoken);
uint32_t StopPLC(bool * success);
+ // Public interface used by runtime + uint32_t LogMessage(uint8_t level, std::string message); // A map of all the blobs
@@ -112,10 +115,9 @@
uint32_t BlobAsFile(const binary_t * BlobID, std::filesystem::path filename);
uint32_t UnLoadPLC(void);
- uint32_t LogMessage(uint8_t level, std::string message);
void PurgeTraceBuffer(void);
void TraceThreadProc(void);
\ No newline at end of file
--- a/CodeFileTreeNode.py Thu Aug 22 12:16:45 2024 +0200
+++ b/CodeFileTreeNode.py Sat Sep 07 12:50:57 2024 +0200
@@ -107,7 +107,7 @@
filepath = self.CodeFileName()
if os.path.isfile(filepath):
- xmlfile = open(filepath, 'r')
+ xmlfile = open(filepath, 'r', encoding='utf-8', errors='backslashreplace') codefile_xml = xmlfile.read()
--- a/IDEFrame.py Thu Aug 22 12:16:45 2024 +0200
+++ b/IDEFrame.py Sat Sep 07 12:50:57 2024 +0200
@@ -1116,7 +1116,7 @@
printout2 = GraphicPrintout(window, page_size, margins, True)
preview = wx.PrintPreview(printout, printout2, data)
preview_frame = wx.PreviewFrame(preview, self, _("Print preview"), style=wx.DEFAULT_FRAME_STYLE | wx.FRAME_FLOAT_ON_PARENT)
preview_frame.Initialize()
@@ -2599,7 +2599,7 @@
self.PageSize = page_size
if self.PageSize[0] == 0 or self.PageSize[1] == 0:
self.PageSize = (1050, 1485)
+ self.IsPreview = lambda *_x : preview @@ -2620,9 +2620,9 @@
def OnBeginDocument(self, startPage, endPage):
- if not self.Preview and isinstance(dc, wx.PostScriptDC):
+ if not self.IsPreview() and isinstance(dc, wx.PostScriptDC): - super(GraphicPrintout, self).OnBeginDocument(startPage, endPage)
+ return super(GraphicPrintout, self).OnBeginDocument(startPage, endPage) def OnPrintPage(self, page):
@@ -2630,21 +2630,21 @@
dc.SetUserScale(1.0, 1.0)
- dc.printing = not self.Preview
+ dc.printing = not self.IsPreview() # Get the size of the DC in pixels
ppiPrinterX, ppiPrinterY = self.GetPPIPrinter()
pw, ph = self.GetPageSizePixels()
- dw, dh = dc.GetSizeTuple()
+ dw, dh = dc.GetSize().Get() Xscale = (dw * ppiPrinterX) / (pw * 25.4)
Yscale = (dh * ppiPrinterY) / (ph * 25.4)
- fontsize = self.FontSize * Yscale
+ fontsize = round(self.FontSize * Yscale) - margin_left = self.Margins[0].x * Xscale
- margin_top = self.Margins[0].y * Yscale
- area_width = dw - self.Margins[1].x * Xscale - margin_left
- area_height = dh - self.Margins[1].y * Yscale - margin_top
+ margin_left = round(self.Margins[0].x * Xscale) + margin_top = round(self.Margins[0].y * Yscale) + area_width = dw - round(self.Margins[1].x * Xscale) - margin_left + area_height = dh - round(self.Margins[1].y * Yscale) - margin_top dc.SetPen(MiterPen(wx.BLACK))
dc.SetBrush(wx.TRANSPARENT_BRUSH)
@@ -2667,7 +2667,7 @@
# Set the scale and origin
dc.SetDeviceOrigin(-posX + margin_left, -posY + margin_top)
- dc.SetClippingRegion(posX, posY, self.PageSize[0] * scale, self.PageSize[1] * scale)
+ dc.SetClippingRegion(posX, posY, round(self.PageSize[0] * scale), round(self.PageSize[1] * scale)) dc.SetUserScale(scale, scale)
self.Viewer.DoDrawing(dc, True)
--- a/LocalRuntimeMixin.py Thu Aug 22 12:16:45 2024 +0200
+++ b/LocalRuntimeMixin.py Sat Sep 07 12:50:57 2024 +0200
@@ -51,7 +51,10 @@
self.local_runtime.kill(gently=False)
- shutil.rmtree(self.local_runtime_tmpdir)
+ shutil.rmtree(self.local_runtime_tmpdir) self.local_runtime = None
--- a/PLCControler.py Thu Aug 22 12:16:45 2024 +0200
+++ b/PLCControler.py Sat Sep 07 12:50:57 2024 +0200
@@ -1373,7 +1373,8 @@
def IsEndType(self, typename):
- if typename is not None:
+ # Check if the type is a base type + if type(typename) == str: return not typename.startswith("ANY")
--- a/ProjectController.py Thu Aug 22 12:16:45 2024 +0200
+++ b/ProjectController.py Sat Sep 07 12:50:57 2024 +0200
@@ -508,6 +508,9 @@
self._setBuildPath(BuildPath)
# get confnodes bloclist (is that usefull at project creation?)
self.RefreshConfNodesBlockLists()
+ for iec_lang in ["FBD", "LD", "SFC"]: + PLCControler.SetProjectProperties(self, properties={"scaling": {iec_lang: (8, 8)}}) # this will create files base XML files
--- a/bacnet/BacnetSlaveEditor.py Thu Aug 22 12:16:45 2024 +0200
+++ b/bacnet/BacnetSlaveEditor.py Sat Sep 07 12:50:57 2024 +0200
@@ -828,7 +828,7 @@
# use only to enable drag'n'drop
# self.VariablesGrid.SetDropTarget(VariableDropTarget(self))
- wx.grid.EVT_GRID_CELL_CHANGING, self.OnVariablesGridCellChange)
+ wx.grid.EVT_GRID_CELL_CHANGED, self.OnVariablesGridCellChange) # self.VariablesGrid.Bind(wx.grid.EVT_GRID_CELL_LEFT_CLICK, self.OnVariablesGridCellLeftClick)
# self.VariablesGrid.Bind(wx.grid.EVT_GRID_EDITOR_SHOWN, self.OnVariablesGridEditorShown)
self.MainSizer.Add(self.VariablesGrid, flag=wx.GROW)
--- a/controls/LocationCellEditor.py Thu Aug 22 12:16:45 2024 +0200
+++ b/controls/LocationCellEditor.py Sat Sep 07 12:50:57 2024 +0200
@@ -188,9 +188,7 @@
var_type = self.CellControl.GetVarType()
self.Table.SetValueByName(row, 'Type', var_type)
- wx.CallAfter(self.Table.Parent.ShowErrorMessage,
- _("Selected location is identical to previous one"))
self.CellControl.Disable()
--- a/controls/SearchResultPanel.py Thu Aug 22 12:16:45 2024 +0200
+++ b/controls/SearchResultPanel.py Sat Sep 07 12:50:57 2024 +0200
@@ -172,7 +172,7 @@
search_results_tree_children = search_results_tree_infos.setdefault("children", [])
for tagname in self.ElementsOrder:
@@ -291,7 +291,7 @@
end_idx = reduce(lambda x, y: x + y, [len(x) + 1 for x in text_lines[:end[0] - start[0]]], end[1] + 1)
style = wx.TextAttr(wx.BLACK, wx.Colour(206, 204, 247))
- elif infos["type"] is not None and infos["matches"] > 1:
+ elif infos["type"] is not None and infos["matches"] > 0: text = _("(%d matches)") % infos["matches"]
start_idx, end_idx = 0, len(text)
style = wx.TextAttr(wx.Colour(0, 127, 174))
--- a/dialogs/ActionBlockDialog.py Thu Aug 22 12:16:45 2024 +0200
+++ b/dialogs/ActionBlockDialog.py Sat Sep 07 12:50:57 2024 +0200
@@ -148,7 +148,7 @@
self.ActionsGrid = CustomGrid(self, size=wx.Size(-1, 250), style=wx.VSCROLL)
self.ActionsGrid.DisableDragGridSize()
self.ActionsGrid.EnableScrolling(False, True)
- self.ActionsGrid.Bind(wx.grid.EVT_GRID_CELL_CHANGING,
+ self.ActionsGrid.Bind(wx.grid.EVT_GRID_CELL_CHANGED, self.OnActionsGridCellChange)
main_sizer.Add(self.ActionsGrid, border=20,
flag=wx.GROW | wx.LEFT | wx.RIGHT)
--- a/dialogs/FBDVariableDialog.py Thu Aug 22 12:16:45 2024 +0200
+++ b/dialogs/FBDVariableDialog.py Sat Sep 07 12:50:57 2024 +0200
@@ -155,10 +155,8 @@
# Get variable expression and select corresponding value in name list
selected = self.Expression.GetValue()
- if selected != "" and self.VariableName.FindString(selected) != wx.NOT_FOUND:
- self.VariableName.SetStringSelection(selected)
- self.VariableName.SetSelection(wx.NOT_FOUND)
+ self.VariableName.SetSelection( + wx.NOT_FOUND if selected == "" else self.VariableName.FindString(selected, True)) # Disable name list box if no name present inside
self.VariableName.Enable(self.VariableName.GetCount() > 0)
@@ -185,10 +183,7 @@
# Set expression text control value
self.Expression.ChangeValue(value)
# Select corresponding text in name list box if it exists
- if self.VariableName.FindString(value) != wx.NOT_FOUND:
- self.VariableName.SetStringSelection(value)
- self.VariableName.SetSelection(wx.NOT_FOUND)
+ self.VariableName.SetSelection(self.VariableName.FindString(value, True)) # Parameter is variable execution order
elif name == "executionOrder":
@@ -265,7 +260,7 @@
# Select the corresponding value in name list box if it exists
self.VariableName.SetSelection(
- self.VariableName.FindString(self.Expression.GetValue()))
+ self.VariableName.FindString(self.Expression.GetValue(), True)) --- a/editors/TextViewer.py Thu Aug 22 12:16:45 2024 +0200
+++ b/editors/TextViewer.py Sat Sep 07 12:50:57 2024 +0200
@@ -877,13 +877,15 @@
lineText = self.Editor.GetTextRange(start_pos, end_pos).replace("\t", " ")
- if key == wx.WXK_SPACE and event.ControlDown():
+ if key == wx.WXK_SPACE and event.RawControlDown(): words = lineText.split(" ")
words = [word for i, word in enumerate(words) if word != '' or i == len(words) - 1]
+ self.RefreshVariableTree() if self.TextSyntax == "IL":
@@ -898,13 +900,17 @@
kw = self.Keywords + list(self.Variables.keys()) + list(self.Functions.keys())
- kw = [keyword for keyword in kw if keyword.startswith(words[-1])]
+ kw = [keyword for keyword in kw if keyword.startswith(words[-1].upper())] self.Editor.AutoCompSetIgnoreCase(True)
self.Editor.AutoCompShow(len(words[-1]), " ".join(kw))
elif key == wx.WXK_RETURN or key == wx.WXK_NUMPAD_ENTER:
- if self.TextSyntax in ["ST", "ALL"]:
+ if self.Editor.AutoCompActive(): + self.Editor.AutoCompComplete() + elif self.TextSyntax in ["ST", "ALL"]: indent = self.Editor.GetLineIndentation(line)
if LineStartswith(lineText.strip(), self.BlockStartKeywords):
indent = (indent // 2 + 1) * 2
--- a/editors/Viewer.py Thu Aug 22 12:16:45 2024 +0200
+++ b/editors/Viewer.py Sat Sep 07 12:50:57 2024 +0200
@@ -1542,15 +1542,18 @@
# -------------------------------------------------------------------------------
- def GetForceVariableMenuFunction(self, iec_path, element):
- iec_type = self.GetDataType(iec_path)
- def ForceVariableFunction(event):
- if iec_type is not None:
- dialog = ForceVariableDialog(self.ParentWindow, iec_type, str(element.GetValue()))
- if dialog.ShowModal() == wx.ID_OK:
- self.ParentWindow.AddDebugVariable(iec_path)
- self.ForceDataValue(iec_path, dialog.GetValue())
+ def GetForceVariableMenuFunction(self, iec_path, iec_type, value, immediate = False): + def ForceVariableFunction(event, value=value): + # use value as default value in dialog + dialog = ForceVariableDialog(self.ParentWindow, iec_type, str(value)) + if dialog.ShowModal() != wx.ID_OK: + value = dialog.GetValue() + self.ParentWindow.AddDebugVariable(iec_path) + self.ForceDataValue(iec_path, value) return ForceVariableFunction
def GetReleaseVariableMenuFunction(self, iec_path):
@@ -1570,13 +1573,39 @@
def PopupForceMenu(self):
iec_path = self.GetElementIECPath(self.SelectedElement)
+ # GetElementIECPath() does not work for variables and coils + # In such case get the IEC path using the instance path + for ElementType in [FBD_Variable, LD_Coil]: + if isinstance(self.SelectedElement, ElementType): + instance_path = self.GetInstancePath(True) + iec_path = "%s.%s" % (instance_path, self.SelectedElement.GetName()) + menu = wx.Menu(title='') - item = self.AppendItem(menu,
- self.GetForceVariableMenuFunction(
+ iec_type = self.GetDataType(iec_path) + self.GetForceVariableMenuFunction( + iec_path.upper(), iec_type, not(self.SelectedElement.GetValue()), True)) + self.GetForceVariableMenuFunction( + iec_path.upper(), iec_type, True, True)) + self.GetForceVariableMenuFunction( + iec_path.upper(), iec_type, False, True)) + self.GetForceVariableMenuFunction( + iec_path.upper(), iec_type, + self.SelectedElement.GetValue())) ritem = self.AppendItem(menu,
@@ -1585,6 +1614,7 @@
if self.Editor.HasCapture():
self.Editor.ReleaseMouse()
self.Editor.PopupMenu(menu)
--- a/etherlab/ConfigEditor.py Thu Aug 22 12:16:45 2024 +0200
+++ b/etherlab/ConfigEditor.py Sat Sep 07 12:50:57 2024 +0200
@@ -672,7 +672,7 @@
self.ProcessVariablesGrid = CustomGrid(self.EthercatMasterEditor, style=wx.VSCROLL)
self.ProcessVariablesGrid.SetMinSize(wx.Size(0, 150))
self.ProcessVariablesGrid.SetDropTarget(ProcessVariableDropTarget(self))
- self.ProcessVariablesGrid.Bind(wx.grid.EVT_GRID_CELL_CHANGING,
+ self.ProcessVariablesGrid.Bind(wx.grid.EVT_GRID_CELL_CHANGED, self.OnProcessVariablesGridCellChange)
self.ProcessVariablesGrid.Bind(wx.grid.EVT_GRID_CELL_LEFT_CLICK,
self.OnProcessVariablesGridCellLeftClick)
@@ -697,7 +697,7 @@
self.StartupCommandsGrid = CustomGrid(self.EthercatMasterEditor, style=wx.VSCROLL)
self.StartupCommandsGrid.SetDropTarget(StartupCommandDropTarget(self))
self.StartupCommandsGrid.SetMinSize(wx.Size(0, 150))
- self.StartupCommandsGrid.Bind(wx.grid.EVT_GRID_CELL_CHANGING,
+ self.StartupCommandsGrid.Bind(wx.grid.EVT_GRID_CELL_CHANGED, self.OnStartupCommandsGridCellChange)
self.StartupCommandsGrid.Bind(wx.grid.EVT_GRID_EDITOR_SHOWN,
self.OnStartupCommandsGridEditorShow)
--- a/graphics/GraphicCommons.py Thu Aug 22 12:16:45 2024 +0200
+++ b/graphics/GraphicCommons.py Sat Sep 07 12:50:57 2024 +0200
@@ -349,10 +349,10 @@
posx, posy = self.GetPosition()
min_width, min_height = self.GetMinSize()
- self.Pos.x = max(0, self.Pos.x - (width - min_width) * x_factor)
+ self.Pos.x = max(0, round(self.Pos.x - (width - min_width) * x_factor)) - self.Pos.y = max(0, self.Pos.y - (height - min_height) * y_factor)
+ self.Pos.y = max(0, round(self.Pos.y - (height - min_height) * y_factor)) self.Pos.x = round_scaling(self.Pos.x, scaling[0])
@@ -1026,16 +1026,16 @@
- def __init__(self, parent, name, type, position, direction, negated=False, edge="none", onlyone=False):
+ def __init__(self, parent, name, Type, position, direction, negated=False, edge="none", onlyone=False): DebugDataConsumer.__init__(self)
ToolTipProducer.__init__(self, parent.Parent)
self.ParentBlock = parent
self.Direction = direction
- if self.ParentBlock.IsOfType("BOOL", type):
+ if self.ParentBlock.IsOfType("BOOL", Type): @@ -1141,8 +1141,8 @@
return self.ParentBlock.IsOfType(type, reference) or self.ParentBlock.IsOfType(reference, type)
# Changes the connector name
- def SetType(self, type):
+ def SetType(self, Type): for wire, _handle in self.Wires:
wire.SetValid(wire.IsConnectedCompatible())
--- a/graphics/LD_Objects.py Thu Aug 22 12:16:45 2024 +0200
+++ b/graphics/LD_Objects.py Sat Sep 07 12:50:57 2024 +0200
@@ -593,11 +593,11 @@
dc.SetBrush(wx.Brush(HIGHLIGHTCOLOR))
dc.SetLogicalFunction(wx.AND)
# Draw two rectangles for representing the contact
- left_left = (self.Pos.x - 1) * scalex - 2
- right_left = (self.Pos.x + self.Size[0] - 2) * scalex - 2
- top = (self.Pos.y - 1) * scaley - 2
- height = (self.Size[1] + 3) * scaley + 5
+ left_left = round((self.Pos.x - 1) * scalex) - 2 + right_left = round((self.Pos.x + self.Size[0] - 2) * scalex) - 2 + top = round((self.Pos.y - 1) * scaley) - 2 + width = round(4 * scalex + 5) + height = round((self.Size[1] + 3) * scaley) + 5 dc.DrawRectangle(left_left, top, width, height)
dc.DrawRectangle(right_left, top, width, height)
@@ -974,31 +974,24 @@
elif self.Type == COIL_FALLING:
- if getattr(dc, "printing", False) and not isinstance(dc, wx.PostScriptDC):
- # Draw an clipped ellipse for representing the coil
- clipping_box = dc.GetClippingBox()
- dc.SetClippingRegion(self.Pos.x - 1, self.Pos.y, self.Size[0] + 2, self.Size[1] + 1)
- dc.DrawEllipse(self.Pos.x, self.Pos.y - int(self.Size[1] * (sqrt(2) - 1.) / 2.) + 1, self.Size[0], int(self.Size[1] * sqrt(2)) - 1)
- dc.DestroyClippingRegion()
- if clipping_box != (0, 0, 0, 0):
- dc.SetClippingRegion(*clipping_box)
- name_size = dc.GetTextExtent(self.Name)
- type_size = dc.GetTextExtent(typetext)
- # Draw a two ellipse arcs for representing the coil
- dc.DrawEllipticArc(self.Pos.x, self.Pos.y - int(self.Size[1] * (sqrt(2) - 1.) / 2.) + 1, self.Size[0], int(self.Size[1] * sqrt(2)) - 1, 135, 225)
- dc.DrawEllipticArc(self.Pos.x, self.Pos.y - int(self.Size[1] * (sqrt(2) - 1.) / 2.) + 1, self.Size[0], int(self.Size[1] * sqrt(2)) - 1, -45, 45)
- # Draw a point to avoid hole in left arc
- if not getattr(dc, "printing", False):
- if self.Value is not None and self.Value:
- dc.SetPen(MiterPen(wx.GREEN))
- dc.SetPen(MiterPen(wx.BLACK))
- dc.DrawPoint(self.Pos.x + 1, self.Pos.y + self.Size[1] // 2 + 1)
- name_size = self.NameSize
- type_size = self.TypeSize
+ printing = getattr(dc, "printing", False) + # Draw a two ellipse arcs for representing the coil + self.Pos.y - round(self.Size[1] * (sqrt(2) - 1.) / 2.) + 1, + self.Size[0], round(self.Size[1] * sqrt(2)) - 1) + # workaround for printing bug with DrawEllipticArc + # add an offset to the y position proportional to the height of the ellipse + # sqrt(2) ratio obtained heuristically + pos = (pos[0], pos[1] + round(sqrt(2)*pos[3]), pos[2], pos[3]) + dc.DrawEllipticArc(*pos, 135, 225) + dc.DrawEllipticArc(*pos, -45, 45) + name_size = self.NameSize + type_size = self.TypeSize name_pos = (self.Pos.x + (self.Size[0] - name_size[0]) // 2,
--- a/plcopen/structures.py Thu Aug 22 12:16:45 2024 +0200
+++ b/plcopen/structures.py Sat Sep 07 12:50:57 2024 +0200
@@ -340,7 +340,7 @@
ST_BLOCK_START_KEYWORDS = ["IF", "ELSIF", "ELSE", "CASE", "FOR", "WHILE", "REPEAT"]
ST_BLOCK_END_KEYWORDS = ["END_IF", "END_CASE", "END_FOR", "END_WHILE", "END_REPEAT"]
- "TRUE", "FALSE", "THEN", "OF", "TO", "BY", "DO", "DO", "UNTIL", "EXIT",
+ "TRUE", "FALSE", "THEN", "OF", "TO", "BY", "DO", "DO", "UNTIL", "EXIT", "CONTINUE", "RETURN", "NOT", "MOD", "AND", "XOR", "OR"
] + ST_BLOCK_START_KEYWORDS + ST_BLOCK_END_KEYWORDS
--- a/requirements.txt Thu Aug 22 12:16:45 2024 +0200
+++ b/requirements.txt Sat Sep 07 12:50:57 2024 +0200
@@ -24,7 +24,7 @@
-Nevow @ git+https://git@github.com/beremiz/nevow-py3.git@6deba7284e159af46a412699f046e060d7026cf9
+Nevow @ git+https://git@github.com/beremiz/nevow-py3.git@81c2eaeaaa20022540a98a3106f72e0199fbcc1b --- a/util/ProcessLogger.py Thu Aug 22 12:16:45 2024 +0200
+++ b/util/ProcessLogger.py Sat Sep 07 12:50:57 2024 +0200
@@ -136,7 +136,7 @@
if _debug and self.logger:
self.logger.write("(DEBUG) launching:\n" + self.Command_str + "\n")
- self.Proc = subprocess.Popen(self.Command, encoding="utf-8", **popenargs)
+ self.Proc = subprocess.Popen(self.Command, encoding="utf-8", errors="backslashreplace", **popenargs) self.outt = outputThread(