# HG changeset patch # User Laurent Bessard # Date 1362001245 -3600 # Node ID 58d07e039896ec9d85ab4531c2b4c2b5fe29b7b3 # Parent c9b0340ea0f5ef6292cf744de0fb2b70c98551af Added panel for managing ESI files from project and from database including module PDO alignment setting diff -r c9b0340ea0f5 -r 58d07e039896 etherlab/ConfigEditor.py --- a/etherlab/ConfigEditor.py Thu Feb 07 00:59:50 2013 +0100 +++ b/etherlab/ConfigEditor.py Wed Feb 27 22:40:45 2013 +0100 @@ -1,9 +1,13 @@ +import os + import wx import wx.grid import wx.gizmos - -from controls import CustomGrid, CustomTable +import wx.lib.buttons + +from controls import CustomGrid, CustomTable, FolderTree from editors.ConfTreeNodeEditor import ConfTreeNodeEditor, SCROLLBAR_UNIT +from util.BitmapLibrary import GetBitmap [ETHERCAT_VENDOR, ETHERCAT_GROUP, ETHERCAT_DEVICE] = range(3) @@ -13,157 +17,59 @@ else: parent.Append(helpString=help, id=id, kind=kind, item=text) -def GetSyncManagersTableColnames(): - _ = lambda x : x - return ["#", _("Name"), _("Start Address"), _("Default Size"), _("Control Byte"), _("Enable")] - -class SyncManagersTable(CustomTable): - - def GetValue(self, row, col): - if row < self.GetNumberRows(): - if col == 0: - return row - return self.data[row].get(self.GetColLabelValue(col, False), "") - def GetVariablesTableColnames(): _ = lambda x : x - return ["#", _("Name"), _("Index"), _("SubIndex"), _("Type"), _("PDO index"), _("PDO name"), _("PDO type")] - -[ID_NODEEDITOR, ID_NODEEDITORVENDORLABEL, - ID_NODEEDITORVENDOR, ID_NODEEDITORPRODUCTCODELABEL, - ID_NODEEDITORPRODUCTCODE, ID_NODEEDITORREVISIONNUMBERLABEL, - ID_NODEEDITORREVISIONNUMBER, ID_NODEEDITORPHYSICSLABEL, - ID_NODEEDITORPHYSICS, ID_NODEEDITORSYNCMANAGERSLABEL, - ID_NODEEDITORSYNCMANAGERSGRID, ID_NODEEDITORVARIABLESLABEL, - ID_NODEEDITORVARIABLESGRID, -] = [wx.NewId() for _init_ctrls in range(13)] + return ["#", _("Name"), _("Index"), _("SubIndex"), _("Type"), _("Access")] + +ACCESS_TYPES = { + 'ro': 'R', + 'wo': 'W', + 'rw': 'R/W'} + +def GetAccessValue(access, pdo_mapping): + value = ACCESS_TYPES.get(access) + if pdo_mapping != "": + value += "/P" + return value class NodeEditor(ConfTreeNodeEditor): - ID = ID_NODEEDITOR CONFNODEEDITOR_TABS = [ (_("Ethercat node"), "_create_EthercatNodeEditor")] - def _init_coll_MainSizer_Items(self, parent): - parent.AddSizer(self.SlaveInfosDetailsSizer, 0, border=5, flag=wx.TOP|wx.LEFT|wx.RIGHT|wx.GROW) - parent.AddWindow(self.SyncManagersLabel, 0, border=5, flag=wx.LEFT|wx.RIGHT|wx.GROW) - parent.AddWindow(self.SyncManagersGrid, 0, border=5, flag=wx.LEFT|wx.RIGHT|wx.GROW) - parent.AddWindow(self.VariablesLabel, 0, border=5, flag=wx.LEFT|wx.RIGHT|wx.GROW) - parent.AddWindow(self.VariablesGrid, 0, border=5, flag=wx.BOTTOM|wx.LEFT|wx.RIGHT|wx.GROW) - - def _init_coll_MainSizer_Growables(self, parent): - parent.AddGrowableCol(0) - parent.AddGrowableRow(2, 1) - parent.AddGrowableRow(4, 2) - - def _init_coll_SlaveInfosDetailsSizer_Items(self, parent): - parent.AddWindow(self.VendorLabel, 0, border=0, flag=wx.ALIGN_CENTER_VERTICAL|wx.GROW) - parent.AddWindow(self.Vendor, 0, border=0, flag=wx.GROW) - parent.AddWindow(self.ProductCodeLabel, 0, border=0, flag=wx.ALIGN_CENTER_VERTICAL|wx.GROW) - parent.AddWindow(self.ProductCode, 0, border=0, flag=wx.GROW) - parent.AddWindow(self.RevisionNumberLabel, 0, border=0, flag=wx.ALIGN_CENTER_VERTICAL|wx.GROW) - parent.AddWindow(self.RevisionNumber, 0, border=0, flag=wx.GROW) - parent.AddWindow(self.PhysicsLabel, 0, border=0, flag=wx.ALIGN_CENTER_VERTICAL|wx.GROW) - parent.AddWindow(self.Physics, 0, border=0, flag=wx.GROW) - - def _init_coll_SlaveInfosDetailsSizer_Growables(self, parent): - parent.AddGrowableCol(1) - parent.AddGrowableCol(3) - - def _init_sizers(self): - self.MainSizer = wx.FlexGridSizer(cols=1, hgap=0, rows=5, vgap=5) - self.SlaveInfosDetailsSizer = wx.FlexGridSizer(cols=4, hgap=5, rows=2, vgap=5) - - self._init_coll_MainSizer_Growables(self.MainSizer) - self._init_coll_MainSizer_Items(self.MainSizer) - self._init_coll_SlaveInfosDetailsSizer_Growables(self.SlaveInfosDetailsSizer) - self._init_coll_SlaveInfosDetailsSizer_Items(self.SlaveInfosDetailsSizer) - - self.EthercatNodeEditor.SetSizer(self.MainSizer) - def _create_EthercatNodeEditor(self, prnt): - self.EthercatNodeEditor = wx.ScrolledWindow(id=-1, name='SlavePanel', parent=prnt, - size=wx.Size(0, 0), style=wx.TAB_TRAVERSAL|wx.SUNKEN_BORDER|wx.HSCROLL|wx.VSCROLL) - self.EthercatNodeEditor.Bind(wx.EVT_SIZE, self.OnEthercatNodeEditorResize) - - self.VendorLabel = wx.StaticText(id=ID_NODEEDITORVENDORLABEL, - label=_('Vendor:'), name='VendorLabel', parent=self.EthercatNodeEditor, - pos=wx.Point(0, 0), size=wx.DefaultSize, style=0) - - self.Vendor = wx.TextCtrl(id=ID_NODEEDITORVENDOR, value='', - name='Vendor', parent=self.EthercatNodeEditor, pos=wx.Point(0, 0), - size=wx.Size(0, 24), style=wx.TE_READONLY) - - self.ProductCodeLabel = wx.StaticText(id=ID_NODEEDITORPRODUCTCODELABEL, - label=_('Product code:'), name='ProductCodeLabel', parent=self.EthercatNodeEditor, - pos=wx.Point(0, 0), size=wx.DefaultSize, style=0) - - self.ProductCode = wx.TextCtrl(id=ID_NODEEDITORPRODUCTCODE, value='', - name='ProductCode', parent=self.EthercatNodeEditor, pos=wx.Point(0, 0), - size=wx.Size(0, 24), style=wx.TE_READONLY) - - self.RevisionNumberLabel = wx.StaticText(id=ID_NODEEDITORREVISIONNUMBERLABEL, - label=_('Revision number:'), name='RevisionNumberLabel', parent=self.EthercatNodeEditor, - pos=wx.Point(0, 0), size=wx.DefaultSize, style=0) - - self.RevisionNumber = wx.TextCtrl(id=ID_NODEEDITORREVISIONNUMBER, value='', - name='RevisionNumber', parent=self.EthercatNodeEditor, pos=wx.Point(0, 0), - size=wx.Size(0, 24), style=wx.TE_READONLY) - - self.PhysicsLabel = wx.StaticText(id=ID_NODEEDITORPHYSICSLABEL, - label=_('Physics:'), name='PhysicsLabel', parent=self.EthercatNodeEditor, - pos=wx.Point(0, 0), size=wx.DefaultSize, style=0) - - self.Physics = wx.TextCtrl(id=ID_NODEEDITORPHYSICS, value='', - name='Physics', parent=self.EthercatNodeEditor, pos=wx.Point(0, 0), - size=wx.Size(0, 24), style=wx.TE_READONLY) - - self.SyncManagersLabel = wx.StaticText(id=ID_NODEEDITORSYNCMANAGERSLABEL, - label=_('Sync managers:'), name='SyncManagersLabel', parent=self.EthercatNodeEditor, - pos=wx.Point(0, 0), size=wx.DefaultSize, style=0) - - self.SyncManagersGrid = CustomGrid(id=ID_NODEEDITORSYNCMANAGERSGRID, - name='SyncManagersGrid', parent=self.EthercatNodeEditor, pos=wx.Point(0, 0), - size=wx.Size(0, 200), style=wx.VSCROLL) - - self.VariablesLabel = wx.StaticText(id=ID_NODEEDITORVARIABLESLABEL, - label=_('Variable entries:'), name='VariablesLabel', parent=self.EthercatNodeEditor, - pos=wx.Point(0, 0), size=wx.DefaultSize, style=0) - - self.VariablesGrid = wx.gizmos.TreeListCtrl(id=ID_NODEEDITORVARIABLESGRID, - name='VariablesGrid', parent=self.EthercatNodeEditor, pos=wx.Point(0, 0), - size=wx.Size(0, 400), style=wx.TR_DEFAULT_STYLE | - wx.TR_ROW_LINES | - wx.TR_COLUMN_LINES | - wx.TR_HIDE_ROOT | - wx.TR_FULL_ROW_HIGHLIGHT) - self.VariablesGrid.GetMainWindow().Bind(wx.EVT_LEFT_DOWN, self.OnVariablesGridLeftClick) + self.EthercatNodeEditor = wx.Panel(prnt, style=wx.TAB_TRAVERSAL) + + main_sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=2, vgap=5) + main_sizer.AddGrowableCol(0) + main_sizer.AddGrowableRow(1) + + variables_label = wx.StaticText(self.EthercatNodeEditor, + label=_('Variable entries:')) + main_sizer.AddWindow(variables_label, border=10, flag=wx.TOP|wx.LEFT|wx.RIGHT) + + self.VariablesGrid = wx.gizmos.TreeListCtrl(self.EthercatNodeEditor, + style=wx.TR_DEFAULT_STYLE | + wx.TR_ROW_LINES | + wx.TR_COLUMN_LINES | + wx.TR_HIDE_ROOT | + wx.TR_FULL_ROW_HIGHLIGHT) + self.VariablesGrid.GetMainWindow().Bind(wx.EVT_LEFT_DOWN, + self.OnVariablesGridLeftClick) + main_sizer.AddWindow(self.VariablesGrid, border=10, + flag=wx.GROW|wx.BOTTOM|wx.LEFT|wx.RIGHT) - self._init_sizers() - + self.EthercatNodeEditor.SetSizer(main_sizer) + return self.EthercatNodeEditor def __init__(self, parent, controler, window): ConfTreeNodeEditor.__init__(self, parent, controler, window) - self.SyncManagersTable = SyncManagersTable(self, [], GetSyncManagersTableColnames()) - self.SyncManagersGrid.SetTable(self.SyncManagersTable) - self.SyncManagersGridColAlignements = [wx.ALIGN_RIGHT, wx.ALIGN_LEFT, wx.ALIGN_RIGHT, - wx.ALIGN_RIGHT, wx.ALIGN_RIGHT, wx.ALIGN_RIGHT] - self.SyncManagersGridColSizes = [40, 150, 100, 100, 100, 100] - self.SyncManagersGrid.SetRowLabelSize(0) - for col in range(self.SyncManagersTable.GetNumberCols()): - attr = wx.grid.GridCellAttr() - attr.SetAlignment(self.SyncManagersGridColAlignements[col], wx.ALIGN_CENTRE) - self.SyncManagersGrid.SetColAttr(col, attr) - self.SyncManagersGrid.SetColMinimalWidth(col, self.SyncManagersGridColSizes[col]) - self.SyncManagersGrid.AutoSizeColumn(col, False) - for colname, colsize, colalign in zip(GetVariablesTableColnames(), - [40, 150, 100, 100, 150, 100, 150, 100], + [40, 150, 100, 100, 150, 100], [wx.ALIGN_RIGHT, wx.ALIGN_LEFT, wx.ALIGN_RIGHT, - wx.ALIGN_RIGHT, wx.ALIGN_LEFT, wx.ALIGN_RIGHT, - wx.ALIGN_LEFT, wx.ALIGN_LEFT]): + wx.ALIGN_RIGHT, wx.ALIGN_LEFT, wx.ALIGN_LEFT]): self.VariablesGrid.AddColumn(_(colname), colsize, colalign) self.VariablesGrid.SetMainColumn(1) @@ -178,20 +84,8 @@ def RefreshSlaveInfos(self): slave_infos = self.Controler.GetSlaveInfos() if slave_infos is not None: - self.Vendor.SetValue(slave_infos["vendor"]) - self.ProductCode.SetValue(slave_infos["product_code"]) - self.RevisionNumber.SetValue(slave_infos["revision_number"]) - self.Physics.SetValue(slave_infos["physics"]) - self.SyncManagersTable.SetData(slave_infos["sync_managers"]) - self.SyncManagersTable.ResetView(self.SyncManagersGrid) self.RefreshVariablesGrid(slave_infos["entries"]) else: - self.Vendor.SetValue("") - self.ProductCode.SetValue("") - self.RevisionNumber.SetValue("") - self.Physics.SetValue("") - self.SyncManagersTable.SetData([]) - self.SyncManagersTable.ResetView(self.SyncManagersGrid) self.RefreshVariablesGrid([]) def RefreshVariablesGrid(self, entries): @@ -202,10 +96,7 @@ self.VariablesGrid.Expand(root) def GenerateVariablesGridBranch(self, root, entries, colnames, idx=0): - if wx.VERSION >= (2, 6, 0): - item, root_cookie = self.VariablesGrid.GetFirstChild(root) - else: - item, root_cookie = self.VariablesGrid.GetFirstChild(root, 0) + item, root_cookie = self.VariablesGrid.GetFirstChild(root) no_more_items = not item.IsOk() for entry in entries: @@ -216,7 +107,10 @@ if col == 0: self.VariablesGrid.SetItemText(item, str(idx), 0) else: - self.VariablesGrid.SetItemText(item, entry.get(colname, ""), col) + value = entry.get(colname, "") + if colname == "Access": + value = GetAccessValue(value, entry.get("PDOMapping", "")) + self.VariablesGrid.SetItemText(item, value, col) if entry["PDOMapping"] == "": self.VariablesGrid.SetItemBackgroundColour(item, wx.LIGHT_GREY) self.VariablesGrid.SetItemPyData(item, entry) @@ -263,16 +157,275 @@ event.Skip() - def OnEthercatNodeEditorResize(self, event): - self.EthercatNodeEditor.GetBestSize() - xstart, ystart = self.EthercatNodeEditor.GetViewStart() - window_size = self.EthercatNodeEditor.GetClientSize() - maxx, maxy = self.EthercatNodeEditor.GetMinSize() +CIA402NodeEditor = NodeEditor + + +def GetModulesTableColnames(): + _ = lambda x : x + return [_("Name"), _("PDO alignment (bits)")] + +class LibraryEditorPanel(wx.ScrolledWindow): + + def __init__(self, parent, module_library, buttons): + wx.ScrolledWindow.__init__(self, parent, + style=wx.TAB_TRAVERSAL|wx.HSCROLL|wx.VSCROLL) + self.Bind(wx.EVT_SIZE, self.OnResize) + + self.ModuleLibrary = module_library + + main_sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=4, vgap=5) + main_sizer.AddGrowableCol(0) + main_sizer.AddGrowableRow(1) + main_sizer.AddGrowableRow(3) + + ESI_files_label = wx.StaticText(self, + label=_("ESI Files:")) + main_sizer.AddWindow(ESI_files_label, border=10, + flag=wx.TOP|wx.LEFT|wx.RIGHT) + + folder_tree_sizer = wx.FlexGridSizer(cols=2, hgap=5, rows=1, vgap=0) + folder_tree_sizer.AddGrowableCol(0) + folder_tree_sizer.AddGrowableRow(0) + main_sizer.AddSizer(folder_tree_sizer, border=10, + flag=wx.GROW|wx.LEFT|wx.RIGHT) + + self.ESIFiles = FolderTree(self, self.GetPath(), editable=False) + self.ESIFiles.SetFilter(".xml") + self.ESIFiles.SetMinSize(wx.Size(600, 300)) + folder_tree_sizer.AddWindow(self.ESIFiles, flag=wx.GROW) + + buttons_sizer = wx.BoxSizer(wx.VERTICAL) + folder_tree_sizer.AddSizer(buttons_sizer, + flag=wx.ALIGN_CENTER_VERTICAL) + + for idx, (name, bitmap, help, callback) in enumerate(buttons): + button = wx.lib.buttons.GenBitmapButton(self, + bitmap=GetBitmap(bitmap), + size=wx.Size(28, 28), style=wx.NO_BORDER) + button.SetToolTipString(help) + setattr(self, name, button) + if idx > 0: + flag = wx.TOP + else: + flag = 0 + if callback is None: + callback = getattr(self, "On" + name, None) + if callback is not None: + self.Bind(wx.EVT_BUTTON, callback, button) + buttons_sizer.AddWindow(button, border=10, flag=flag) + + modules_label = wx.StaticText(self, + label=_("Modules library:")) + main_sizer.AddSizer(modules_label, border=10, + flag=wx.LEFT|wx.RIGHT) + + self.ModulesGrid = wx.gizmos.TreeListCtrl(self, + style=wx.TR_DEFAULT_STYLE | + wx.TR_ROW_LINES | + wx.TR_COLUMN_LINES | + wx.TR_HIDE_ROOT | + wx.TR_FULL_ROW_HIGHLIGHT) + self.ModulesGrid.SetMinSize(wx.Size(600, 300)) + self.ModulesGrid.GetMainWindow().Bind(wx.EVT_LEFT_DCLICK, + self.OnModulesGridLeftDClick) + main_sizer.AddWindow(self.ModulesGrid, border=10, + flag=wx.GROW|wx.BOTTOM|wx.LEFT|wx.RIGHT) + + self.SetSizer(main_sizer) + + for colname, colsize, colalign in zip(GetModulesTableColnames(), + [400, 150], + [wx.ALIGN_LEFT, wx.ALIGN_RIGHT]): + self.ModulesGrid.AddColumn(_(colname), colsize, colalign) + self.ModulesGrid.SetMainColumn(0) + + def GetPath(self): + return self.ModuleLibrary.GetPath() + + def GetSelectedFilePath(self): + return self.ESIFiles.GetPath() + + def RefreshView(self): + self.ESIFiles.RefreshTree() + self.RefreshModulesGrid() + + def RefreshModulesGrid(self): + root = self.ModulesGrid.GetRootItem() + if not root.IsOk(): + root = self.ModulesGrid.AddRoot("Modules") + self.GenerateModulesGridBranch(root, + self.ModuleLibrary.GetModulesLibrary(), + GetVariablesTableColnames()) + self.ModulesGrid.Expand(root) + + def GenerateModulesGridBranch(self, root, modules, colnames): + item, root_cookie = self.ModulesGrid.GetFirstChild(root) + + no_more_items = not item.IsOk() + for module in modules: + if no_more_items: + item = self.ModulesGrid.AppendItem(root, "") + self.ModulesGrid.SetItemText(item, module["name"], 0) + if module["infos"] is not None: + self.ModulesGrid.SetItemText(item, str(module["infos"]["alignment"]), 1) + else: + self.ModulesGrid.SetItemBackgroundColour(item, wx.LIGHT_GREY) + self.ModulesGrid.SetItemPyData(item, module["infos"]) + self.GenerateModulesGridBranch(item, module["children"], colnames) + if not no_more_items: + item, root_cookie = self.ModulesGrid.GetNextChild(root, root_cookie) + no_more_items = not item.IsOk() + + if not no_more_items: + to_delete = [] + while item.IsOk(): + to_delete.append(item) + item, root_cookie = self.ModulesGrid.GetNextChild(root, root_cookie) + for item in to_delete: + self.ModulesGrid.Delete(item) + + def OnImportButton(self, event): + dialog = wx.FileDialog(self, + _("Choose an XML file"), + os.getcwd(), "", + _("XML files (*.xml)|*.xml|All files|*.*"), wx.OPEN) + + if dialog.ShowModal() == wx.ID_OK: + filepath = dialog.GetPath() + if self.ModuleLibrary.ImportModuleLibrary(filepath): + wx.CallAfter(self.RefreshView) + else: + message = wx.MessageDialog(self, + _("No such XML file: %s\n") % filepath, + _("Error"), wx.OK|wx.ICON_ERROR) + message.ShowModal() + message.Destroy() + dialog.Destroy() + + event.Skip() + + def OnDeleteButton(self, event): + filepath = self.GetSelectedFilePath() + if os.path.isfile(filepath): + folder, filename = os.path.split(filepath) + + dialog = wx.MessageDialog(self, + _("Do you really want to delete the file '%s'?") % filename, + _("Delete File"), wx.YES_NO|wx.ICON_QUESTION) + remove = dialog.ShowModal() == wx.ID_YES + dialog.Destroy() + + if remove: + os.remove(filepath) + self.ModuleLibrary.LoadModules() + wx.CallAfter(self.RefreshView) + event.Skip() + + def OnModulesGridLeftDClick(self, event): + item, flags, col = self.ModulesGrid.HitTest(event.GetPosition()) + if item.IsOk(): + entry_infos = self.ModulesGrid.GetItemPyData(item) + if entry_infos is not None and col == 1: + dialog = wx.TextEntryDialog(self, + _("Set PDO alignment (bits):"), + _("%s PDO alignment") % self.ModulesGrid.GetItemText(item), + str(entry_infos["alignment"])) + + if dialog.ShowModal() == wx.ID_OK: + try: + self.ModuleLibrary.SetAlignment( + entry_infos["vendor"], + entry_infos["product_code"], + entry_infos["revision_number"], + int(dialog.GetValue())) + wx.CallAfter(self.RefreshModulesGrid) + except ValueError: + message = wx.MessageDialog(self, + _("Module PDO alignment must be an integer!"), + _("Error"), wx.OK|wx.ICON_ERROR) + message.ShowModal() + message.Destroy() + + dialog.Destroy() + + event.Skip() + + def OnResize(self, event): + self.GetBestSize() + xstart, ystart = self.GetViewStart() + window_size = self.GetClientSize() + maxx, maxy = self.GetMinSize() posx = max(0, min(xstart, (maxx - window_size[0]) / SCROLLBAR_UNIT)) posy = max(0, min(ystart, (maxy - window_size[1]) / SCROLLBAR_UNIT)) - self.EthercatNodeEditor.Scroll(posx, posy) - self.EthercatNodeEditor.SetScrollbars(SCROLLBAR_UNIT, SCROLLBAR_UNIT, + self.Scroll(posx, posy) + self.SetScrollbars(SCROLLBAR_UNIT, SCROLLBAR_UNIT, maxx / SCROLLBAR_UNIT, maxy / SCROLLBAR_UNIT, posx, posy) event.Skip() -CIA402NodeEditor = NodeEditor +class DatabaseManagementDialog(wx.Dialog): + + def __init__(self, parent, database): + wx.Dialog.__init__(self, parent, + size=wx.Size(700, 500), title=_('ESI Files Database management'), + style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER) + + main_sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=2, vgap=10) + main_sizer.AddGrowableCol(0) + main_sizer.AddGrowableRow(0) + + self.DatabaseEditor = LibraryEditorPanel(self, database, + [("ImportButton", "ImportESI", _("Import file to ESI files database"), None), + ("DeleteButton", "remove_element", _("Remove file from database"), None)]) + main_sizer.AddWindow(self.DatabaseEditor, border=10, + flag=wx.GROW|wx.TOP|wx.LEFT|wx.RIGHT) + + button_sizer = self.CreateButtonSizer(wx.OK|wx.CANCEL|wx.CENTRE) + button_sizer.GetAffirmativeButton().SetLabel(_("Add file to project")) + button_sizer.GetCancelButton().SetLabel(_("Close")) + main_sizer.AddSizer(button_sizer, border=10, + flag=wx.ALIGN_RIGHT|wx.BOTTOM|wx.LEFT|wx.RIGHT) + + self.SetSizer(main_sizer) + + self.DatabaseEditor.RefreshView() + + def GetValue(self): + return self.DatabaseEditor.GetSelectedFilePath() + +class LibraryEditor(ConfTreeNodeEditor): + + CONFNODEEDITOR_TABS = [ + (_("Modules Library"), "_create_ModuleLibraryEditor")] + + def _create_ModuleLibraryEditor(self, prnt): + self.ModuleLibraryEditor = LibraryEditorPanel(prnt, + self.Controler.GetModulesLibraryInstance(), + [("ImportButton", "ImportESI", _("Import ESI file"), None), + ("AddButton", "ImportDatabase", _("Add file from ESI files database"), self.OnAddButton), + ("DeleteButton", "remove_element", _("Remove file from library"), None)]) + + return self.ModuleLibraryEditor + + def __init__(self, parent, controler, window): + ConfTreeNodeEditor.__init__(self, parent, controler, window) + + self.RefreshView() + + def RefreshView(self): + ConfTreeNodeEditor.RefreshView(self) + self.ModuleLibraryEditor.RefreshView() + + def OnAddButton(self, event): + dialog = DatabaseManagementDialog(self, + self.Controler.GetModulesDatabaseInstance()) + + if dialog.ShowModal() == wx.ID_OK: + module_library = self.Controler.GetModulesLibraryInstance() + module_library.ImportModuleLibrary(dialog.GetValue()) + + dialog.Destroy() + + wx.CallAfter(self.ModuleLibraryEditor.RefreshView) + + event.Skip() + diff -r c9b0340ea0f5 -r 58d07e039896 etherlab/etherlab.py --- a/etherlab/etherlab.py Thu Feb 07 00:59:50 2013 +0100 +++ b/etherlab/etherlab.py Wed Feb 27 22:40:45 2013 +0100 @@ -3,12 +3,13 @@ from xml.dom import minidom import wx +import csv from xmlclass import * from POULibrary import POULibrary from ConfigTreeNode import ConfigTreeNode from PLCControler import UndoBuffer, LOCATION_CONFNODE, LOCATION_MODULE, LOCATION_GROUP, LOCATION_VAR_INPUT, LOCATION_VAR_OUTPUT, LOCATION_VAR_MEMORY -from ConfigEditor import NodeEditor, CIA402NodeEditor, ETHERCAT_VENDOR, ETHERCAT_GROUP, ETHERCAT_DEVICE +from ConfigEditor import NodeEditor, CIA402NodeEditor, LibraryEditor, ETHERCAT_VENDOR, ETHERCAT_GROUP, ETHERCAT_DEVICE try: from MotionLibrary import Headers, AxisXSD @@ -571,7 +572,7 @@ "product_code": slave["product_code"], "revision_number":slave["revision_number"], } - device = self.GetModuleInfos(type_infos) + device, alignment = self.GetModuleInfos(type_infos) if device is not None: if HAS_MCL and _EthercatCIA402SlaveCTN.NODE_PROFILE in device.GetProfileNumbers(): CTNType = "EthercatCIA402Slave" @@ -651,7 +652,7 @@ slave = self.GetSlave(slave_pos) if slave is not None: type_infos = slave.getType() - device = self.GetModuleInfos(type_infos) + device, alignement = self.GetModuleInfos(type_infos) if device is not None: infos = type_infos.copy() entries = device.GetEntriesList() @@ -688,7 +689,7 @@ if slave is not None: type_infos = slave.getType() - device = self.GetModuleInfos(type_infos) + device, alignement = self.GetModuleInfos(type_infos) if device is not None: sync_managers = [] for sync_manager in device.getSm(): @@ -1016,7 +1017,7 @@ slave_pos = (slave_alias, alias[slave_alias]) # Extract slave device informations - device = self.Controler.GetModuleInfos(type_infos) + device, alignement = self.Controler.GetModuleInfos(type_infos) if device is not None: # Extract slaves variables to be mapped @@ -1473,10 +1474,7 @@ "Type": subitem.getType(), "BitSize": subitem.getBitSize(), "Access": subitem_access, - "PDOMapping": subitem_pdomapping, - "PDO index": "", - "PDO name": "", - "PDO type": ""} + "PDOMapping": subitem_pdomapping} else: entry_access = "" entry_pdomapping = "" @@ -1495,10 +1493,7 @@ "Type": entry_type, "BitSize": object.getBitSize(), "Access": entry_access, - "PDOMapping": entry_pdomapping, - "PDO index": "", - "PDO name": "", - "PDO type": ""} + "PDOMapping": entry_pdomapping} for TxPdo in self.getTxPdo(): ExtractPdoInfos(TxPdo, "Transmit", entries) @@ -1578,53 +1573,33 @@ "Name": ExtractName(pdo_entry.getName()), "Type": entry_type.getcontent(), "Access": access, - "PDOMapping": pdomapping, - "PDO index": pdo_index, - "PDO name": pdo_name, - "PDO type": pdo_type} - -class RootClass: - - CTNChildrenTypes = [("EthercatNode",_EthercatCTN,"Ethercat Master")] - - def __init__(self): - self.LoadModulesLibrary() - - def GetModulesLibraryPath(self): - library_path = os.path.join(self.CTNPath(), "modules") - if not os.path.exists(library_path): - os.mkdir(library_path) - return library_path - - def _ImportModuleLibrary(self): - dialog = wx.FileDialog(self.GetCTRoot().AppFrame, _("Choose an XML file"), os.getcwd(), "", _("XML files (*.xml)|*.xml|All files|*.*"), wx.OPEN) - if dialog.ShowModal() == wx.ID_OK: - filepath = dialog.GetPath() - if os.path.isfile(filepath): - shutil.copy(filepath, self.GetModulesLibraryPath()) - self.LoadModulesLibrary() - else: - self.GetCTRoot().logger.write_error(_("No such XML file: %s\n") % filepath) - dialog.Destroy() - - ConfNodeMethods = [ - {"bitmap" : "ImportESI", - "name" : _("Import module library"), - "tooltip" : _("Import module library"), - "method" : "_ImportModuleLibrary"}, - ] - - def CTNGenerate_C(self, buildpath, locations): - return [],"",False - - def LoadModulesLibrary(self): - self.ModulesLibrary = {} - - library_path = self.GetModulesLibraryPath() - - files = os.listdir(library_path) + "PDOMapping": pdomapping} + +DEFAULT_ALIGNMENT = 8 + +class ModulesLibrary: + + def __init__(self, path, parent_library=None): + self.Path = path + if not os.path.exists(self.Path): + os.makedirs(self.Path) + self.ParentLibrary = parent_library + + self.LoadModules() + self.LoadAlignments() + + def GetPath(self): + return self.Path + + def GetAlignmentFilePath(self): + return os.path.join(self.Path, "alignments.cfg") + + def LoadModules(self): + self.Library = {} + + files = os.listdir(self.Path) for file in files: - filepath = os.path.join(library_path, file) + filepath = os.path.join(self.Path, file) if os.path.isfile(filepath) and os.path.splitext(filepath)[-1] == ".xml": xmlfile = open(filepath, 'r') xml_tree = minidom.parse(xmlfile) @@ -1639,9 +1614,9 @@ if modules_infos is not None: vendor = modules_infos.getVendor() - vendor_category = self.ModulesLibrary.setdefault(ExtractHexDecValue(vendor.getId()), - {"name": ExtractName(vendor.getName(), _("Miscellaneous")), - "groups": {}}) + vendor_category = self.Library.setdefault(ExtractHexDecValue(vendor.getId()), + {"name": ExtractName(vendor.getName(), _("Miscellaneous")), + "groups": {}}) for group in modules_infos.getDescriptions().getGroups().getGroup(): group_type = group.getType() @@ -1656,10 +1631,10 @@ if not vendor_category["groups"].has_key(device_group): raise ValueError, "Not such group \"%\"" % device_group vendor_category["groups"][device_group]["devices"].append((device.getType().getcontent(), device)) - + def GetModulesLibrary(self, profile_filter=None): library = [] - for vendor_id, vendor in self.ModulesLibrary.iteritems(): + for vendor_id, vendor in self.Library.iteritems(): groups = [] children_dict = {} for group_type, group in vendor["groups"].iteritems(): @@ -1671,12 +1646,16 @@ device_dict = {} for device_type, device in group["devices"]: if profile_filter is None or profile_filter in device.GetProfileNumbers(): + product_code = device.getType().getProductCode() + revision_number = device.getType().getRevisionNo() + alignment = self.GetAlignment(vendor_id, product_code, revision_number) device_infos = {"name": ExtractName(device.getName()), "type": ETHERCAT_DEVICE, "infos": {"device_type": device_type, "vendor": vendor_id, - "product_code": device.getType().getProductCode(), - "revision_number": device.getType().getRevisionNo()}, + "product_code": product_code, + "revision_number": revision_number, + "alignment": alignment}, "children": []} group_infos["children"].append(device_infos) device_type_occurrences = device_dict.setdefault(device_type, []) @@ -1698,17 +1677,115 @@ "children": groups}) library.sort(lambda x, y: cmp(x["name"], y["name"])) return library - - def GetModuleInfos(self, type_infos): - vendor = self.ModulesLibrary.get(ExtractHexDecValue(type_infos["vendor"]), None) - if vendor is not None: - for group_name, group in vendor["groups"].iteritems(): - for device_type, device in group["devices"]: - product_code = ExtractHexDecValue(device.getType().getProductCode()) - revision_number = ExtractHexDecValue(device.getType().getRevisionNo()) - if (product_code == ExtractHexDecValue(type_infos["product_code"]) and - revision_number == ExtractHexDecValue(type_infos["revision_number"])): - return device - return None + + def GetModuleInfos(self, module_infos): + vendor = ExtractHexDecValue(module_infos["vendor"]) + vendor_infos = self.Library.get(vendor) + if vendor_infos is not None: + for group_name, group_infos in vendor_infos["groups"].iteritems(): + for device_type, device_infos in group_infos["devices"]: + product_code = ExtractHexDecValue(device_infos.getType().getProductCode()) + revision_number = ExtractHexDecValue(device_infos.getType().getRevisionNo()) + if (product_code == ExtractHexDecValue(module_infos["product_code"]) and + revision_number == ExtractHexDecValue(module_infos["revision_number"])): + return device_infos, self.GetAlignment(vendor, product_code, revision_number) + return None, None + + def ImportModuleLibrary(self, filepath): + if os.path.isfile(filepath): + shutil.copy(filepath, self.Path) + self.LoadModules() + return True + return False + + def LoadAlignments(self): + self.Alignments = {} + + csvfile_path = self.GetAlignmentFilePath() + if os.path.exists(csvfile_path): + csvfile = open(csvfile_path, "rb") + sample = csvfile.read(1024) + csvfile.seek(0) + dialect = csv.Sniffer().sniff(sample) + has_header = csv.Sniffer().has_header(sample) + reader = csv.reader(csvfile, dialect) + for row in reader: + if has_header: + has_header = False + else: + try: + self.Alignments[tuple(map(int, row[:3]))] = row[3] + except: + pass + csvfile.close() + + def SaveAlignments(self): + csvfile = open(self.GetAlignmentFilePath(), "wb") + writer = csv.writer(csvfile, delimiter=';') + writer.writerow(['Vendor', 'product_code', 'revision_number', 'alignment']) + for (vendor, product_code, revision_number), alignment in self.Alignments.iteritems(): + writer.writerow([vendor, product_code, revision_number, alignment]) + csvfile.close() + + def SetAlignment(self, vendor, product_code, revision_number, alignment): + vendor = ExtractHexDecValue(vendor) + product_code = ExtractHexDecValue(product_code) + revision_number = ExtractHexDecValue(revision_number) + + self.Alignments[tuple([vendor, product_code, revision_number])] = alignment + self.SaveAlignments() + + def GetAlignment(self, vendor, product_code, revision_number): + vendor = ExtractHexDecValue(vendor) + product_code = ExtractHexDecValue(product_code) + revision_number = ExtractHexDecValue(revision_number) + + alignment = self.Alignments.get(tuple([vendor, product_code, revision_number])) + if alignment is not None: + return alignment + + if self.ParentLibrary is not None: + return self.ParentLibrary.GetAlignment(vendor, product_code, revision_number) + return DEFAULT_ALIGNMENT + +USERDATA_DIR = wx.StandardPaths.Get().GetUserDataDir() +if wx.Platform != '__WXMSW__': + USERDATA_DIR += '_files' + +ModulesDatabase = ModulesLibrary( + os.path.join(USERDATA_DIR, "ethercat_modules")) + +class RootClass: + + CTNChildrenTypes = [("EthercatNode",_EthercatCTN,"Ethercat Master")] + EditorType = LibraryEditor + + def __init__(self): + self.ModulesLibrary = None + self.LoadModulesLibrary() + + def GetModulesLibraryPath(self): + return os.path.join(self.CTNPath(), "modules") + + def CTNGenerate_C(self, buildpath, locations): + return [],"",False + + def LoadModulesLibrary(self): + if self.ModulesLibrary is None: + self.ModulesLibrary = ModulesLibrary(self.GetModulesLibraryPath(), ModulesDatabase) + else: + self.ModulesLibrary.LoadModulesLibrary() + + def GetModulesDatabaseInstance(self): + return ModulesDatabase + + def GetModulesLibraryInstance(self): + return self.ModulesLibrary + + def GetModulesLibrary(self, profile_filter=None): + return self.ModulesLibrary.GetModulesLibrary(profile_filter) + + def GetModuleInfos(self, module_infos): + return self.ModulesLibrary.GetModuleInfos(module_infos) diff -r c9b0340ea0f5 -r 58d07e039896 etherlab/images/ImportDatabase.png Binary file etherlab/images/ImportDatabase.png has changed diff -r c9b0340ea0f5 -r 58d07e039896 etherlab/images/icons.svg --- a/etherlab/images/icons.svg Thu Feb 07 00:59:50 2013 +0100 +++ b/etherlab/images/icons.svg Wed Feb 27 22:40:45 2013 +0100 @@ -15,7 +15,7 @@ height="1052.3622" id="svg2" sodipodi:version="0.32" - inkscape:version="0.48.2 r9819" + inkscape:version="0.48.3.1 r9886" sodipodi:docname="icons.svg" inkscape:output_extension="org.inkscape.output.svg.inkscape"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + %%ImportESI ScanNetwork editSlave editCIA402Slave CIA402AxisRef %% + y="120.42097">%%ImportESI ImportDatabase ScanNetwork editSlave editCIA402Slave CIA402AxisRef %% @@ -59285,7 +61653,7 @@ @@ -59573,7 +61941,8 @@ height="1052.3622" /> + id="g12739" + transform="translate(110,0)"> + style="fill-rule:evenodd;stroke:url(#linearGradient13636);stroke-width:0.12755789pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:url(#linearGradient13638);fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient13640);stroke-width:0.02369117pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:url(#linearGradient13642);fill-opacity:1;fill-rule:evenodd;stroke:none" /> + style="fill:url(#linearGradient13644);fill-opacity:1;fill-rule:evenodd;stroke:#7f755d;stroke-width:0.0312406pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:url(#linearGradient13646);fill-opacity:1;fill-rule:evenodd;stroke:none" /> + style="fill:url(#linearGradient13648);fill-opacity:1;fill-rule:evenodd;stroke:none" /> + style="opacity:0.66134183;fill:url(#linearGradient13650);fill-opacity:1;fill-rule:evenodd;stroke:none" /> + style="fill:url(#linearGradient13652);fill-opacity:1;fill-rule:evenodd;stroke:none" /> + style="fill:url(#linearGradient13654);fill-opacity:1;fill-rule:evenodd;stroke:none" /> + style="fill:url(#linearGradient13656);fill-opacity:1;fill-rule:evenodd;stroke:none" /> + style="fill:url(#linearGradient13602);fill-opacity:1;stroke:none" /> + style="fill:url(#linearGradient13604);fill-opacity:1;stroke:none" /> @@ -60835,7 +63204,7 @@ sodipodi:nodetypes="cccccc" id="path1919-2" d="m 6.3306155,244.87972 c 0,0 -2.616026,-2.68246 -3.762417,-3.01369 -1.146391,-0.33124 -2.78605395,0.63625 -2.78605395,0.63625 l -8.95013235,8.40586 4.9440407,3.25217 10.5545626,-9.28059 z" - style="fill-rule:evenodd;stroke:url(#linearGradient20066-5);stroke-width:0.12755789pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill-rule:evenodd;stroke:url(#linearGradient13614);stroke-width:0.12755789pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:url(#linearGradient13616);fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient13618);stroke-width:0.02369117pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:url(#linearGradient13620);fill-opacity:1;fill-rule:evenodd;stroke:none" /> + style="fill:url(#linearGradient13622);fill-opacity:1;fill-rule:evenodd;stroke:#7f755d;stroke-width:0.0312406pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:url(#linearGradient13624);fill-opacity:1;fill-rule:evenodd;stroke:none" /> + style="fill:url(#linearGradient13626);fill-opacity:1;fill-rule:evenodd;stroke:none" /> + style="opacity:0.66134183;fill:url(#linearGradient13628);fill-opacity:1;fill-rule:evenodd;stroke:none" /> + style="fill:url(#linearGradient13630);fill-opacity:1;fill-rule:evenodd;stroke:none" /> + style="fill:url(#linearGradient13632);fill-opacity:1;fill-rule:evenodd;stroke:none" /> + style="fill:url(#linearGradient13634);fill-opacity:1;fill-rule:evenodd;stroke:none" /> + + + + + + + + + + + + + + + + + + + + + +