merge MQTT grafted from python2 branch - untested
authorEdouard Tisserant <edouard.tisserant@gmail.com>
Sat, 07 Sep 2024 12:50:57 +0200
changeset 4007 76dede1e3403
parent 4006 e16c8443e877 (current diff)
parent 3978 22ba103801ee (diff)
child 4008 f30573e98600
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 @@
 doc/locale/**
 
 C_runtime/**/*.d
-C_runtime/**/*.o
\ No newline at end of file
+C_runtime/**/*.o
+beremiz_runtime
+**/tmp/**
--- 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
     std::string md5sum;
     try {
--- 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 StartPLC(void);
         uint32_t StopPLC(bool * success);
+
+        // Public interface used by runtime
         uint32_t AutoLoad();
+        uint32_t LogMessage(uint8_t level, std::string message);
 
     private:
         // A map of all the blobs
@@ -112,10 +115,9 @@
         uint32_t BlobAsFile(const binary_t * BlobID, std::filesystem::path filename);
         uint32_t LoadPLC(void);
         uint32_t UnLoadPLC(void);
-        uint32_t LogMessage(uint8_t level, std::string message);
         uint32_t PurgePLC(void);
         void PurgeTraceBuffer(void);
         void TraceThreadProc(void);
 };
 
-#endif
\ No newline at end of file
+#endif
--- 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()
             xmlfile.close()
 
--- 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)
 
-            if preview.Ok():
+            if preview.IsOk():
                 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.Preview = preview
+        self.IsPreview = lambda *_x : preview
         self.Margins = margins
         self.FontSize = 5
         self.TextMargin = 3
@@ -2620,9 +2620,9 @@
 
     def OnBeginDocument(self, startPage, endPage):
         dc = self.GetDC()
-        if not self.Preview and isinstance(dc, wx.PostScriptDC):
+        if not self.IsPreview() and isinstance(dc, wx.PostScriptDC):
             dc.SetResolution(720)
-        super(GraphicPrintout, self).OnBeginDocument(startPage, endPage)
+        return super(GraphicPrintout, self).OnBeginDocument(startPage, endPage)
 
     def OnPrintPage(self, page):
         dc = self.GetDC()
@@ -2630,21 +2630,21 @@
         dc.Clear()
         dc.SetUserScale(1.0, 1.0)
         dc.SetDeviceOrigin(0, 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
-
-        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
+        fontsize = round(self.FontSize * Yscale)
+
+        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 @@
             # shutdown local runtime
             self.local_runtime.kill(gently=False)
             # clear temp dir
-            shutil.rmtree(self.local_runtime_tmpdir)
+            try:
+                shutil.rmtree(self.local_runtime_tmpdir)
+            except:
+                pass
 
             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 @@
         return False
 
     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")
         return True
 
--- 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()
+        # default scaling
+        for iec_lang in ["FBD", "LD", "SFC"]:
+            PLCControler.SetProjectProperties(self, properties={"scaling": {iec_lang: (8, 8)}})
         # this will create files base XML files
         self.SaveProject()
         return None
--- 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))
         self.VariablesGrid.Bind(
-            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()
             if var_type is not None:
                 self.Table.SetValueByName(row, 'Type', var_type)
-        else:
-            wx.CallAfter(self.Table.Parent.ShowErrorMessage,
-                         _("Selected location is identical to previous one"))
+
         self.CellControl.Disable()
         return changed
 
--- 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 @@
                 "type": ITEM_PROJECT,
                 "data": None,
                 "text": None,
-                "matches": None,
+                "matches": 0,
             }
             search_results_tree_children = search_results_tree_infos.setdefault("children", [])
             for tagname in self.ElementsOrder:
@@ -291,7 +291,7 @@
             start_idx = start[1]
             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
         # box if it exists
         selected = self.Expression.GetValue()
-        if selected != "" and self.VariableName.FindString(selected) != wx.NOT_FOUND:
-            self.VariableName.SetStringSelection(selected)
-        else:
-            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)
-                else:
-                    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))
 
         self.Refresh()
         event.Skip()
--- 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", " ")
 
             # Code completion
-            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]
 
                 kw = []
 
+                self.RefreshVariableTree()
+
                 if self.TextSyntax == "IL":
                     if len(words) == 1:
                         kw = self.Keywords
@@ -898,13 +900,17 @@
                     kw = self.Keywords + list(self.Variables.keys()) + list(self.Functions.keys())
                 if len(kw) > 0:
                     if len(words[-1]) > 0:
-                        kw = [keyword for keyword in kw if keyword.startswith(words[-1])]
+                        kw = [keyword for keyword in kw if keyword.startswith(words[-1].upper())]
+                if len(kw) > 0:
                     kw.sort()
                     self.Editor.AutoCompSetIgnoreCase(True)
                     self.Editor.AutoCompShow(len(words[-1]), " ".join(kw))
                 key_handled = True
             elif key == wx.WXK_RETURN or key == wx.WXK_NUMPAD_ENTER:
-                if self.TextSyntax in ["ST", "ALL"]:
+                if self.Editor.AutoCompActive():
+                    self.Editor.AutoCompComplete()
+                    key_handled = True
+                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 @@
     #                           Popup menu functions
     # -------------------------------------------------------------------------------
 
-    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):
+            if not immediate:
+                # use value as default value in dialog
+                dialog = ForceVariableDialog(self.ParentWindow, iec_type, str(value))
+                if dialog.ShowModal() != wx.ID_OK:
+                    return
+                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)
+        
+        if iec_path is None:
+            # 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='')
+                    break
+
         if iec_path is not None:
             menu = wx.Menu(title='')
-            item = self.AppendItem(menu,
-                _("Force value"),
-                self.GetForceVariableMenuFunction(
-                    iec_path.upper(),
-                    self.SelectedElement))
+            iec_type = self.GetDataType(iec_path)
+            if iec_type == "BOOL":
+                self.AppendItem(menu, 
+                    _("Force Toggle"), 
+                    self.GetForceVariableMenuFunction(
+                        iec_path.upper(), iec_type, not(self.SelectedElement.GetValue()), True))
+                self.AppendItem(menu, 
+                    _("Force True"), 
+                    self.GetForceVariableMenuFunction(
+                        iec_path.upper(), iec_type, True, True))
+                self.AppendItem(menu, 
+                    _("Force False"), 
+                    self.GetForceVariableMenuFunction(
+                        iec_path.upper(), iec_type, False, True))
+            else:
+                self.AppendItem(menu,
+                    _("Force value"),
+                    self.GetForceVariableMenuFunction(
+                        iec_path.upper(), iec_type,
+                        self.SelectedElement.GetValue()))
 
             ritem = self.AppendItem(menu,
                 _("Release value"),
@@ -1585,6 +1614,7 @@
                 ritem.Enable(True)
             else:
                 ritem.Enable(False)
+
             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()
         if width < min_width:
-            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))
             width = min_width
         if height < min_height:
-            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))
             height = min_height
         if scaling is not None:
             self.Pos.x = round_scaling(self.Pos.x, scaling[0])
@@ -1026,16 +1026,16 @@
     """
 
     # Create a new connector
-    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.Name = name
-        self.Type = type
+        self.Type = Type
         self.Pos = position
         self.Direction = direction
         self.Wires = []
-        if self.ParentBlock.IsOfType("BOOL", type):
+        if self.ParentBlock.IsOfType("BOOL", Type):
             self.Negated = negated
             self.Edge = edge
         else:
@@ -1141,8 +1141,8 @@
         return self.ParentBlock.IsOfType(type, reference) or self.ParentBlock.IsOfType(reference, type)
 
     # Changes the connector name
-    def SetType(self, type):
-        self.Type = type
+    def SetType(self, Type):
+        self.Type = 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
-        width = 4 * scalex + 5
-        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:
             typetext = "N"
 
-        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)
-            if typetext != "":
-                type_size = dc.GetTextExtent(typetext)
-        else:
-            # 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))
-                else:
-                    dc.SetPen(MiterPen(wx.BLACK))
-                dc.DrawPoint(self.Pos.x + 1, self.Pos.y + self.Size[1] // 2 + 1)
-            name_size = self.NameSize
-            if typetext != "":
-                type_size = self.TypeSize
+        printing = getattr(dc, "printing", False)
+        # Draw a two ellipse arcs for representing the coil
+        pos = (self.Pos.x,  
+               self.Pos.y - round(self.Size[1] * (sqrt(2) - 1.) / 2.) + 1, 
+               self.Size[0], round(self.Size[1] * sqrt(2)) - 1)
+        
+        if printing:
+            # 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
+        if typetext != "":
+            type_size = self.TypeSize
 
         # Draw coil name
         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"]
 ST_KEYWORDS = [
-    "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 @@
 lxml==4.9.2
 matplotlib==3.7.1
 msgpack==1.0.5
-Nevow @ git+https://git@github.com/beremiz/nevow-py3.git@6deba7284e159af46a412699f046e060d7026cf9
+Nevow @ git+https://git@github.com/beremiz/nevow-py3.git@81c2eaeaaa20022540a98a3106f72e0199fbcc1b
 numpy==1.24.3
 packaging==23.1
 Pillow==9.5.0
--- 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(
             self.Proc,