#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # AWL simulator - GUI # # Copyright 2012-2013 Michael Buesch # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # import sys from awlsim.util import * if isPyPy: # PySide does not work on PyPy, yet. printError("Running awlsimgui on the PyPy interpreter is not supported.") printError("Please use CPython 2.7 or CPython 3.x") sys.exit(1) import os import getopt import traceback if isPy2Compat: input = raw_input try: from PySide.QtCore import * from PySide.QtGui import * except ImportError as e: printError("PLEASE INSTALL PySide (http://www.pyside.org/)") input("Press enter to continue.") sys.exit(1) from awlsim.version import * from awlsim.main import * from awlsim.insntrans import * opt_awlSource = None opt_extInsns = False def handleFatalException(parentWidget=None): QMessageBox.critical(parentWidget, "A fatal exception occurred", "A fatal exception occurred:\n\n" "%s\n\n" "Awlsim will be terminated." %\ (traceback.format_exc(),)) sys.exit(1) class MessageBox(QMessageBox): def __init__(self, parent, title, text, details=None): QMessageBox.__init__(self, parent) self.setWindowTitle(title) self.setText(text) self.setIcon(QMessageBox.Critical) if details: self.setDetailedText(details) @classmethod def error(cls, parent, text, details=None): return cls(parent, "Awlsim - simulator error", text, details).exec_() @classmethod def handleAwlSimError(cls, parent, description, exception): cpu = exception.getCpu() text = "A simulator exception occurred:" if description: text += "\n" text += " " + description + "." text += "\n\n" text += " " + str(exception) text += "\n\n" insnStr = exception.getFailingInsnStr() if insnStr: text += " At statement:\n" text += " AWL line %s: %s" % (exception.getLineNrStr(), insnStr) else: text += " At AWL line %s" % exception.getLineNrStr() details = None if cpu: details = str(exception) + "\n\n" + str(cpu) return cls.error(parent, text, details) @classmethod def handleAwlParserError(cls, parent, exception): return cls.handleAwlSimError(parent = parent, description = None, exception = exception) class StoreRequest(object): """CPU store request buffer.""" def __init__(self, operator, value, failureCallback=None): self.operator = operator self.value = value self.failureCallback = failureCallback class StateWindow(QWidget): closed = Signal() def __init__(self, sim, parent=None): QWidget.__init__(self, parent) self.setLayout(QGridLayout(self)) pixmap = QPixmap(16, 16) pixmap.fill(QColor(0, 0, 192)) self.setWindowIcon(QIcon(pixmap)) self.sim = sim self.storeRequests = [] def update(self): size, hint = self.size(), self.minimumSizeHint() if size.width() < hint.width() or\ size.height() < hint.height(): self.resize(hint) # Queue a CPU store request for handling in PAA-push def queueStoreRequest(self, storeRequest): self.storeRequests.append(storeRequest) # Get the queued CPU store requests def getQueuedStoreRequests(self): queue = self.storeRequests self.storeRequests = [] return queue def closeEvent(self, ev): self.closed.emit() ev.accept() class State_CPU(StateWindow): def __init__(self, sim, parent=None): StateWindow.__init__(self, sim, parent) self.setWindowTitle("CPU Details") self.label = QLabel(self) font = self.label.font() font.setFamily("Mono") font.setFixedPitch(True) font.setKerning(False) self.label.setFont(font) self.layout().addWidget(self.label, 0, 0) self.label.setText("No CPU status available, yet.") def update(self): newText = str(self.sim.getCPU()) if newText: self.label.setText(newText) StateWindow.update(self) class AbstractDisplayWidget(QWidget): ADDRSPACE_E = AwlOperator.MEM_E ADDRSPACE_A = AwlOperator.MEM_A ADDRSPACE_M = AwlOperator.MEM_M ADDRSPACE_DB = AwlOperator.MEM_DB addrspace2name = { ADDRSPACE_E : ("I", "Inputs"), ADDRSPACE_A : ("Q", "Outputs"), ADDRSPACE_M : ("M", "Flags"), ADDRSPACE_DB : ("DB", "Data block"), } changed = Signal() def __init__(self, sim, addrSpace, addr, width, db, parent=None): QWidget.__init__(self, parent) self.setLayout(QGridLayout(self)) self.sim = sim self.addrSpace = addrSpace self.addr = addr self.width = width self.db = db def get(self): pass def update(self): pass def _createOperator(self): dbNumber = None if self.addrSpace == AbstractDisplayWidget.ADDRSPACE_DB: dbNumber = self.db return AwlOperator(self.addrSpace, self.width, AwlOffset(self.addr, dbNumber=dbNumber)) class BitDisplayWidget(AbstractDisplayWidget): def __init__(self, sim, addrSpace, addr, width, db, parent=None, displayPushButtons=True): AbstractDisplayWidget.__init__(self, sim, addrSpace, addr, width, db, parent) self.cbs = [] self.pbs = [] self.prevButtonStates = [] for i in range(self.width): cb = QCheckBox(str(i), self) self.layout().addWidget(cb, 0, self.width - i - 1) self.cbs.append(cb) cb.stateChanged.connect(self.changed) if displayPushButtons: pb = QPushButton("", self) self.layout().addWidget(pb, 1, self.width - i - 1) self.pbs.append(pb) self.prevButtonStates.append(False) pb.pressed.connect(self.__buttonUpdate) pb.released.connect(self.__buttonUpdate) self.update() def __buttonUpdate(self): for i, pb in enumerate(self.pbs): pressed = bool(pb.isDown()) if pressed == self.prevButtonStates[i]: continue self.prevButtonStates[i] = pressed if self.cbs[i].checkState() == Qt.Checked: self.cbs[i].setCheckState(Qt.Unchecked) else: self.cbs[i].setCheckState(Qt.Checked) def get(self): value = 0 for i in range(self.width): if self.cbs[i].checkState() == Qt.Checked: value |= (1 << i) return value def update(self): try: value = self.sim.getCPU().fetch(self._createOperator()) except AwlSimError as e: self.setEnabled(False) return for i in range(self.width): if value & (1 << i): self.cbs[i].setCheckState(Qt.Checked) else: self.cbs[i].setCheckState(Qt.Unchecked) class NumberDisplayWidget(AbstractDisplayWidget): def __init__(self, sim, base, addrSpace, addr, width, db, parent=None): AbstractDisplayWidget.__init__(self, sim, addrSpace, addr, width, db, parent) self.base = base self.displayedValue = -1 self.line = QLineEdit(self) self.line.setAlignment(Qt.AlignRight) self.layout().addWidget(self.line) self.line.returnPressed.connect(self.__returnPressed) self.line.textChanged.connect(self.__textChanged) self.update() def __returnPressed(self): self.changed.emit() def __convertValue(self): try: value = int(self.line.text(), self.base) if self.base == 10: if value > (1 << (self.width - 1)) - 1 or\ value < -(1 << (self.width - 1)): raise ValueError else: if value > (1 << self.width) - 1: raise ValueError except ValueError as e: return None return value def __textChanged(self): value = self.__convertValue() if value is None: pal = self.palette() pal.setColor(QPalette.Text, Qt.red) self.setPalette(pal) else: pal = self.palette() pal.setColor(QPalette.Text, Qt.black) self.setPalette(pal) def get(self): value = self.__convertValue() if value is None: return self.displayedValue return value def update(self): try: value = self.sim.getCPU().fetch(self._createOperator()) except AwlSimError as e: self.setEnabled(False) return if value == self.displayedValue: return self.displayedValue = value if self.base == 2: string = "".join( '1' if (value & (1 << bitnr)) else '0' for bitnr in range(self.width - 1, -1, -1) ) elif self.base == 10: if self.width == 8: value &= 0xFF if value & 0x80: value = -((~value + 1) & 0xFF) string = "%d" % value elif self.width == 16: value &= 0xFFFF if value & 0x8000: value = -((~value + 1) & 0xFFFF) string = "%d" % value elif self.width == 32: value &= 0xFFFFFFFF if value & 0x80000000: value = -((~value + 1) & 0xFFFFFFFF) string = "%d" % value else: assert(0) elif self.base == 16: if self.width == 8: string = "%02X" % (value & 0xFF) elif self.width == 16: string = "%04X" % (value & 0xFFFF) elif self.width == 32: string = "%08X" % (value & 0xFFFFFFFF) else: assert(0) else: assert(0) self.line.setText(string) class HexDisplayWidget(NumberDisplayWidget): def __init__(self, sim, addrSpace, addr, width, db, parent=None): NumberDisplayWidget.__init__(self, sim, 16, addrSpace, addr, width, db, parent) class DecDisplayWidget(NumberDisplayWidget): def __init__(self, sim, addrSpace, addr, width, db, parent=None): NumberDisplayWidget.__init__(self, sim, 10, addrSpace, addr, width, db, parent) class BinDisplayWidget(NumberDisplayWidget): def __init__(self, sim, addrSpace, addr, width, db, parent=None): NumberDisplayWidget.__init__(self, sim, 2, addrSpace, addr, width, db, parent) class State_Mem(StateWindow): def __init__(self, sim, addrSpace, parent=None): StateWindow.__init__(self, sim, parent) self.addrSpace = addrSpace x = 0 if addrSpace == AbstractDisplayWidget.ADDRSPACE_DB: self.dbSpin = QSpinBox(self) self.dbSpin.setPrefix("DB ") self.layout().addWidget(self.dbSpin, 0, x) x += 1 self.addrSpin = QSpinBox(self) self.layout().addWidget(self.addrSpin, 0, x) x += 1 self.widthCombo = QComboBox(self) self.widthCombo.addItem("Byte", 8) self.widthCombo.addItem("Word", 16) self.widthCombo.addItem("DWord", 32) self.layout().addWidget(self.widthCombo, 0, x) x += 1 self.fmtCombo = QComboBox(self) self.fmtCombo.addItem("Checkboxes", "cb") self.fmtCombo.addItem("Hexadecimal", "hex") self.fmtCombo.addItem("Decimal", "dec") self.fmtCombo.addItem("Dual", "bin") self.layout().addWidget(self.fmtCombo, 0, x) x += 1 self.contentLayout = QGridLayout() self.contentLayout.setContentsMargins(QMargins()) self.layout().addLayout(self.contentLayout, 1, 0, 1, x) self.contentWidget = None if addrSpace == AbstractDisplayWidget.ADDRSPACE_DB: self.dbSpin.valueChanged.connect(self.rebuild) self.addrSpin.valueChanged.connect(self.rebuild) self.widthCombo.currentIndexChanged.connect(self.rebuild) self.fmtCombo.currentIndexChanged.connect(self.rebuild) self.__changeBlocked = 0 self.rebuild() def rebuild(self): if self.contentWidget: self.contentLayout.removeWidget(self.contentWidget) self.contentWidget.deleteLater() self.contentWidget = None addr = self.addrSpin.value() index = self.fmtCombo.currentIndex() fmt = self.fmtCombo.itemData(index) index = self.widthCombo.currentIndex() width = self.widthCombo.itemData(index) if self.addrSpace == AbstractDisplayWidget.ADDRSPACE_DB: db = self.dbSpin.value() else: db = None if fmt == "cb": # If checkboxes are selected with word or dword # width, change to hex display. if width != 8: index = self.fmtCombo.findData("hex") # This will re-trigger the "rebuild" slot. self.fmtCombo.setCurrentIndex(index) return name, longName = AbstractDisplayWidget.addrspace2name[self.addrSpace] width2suffix = { 8 : "B", 16 : "W", 32 : "D", } name += width2suffix[width] self.addrSpin.setPrefix(name + " ") self.setWindowTitle(longName) if fmt == "cb": self.contentWidget = BitDisplayWidget(self.sim, self.addrSpace, addr, width, db, self, displayPushButtons=True) self.contentLayout.addWidget(self.contentWidget) elif fmt == "hex": self.contentWidget = HexDisplayWidget(self.sim, self.addrSpace, addr, width, db, self) self.contentLayout.addWidget(self.contentWidget) elif fmt == "dec": self.contentWidget = DecDisplayWidget(self.sim, self.addrSpace, addr, width, db, self) self.contentLayout.addWidget(self.contentWidget) elif fmt == "bin": self.contentWidget = BinDisplayWidget(self.sim, self.addrSpace, addr, width, db, self) self.contentLayout.addWidget(self.contentWidget) else: assert(0) self.contentWidget.changed.connect(self.__changed) self.contentWidget.setEnabled(True) self.update() def __storeFailureCallback(self): # A CPU store request related to this widget failed self.contentWidget.setEnabled(False) def __changed(self): if self.__changeBlocked or not self.contentWidget: return value = self.contentWidget.get() addr = self.addrSpin.value() index = self.widthCombo.currentIndex() width = self.widthCombo.itemData(index) self.queueStoreRequest(StoreRequest(self.contentWidget._createOperator(), value, self.__storeFailureCallback)) def update(self): if self.contentWidget: self.__changeBlocked += 1 self.contentWidget.update() self.__changeBlocked -= 1 StateWindow.update(self) class State_LCD(StateWindow): def __init__(self, sim, parent=None): StateWindow.__init__(self, sim, parent) self.setWindowTitle("LCD") self.addrSpin = QSpinBox(self) self.addrSpin.setPrefix("A ") self.layout().addWidget(self.addrSpin, 0, 0) self.widthCombo = QComboBox(self) self.widthCombo.addItem("Byte", 8) self.widthCombo.addItem("Word", 16) self.widthCombo.addItem("DWord", 32) self.layout().addWidget(self.widthCombo, 0, 1) self.endianCombo = QComboBox(self) self.endianCombo.addItem("Big-endian", "be") self.endianCombo.addItem("Little-endian", "le") self.layout().addWidget(self.endianCombo, 1, 0) self.fmtCombo = QComboBox(self) self.fmtCombo.addItem("BCD", "bcd") self.fmtCombo.addItem("Signed BCD", "signed-bcd") self.fmtCombo.addItem("Binary", "bin") self.fmtCombo.addItem("Signed binary", "signed-bin") self.layout().addWidget(self.fmtCombo, 1, 1) self.lcd = QLCDNumber(self) self.lcd.setMinimumHeight(50) self.layout().addWidget(self.lcd, 2, 0, 1, 2) self.addrSpin.valueChanged.connect(self.rebuild) self.widthCombo.currentIndexChanged.connect(self.rebuild) self.endianCombo.currentIndexChanged.connect(self.rebuild) self.fmtCombo.currentIndexChanged.connect(self.rebuild) self.__changeBlocked = 0 self.rebuild() def getDataWidth(self): index = self.widthCombo.currentIndex() return self.widthCombo.itemData(index) def getFormat(self): index = self.fmtCombo.currentIndex() return self.fmtCombo.itemData(index) def getEndian(self): index = self.endianCombo.currentIndex() return self.endianCombo.itemData(index) def rebuild(self): self.update() def update(self): addr = self.addrSpin.value() width = self.getDataWidth() fmt = self.getFormat() endian = self.getEndian() try: oper = AwlOperator(AwlOperator.MEM_A, width, AwlOffset(addr)) value = self.sim.getCPU().fetch(oper) except AwlSimError as e: MessageBox.handleAwlSimError(self, "Failed to fetch memory", e) return if endian == "le": if width == 16: value = swapEndianWord(value) elif width == 32: value = swapEndianDWord(value) if fmt == "bcd": if width == 8: value = "%02X" % (value & 0xFF) elif width == 16: value = "%04X" % (value & 0xFFFF) elif width == 32: value = "%08X" % (value & 0xFFFFFFFF) else: assert(0) elif fmt == "signed-bcd": if width == 8: sign = '-' if (value & 0xF0) else '' value = "%s%01X" % (sign, value & 0x0F) elif width == 16: sign = '-' if (value & 0xF000) else '' value = "%s%03X" % (sign, value & 0x0FFF) elif width == 32: sign = '-' if (value & 0xF0000000) else '' value = "%s%07X" % (sign, value & 0x0FFFFFFF) else: assert(0) elif fmt == "bin": if width == 8: value = "%d" % (value & 0xFF) elif width == 16: value = "%d" % (value & 0xFFFF) elif width == 32: value = "%d" % (value & 0xFFFFFFFF) else: assert(0) elif fmt == "signed-bin": if width == 8: value = "%d" % byteToSignedPyInt(value) elif width == 16: value = "%d" % wordToSignedPyInt(value) elif width == 32: value = "%d" % dwordToSignedPyInt(value) else: assert(0) else: assert(0) self.__changeBlocked += 1 self.lcd.setDigitCount(len(value)) self.lcd.display(value) self.__changeBlocked -= 1 StateWindow.update(self) class StateWorkspace(QWorkspace): def __init__(self, parent=None): QWorkspace.__init__(self, parent) class GuiPseudoHardwareInterface(AbstractHardwareInterface): """Input/output to the CPU is handled by this pseudo hardware interface.""" name = "GUI" def __init__(self, sim, cpuWidget): AbstractHardwareInterface.__init__(self, sim = sim) self.cpuWidget = cpuWidget self.cpu = cpuWidget.sim.getCPU() self.__nextUpdate = 0.0 def readInputs(self): # Read the "hardware inputs" a.k.a. GUI buttons. # This is done by processing the queued store-requests. for storeRequest in self.cpuWidget.getQueuedStoreRequests(): try: self.cpu.store(storeRequest.operator, storeRequest.value) except AwlSimError as e: if storeRequest.failureCallback: storeRequest.failureCallback() def writeOutputs(self): # Write the "hardware outputs" a.k.a. GUI display elements. # This is only done one in a while for performance reasons. if self.cpu.now >= self.__nextUpdate: self.__nextUpdate = self.cpu.now + 0.15 self.cpuWidget.update() def directReadInput(self, accessWidth, accessOffset): #TODO: Read input widgets. As workaround we just read the current CPU value. return self.sim.cpu.fetch(AwlOperator(AwlOperator.MEM_E, accessWidth, AwlOffset(accessOffset))) def directWriteOutput(self, accessWidth, accessOffset, data): #TODO: Trigger output widgets update. return True class CpuWidget(QWidget): runStateChanged = Signal(int) enum.start STATE_STOP = enum.item STATE_PARSE = enum.item STATE_INIT = enum.item STATE_LOAD = enum.item STATE_RUN = enum.item enum.end def __init__(self, mainWidget, parent=None): QWidget.__init__(self, parent) self.setLayout(QGridLayout(self)) self.mainWidget = mainWidget self.sim = mainWidget.getSim() self.state = self.STATE_STOP self.__nextCpuWidgetUpdate = 0.0 self.pseudoHw = GuiPseudoHardwareInterface(sim = self.sim, cpuWidget = self) self.sim.registerHardware(self.pseudoHw) group = QGroupBox("CPU status", self) group.setLayout(QGridLayout(group)) self.runButton = QRadioButton("RUN", group) group.layout().addWidget(self.runButton, 0, 0) self.stopButton = QRadioButton("STOP", group) group.layout().addWidget(self.stopButton, 1, 0) self.onlineViewCheckBox = QCheckBox("Online diag.", group) group.layout().addWidget(self.onlineViewCheckBox, 2, 0) self.layout().addWidget(group, 0, 0) group = QGroupBox("Add window", self) group.setLayout(QGridLayout(group)) self.newCpuStateButton = QPushButton("CPU", group) group.layout().addWidget(self.newCpuStateButton, 0, 0) self.newLCDButton = QPushButton("LCD", group) group.layout().addWidget(self.newLCDButton, 0, 1) self.newDBButton = QPushButton("DB", group) group.layout().addWidget(self.newDBButton, 0, 2) self.newEButton = QPushButton("E (I)", group) group.layout().addWidget(self.newEButton, 1, 0) self.newAButton = QPushButton("A (Q)", group) group.layout().addWidget(self.newAButton, 1, 1) self.newMButton = QPushButton("M", group) group.layout().addWidget(self.newMButton, 1, 2) self.layout().addWidget(group, 0, 1) self.stateWs = StateWorkspace(self) self.stateWs.setScrollBarsEnabled(True) self.layout().addWidget(self.stateWs, 1, 0, 1, 2) self.stopButton.setChecked(Qt.Checked) self.runButton.toggled.connect(self.__runStateToggled) self.stopButton.toggled.connect(self.__runStateToggled) self.onlineViewCheckBox.stateChanged.connect(self.__updateOnlineViewState) self.newCpuStateButton.released.connect(self.__newWin_CPU) self.newDBButton.released.connect(self.__newWin_DB) self.newEButton.released.connect(self.__newWin_E) self.newAButton.released.connect(self.__newWin_A) self.newMButton.released.connect(self.__newWin_M) self.newLCDButton.released.connect(self.__newWin_LCD) self.__newWin_CPU() self.update() def __addWindow(self, win): self.stateWs.addWindow(win, Qt.Window) win.show() self.update() def __newWin_CPU(self): self.__addWindow(State_CPU(self.mainWidget.getSim(), self)) def __newWin_DB(self): self.__addWindow(State_Mem(self.mainWidget.getSim(), AbstractDisplayWidget.ADDRSPACE_DB, self)) def __newWin_E(self): self.__addWindow(State_Mem(self.mainWidget.getSim(), AbstractDisplayWidget.ADDRSPACE_E, self)) def __newWin_A(self): self.__addWindow(State_Mem(self.mainWidget.getSim(), AbstractDisplayWidget.ADDRSPACE_A, self)) def __newWin_M(self): self.__addWindow(State_Mem(self.mainWidget.getSim(), AbstractDisplayWidget.ADDRSPACE_M, self)) def __newWin_LCD(self): self.__addWindow(State_LCD(self.mainWidget.getSim(), self)) def update(self): for win in self.stateWs.windowList(): win.update() # Get the queued CPU store requests def getQueuedStoreRequests(self): reqList = [] for win in self.stateWs.windowList(): reqList.extend(win.getQueuedStoreRequests()) return reqList def __cycleExitCallback(self, cpu): if self.state == self.STATE_RUN: self.mainWidget.codeEdit.updateCpuStats_afterCycle(cpu) def __blockExitCallback(self, cpu): if self.state == self.STATE_RUN: self.mainWidget.codeEdit.updateCpuStats_afterBlock(cpu) # Special case: May update the CPU-state-widgets (if any) # on block exit. if cpu.now >= self.__nextCpuWidgetUpdate: self.__nextCpuWidgetUpdate = cpu.now + 0.15 for win in self.stateWs.windowList(): if isinstance(win, State_CPU): win.update() def __postInsnCallback(self, cpu): if self.state == self.STATE_RUN: self.mainWidget.codeEdit.updateCpuStats_afterInsn(cpu) def __screenUpdateCallback(self, cpu): self.__postInsnCallback(cpu) self.__blockExitCallback(cpu) self.__cycleExitCallback(cpu) QApplication.processEvents(QEventLoop.AllEvents, 100) def __run(self): sim = self.mainWidget.getSim() self.__updateOnlineViewState() self.__setState(self.STATE_PARSE) self.runButton.setChecked(True) self.runButton.setEnabled(False) # Redraws the radio button self.runButton.setEnabled(True) ob1_awl = self.mainWidget.getCodeEditWidget().getCode() if not ob1_awl.strip(): MessageBox.error(self, "No AWL code") self.stop() return try: parser = AwlParser() parser.parseData(ob1_awl) self.__setState(self.STATE_INIT) cpu = sim.getCPU() cpu.setBlockExitCallback(self.__blockExitCallback, cpu) cpu.setCycleExitCallback(self.__cycleExitCallback, cpu) cpu.setScreenUpdateCallback( self.__screenUpdateCallback, cpu) self.__setState(self.STATE_LOAD) sim.load(parser.getParseTree()) except AwlParserError as e: MessageBox.handleAwlParserError(self, e) self.stop() sim.shutdown() return except AwlSimError as e: MessageBox.handleAwlSimError(self, "Error while loading code", e) self.stop() sim.shutdown() return except Exception: handleFatalException(self) self.__setState(self.STATE_RUN) try: while self.state == self.STATE_RUN: sim.runCycle() QApplication.processEvents(QEventLoop.AllEvents, 100) except AwlSimError as e: MessageBox.handleAwlSimError(self, "Error while executing code", e) self.stop() except Exception: handleFatalException(self) sim.shutdown() def stop(self): if self.state == self.STATE_STOP: return self.stopButton.setChecked(True) self.runButton.setEnabled(True) self.__setState(self.STATE_STOP) def getState(self): return self.state def __setState(self, newState): if newState != self.state: self.state = newState self.runStateChanged.emit(self.state) QApplication.processEvents(QEventLoop.AllEvents, 1000) def __runStateToggled(self): if self.runButton.isChecked(): if self.state == self.STATE_STOP: self.__run() if self.stopButton.isChecked(): if self.state != self.STATE_STOP: self.stop() def __updateOnlineViewState(self): en = self.onlineViewCheckBox.checkState() == Qt.Checked self.mainWidget.codeEdit.enableCpuStats(en) cpu = self.mainWidget.getSim().getCPU() if en: cpu.setPostInsnCallback(self.__postInsnCallback, cpu) else: cpu.setPostInsnCallback(None) class EditSubWidget(QWidget): needRepaint = Signal(QPaintEvent) wasScrolled = Signal(QWheelEvent) def __init__(self, editWidget): QWidget.__init__(self, editWidget) self.editWidget = editWidget def paintEvent(self, ev): self.needRepaint.emit(ev) def wheelEvent(self, ev): self.wasScrolled.emit(ev) def getPainter(self): p = QPainter(self) font = p.font() font.setFamily("Mono") font.setKerning(False) font.setFixedPitch(True) font.setStyleStrategy(QFont.PreferBitmap) p.setFont(font) return p class HeaderSubWidget(EditSubWidget): def __init__(self, editWidget): EditSubWidget.__init__(self, editWidget) def sizeHint(self): return QSize(0, self.editWidget.headerHeight()) class LineNumSubWidget(EditSubWidget): def __init__(self, editWidget): EditSubWidget.__init__(self, editWidget) def sizeHint(self): return QSize(self.editWidget.lineNumWidgetWidth(), 0) class CpuStatsSubWidget(EditSubWidget): def __init__(self, editWidget): EditSubWidget.__init__(self, editWidget) def sizeHint(self): return QSize(self.editWidget.cpuStatsWidgetWidth(), 0) def getBanner(self): return "STW ACCU 1 ACCU 2" class CpuStatsEntry(object): def __init__(self, stamp, statusWord, accu1, accu2): self.stamp = stamp self.obsolete = False self.statusWord = statusWord.getWord() self.accu1 = accu1.getDWord() self.accu2 = accu2.getDWord() @staticmethod def getTextWidth(): return 11 + 2 + 8 + 2 + 8 def __repr__(self): stw = [] for i in range(S7StatusWord.NR_BITS - 1, -1, -1): stw.append('1' if (self.statusWord & (1 << i)) else '0') if i % 4 == 0 and i: stw.append('_') return "%s %08X %08X" %\ ("".join(stw), self.accu1, self.accu2) class EditWidget(QPlainTextEdit): codeChanged = Signal() __aniChars = ( ' ', '.', 'o', '0', 'O', '0', 'o', '.' ) def __init__(self, mainWidget): QPlainTextEdit.__init__(self, mainWidget) self.mainWidget = mainWidget self.__updateFonts() self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.setLineWrapMode(QPlainTextEdit.NoWrap) self.setTabStopWidth(self.tabStopWidth() // 2) self.headerWidget = HeaderSubWidget(self) self.lineNumWidget = LineNumSubWidget(self) self.cpuStatsWidget = CpuStatsSubWidget(self) self.__runStateCopy = CpuWidget.STATE_STOP self.__nextHdrUpdate = 0 self.__hdrAniStat = 0 self.enableCpuStats(False) self.resetCpuStats(True) self.__textChangeBlocked = 0 self.textChanged.connect(self.__textChanged) self.blockCountChanged.connect(self.__updateMargins) self.updateRequest.connect(self.__updateExtraWidgets) self.headerWidget.needRepaint.connect(self.__repaintHeaderWidget) self.lineNumWidget.needRepaint.connect(self.__repaintLineNumWidget) self.cpuStatsWidget.needRepaint.connect(self.__repaintCpuStatsWidget) self.headerWidget.wasScrolled.connect(self.__forwardWheelEvent) self.lineNumWidget.wasScrolled.connect(self.__forwardWheelEvent) self.cpuStatsWidget.wasScrolled.connect(self.__forwardWheelEvent) def runStateChanged(self, newState): self.__runStateCopy = newState if newState == CpuWidget.STATE_PARSE: self.resetCpuStats() if self.__cpuStatsEnabled: self.cpuStatsWidget.update() self.headerWidget.update() def __updateFonts(self): fmt = self.currentCharFormat() fmt.setFontFamily("Mono") fmt.setFontKerning(False) fmt.setFontFixedPitch(True) fmt.setFontStyleStrategy(QFont.PreferBitmap) self.setCurrentCharFormat(fmt) self.__charWidth = self.fontMetrics().width('X') self.__charHeight = self.fontMetrics().height() def enableCpuStats(self, enabled=True): self.__cpuStatsEnabled = enabled self.__updateMargins() self.__updateGeo() def resetCpuStats(self, force=False): if not force and not self.__lineCpuStats: return self.__lineCpuStats = { } self.__cpuStatsCount = 0 self.__cpuStatsUpdate = 1 self.__cpuStatsStamp = 0 self.__updateMargins() self.headerWidget.update() self.cpuStatsWidget.update() def updateCpuStats_afterInsn(self, cpu): insn = cpu.getCurrentInsn() if not self.__cpuStatsEnabled or not insn: return self.__lineCpuStats[insn.getLineNr() - 1] =\ CpuStatsEntry(self.__cpuStatsStamp, cpu.getStatusWord(), cpu.getAccu(1), cpu.getAccu(2)) self.__cpuStatsCount += 1 def updateCpuStats_afterBlock(self, cpu): if cpu.now >= self.__nextHdrUpdate: self.__nextHdrUpdate = cpu.now + 0.2 self.__hdrAniStat = (self.__hdrAniStat + 1) %\ len(self.__aniChars) self.headerWidget.update() if not self.__cpuStatsEnabled: return # Update the 'obsolete'-flag based on the timestamp for ent in self.__lineCpuStats.values(): ent.obsolete = (ent.stamp != self.__cpuStatsStamp) # Update the stats widget if self.__cpuStatsCount >= self.__cpuStatsUpdate: self.__cpuStatsCount = 0 self.__cpuStatsUpdate = cpu.getRandomInt(0, 255) + 1 self.cpuStatsWidget.update() def updateCpuStats_afterCycle(self, cpu): if self.__cpuStatsEnabled: self.__cpuStatsStamp += 1 def __forwardWheelEvent(self, ev): self.wheelEvent(ev) def lineNumWidgetWidth(self): digi, bcnt = 1, self.blockCount() while bcnt > 9: digi, bcnt = digi + 1, bcnt // 10 digi += 1 # colon return 5 + 5 + digi * self.__charWidth def cpuStatsWidgetWidth(self): if not self.__cpuStatsEnabled: return 0 return 5 + 5 + CpuStatsEntry.getTextWidth() * self.__charWidth def headerHeight(self): return 5 + 5 + self.__charHeight def __updateMargins(self): self.setViewportMargins(self.lineNumWidgetWidth(), self.headerHeight(), self.cpuStatsWidgetWidth(), 0) def sizeHint(self): sh = QPlainTextEdit.sizeHint(self) sh.setWidth(650 +\ self.lineNumWidgetWidth() +\ self.cpuStatsWidgetWidth()) return sh def __updateHeaderWidgetGeo(self): cont = self.contentsRect() rect = QRect(cont.left(), cont.top(), cont.width(), self.headerHeight()) self.headerWidget.setGeometry(rect) def __updateLineNumWidgetGeo(self): cont = self.contentsRect() rect = QRect(cont.left(), cont.top() + self.headerHeight(), self.lineNumWidgetWidth(), cont.height()) self.lineNumWidget.setGeometry(rect) def __updateCpuStatsWidgetGeo(self): if self.__cpuStatsEnabled: vp, cont = self.viewport(), self.contentsRect() rect = QRect(vp.width() + self.lineNumWidgetWidth(), cont.top() + self.headerHeight(), self.cpuStatsWidgetWidth(), cont.height()) self.cpuStatsWidget.setGeometry(rect) self.cpuStatsWidget.show() else: self.cpuStatsWidget.hide() def __updateGeo(self): self.__updateHeaderWidgetGeo() self.__updateLineNumWidgetGeo() self.__updateCpuStatsWidgetGeo() def __updateExtraWidgets(self, rect, dy): if dy: self.headerWidget.scroll(0, dy) self.lineNumWidget.scroll(0, dy) self.cpuStatsWidget.scroll(0, dy) return self.headerWidget.update(0, rect.y(), self.headerWidget.width(), rect.height()) self.lineNumWidget.update(0, rect.y(), self.lineNumWidget.width(), rect.height()) self.cpuStatsWidget.update(0, rect.y(), self.cpuStatsWidget.width(), rect.height()) if rect.contains(self.viewport().rect()): self.__updateMargins() def resizeEvent(self, ev): QPlainTextEdit.resizeEvent(self, ev) self.__updateGeo() def __repaintHeaderWidget(self, ev): p = self.headerWidget.getPainter() p.fillRect(ev.rect(), Qt.lightGray) if self.__runStateCopy == CpuWidget.STATE_RUN: runText = self.__aniChars[self.__hdrAniStat] else: runText = { CpuWidget.STATE_STOP: "-- CPU STOPPED --", CpuWidget.STATE_PARSE: "Parsing code...", CpuWidget.STATE_INIT: "Initializing simulator...", CpuWidget.STATE_LOAD: "Loading code...", }[self.__runStateCopy] p.drawText(5, 5, self.__charWidth * len(runText) + 1, self.headerWidget.height(), Qt.AlignLeft, runText) if self.__cpuStatsEnabled: # Map the starting point pt = self.cpuStatsWidget.mapToGlobal(QPoint(0, 0)) pt = self.headerWidget.mapFromGlobal(pt) p.drawText(pt.x() + 5, 5, self.headerWidget.width() - pt.x() - 5, self.headerWidget.height(), Qt.AlignLeft, self.cpuStatsWidget.getBanner()) def __repaintLineNumWidget(self, ev): p = self.lineNumWidget.getPainter() rect = ev.rect() p.fillRect(rect, Qt.lightGray) rect.setLeft(rect.left() + rect.width() - 3) rect.setWidth(3) p.fillRect(rect, Qt.white) p.setPen(Qt.black) block = self.firstVisibleBlock() bn = block.blockNumber() top = self.blockBoundingGeometry(block).translated(self.contentOffset()).top() bottom = top + self.blockBoundingRect(block).height() while block.isValid() and top <= ev.rect().bottom(): if block.isVisible() and bottom >= ev.rect().top(): p.drawText(-5, top, self.lineNumWidget.width(), self.__charHeight, Qt.AlignRight, str(bn + 1) + ':') block = block.next() top = bottom bottom = top + self.blockBoundingRect(block).height() bn += 1 def __repaintCpuStatsWidget(self, ev): p = self.cpuStatsWidget.getPainter() rect = ev.rect() p.fillRect(rect, Qt.lightGray) rect.setWidth(3) p.fillRect(rect, Qt.white) block = self.firstVisibleBlock() bn = block.blockNumber() top = self.blockBoundingGeometry(block).translated(self.contentOffset()).top() bottom = top + self.blockBoundingRect(block).height() while block.isValid() and top <= ev.rect().bottom(): statsEnt = self.__lineCpuStats.get(bn) if statsEnt and block.isVisible() and bottom >= ev.rect().top(): if statsEnt.obsolete: p.setPen(Qt.darkGray) else: p.setPen(Qt.black) p.drawText(5, top, self.cpuStatsWidget.width(), self.__charHeight, Qt.AlignLeft, str(statsEnt)) block = block.next() top = bottom bottom = top + self.blockBoundingRect(block).height() bn += 1 def __textChanged(self): self.__updateFonts() if self.__textChangeBlocked: return self.codeChanged.emit() self.resetCpuStats() def loadCode(self, code): self.__textChangeBlocked += 1 self.setPlainText(code) self.resetCpuStats() self.__textChangeBlocked -= 1 def getCode(self): return self.toPlainText() class CpuConfigDialog(QDialog): def __init__(self, parent, sim): QDialog.__init__(self, parent) self.sim = sim self.setWindowTitle("CPU configuration") self.__updateBlocked = 0 self.setLayout(QGridLayout(self)) label = QLabel("Number of accumulator registers", self) self.layout().addWidget(label, 0, 0) self.accuCombo = QComboBox(self) self.accuCombo.addItem("2 accus", 2) self.accuCombo.addItem("4 accus", 4) self.layout().addWidget(self.accuCombo, 0, 1) label = QLabel("Mnemonics", self) self.layout().addWidget(label, 1, 0) self.mnemonicsCombo = QComboBox(self) self.mnemonicsCombo.addItem("Automatic", S7CPUSpecs.MNEMONICS_AUTO) self.mnemonicsCombo.addItem("English", S7CPUSpecs.MNEMONICS_EN) self.mnemonicsCombo.addItem("German", S7CPUSpecs.MNEMONICS_DE) self.layout().addWidget(self.mnemonicsCombo, 1, 1) self.obTempCheckBox = QCheckBox("Enable writing of OB TEMP " "entry-variables", self) self.layout().addWidget(self.obTempCheckBox, 2, 0, 1, 2) self.closeButton = QPushButton("Close", self) self.layout().addWidget(self.closeButton, 3, 1) self.accuCombo.currentIndexChanged.connect(self.__accuConfigChanged) self.mnemonicsCombo.currentIndexChanged.connect(self.__mnemonicsConfigChanged) self.obTempCheckBox.stateChanged.connect(self.__obTempConfigChanged) self.closeButton.released.connect(self.accept) self.loadConfig() def loadConfig(self): cpu = self.sim.getCPU() specs = cpu.getSpecs() self.__updateBlocked += 1 index = self.accuCombo.findData(specs.nrAccus) assert(index >= 0) self.accuCombo.setCurrentIndex(index) index = self.mnemonicsCombo.findData(specs.getConfiguredMnemonics()) assert(index >= 0) self.mnemonicsCombo.setCurrentIndex(index) self.obTempCheckBox.setCheckState( Qt.Checked if cpu.obTempPresetsEnabled() else\ Qt.Unchecked ) self.__updateBlocked -= 1 def __accuConfigChanged(self): if self.__updateBlocked: return specs = self.sim.getCPU().getSpecs() index = self.accuCombo.currentIndex() specs.setNrAccus(self.accuCombo.itemData(index)) def __mnemonicsConfigChanged(self): if self.__updateBlocked: return specs = self.sim.getCPU().getSpecs() index = self.mnemonicsCombo.currentIndex() specs.setConfiguredMnemonics(self.mnemonicsCombo.itemData(index)) def __obTempConfigChanged(self): if self.__updateBlocked: return cpu = self.sim.getCPU() cpu.enableObTempPresets(self.obTempCheckBox.checkState() == Qt.Checked) class MainWidget(QWidget): dirtyChanged = Signal(bool) runStateChanged = Signal(int) def __init__(self, parent=None): QWidget.__init__(self, parent) self.setLayout(QGridLayout(self)) self.sim = AwlSim() if opt_extInsns: self.sim.getCPU().enableExtendedInsns(True) self.splitter = QSplitter(Qt.Horizontal) self.layout().addWidget(self.splitter, 0, 0) self.codeEdit = EditWidget(self) self.splitter.addWidget(self.codeEdit) self.cpuWidget = CpuWidget(self, self) self.splitter.addWidget(self.cpuWidget) self.filename = None self.dirty = False self.codeEdit.codeChanged.connect(self.__codeChanged) self.codeEdit.codeChanged.connect(self.cpuWidget.stop) self.cpuWidget.runStateChanged.connect(self.__runStateChanged) self.runStateChanged.connect(self.codeEdit.runStateChanged) def isDirty(self): return self.dirty def __runStateChanged(self, newState): self.runStateChanged.emit(newState) def getSim(self): return self.sim def getCodeEditWidget(self): return self.codeEdit def getCpuWidget(self): return self.cpuWidget def __codeChanged(self): self.dirty = True self.dirtyChanged.emit(self.dirty) def loadFile(self, filename): try: data = awlFileRead(filename) except AwlParserError as e: QMessageBox.critical(self, "File read failed", str(e)) return False self.codeEdit.loadCode(data) self.filename = filename return True def load(self): fn, fil = QFileDialog.getOpenFileName(self, "Open AWL source", "", "AWL source (*.awl);;" "All files (*)") if not fn: return self.loadFile(fn) def saveFile(self, filename): code = self.codeEdit.getCode() try: awlFileWrite(filename, code) except AwlParserError as e: QMessageBox.critical(self, "Failed to write file", str(e)) return False self.dirty = False self.dirtyChanged.emit(self.dirty) self.filename = filename return True def save(self, newFile=False): if newFile or not self.filename: fn, fil = QFileDialog.getSaveFileName(self, "AWL source save as", "", "AWL source (*.awl)", "*.awl") if not fn: return if not fn.endswith(".awl"): fn += ".awl" return self.saveFile(fn) else: return self.saveFile(self.filename) def cpuConfig(self): dlg = CpuConfigDialog(self, self.sim) dlg.exec_() class MainWindow(QMainWindow): def __init__(self): QMainWindow.__init__(self) self.setWindowTitle("S7 AWL simulator v%d.%d" %\ (VERSION_MAJOR, VERSION_MINOR)) self.setCentralWidget(MainWidget(self)) self.setMenuBar(QMenuBar(self)) menu = QMenu("&File", self) menu.addAction("&Open...", self.load) self.saveAct = menu.addAction("&Save", self.save) menu.addAction("&Save as...", self.saveAs) menu.addSeparator() menu.addAction("&Exit...", self.close) self.menuBar().addMenu(menu) menu = QMenu("&Simulator", self) menu.addAction("&CPU config...", self.cpuConfig) self.menuBar().addMenu(menu) menu = QMenu("Help", self) menu.addAction("&About...", self.about) self.menuBar().addMenu(menu) self.tb = QToolBar(self) self.tb.addAction("Open", self.load) self.tbSaveAct = self.tb.addAction("Save", self.save) self.addToolBar(self.tb) self.__dirtyChanged(False) self.centralWidget().dirtyChanged.connect(self.__dirtyChanged) self.centralWidget().runStateChanged.connect(self.__runStateChanged) if opt_awlSource: self.centralWidget().loadFile(opt_awlSource) def __runStateChanged(self, newState): self.menuBar().setEnabled(newState == CpuWidget.STATE_STOP) self.tb.setEnabled(newState == CpuWidget.STATE_STOP) def __dirtyChanged(self, isDirty): self.saveAct.setEnabled(isDirty) self.tbSaveAct.setEnabled(isDirty) def closeEvent(self, ev): cpuWidget = self.centralWidget().getCpuWidget() if cpuWidget.getState() != CpuWidget.STATE_STOP: res = QMessageBox.question(self, "CPU is in RUN state", "CPU is in RUN state.\n" "STOP CPU and quit application?", QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) if res != QMessageBox.Yes: ev.ignore() return self.centralWidget().getCpuWidget().stop() if self.centralWidget().isDirty(): res = QMessageBox.question(self, "Unsaved AWL code", "The editor contains unsaved AWL code.\n" "AWL code will be lost by exiting without saving.", QMessageBox.Discard | QMessageBox.Save | QMessageBox.Cancel, QMessageBox.Cancel) if res == QMessageBox.Save: if not self.centralWidget().save(): ev.ignore() return elif res == QMessageBox.Cancel: ev.ignore() return ev.accept() QMainWindow.closeEvent(self, ev) def about(self): QMessageBox.information(self, "About S7 AWL simulator", "awlsim version %d.%d\n\n" "Copyright 2012-2013 Michael Büsch \n" "Licensed under the terms of the " "GNU GPL version 2 or (at your option) " "any later version." %\ (VERSION_MAJOR, VERSION_MINOR)) def load(self): self.centralWidget().load() def save(self): self.centralWidget().save() def saveAs(self): self.centralWidget().save(True) def cpuConfig(self): self.centralWidget().cpuConfig() def usage(): printInfo("awlsimgui version %d.%d" % (VERSION_MAJOR, VERSION_MINOR)) printInfo("") printInfo("%s [OPTIONS] [AWL-source]" % sys.argv[0]) printInfo("") printInfo("Options:") printInfo(" -x|--extended-insns Enable extended instructions") def main(): global opt_awlSource global opt_extInsns try: (opts, args) = getopt.getopt(sys.argv[1:], "hx", [ "help", "extended-insns", ]) except getopt.GetoptError as e: printError(str(e)) usage() return 1 for (o, v) in opts: if o in ("-h", "--help"): usage() return 0 if o in ("-x", "--extended-insns"): opt_extInsns = True if args: if len(args) == 1: opt_awlSource = args[0] else: usage() return 1 app = QApplication(sys.argv) mainwnd = MainWindow() mainwnd.show() return app.exec_() if __name__ == "__main__": sys.exit(main())