andrej@1511: #!/usr/bin/env python
andrej@1511: # -*- coding: utf-8 -*-
andrej@1511: 
andrej@1511: # This file is part of Beremiz, a Integrated Development Environment for
andrej@1511: # programming IEC 61131-3 automates supporting plcopen standard and CanFestival.
andrej@1511: #
andrej@1511: # Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD
andrej@1680: # Copyright (C) 2017: Andrey Skvortsov
andrej@1511: #
andrej@1511: # See COPYING file for copyrights details.
andrej@1511: #
andrej@1511: # This program is free software; you can redistribute it and/or
andrej@1511: # modify it under the terms of the GNU General Public License
andrej@1511: # as published by the Free Software Foundation; either version 2
andrej@1511: # of the License, or (at your option) any later version.
andrej@1511: #
andrej@1511: # This program is distributed in the hope that it will be useful,
andrej@1511: # but WITHOUT ANY WARRANTY; without even the implied warranty of
andrej@1511: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
andrej@1511: # GNU General Public License for more details.
andrej@1511: #
andrej@1511: # You should have received a copy of the GNU General Public License
andrej@1511: # along with this program; if not, write to the Free Software
andrej@1511: # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
andrej@1511: 
andrej@1853: 
andrej@1732: import os
andrej@1732: import re
andrej@2434: 
andrej@1680: import util.paths as paths
Laurent@1315: from xmlclass import GenerateParserFromXSD
Laurent@1315: 
Laurent@1315: from CodeFileTreeNode import CodeFile
andrej@1853: from py_ext.PythonEditor import PythonEditor
laurent@366: 
andrej@1736: 
edouard@2693: 
edouard@2693: 
Laurent@1097: class PythonFileCTNMixin(CodeFile):
Edouard@1436: 
Laurent@1124:     CODEFILE_NAME = "PyFile"
Laurent@1124:     SECTIONS_NAMES = [
Laurent@1124:         "globals",
Laurent@1124:         "init",
Laurent@1124:         "cleanup",
Laurent@1124:         "start",
Laurent@1124:         "stop"]
laurent@657:     EditorType = PythonEditor
Edouard@1436: 
laurent@366:     def __init__(self):
Laurent@1097:         CodeFile.__init__(self)
Edouard@1436: 
laurent@366:         filepath = self.PythonFileName()
Edouard@1436: 
laurent@366:         if os.path.isfile(filepath):
andrej@1680:             PythonParser = GenerateParserFromXSD(paths.AbsNeighbourFile(__file__, "py_ext_xsd.xsd"))
Edouard@1436: 
laurent@366:             xmlfile = open(filepath, 'r')
Laurent@1315:             pythonfile_xml = xmlfile.read()
laurent@366:             xmlfile.close()
Edouard@1436: 
Laurent@1315:             pythonfile_xml = pythonfile_xml.replace(
Edouard@1436:                 'xmlns="http://www.w3.org/2001/XMLSchema"',
Laurent@1315:                 'xmlns:xhtml="http://www.w3.org/1999/xhtml"')
Laurent@1315:             for cre, repl in [
andrej@2439:                     (re.compile(r"(?<!<xhtml:p>)(?:<!\[CDATA\[)"), "<xhtml:p><![CDATA["),
andrej@2439:                     (re.compile(r"(?:]]>)(?!</xhtml:p>)"), "]]></xhtml:p>")]:
Laurent@1315:                 pythonfile_xml = cre.sub(repl, pythonfile_xml)
Edouard@1436: 
Laurent@1330:             try:
Laurent@1330:                 python_code, error = PythonParser.LoadXMLString(pythonfile_xml)
Edouard@1436:                 if error is None:
Laurent@1330:                     self.CodeFile.globals.setanyText(python_code.getanyText())
Laurent@1330:                     os.remove(filepath)
Laurent@1330:                     self.CreateCodeFileBuffer(False)
Laurent@1330:                     self.OnCTNSave()
andrej@2418:             except Exception as exc:
kinsamanka@3752:                 error = str(exc)
Edouard@1436: 
Laurent@1330:             if error is not None:
Laurent@1330:                 self.GetCTRoot().logger.write_error(
Edouard@1436:                     _("Couldn't import old %s file.") % self.CTNName())
Edouard@1436: 
Laurent@1097:     def CodeFileName(self):
Laurent@1097:         return os.path.join(self.CTNPath(), "pyfile.xml")
Edouard@1436: 
laurent@366:     def PythonFileName(self):
Edouard@721:         return os.path.join(self.CTNPath(), "py_ext.xml")
laurent@366: 
Edouard@1132:     PreSectionsTexts = {}
Edouard@1132:     PostSectionsTexts = {}
andrej@1751: 
andrej@1740:     def GetSection(self, section):
andrej@1740:         return self.PreSectionsTexts.get(section, "") + "\n" + \
Laurent@1315:                getattr(self.CodeFile, section).getanyText() + "\n" + \
andrej@1740:                self.PostSectionsTexts.get(section, "")
Edouard@1436: 
edouard@2692:     def CTNGlobalInstances(self):
edouard@2692:         variables = self.CodeFileVariables(self.CodeFile)
edouard@2692:         ret = [(variable.getname(),
edouard@2692:                 variable.gettype(),
edouard@2692:                 variable.getinitial())
edouard@2692:                for variable in variables]
edouard@2692:         location_str = "_".join(map(str, self.GetCurrentLocation()))
edouard@2692:         ret.append(("On_"+location_str+"_Change", "python_poll", ""))
edouard@2692:         return ret
edouard@2692: 
edouard@2693:     @staticmethod
edouard@2693:     def GetVarOnChangeContent(var):
edouard@2693:         """
edouard@2693:         returns given variable onchange field
edouard@2693:         function is meant to allow customization 
edouard@2693:         """
edouard@2693:         return var.getonchange()
edouard@2693: 
Edouard@1132:     def CTNGenerate_C(self, buildpath, locations):
Edouard@1436:         # location string for that CTN
andrej@1833:         location_str = "_".join(map(str, self.GetCurrentLocation()))
Edouard@1144:         configname = self.GetCTRoot().GetProjectConfigNames()[0]
Edouard@1436: 
andrej@1878:         def _onchangecode(var):
Edouard@2694:             result = []
Edouard@2694:             for onchangecall in self.GetVarOnChangeContent(var).split(','):
Edouard@2694:                 onchangecall = onchangecall.strip()
Edouard@2694:                 if onchangecall:
Edouard@2694:                     result.append(onchangecall + "('" + var.getname() + "')")
Edouard@2694:             return result
Edouard@2694: 
andrej@1878: 
andrej@1878:         def _onchange(var):
edouard@2693:             content = self.GetVarOnChangeContent(var)
edouard@2693:             return repr(content) if content else None
andrej@1878: 
Edouard@1447:         pyextname = self.CTNName()
kinsamanka@3750:         varinfos = [{
andrej@1878:                 "name": variable.getname(),
andrej@1878:                 "desc": repr(variable.getdesc()),
andrej@1878:                 "onchangecode": _onchangecode(variable),
andrej@1878:                 "onchange": _onchange(variable),
andrej@1878:                 "opts": repr(variable.getopts()),
andrej@1878:                 "configname": configname.upper(),
andrej@1878:                 "uppername": variable.getname().upper(),
Edouard@2681:                 "IECtype": self.GetCTRoot().GetBaseType(variable.gettype()),
Edouard@1901:                 "initial": repr(variable.getinitial()),
andrej@1878:                 "pyextname": pyextname
kinsamanka@3750:             } for variable in self.CodeFile.variables.variable]
edouard@2692: 
edouard@2692:         onchange_var_count = len([None for varinfo in varinfos if varinfo["onchange"]])
edouard@2692: 
Edouard@1144:         # python side PLC global variables access stub
andrej@1768:         globalstubs = "\n".join([
andrej@1768:             """\
Edouard@1144: _%(name)s_ctype, _%(name)s_unpack, _%(name)s_pack = \\
Edouard@1144:     TypeTranslator["%(IECtype)s"]
Edouard@1144: _PySafeGetPLCGlob_%(name)s = PLCBinary.__SafeGetPLCGlob_%(name)s
Edouard@1145: _PySafeGetPLCGlob_%(name)s.restype = None
Edouard@1436: _PySafeGetPLCGlob_%(name)s.argtypes = [ctypes.POINTER(_%(name)s_ctype)]
Edouard@1144: _PySafeSetPLCGlob_%(name)s = PLCBinary.__SafeSetPLCGlob_%(name)s
Edouard@1144: _PySafeSetPLCGlob_%(name)s.restype = None
Edouard@1144: _PySafeSetPLCGlob_%(name)s.argtypes = [ctypes.POINTER(_%(name)s_ctype)]
Edouard@1448: _%(pyextname)sGlobalsDesc.append((
Edouard@1448:     "%(name)s",
Edouard@1448:     "%(IECtype)s",
Edouard@1897:     %(initial)s,
Edouard@1448:     %(desc)s,
Edouard@1449:     %(onchange)s,
Edouard@1448:     %(opts)s))
edouard@2697: """ % varinfo + ("""
edouard@2697: _PyOnChangeCount_%(name)s = ctypes.c_uint.in_dll(PLCBinary,"__%(name)s_onchange_count")
edouard@2697: _PyOnChangeFirst_%(name)s = _%(name)s_ctype.in_dll(PLCBinary,"__%(name)s_onchange_firstval")
edouard@2697: _PyOnChangeLast_%(name)s = _%(name)s_ctype.in_dll(PLCBinary,"__%(name)s_onchange_lastval")
edouard@2697: """ % varinfo if varinfo["onchange"] else "") for varinfo in varinfos])
Edouard@1132: 
edouard@2692:         on_change_func_body = "\n".join(["""
edouard@2697:     if _PyOnChangeCount_%(name)s.value > 0:
edouard@2692:         # %(name)s
Edouard@2694:         try:""" % varinfo + """
edouard@2692:             """ + """
edouard@2692:             """.join(varinfo['onchangecode'])+"""
edouard@2692:         except Exception as e:
edouard@2692:             errors.append("%(name)s: "+str(e))
edouard@2692: """ % varinfo for varinfo in varinfos if varinfo["onchange"]])
Edouard@1132:         # Runtime calls (start, stop, init, and cleanup)
Edouard@1132:         rtcalls = ""
Laurent@1124:         for section in self.SECTIONS_NAMES:
Laurent@1124:             if section != "globals":
Edouard@1132:                 rtcalls += "def _runtime_%s_%s():\n" % (location_str, section)
Edouard@1132:                 sectiontext = self.GetSection(section).strip()
Edouard@1132:                 if sectiontext:
Edouard@1132:                     rtcalls += '    ' + \
Edouard@1154:                         sectiontext.replace('\n', '\n    ')+"\n\n"
Edouard@1132:                 else:
Edouard@1132:                     rtcalls += "    pass\n\n"
Edouard@1144: 
Edouard@1436:         globalsection = self.GetSection("globals")
Edouard@1436: 
andrej@1846:         loc_dict = {
andrej@1846:             "pyextname": pyextname,
andrej@1846:             "globalstubs": globalstubs,
andrej@1846:             "globalsection": globalsection,
andrej@1846:             "rtcalls": rtcalls,
edouard@2692:             "location_str": location_str,
edouard@2692:             "on_change_func_body":on_change_func_body,
edouard@2692:             "onchange_var_count": onchange_var_count
andrej@1846:         }
andrej@1846: 
Edouard@1144:         PyFileContent = """\
Edouard@1132: #!/usr/bin/env python
Edouard@1132: # -*- coding: utf-8 -*-
Edouard@1132: ## Code generated by Beremiz python mixin confnode
Edouard@1436: ##
Edouard@1436: 
Edouard@1132: ## Code for PLC global variable access
Edouard@1902: from runtime.typemapping import TypeTranslator
Edouard@1436: import ctypes
edouard@2692: 
edouard@2692: _PySafeGetChanges_%(pyextname)s = PLCBinary.PySafeGetChanges_%(location_str)s
edouard@2697: _PySafeGetChanges_%(pyextname)s.restype = None
edouard@2692: _PySafeGetChanges_%(pyextname)s.argtypes = None
edouard@2692: 
Edouard@1447: _%(pyextname)sGlobalsDesc = []
Edouard@1449: __ext_name__ = "%(pyextname)s"
Edouard@1452: PLCGlobalsDesc.append(( "%(pyextname)s" , _%(pyextname)sGlobalsDesc ))
Edouard@1144: %(globalstubs)s
Edouard@1436: 
Edouard@1132: ## User code in "global" scope
Edouard@1144: %(globalsection)s
laurent@366: 
Edouard@1132: ## Beremiz python runtime calls
Edouard@1144: %(rtcalls)s
Edouard@1144: 
edouard@2692: def On_%(pyextname)s_Change():
edouard@2697:     _PySafeGetChanges_%(pyextname)s()
edouard@2692:     errors = []
edouard@2692: %(on_change_func_body)s
edouard@2692:     if len(errors)>0 :
edouard@2692:         raise Exception("Exception in %(pyextname)s OnChange call:\\\\n" + "\\\\n".join(errors))
edouard@2692: 
Edouard@1449: del __ext_name__
Edouard@1449: 
andrej@1846: """ % loc_dict
Edouard@1144: 
Edouard@1144:         # write generated content to python file
Edouard@1436:         runtimefile_path = os.path.join(buildpath,
andrej@1768:                                         "runtime_%s.py" % location_str)
edouard@3806:         runtimefile = open(runtimefile_path, 'wb')
Edouard@1144:         runtimefile.write(PyFileContent.encode('utf-8'))
Edouard@1132:         runtimefile.close()
Edouard@1132: 
Edouard@1144:         # C code for safe global variables access
Edouard@1436: 
Edouard@1144:         vardecfmt = """\
Edouard@1154: extern  __IEC_%(IECtype)s_t %(configname)s__%(uppername)s;
Edouard@1154: IEC_%(IECtype)s __%(name)s_rbuffer = __INIT_%(IECtype)s;
Edouard@1154: IEC_%(IECtype)s __%(name)s_wbuffer;
Edouard@1144: long __%(name)s_rlock = 0;
Edouard@1144: long __%(name)s_wlock = 0;
Edouard@1144: int __%(name)s_wbuffer_written = 0;
Edouard@1154: void __SafeGetPLCGlob_%(name)s(IEC_%(IECtype)s *pvalue){
Edouard@1144:     while(AtomicCompareExchange(&__%(name)s_rlock, 0, 1));
Edouard@1145:     *pvalue = __%(name)s_rbuffer;
Edouard@1144:     AtomicCompareExchange((long*)&__%(name)s_rlock, 1, 0);
Edouard@1144: }
Edouard@1160: void __SafeSetPLCGlob_%(name)s(IEC_%(IECtype)s *value){
Edouard@1144:     while(AtomicCompareExchange(&__%(name)s_wlock, 0, 1));
Edouard@1144:     __%(name)s_wbuffer = *value;
Edouard@1144:     __%(name)s_wbuffer_written = 1;
Edouard@1144:     AtomicCompareExchange((long*)&__%(name)s_wlock, 1, 0);
Edouard@1144: }
Edouard@1144: 
Edouard@1144: """
Edouard@1448: 
Edouard@1448:         vardeconchangefmt = """\
edouard@2697: unsigned int __%(name)s_rbuffer_written = 0;
edouard@2697: IEC_%(IECtype)s __%(name)s_rbuffer_firstval;
edouard@2697: IEC_%(IECtype)s __%(name)s_rbuffer_lastval;
edouard@2697: unsigned int __%(name)s_onchange_count = 0;
edouard@2697: IEC_%(IECtype)s __%(name)s_onchange_firstval;
edouard@2697: IEC_%(IECtype)s __%(name)s_onchange_lastval;
Edouard@1448: """
Edouard@1448: 
Edouard@1144:         varretfmt = """\
Edouard@1144:     if(!AtomicCompareExchange(&__%(name)s_wlock, 0, 1)){
Edouard@1144:         if(__%(name)s_wbuffer_written == 1){
Edouard@1144:             %(configname)s__%(uppername)s.value = __%(name)s_wbuffer;
Edouard@1144:             __%(name)s_wbuffer_written = 0;
Edouard@1144:         }
Edouard@1144:         AtomicCompareExchange((long*)&__%(name)s_wlock, 1, 0);
Edouard@1144:     }
Edouard@1436: """
Edouard@1144:         varpubfmt = """\
Edouard@1144:     if(!AtomicCompareExchange(&__%(name)s_rlock, 0, 1)){
Edouard@1448:         __%(name)s_rbuffer = __GET_VAR(%(configname)s__%(uppername)s);
Edouard@1144:         AtomicCompareExchange((long*)&__%(name)s_rlock, 1, 0);
Edouard@1144:     }
Edouard@1436: """
Edouard@1144: 
Edouard@1448:         varpubonchangefmt = """\
Edouard@1448:     if(!AtomicCompareExchange(&__%(name)s_rlock, 0, 1)){
Edouard@1448:         IEC_%(IECtype)s tmp = __GET_VAR(%(configname)s__%(uppername)s);
Edouard@2679:         if(NE_%(IECtype)s(1, NULL, __%(name)s_rbuffer, tmp)){
edouard@2697:             if(__%(name)s_rbuffer_written == 0);
edouard@2697:                 __%(name)s_rbuffer_firstval = __%(name)s_rbuffer;
edouard@2697:             __%(name)s_rbuffer_lastval = tmp;
Edouard@2679:             __%(name)s_rbuffer = tmp;
edouard@2697:             /* count one more change */
edouard@2697:             __%(name)s_rbuffer_written += 1;
edouard@2697:             some_change_found = 1;
Edouard@1448:         }
Edouard@1448:         AtomicCompareExchange((long*)&__%(name)s_rlock, 1, 0);
Edouard@1448:     }
Edouard@1448: """
edouard@2692: 
edouard@2692:         varcollectchangefmt = """\
edouard@2697:     while(AtomicCompareExchange(&__%(name)s_rlock, 0, 1));
edouard@2697:     __%(name)s_onchange_count = __%(name)s_rbuffer_written;
edouard@2697:     __%(name)s_onchange_firstval = __%(name)s_rbuffer_firstval;
edouard@2697:     __%(name)s_onchange_lastval = __%(name)s_rbuffer_lastval;
edouard@2692:     /* mark variable as unchanged */
edouard@2692:     __%(name)s_rbuffer_written = 0;
edouard@2697:     AtomicCompareExchange((long*)&__%(name)s_rlock, 1, 0);
edouard@2692: 
Edouard@1448: """
Edouard@1452:         vardec = "\n".join([(vardecfmt + vardeconchangefmt
andrej@1742:                              if varinfo["onchange"] else vardecfmt) % varinfo
Edouard@1448:                             for varinfo in varinfos])
Edouard@1448:         varret = "\n".join([varretfmt % varinfo for varinfo in varinfos])
Edouard@1448:         varpub = "\n".join([(varpubonchangefmt if varinfo["onchange"] else
Edouard@1448:                              varpubfmt) % varinfo
Edouard@1448:                             for varinfo in varinfos])
edouard@2692:         varcollectchange = "\n".join([varcollectchangefmt % varinfo
andrej@1878:                              for varinfo in varinfos if varinfo["onchange"]])
Edouard@1436: 
edouard@2692:         pysafe_pypoll_code = "On_"+pyextname+"_Change()"
edouard@2692: 
andrej@1846:         loc_dict = {
andrej@1846:             "vardec": vardec,
andrej@1846:             "varret": varret,
andrej@1846:             "varpub": varpub,
andrej@1846:             "location_str": location_str,
edouard@2692:             "pysafe_pypoll_code": '"'+pysafe_pypoll_code+'"',
edouard@2692:             "pysafe_pypoll_code_len": len(pysafe_pypoll_code),
edouard@2692:             "varcollectchange": varcollectchange,
edouard@2692:             "onchange_var_count": onchange_var_count
andrej@1846:         }
andrej@1846: 
Edouard@1452:         # TODO : use config name obtained from model instead of default
Edouard@1452:         # "config.h". User cannot change config name, but project imported
Edouard@1452:         # or created in older beremiz vesion could use different name.
Edouard@1144:         PyCFileContent = """\
Edouard@1436: /*
Edouard@1436:  * Code generated by Beremiz py_ext confnode
Edouard@1132:  * for safe global variables access
Edouard@1132:  */
Edouard@1132: #include "iec_types_all.h"
Edouard@1448: #include "POUS.h"
Edouard@1448: #include "config.h"
Edouard@1144: #include "beremiz.h"
Edouard@1144: 
edouard@2692: PYTHON_POLL* __%(location_str)s_notifier;
edouard@2692: 
Edouard@1144: /* User variables reference */
Edouard@1144: %(vardec)s
Edouard@1144: 
Edouard@1144: /* Beremiz confnode functions */
Edouard@1144: int __init_%(location_str)s(int argc,char **argv){
edouard@2692:     __%(location_str)s_notifier = __GET_GLOBAL_ON_%(location_str)s_CHANGE();
edouard@2692:     __SET_VAR(__%(location_str)s_notifier->,TRIG,,__BOOL_LITERAL(TRUE));
edouard@2692:     __SET_VAR(__%(location_str)s_notifier->,CODE,,__STRING_LITERAL(%(pysafe_pypoll_code_len)d,%(pysafe_pypoll_code)s));
edouard@2692: 
Edouard@1144:     return 0;
Edouard@1144: }
Edouard@1144: 
Edouard@1144: void __cleanup_%(location_str)s(void){
Edouard@1144: }
Edouard@1144: 
Edouard@1144: void __retrieve_%(location_str)s(void){
Edouard@1144: %(varret)s
Edouard@1144: }
Edouard@1144: 
edouard@2697: static int passing_changes_to_python = 0;
Edouard@1144: void __publish_%(location_str)s(void){
edouard@2697:     int some_change_found = 0;
Edouard@1144: %(varpub)s
edouard@2697:     passing_changes_to_python |= some_change_found;
edouard@2692:     // call python part if there was at least a change
edouard@2697:     if(passing_changes_to_python){
edouard@2692:         PYTHON_POLL_body__(__%(location_str)s_notifier);
edouard@2697:         passing_changes_to_python &= !(__GET_VAR(__%(location_str)s_notifier->ACK,));
edouard@2692:     }
edouard@2692: }
edouard@2692: 
edouard@2692: void* PySafeGetChanges_%(location_str)s(void){
edouard@2692: %(varcollectchange)s
edouard@2692: }
edouard@2692: 
andrej@1846: """ % loc_dict
Edouard@1436: 
andrej@1734:         Gen_PyCfile_path = os.path.join(buildpath, "PyCFile_%s.c" % location_str)
andrej@1740:         pycfile = open(Gen_PyCfile_path, 'w')
Edouard@1144:         pycfile.write(PyCFileContent)
Edouard@1132:         pycfile.close()
Edouard@1436: 
andrej@1734:         matiec_CFLAGS = '"-I%s"' % os.path.abspath(
Edouard@1132:             self.GetCTRoot().GetIECLibPath())
Edouard@1436: 
andrej@1503:         return ([(Gen_PyCfile_path, matiec_CFLAGS)],
Edouard@1132:                 "",
Edouard@1145:                 True,
andrej@2442:                 ("runtime_%s.py" % location_str, open(runtimefile_path, "rb")))