|
1 #!/usr/bin/env python |
|
2 # -*- coding: utf-8 -*- |
|
3 |
|
4 # This file is part of Beremiz, a Integrated Development Environment for |
|
5 # programming IEC 61131-3 automates supporting plcopen standard. |
|
6 # This files implements the bacnet plugin for Beremiz, adding BACnet server support. |
|
7 # |
|
8 # Copyright (c) 2017 Mario de Sousa (msousa@fe.up.pt) |
|
9 # |
|
10 # This program is free software: you can redistribute it and/or modify |
|
11 # it under the terms of the GNU General Public License as published by |
|
12 # the Free Software Foundation, either version 2 of the License, or |
|
13 # (at your option) any later version. |
|
14 # |
|
15 # This program is distributed in the hope that it will be useful, |
|
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
18 # GNU General Public License for more details. |
|
19 # |
|
20 # You should have received a copy of the GNU General Public License |
|
21 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
22 # |
|
23 # This code is made available on the understanding that it will not be |
|
24 # used in safety-critical situations without a full and competent review. |
|
25 |
|
26 |
|
27 |
|
28 import os, sys |
|
29 from collections import Counter |
|
30 from datetime import datetime |
|
31 |
|
32 base_folder = os.path.split(os.path.dirname(os.path.realpath(__file__)))[0] |
|
33 base_folder = os.path.join(base_folder, "..") |
|
34 BacnetPath = os.path.join(base_folder, "BACnet") |
|
35 BacnetLibraryPath = os.path.join(BacnetPath, "lib") |
|
36 BacnetIncludePath = os.path.join(BacnetPath, "include") |
|
37 BacnetIncludePortPath = os.path.join(BacnetPath, "ports") |
|
38 BacnetIncludePortPath = os.path.join(BacnetIncludePortPath, "linux") |
|
39 |
|
40 import wx |
|
41 import pickle |
|
42 |
|
43 from BacnetSlaveEditor import * |
|
44 from BacnetSlaveEditor import ObjectProperties |
|
45 from ConfigTreeNode import ConfigTreeNode |
|
46 from PLCControler import LOCATION_CONFNODE, LOCATION_MODULE, LOCATION_GROUP, LOCATION_VAR_INPUT, LOCATION_VAR_OUTPUT, LOCATION_VAR_MEMORY |
|
47 |
|
48 # Parameters to be monkey patched in beremiz customizations |
|
49 BACNET_VENDOR_ID = 9999 |
|
50 BACNET_VENDOR_NAME = "Beremiz.org" |
|
51 BACNET_DEVICE_MODEL_NAME = "Beremiz PLC" |
|
52 |
|
53 ################################################### |
|
54 ################################################### |
|
55 # # |
|
56 # S L A V E D E V I C E # |
|
57 # # |
|
58 ################################################### |
|
59 ################################################### |
|
60 |
|
61 # NOTE: Objects of class _BacnetSlavePlug are never instantiated directly. |
|
62 # The objects are instead instantiated from class FinalCTNClass |
|
63 # FinalCTNClass inherits from: - ConfigTreeNode |
|
64 # - The tree node plug (in our case _BacnetSlavePlug) |
|
65 #class _BacnetSlavePlug: |
|
66 class RootClass: |
|
67 XSD = """<?xml version="1.0" encoding="ISO-8859-1" ?> |
|
68 <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"> |
|
69 <xsd:element name="BACnetServerNode"> |
|
70 <xsd:complexType> |
|
71 <xsd:attribute name="Network_Interface" type="xsd:string" use="optional" default="eth0"/> |
|
72 <xsd:attribute name="UDP_Port_Number" use="optional" default="47808"> |
|
73 <xsd:simpleType> |
|
74 <xsd:restriction base="xsd:integer"> |
|
75 <xsd:minInclusive value="0"/> |
|
76 <xsd:maxInclusive value="65535"/> |
|
77 </xsd:restriction> |
|
78 </xsd:simpleType> |
|
79 </xsd:attribute> |
|
80 <xsd:attribute name="BACnet_Communication_Control_Password" |
|
81 type="xsd:string" use="optional" default="Malba Tahan"/> |
|
82 <xsd:attribute name="BACnet_Device_ID" use="optional" default="0"> |
|
83 <xsd:simpleType> |
|
84 <xsd:restriction base="xsd:integer"> |
|
85 <xsd:minInclusive value="0"/> |
|
86 <xsd:maxInclusive value="4194302"/> |
|
87 </xsd:restriction> |
|
88 </xsd:simpleType> |
|
89 </xsd:attribute> |
|
90 <xsd:attribute name="BACnet_Device_Name" type="xsd:string" use="optional" default="Beremiz device 0"/> |
|
91 <xsd:attribute name="BACnet_Device_Location" type="xsd:string" use="optional" default=""/> |
|
92 <xsd:attribute name="BACnet_Device_Description" type="xsd:string" use="optional" default="Beremiz device 0"/> |
|
93 <xsd:attribute name="BACnet_Device_Application_Software_Version" type="xsd:string" use="optional" default="1.0"/> |
|
94 </xsd:complexType> |
|
95 </xsd:element> |
|
96 </xsd:schema> |
|
97 """ |
|
98 # NOTE: BACnet device (object) IDs are 22 bits long (not counting the 10 bits for the type ID) |
|
99 # so the Device instance ID is limited from 0 to 22^2-1 = 4194303 |
|
100 # However, 4194303 is reserved for special use (similar to NULL pointer), so last |
|
101 # valid ID becomes 4194302 |
|
102 |
|
103 |
|
104 # The class/object that will render the graphical interface to edit the |
|
105 # BacnetSlavePlug's configuration parameters. The object of class BacnetSlaveEditorPlug |
|
106 # will be instantiated by the ConfigTreeNode class. |
|
107 # This BacnetSlaveEditorPlug object can be accessed from _BacnetSlavePlug as |
|
108 # 'self._View' |
|
109 # See the following note to understand how this is possible! |
|
110 # |
|
111 # NOTE: Objects of class _BacnetSlavePlug are never instantiated directly. |
|
112 # The objects are instead instantiated from class FinalCTNClass |
|
113 # FinalCTNClass inherits from: - ConfigTreeNode |
|
114 # - The tree node plug (in our case _BacnetSlavePlug) |
|
115 # |
|
116 # This means that objects of class _BacnetSlavePlug may safely access all the members |
|
117 # of classes ConfigTreeNode as well as FinalCTNClass (since they are always instantiated |
|
118 # as a FinalCTNClass) |
|
119 EditorType = BacnetSlaveEditorPlug |
|
120 |
|
121 # The following classes follow the model/viewer design pattern |
|
122 # |
|
123 # _BacnetSlavePlug - contains the model (i.e. configuration parameters) |
|
124 # BacnetSlaveEditorPlug - contains the viewer (and editor, so it includes the 'controller' part of the |
|
125 # design pattern which in this case is not separated from the viewer) |
|
126 # |
|
127 # The _BacnetSlavePlug object is 'permanent', i.e. it exists as long as the beremiz project is open |
|
128 # The BacnetSlaveEditorPlug object is 'transient', i.e. it exists only while the editor is visible/open |
|
129 # in the editing panel. It is destoryed whenever |
|
130 # the user closes the corresponding tab in the |
|
131 # editing panel, and a new object is created when |
|
132 # the editor is re-opened. |
|
133 # |
|
134 # _BacnetSlavePlug contains: AV_ObjTable, ... |
|
135 # (these are the objects that actually store the config parameters or 'model' |
|
136 # and are therefore stored to a file) |
|
137 # |
|
138 # _BacnetSlavePlug contains: AV_VarEditor, ... |
|
139 # (these are the objects that implement a grid table to edit/view the |
|
140 # corresponding mode parameters) |
|
141 # |
|
142 # Logic: |
|
143 # - The xx_VarEditor classes inherit from wx.grid.Grid |
|
144 # - The xx_ObjTable classes inherit from wx.grid.PyGridTableBase |
|
145 # To be more precise, the inheritance tree is actually: |
|
146 # xx_VarEditor -> ObjectGrid -> CustomGrid -> wx.grid.Grid |
|
147 # xx_ObjTable -> ObjectTable -> CustomTable -> wx.grid.PyGridTableBase) |
|
148 # |
|
149 # Note that wx.grid.Grid is prepared to work with wx.grid.PyGridTableBase as the container of |
|
150 # data that is displayed and edited in the Grid. |
|
151 |
|
152 |
|
153 ConfNodeMethods = [ |
|
154 {"bitmap" : "ExportSlave", |
|
155 "name" : _("Export slave"), |
|
156 "tooltip" : _("Export BACnet slave to EDE file"), |
|
157 "method" : "_ExportBacnetSlave"}, |
|
158 ] |
|
159 |
|
160 def __init__(self): |
|
161 # Initialize the dictionary that stores the current configuration for the Analog/Digital/MultiValued Variables |
|
162 # in this BACnet server. |
|
163 self.ObjTablesData = {} |
|
164 self.ObjTablesData[ "AV_Obj"] = [] # Each list will contain an entry for each row in the xxxxVar grid!! |
|
165 self.ObjTablesData[ "AO_Obj"] = [] # Each entry/row will be a dictionary |
|
166 self.ObjTablesData[ "AI_Obj"] = [] # Each dictionary will contain all entries/data |
|
167 self.ObjTablesData[ "BV_Obj"] = [] # for one row in the grid. |
|
168 self.ObjTablesData[ "BO_Obj"] = [] # Same structure as explained above... |
|
169 self.ObjTablesData[ "BI_Obj"] = [] # Same structure as explained above... |
|
170 self.ObjTablesData["MSV_Obj"] = [] # Same structure as explained above... |
|
171 self.ObjTablesData["MSO_Obj"] = [] # Same structure as explained above... |
|
172 self.ObjTablesData["MSI_Obj"] = [] # Same structure as explained above... |
|
173 |
|
174 self.ObjTablesData["EDEfile_parm"] = {"next_EDE_file_version":1} |
|
175 # EDE files inlcude extra parameters (ex. file version) |
|
176 # We would like to save the parameters the user configures |
|
177 # so they are available the next time the user opens the project. |
|
178 # Since this plugin is only storing the ObjTablesData[] dict |
|
179 # to file, we add that info to this dictionary too. |
|
180 # Yes, I know this is kind of a hack. |
|
181 |
|
182 filepath = self.GetFileName() |
|
183 if(os.path.isfile(filepath)): |
|
184 self.LoadFromFile(filepath) |
|
185 |
|
186 self.ObjTables = {} |
|
187 self.ObjTables[ "AV_Obj"] = ObjectTable(self, self.ObjTablesData[ "AV_Obj"], AVObject) |
|
188 self.ObjTables[ "AO_Obj"] = ObjectTable(self, self.ObjTablesData[ "AO_Obj"], AOObject) |
|
189 self.ObjTables[ "AI_Obj"] = ObjectTable(self, self.ObjTablesData[ "AI_Obj"], AIObject) |
|
190 self.ObjTables[ "BV_Obj"] = ObjectTable(self, self.ObjTablesData[ "BV_Obj"], BVObject) |
|
191 self.ObjTables[ "BO_Obj"] = ObjectTable(self, self.ObjTablesData[ "BO_Obj"], BOObject) |
|
192 self.ObjTables[ "BI_Obj"] = ObjectTable(self, self.ObjTablesData[ "BI_Obj"], BIObject) |
|
193 self.ObjTables["MSV_Obj"] = ObjectTable(self, self.ObjTablesData["MSV_Obj"], MSVObject) |
|
194 self.ObjTables["MSO_Obj"] = ObjectTable(self, self.ObjTablesData["MSO_Obj"], MSOObject) |
|
195 self.ObjTables["MSI_Obj"] = ObjectTable(self, self.ObjTablesData["MSI_Obj"], MSIObject) |
|
196 # list containing the data in the table <--^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
|
197 |
|
198 |
|
199 ###################################### |
|
200 # Functions to be called by CTNClass # |
|
201 ###################################### |
|
202 # The following functions would be somewhat equvalent to virtual functions/methods in C++ classes |
|
203 # They will be called by the base class (CTNClass) from which this _BacnetSlavePlug class derives. |
|
204 |
|
205 def GetCurrentNodeName(self): |
|
206 return self.CTNName() |
|
207 |
|
208 def GetFileName(self): |
|
209 return os.path.join(self.CTNPath(), 'bacnet_slave') |
|
210 |
|
211 def OnCTNSave(self, from_project_path=None): |
|
212 return self.SaveToFile(self.GetFileName()) |
|
213 |
|
214 |
|
215 def CTNTestModified(self): |
|
216 # self.ChangesToSave: Check whether any of the parameters, defined in the XSD above, were changed. |
|
217 # This is handled by the ConfigTreeNode class |
|
218 # (Remember that no objects are ever instantiated from _BacnetSlavePlug. |
|
219 # Objects are instead created from FinalCTNClass, which derives from |
|
220 # _BacnetSlavePlug and ConfigTreeNode. This means that we can exceptionally |
|
221 # consider that all objects of type _BacnetSlavePlug will also be a ConfigTreeNode). |
|
222 result = self.ChangesToSave or self.ObjTables[ "AV_Obj"].ChangesToSave \ |
|
223 or self.ObjTables[ "AO_Obj"].ChangesToSave \ |
|
224 or self.ObjTables[ "AI_Obj"].ChangesToSave \ |
|
225 or self.ObjTables[ "BV_Obj"].ChangesToSave \ |
|
226 or self.ObjTables[ "BO_Obj"].ChangesToSave \ |
|
227 or self.ObjTables[ "BI_Obj"].ChangesToSave \ |
|
228 or self.ObjTables["MSV_Obj"].ChangesToSave \ |
|
229 or self.ObjTables["MSO_Obj"].ChangesToSave \ |
|
230 or self.ObjTables["MSI_Obj"].ChangesToSave |
|
231 return result |
|
232 |
|
233 ### Currently not needed. Override _OpenView() in case we need to do some special stuff whenever the editor is opened! |
|
234 ##def _OpenView(self, name=None, onlyopened=False): |
|
235 ##print "_BacnetSlavePlug._OpenView() Called!!!" |
|
236 ##ConfigTreeNode._OpenView(self, name, onlyopened) |
|
237 ###print self._View |
|
238 #####if self._View is not None: |
|
239 #####self._View.SetBusId(self.GetCurrentLocation()) |
|
240 ##return self._View |
|
241 |
|
242 |
|
243 def GetVariableLocationTree(self): |
|
244 current_location = self.GetCurrentLocation() |
|
245 # see comment in CTNGenerate_C regarding identical line of code! |
|
246 locstr = ".".join(map(str,current_location)) |
|
247 |
|
248 # IDs used by BACnet to identify object types/class. |
|
249 # OBJECT_ANALOG_INPUT = 0, |
|
250 # OBJECT_ANALOG_OUTPUT = 1, |
|
251 # OBJECT_ANALOG_VALUE = 2, |
|
252 # OBJECT_BINARY_INPUT = 3, |
|
253 # OBJECT_BINARY_OUTPUT = 4, |
|
254 # OBJECT_BINARY_VALUE = 5, |
|
255 # OBJECT_MULTI_STATE_INPUT = 13, |
|
256 # OBJECT_MULTI_STATE_OUTPUT = 14, |
|
257 # OBJECT_MULTI_STATE_VALUE = 19, |
|
258 # |
|
259 # Since Binary Value, Analog Value, etc. objects may use the same |
|
260 # object ID (since they have distinct class IDs), we must also distinguish them in some way in |
|
261 # the %MX0.3.4 IEC 61131-3 syntax. |
|
262 # |
|
263 # For this reason we add the BACnet class identifier to the %MX0.5.3 location. |
|
264 # For example, for a BACnet plugin in location '0' of the Beremiz configuration tree, |
|
265 # all Binary Values will be mapped onto: %MX0.5.xxx (xxx is object ID) |
|
266 # all Multi State Values will be mapped onto: %MB0.19.xxx (xxx is object ID) |
|
267 # all Analog Values will be mapped onto: %MD0.2.xxx (xxx is object ID) |
|
268 # etc.. |
|
269 # |
|
270 # Value objects will be mapped onto %M |
|
271 # Input objects will be mapped onto %I |
|
272 # Output objects will be mapped onto %Q |
|
273 |
|
274 BACnetEntries = [] |
|
275 BACnetEntries.append(self.GetSlaveLocationTree( |
|
276 self.ObjTablesData[ "AV_Obj"], 32, 'REAL', 'D', locstr+ '.2', 'Analog Values')) |
|
277 BACnetEntries.append(self.GetSlaveLocationTree( |
|
278 self.ObjTablesData[ "AO_Obj"], 32, 'REAL', 'D', locstr+ '.1', 'Analog Outputs')) |
|
279 BACnetEntries.append(self.GetSlaveLocationTree( |
|
280 self.ObjTablesData[ "AI_Obj"], 32, 'REAL', 'D', locstr+ '.0', 'Analog Inputs')) |
|
281 BACnetEntries.append(self.GetSlaveLocationTree( |
|
282 self.ObjTablesData[ "BV_Obj"], 1, 'BOOL', 'X', locstr+ '.5', 'Binary Values')) |
|
283 BACnetEntries.append(self.GetSlaveLocationTree( |
|
284 self.ObjTablesData[ "BO_Obj"], 1, 'BOOL', 'X', locstr+ '.4', 'Binary Outputs')) |
|
285 BACnetEntries.append(self.GetSlaveLocationTree( |
|
286 self.ObjTablesData[ "BI_Obj"], 1, 'BOOL', 'X', locstr+ '.3', 'Binary Inputs')) |
|
287 BACnetEntries.append(self.GetSlaveLocationTree( |
|
288 self.ObjTablesData["MSV_Obj"], 8, 'BYTE', 'B', locstr+'.19', 'Multi State Values')) |
|
289 BACnetEntries.append(self.GetSlaveLocationTree( |
|
290 self.ObjTablesData["MSO_Obj"], 8, 'BYTE', 'B', locstr+'.14', 'Multi State Outputs')) |
|
291 BACnetEntries.append(self.GetSlaveLocationTree( |
|
292 self.ObjTablesData["MSI_Obj"], 8, 'BYTE', 'B', locstr+'.13', 'Multi State Inputs')) |
|
293 |
|
294 return {"name": self.BaseParams.getName(), |
|
295 "type": LOCATION_CONFNODE, |
|
296 "location": locstr + ".x", |
|
297 "children": BACnetEntries} |
|
298 |
|
299 |
|
300 ############################ |
|
301 # Helper functions/methods # |
|
302 ############################ |
|
303 # a helper function to GetVariableLocationTree() |
|
304 def GetSlaveLocationTree(self, ObjTablesData, size_in_bits, IECdatatype, location_size, location_str, name): |
|
305 BACnetObjectEntries = [] |
|
306 for xx_ObjProp in ObjTablesData: |
|
307 BACnetObjectEntries.append({ |
|
308 "name": str(xx_ObjProp["Object Identifier"]) + ': ' + xx_ObjProp["Object Name"], |
|
309 "type": LOCATION_VAR_MEMORY, # LOCATION_VAR_INPUT, LOCATION_VAR_OUTPUT, or LOCATION_VAR_MEMORY |
|
310 "size": size_in_bits, # 1 or 16 |
|
311 "IEC_type": IECdatatype, # 'BOOL', 'WORD', ... |
|
312 "var_name": "var_name", # seems to be ignored?? |
|
313 "location": location_size + location_str + "." + str(xx_ObjProp["Object Identifier"]), |
|
314 "description": "description", # seems to be ignored? |
|
315 "children": []}) |
|
316 |
|
317 BACnetEntries = [] |
|
318 return {"name": name, |
|
319 "type": LOCATION_CONFNODE, |
|
320 "location": location_str + ".x", |
|
321 "children": BACnetObjectEntries} |
|
322 |
|
323 |
|
324 # Returns a dictionary with: |
|
325 # keys: names of BACnet objects |
|
326 # value: number of BACnet objects using this same name |
|
327 # (values larger than 1 indicates an error as BACnet requires unique names) |
|
328 def GetObjectNamesCount(self): |
|
329 # The dictionary is built by first creating a list containing the names of _ALL_ |
|
330 # BACnet objects currently configured by the user (using the GUI) |
|
331 ObjectNames = [] |
|
332 ObjectNames.extend(self.ObjTables[ "AV_Obj"].GetAllValuesByName("Object Name")) |
|
333 ObjectNames.extend(self.ObjTables[ "AO_Obj"].GetAllValuesByName("Object Name")) |
|
334 ObjectNames.extend(self.ObjTables[ "AI_Obj"].GetAllValuesByName("Object Name")) |
|
335 ObjectNames.extend(self.ObjTables[ "BV_Obj"].GetAllValuesByName("Object Name")) |
|
336 ObjectNames.extend(self.ObjTables[ "BO_Obj"].GetAllValuesByName("Object Name")) |
|
337 ObjectNames.extend(self.ObjTables[ "BI_Obj"].GetAllValuesByName("Object Name")) |
|
338 ObjectNames.extend(self.ObjTables["MSV_Obj"].GetAllValuesByName("Object Name")) |
|
339 ObjectNames.extend(self.ObjTables["MSO_Obj"].GetAllValuesByName("Object Name")) |
|
340 ObjectNames.extend(self.ObjTables["MSI_Obj"].GetAllValuesByName("Object Name")) |
|
341 # This list is then transformed into a collections.Counter class |
|
342 # Which is then transformed into a dictionary using dict() |
|
343 return dict(Counter(ObjectNames)) |
|
344 |
|
345 # Check whether the current configuration contains BACnet objects configured |
|
346 # with the same identical object name (returns True or False) |
|
347 def HasDuplicateObjectNames(self): |
|
348 ObjectNamesCount = self.GetObjectNamesCount() |
|
349 for ObjName in ObjectNamesCount: |
|
350 if ObjectNamesCount[ObjName] > 1: |
|
351 return True |
|
352 return False |
|
353 |
|
354 # Check whether any object ID is used more than once (not valid in BACnet) |
|
355 # (returns True or False) |
|
356 def HasDuplicateObjectIDs(self): |
|
357 res = self.ObjTables[ "AV_Obj"].HasDuplicateObjectIDs() |
|
358 res = res or self.ObjTables[ "AO_Obj"].HasDuplicateObjectIDs() |
|
359 res = res or self.ObjTables[ "AI_Obj"].HasDuplicateObjectIDs() |
|
360 res = res or self.ObjTables[ "BV_Obj"].HasDuplicateObjectIDs() |
|
361 res = res or self.ObjTables[ "BO_Obj"].HasDuplicateObjectIDs() |
|
362 res = res or self.ObjTables[ "BI_Obj"].HasDuplicateObjectIDs() |
|
363 res = res or self.ObjTables["MSV_Obj"].HasDuplicateObjectIDs() |
|
364 res = res or self.ObjTables["MSO_Obj"].HasDuplicateObjectIDs() |
|
365 res = res or self.ObjTables["MSI_Obj"].HasDuplicateObjectIDs() |
|
366 return res |
|
367 |
|
368 |
|
369 ####################################################### |
|
370 # Methods related to files (saving/loading/exporting) # |
|
371 ####################################################### |
|
372 def SaveToFile(self, filepath): |
|
373 # Save node data in file |
|
374 # The configuration data declared in the XSD string will be saved by the ConfigTreeNode class, |
|
375 # so we only need to save the data that is stored in ObjTablesData objects |
|
376 # Note that we do not store the ObjTables objects. ObjTables is of a class that |
|
377 # contains more stuff we do not need to store. Actually it is a bad idea to store |
|
378 # this extra stuff (as we would make the files we generate dependent on the actual |
|
379 # version of the wx library we are using!!! Remember that ObjTables evetually |
|
380 # derives/inherits from wx.grid.PyGridTableBase). Another reason not to store the whole |
|
381 # object is because it is not pickable (i.e. pickle.dump() cannot handle it)!! |
|
382 try: |
|
383 fd = open(filepath, "w") |
|
384 pickle.dump(self.ObjTablesData, fd) |
|
385 fd.close() |
|
386 # On successfull save, reset flags to indicate no more changes that need saving |
|
387 self.ObjTables[ "AV_Obj"].ChangesToSave = False |
|
388 self.ObjTables[ "AO_Obj"].ChangesToSave = False |
|
389 self.ObjTables[ "AI_Obj"].ChangesToSave = False |
|
390 self.ObjTables[ "BV_Obj"].ChangesToSave = False |
|
391 self.ObjTables[ "BO_Obj"].ChangesToSave = False |
|
392 self.ObjTables[ "BI_Obj"].ChangesToSave = False |
|
393 self.ObjTables["MSV_Obj"].ChangesToSave = False |
|
394 self.ObjTables["MSO_Obj"].ChangesToSave = False |
|
395 self.ObjTables["MSI_Obj"].ChangesToSave = False |
|
396 return True |
|
397 except: |
|
398 return _("Unable to save to file \"%s\"!")%filepath |
|
399 |
|
400 def LoadFromFile(self, filepath): |
|
401 # Load the data that is saved in SaveToFile() |
|
402 try: |
|
403 fd = open(filepath, "r") |
|
404 self.ObjTablesData = pickle.load(fd) |
|
405 fd.close() |
|
406 return True |
|
407 except: |
|
408 return _("Unable to load file \"%s\"!")%filepath |
|
409 |
|
410 def _ExportBacnetSlave(self): |
|
411 dialog = wx.FileDialog(self.GetCTRoot().AppFrame, |
|
412 _("Choose a file"), |
|
413 os.path.expanduser("~"), |
|
414 "%s_EDE.csv" % self.CTNName(), |
|
415 _("EDE files (*_EDE.csv)|*_EDE.csv|All files|*.*"), |
|
416 wx.SAVE|wx.OVERWRITE_PROMPT) |
|
417 if dialog.ShowModal() == wx.ID_OK: |
|
418 result = self.GenerateEDEFile(dialog.GetPath()) |
|
419 result = False |
|
420 if result: |
|
421 self.GetCTRoot().logger.write_error(_("Error: Export slave failed\n")) |
|
422 dialog.Destroy() |
|
423 |
|
424 |
|
425 def GenerateEDEFile(self, filename): |
|
426 template_file_dir = os.path.join(os.path.split(__file__)[0],"ede_files") |
|
427 |
|
428 #The BACnetServerNode attribute is added dynamically by ConfigTreeNode._AddParamsMembers() |
|
429 # It will be an XML parser object created by GenerateParserFromXSDstring(self.XSD).CreateRoot() |
|
430 BACnet_Device_ID = self.BACnetServerNode.getBACnet_Device_ID() |
|
431 |
|
432 # The EDE file contains a header that includes general project data (name, author, ...) |
|
433 # Instead of asking the user for this data, we get it from the configuration |
|
434 # of the Beremiz project itself. |
|
435 # We ask the root Config Tree Node for the data... |
|
436 ProjProp = {} |
|
437 FileProp = {} |
|
438 CTN_Root = self.GetCTRoot() # this should be an object of class ProjectController |
|
439 Project = CTN_Root.Project # this should be an object capable of parsing |
|
440 # PLCopen XML files. The parser is created automatically |
|
441 # (i.e. using GenerateParserFromXSD() from xmlclass module) |
|
442 # using the PLCopen XSD file defining the format of the XML. |
|
443 # See the file plcopen/plcopen.py |
|
444 if Project is not None: |
|
445 # getcontentHeader() and getfileHeader() are functions that are conditionally defined in |
|
446 # plcopn/plcopen.py We cannot rely on their existance |
|
447 if getattr(Project, "getcontentHeader", None) is not None: |
|
448 ProjProp = Project.getcontentHeader() |
|
449 # getcontentHeader() returns a dictionary. Available keys are: |
|
450 # "projectName", "projectVersion", "modificationDateTime", |
|
451 # "organization", "authorName", "language", "pageSize", "scaling" |
|
452 if getattr(Project, "getfileHeader", None) is not None: |
|
453 FileProp = Project.getfileHeader() |
|
454 # getfileHeader() returns a dictionary. Available keys are: |
|
455 # "companyName", "companyURL", "productName", "productVersion", |
|
456 # "productRelease", "creationDateTime", "contentDescription" |
|
457 |
|
458 ProjName = "" |
|
459 if "projectName" in ProjProp: |
|
460 ProjName = ProjProp["projectName"] |
|
461 ProjAuthor = "" |
|
462 if "companyName" in FileProp: |
|
463 ProjAuthor += "(" + FileProp["companyName"] + ")" |
|
464 if "authorName" in ProjProp: |
|
465 ProjAuthor = ProjProp["authorName"] + " " + ProjAuthor |
|
466 |
|
467 projdata_dict = {} |
|
468 projdata_dict["Project Name"] = ProjName |
|
469 projdata_dict["Project Author"] = ProjAuthor |
|
470 projdata_dict["Current Time"] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') |
|
471 projdata_dict["EDE file version"] = self.ObjTablesData["EDEfile_parm"]["next_EDE_file_version"] |
|
472 |
|
473 # Next time we generate an EDE file, use another version! |
|
474 self.ObjTablesData["EDEfile_parm"]["next_EDE_file_version"] += 1 |
|
475 |
|
476 AX_params_format = "%(Object Name)s;" + str(BACnet_Device_ID) + ";%(Object Name)s;%(BACnetObjTypeID)s;%(Object Identifier)s;%(Description)s;0;;;%(Settable)s;N;;;;%(Unit ID)s;" |
|
477 |
|
478 BX_params_format = "%(Object Name)s;" + str(BACnet_Device_ID) + ";%(Object Name)s;%(BACnetObjTypeID)s;%(Object Identifier)s;%(Description)s;0;0;1;%(Settable)s;N;;;;;" |
|
479 |
|
480 MSX_params_format = "%(Object Name)s;" + str(BACnet_Device_ID) + ";%(Object Name)s;%(BACnetObjTypeID)s;%(Object Identifier)s;%(Description)s;1;1;%(Number of States)s;%(Settable)s;N;;;;;" |
|
481 |
|
482 Objects_List = [] |
|
483 for ObjType, params_format in [ |
|
484 ("AV" , AX_params_format ), ("AO" , AX_params_format ), ("AI" , AX_params_format ), |
|
485 ("BV" , BX_params_format ), ("BO" , BX_params_format ), ("BI" , BX_params_format ), |
|
486 ("MSV", MSX_params_format ), ("MSO", MSX_params_format ), ("MSI", MSX_params_format ) |
|
487 ]: |
|
488 self.ObjTables[ObjType + "_Obj"].UpdateAllVirtualProperties() |
|
489 for ObjProp in self.ObjTablesData[ObjType + "_Obj"]: |
|
490 Objects_List.append(params_format % ObjProp) |
|
491 |
|
492 # Normalize filename |
|
493 for extension in ["_EDE.csv", "_ObjTypes.csv", "_StateTexts.csv", "_Units.csv"]: |
|
494 if filename.lower().endswith(extension.lower()): |
|
495 filename = filename[:-len(extension)] |
|
496 |
|
497 # EDE_header |
|
498 generate_file_name = filename + "_EDE.csv" |
|
499 template_file_name = os.path.join(template_file_dir,"template_EDE.csv") |
|
500 generate_file_content = open(template_file_name).read() % projdata_dict |
|
501 generate_file_handle = open(generate_file_name,'w') |
|
502 generate_file_handle .write(generate_file_content) |
|
503 generate_file_handle .write("\n".join(Objects_List)) |
|
504 generate_file_handle .close() |
|
505 |
|
506 # templates of remaining files do not need changes. They are simply copied unchanged! |
|
507 for extension in ["_ObjTypes.csv", "_StateTexts.csv", "_Units.csv"]: |
|
508 generate_file_name = filename + extension |
|
509 template_file_name = os.path.join(template_file_dir,"template" + extension) |
|
510 generate_file_content = open(template_file_name).read() |
|
511 generate_file_handle = open(generate_file_name,'w') |
|
512 generate_file_handle .write(generate_file_content) |
|
513 generate_file_handle .close() |
|
514 |
|
515 |
|
516 ############################# |
|
517 # Generate the source files # |
|
518 ############################# |
|
519 def CTNGenerate_C(self, buildpath, locations): |
|
520 # Determine the current location in Beremiz's project configuration tree |
|
521 current_location = self.GetCurrentLocation() |
|
522 # The current location of this plugin in Beremiz's configuration tree, separated by underscores |
|
523 # NOTE: Since BACnet plugin currently does not use sub-branches in the tree (in other words, this |
|
524 # _BacnetSlavePlug class was actually renamed as the RootClass), the current_location_dots |
|
525 # will actually be a single number (e.g.: 0 or 3 or 6, corresponding to the location |
|
526 # in which the plugin was inserted in the Beremiz configuration tree on Beremiz's left panel). |
|
527 locstr = "_".join(map(str,current_location)) |
|
528 |
|
529 # First check whether all the current parameters (inserted by user in the GUI) are valid... |
|
530 if self.HasDuplicateObjectNames(): |
|
531 self.GetCTRoot().logger.write_warning(_("Error: BACnet server '%s.x: %s' contains objects with duplicate object names.\n")%(locstr, self.CTNName())) |
|
532 raise Exception, False |
|
533 # TODO: return an error code instead of raising an exception (currently unsupported by Beremiz) |
|
534 |
|
535 if self.HasDuplicateObjectIDs(): |
|
536 self.GetCTRoot().logger.write_warning(_("Error: BACnet server '%s.x: %s' contains objects with duplicate object identifiers.\n")%(locstr, self.CTNName())) |
|
537 raise Exception, False |
|
538 # TODO: return an error code instead of raising an exception (currently unsupported by Beremiz) |
|
539 |
|
540 #------------------------------------------------------------------------------- |
|
541 # Create and populate the loc_dict dictionary with all parameters needed to configure |
|
542 # the generated source code (.c and .h files) |
|
543 #------------------------------------------------------------------------------- |
|
544 |
|
545 # 1) Create the dictionary (loc_dict = {}) |
|
546 loc_dict = {} |
|
547 loc_dict["locstr"] = locstr |
|
548 |
|
549 #The BACnetServerNode attribute is added dynamically by ConfigTreeNode._AddParamsMembers() |
|
550 # It will be an XML parser object created by GenerateParserFromXSDstring(self.XSD).CreateRoot() |
|
551 loc_dict["network_interface"] = self.BACnetServerNode.getNetwork_Interface() |
|
552 loc_dict["port_number"] = self.BACnetServerNode.getUDP_Port_Number() |
|
553 loc_dict["BACnet_Device_ID"] = self.BACnetServerNode.getBACnet_Device_ID() |
|
554 loc_dict["BACnet_Device_Name"] = self.BACnetServerNode.getBACnet_Device_Name() |
|
555 loc_dict["BACnet_Comm_Control_Password"] = self.BACnetServerNode.getBACnet_Communication_Control_Password() |
|
556 loc_dict["BACnet_Device_Location"] = self.BACnetServerNode.getBACnet_Device_Location() |
|
557 loc_dict["BACnet_Device_Description"] = self.BACnetServerNode.getBACnet_Device_Description() |
|
558 loc_dict["BACnet_Device_AppSoft_Version"]= self.BACnetServerNode.getBACnet_Device_Application_Software_Version() |
|
559 loc_dict["BACnet_Vendor_ID"] = BACNET_VENDOR_ID |
|
560 loc_dict["BACnet_Vendor_Name"] = BACNET_VENDOR_NAME |
|
561 loc_dict["BACnet_Model_Name"] = BACNET_DEVICE_MODEL_NAME |
|
562 |
|
563 # 2) Add the data specific to each BACnet object type |
|
564 # For each BACnet object type, start off by creating some intermediate helpful lists |
|
565 # a) parameters_list containing the strings that will |
|
566 # be included in the C source code, and which will initialize the struct with the |
|
567 # object (Analog Value, Binary Value, or Multi State Value) parameters |
|
568 # b) locatedvar_list containing the strings that will |
|
569 # declare the memory to store the located variables, as well as the |
|
570 # pointers (required by matiec) that point to that memory. |
|
571 |
|
572 # format for delaring IEC 61131-3 variable (and pointer) onto which BACnet object is mapped |
|
573 locvar_format = '%(Ctype)s ___%(loc)s_%(Object Identifier)s; ' + \ |
|
574 '%(Ctype)s *__%(loc)s_%(Object Identifier)s = &___%(loc)s_%(Object Identifier)s;' |
|
575 |
|
576 # format for initializing a ANALOG_VALUE_DESCR struct in C code |
|
577 # also valid for ANALOG_INPUT and ANALOG_OUTPUT |
|
578 AX_params_format = '{&___%(loc)s_%(Object Identifier)s, ' + \ |
|
579 '%(Object Identifier)s, "%(Object Name)s", "%(Description)s", %(Unit ID)d}' |
|
580 # format for initializing a BINARY_VALUE_DESCR struct in C code |
|
581 # also valid for BINARY_INPUT and BINARY_OUTPUT |
|
582 BX_params_format = '{&___%(loc)s_%(Object Identifier)s, ' + \ |
|
583 '%(Object Identifier)s, "%(Object Name)s", "%(Description)s"}' |
|
584 |
|
585 # format for initializing a MULTISTATE_VALUE_DESCR struct in C code |
|
586 # also valid for MULTISTATE_INPUT and MULTISTATE_OUTPUT |
|
587 MSX_params_format = '{&___%(loc)s_%(Object Identifier)s, ' + \ |
|
588 '%(Object Identifier)s, "%(Object Name)s", "%(Description)s", %(Number of States)s}' |
|
589 |
|
590 AV_locstr = 'MD' + locstr+'_2' # see the comment in GetVariableLocationTree() to grok the '_2' |
|
591 AO_locstr = 'QD' + locstr+'_1' # see the comment in GetVariableLocationTree() to grok the '_1' |
|
592 AI_locstr = 'ID' + locstr+'_0' # see the comment in GetVariableLocationTree() to grok the '_0' |
|
593 BV_locstr = 'MX' + locstr+'_5' # see the comment in GetVariableLocationTree() to grok the '_5' |
|
594 BO_locstr = 'QX' + locstr+'_4' # see the comment in GetVariableLocationTree() to grok the '_4' |
|
595 BI_locstr = 'IX' + locstr+'_3' # see the comment in GetVariableLocationTree() to grok the '_3' |
|
596 MSV_locstr = 'MB' + locstr+'_19' # see the comment in GetVariableLocationTree() to grok the '_19' |
|
597 MSO_locstr = 'QB' + locstr+'_14' # see the comment in GetVariableLocationTree() to grok the '_14' |
|
598 MSI_locstr = 'IB' + locstr+'_13' # see the comment in GetVariableLocationTree() to grok the '_13' |
|
599 |
|
600 |
|
601 for ObjType, ObjLocStr, params_format in [ |
|
602 ("AV" , AV_locstr, AX_params_format ), |
|
603 ("AO" , AO_locstr, AX_params_format ), |
|
604 ("AI" , AI_locstr, AX_params_format ), |
|
605 ("BV" , BV_locstr, BX_params_format ), |
|
606 ("BO" , BO_locstr, BX_params_format ), |
|
607 ("BI" , BI_locstr, BX_params_format ), |
|
608 ("MSV" , MSV_locstr, MSX_params_format ), |
|
609 ("MSO" , MSO_locstr, MSX_params_format ), |
|
610 ("MSI" , MSI_locstr, MSX_params_format ) |
|
611 ]: |
|
612 parameters_list = [] |
|
613 locatedvar_list = [] |
|
614 self.ObjTables[ObjType + "_Obj"].UpdateAllVirtualProperties() |
|
615 for ObjProp in self.ObjTablesData[ObjType + "_Obj"]: |
|
616 ObjProp["loc" ] = ObjLocStr |
|
617 parameters_list.append(params_format % ObjProp) |
|
618 locatedvar_list.append(locvar_format % ObjProp) |
|
619 loc_dict[ ObjType + "_count"] = len( parameters_list ) |
|
620 loc_dict[ ObjType + "_param"] = ",\n".join( parameters_list ) |
|
621 loc_dict[ ObjType + "_lvars"] = "\n".join( locatedvar_list ) |
|
622 |
|
623 #---------------------------------------------------------------------- |
|
624 # Create the C source files that implement the BACnet server |
|
625 #---------------------------------------------------------------------- |
|
626 |
|
627 # Names of the .c files that will be generated, based on a template file with same name |
|
628 # (names without '.c' --> this will be added later) |
|
629 # main server.c file is handled separately |
|
630 Generated_BACnet_c_mainfile = "server" |
|
631 Generated_BACnet_c_files = ["ai", "ao", "av", "bi", "bo", "bv", "msi", "mso", "msv", "device"] |
|
632 |
|
633 # Names of the .h files that will be generated, based on a template file with same name |
|
634 # (names without '.h' --> this will be added later) |
|
635 Generated_BACnet_h_files = ["server", "device", "config_bacnet_for_beremiz", |
|
636 "ai", "ao", "av", "bi", "bo", "bv", "msi", "mso", "msv" |
|
637 ] |
|
638 |
|
639 # Generate the files with the source code |
|
640 postfix = "_".join(map(str, current_location)) |
|
641 template_file_dir = os.path.join(os.path.split(__file__)[0],"runtime") |
|
642 |
|
643 def generate_file(file_name, extension): |
|
644 generate_file_name = os.path.join(buildpath, "%s_%s.%s"%(file_name,postfix,extension)) |
|
645 template_file_name = os.path.join(template_file_dir,"%s.%s"%(file_name,extension)) |
|
646 generate_file_content = open(template_file_name).read() % loc_dict |
|
647 generate_file_handle = open(generate_file_name,'w') |
|
648 generate_file_handle .write(generate_file_content) |
|
649 generate_file_handle .close() |
|
650 |
|
651 for file_name in Generated_BACnet_c_files: |
|
652 generate_file(file_name, "c") |
|
653 for file_name in Generated_BACnet_h_files: |
|
654 generate_file(file_name, "h") |
|
655 generate_file(Generated_BACnet_c_mainfile, "c") |
|
656 Generated_BACnet_c_mainfile_name = \ |
|
657 os.path.join(buildpath, "%s_%s.%s"%(Generated_BACnet_c_mainfile,postfix,"c")) |
|
658 |
|
659 #---------------------------------------------------------------------- |
|
660 # Finally, define the compilation and linking commands and flags |
|
661 #---------------------------------------------------------------------- |
|
662 |
|
663 LDFLAGS = [] |
|
664 # when using dynamically linked library... |
|
665 #LDFLAGS.append(' -lbacnet') |
|
666 #LDFLAGS.append(' -L"'+BacnetLibraryPath+'"') |
|
667 #LDFLAGS.append(' "-Wl,-rpath,' + BacnetLibraryPath + '"') |
|
668 # when using static library: |
|
669 LDFLAGS.append(' "'+os.path.join(BacnetLibraryPath, "libbacnet.a")+'"') |
|
670 |
|
671 CFLAGS = ' -I"'+BacnetIncludePath+'"' |
|
672 CFLAGS += ' -I"'+BacnetIncludePortPath+'"' |
|
673 |
|
674 return [(Generated_BACnet_c_mainfile_name, CFLAGS)], LDFLAGS, True |
|
675 |
|
676 |
|
677 |
|
678 ################################################### |
|
679 ################################################### |
|
680 # # |
|
681 # R O O T C L A S S # |
|
682 # # |
|
683 ################################################### |
|
684 ################################################### |
|
685 #class RootClass: |
|
686 #XSD = """<?xml version="1.0" encoding="ISO-8859-1" ?> |
|
687 #<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"> |
|
688 #<xsd:element name="BACnetRoot"> |
|
689 #</xsd:element> |
|
690 #</xsd:schema> |
|
691 #""" |
|
692 #CTNChildrenTypes = [("BacnetSlave", _BacnetSlavePlug, "Bacnet Slave") |
|
693 ##,("XXX",_XXXXPlug, "XXX") |
|
694 #] |
|
695 |
|
696 #def CTNGenerate_C(self, buildpath, locations): |
|
697 #return [], "", True |