Browse Source

Got digital watch pretty much working, with some bugs.

Joeri Exelmans 5 years ago
parent
commit
a57ca82fd6

+ 75 - 94
examples/digitalwatch/DigitalWatchGUI.py

@@ -34,54 +34,50 @@ class DigitalWatchGUI:
     def __init__(self, parent):
         self.controller = DigitalWatchGUI_Controller()
         self.staticGUI = DigitalWatchGUI_Static(parent, self.controller)
-        self.dynamicGUI = DigitalWatchGUI_Dynamic(self.controller)
 
-                     
+
 class DigitalWatchGUI_Controller:
 
     def __init__(self):
-        self.statechart = None
+        self.send_event = None
         self.GUI = None
         self.battery = True
-                
-    def bindDynamic(self, statechart):
-        self.statechart = statechart
-    
+                    
     def bindStatic(self, GUI):
         self.GUI = GUI
-                            
-    # Interface for the GUI                                                                 
+
+    # Interface for the GUI
     def window_close(self):
         import sys
         sys.exit(0)
-        self.statechart.event('GUIQuit')
+        self.send_event('GUIQuit')
 
     def topRightPressed(self):
-        self.statechart.event('topRightPressed')
+        self.send_event('topRightPressed')
 
     def topRightReleased(self):
-        self.statechart.event('topRightReleased')
+        self.send_event('topRightReleased')
     
     def topLeftPressed(self):
-        self.statechart.event('topLeftPressed')
+        self.send_event('topLeftPressed')
     
     def topLeftReleased(self):
-        self.statechart.event('topLeftReleased')
+        self.send_event('topLeftReleased')
         
     def bottomRightPressed(self):
-        self.statechart.event('bottomRightPressed')
+        self.send_event('bottomRightPressed')
 
     def bottomRightReleased(self):
-        self.statechart.event('bottomRightReleased')
+        self.send_event('bottomRightReleased')
     
     def bottomLeftPressed(self):
-        self.statechart.event('bottomLeftPressed')
+        self.send_event('bottomLeftPressed')
     
     def bottomLeftReleased(self):
-        self.statechart.event('bottomLeftReleased')
+        self.send_event('bottomLeftReleased')
 
     def alarm(self):
-        self.statechart.event('alarmStart')
+        self.send_event('alarmStart')
         
     # Interface for the statechart
      
@@ -99,7 +95,7 @@ class DigitalWatchGUI_Controller:
  
     def refreshTimeDisplay(self):
         self.GUI.drawTime()
-                        
+
     def refreshChronoDisplay(self):
         self.GUI.drawChrono()
     
@@ -160,19 +156,6 @@ class DigitalWatchGUI_Controller:
         else:
             return False
 
-        
-class DigitalWatchGUI_Dynamic:
-    
-    def __init__(self, controller):
-        self.controller = controller
-        self.controller.bindDynamic(self)
-
-    def setStatechart(self, controller):
-        print("STATECHART set")
-        self.controller_Yakindu = controller
-        
-    def event(self, evt):
-        self.controller_Yakindu.add_input(evt)
 
 #======================================================================#
 # GUI Static class
@@ -208,7 +191,7 @@ class DigitalWatchGUI_Static(Frame):
         
         self.curSelectionInfo = None
         self.curSelection = ["hours", "minutes", "seconds",
-                                                         "months", "days", "years"]
+                             "months", "days", "years"]
         self.curSelectionIndex = 0 
         
         self.lastPressed = ""
@@ -220,7 +203,7 @@ class DigitalWatchGUI_Static(Frame):
         
         self.drawTime()
         self.drawDate()
-                    
+
     def getTime(self):
         return self.curTime
 
@@ -241,38 +224,37 @@ class DigitalWatchGUI_Static(Frame):
         self.watch = self.displayCanvas.create_image(0, 0, image=self.watchImage, anchor="nw")
                 
         self.display = self.displayCanvas.create_rectangle(RECT_X0,
-                                                                                                             RECT_Y0,
-                                                                                                             RECT_X1,
-                                                                                                             RECT_Y1,
-                                                                                                             fill="#DCDCDC")
-                                                                                                             
+                                                         RECT_Y0,
+                                                         RECT_X1,
+                                                         RECT_Y1,
+                                                         fill="#DCDCDC")
         self.topRightButton = self.displayCanvas.create_rectangle(CANVAS_W - 13,
-                                                                                                                            60,
-                                                                                                                            CANVAS_W - 3,
-                                                                                                                            70,
-                                                                                                                            fill='')
+                                                                    60,
+                                                                    CANVAS_W - 3,
+                                                                    70,
+                                                                    fill='')
 
         self.topLeftButton = self.displayCanvas.create_rectangle(3,
-                                                                                                                         62,
-                                                                                                                         13,
-                                                                                                                         72,
-                                                                                                                         fill='')
-                                                                                                                            
+                                                                 62,
+                                                                 13,
+                                                                 72,
+                                                                 fill='')
+
         self.bottomRightButton = self.displayCanvas.create_rectangle(CANVAS_W - 10,
-                                                                                                                     162,
-                                                                                                                     CANVAS_W,
-                                                                                                                     172,
-                                                                                                                     fill='')
-                                                                                                                     
+                                                                     162,
+                                                                     CANVAS_W,
+                                                                     172,
+                                                                     fill='')
+
         self.bottomLeftButton = self.displayCanvas.create_rectangle(3,
-                                                                                                                                161,
-                                                                                                                                13,
-                                                                                                                                171,
-                                                                                                                                fill='')
-                                                                                                                                                                                                                                                                    
+                                                                    161,
+                                                                    13,
+                                                                    171,
+                                                                    fill='')
+
         self.displayCanvas.bind("<ButtonPress-1>", self.mouse1Click)
-        self.displayCanvas.bind("<ButtonRelease-1>", self.mouse1Release)                                                                                                                            
-                                                                                                                                                                                             
+        self.displayCanvas.bind("<ButtonRelease-1>", self.mouse1Release)
+
     def mouse1Click(self, event):
         X = self.displayCanvas.canvasx(event.x)
         Y = self.displayCanvas.canvasy(event.y)
@@ -315,14 +297,14 @@ class DigitalWatchGUI_Static(Frame):
         hours = self.__intToString(self.curTime[0])
         minutes = self.__intToString(self.curTime[1])
         seconds = self.__intToString(self.curTime[2])                
-                                    
+
         return hours + ":" + minutes + ":" + seconds
         
     def __getAlarmAsString(self):    
         hours = self.__intToString(self.curAlarm[0])
         minutes = self.__intToString(self.curAlarm[1])
         seconds = self.__intToString(self.curAlarm[2])                
-                                    
+
         return hours + ":" + minutes + ":" + seconds    
         
     def __getChronoAsString(self):
@@ -443,7 +425,7 @@ class DigitalWatchGUI_Static(Frame):
     def startSelection(self):
         self.curSelectionIndex = 0
         self.animateSelection()
-                    
+
     def animateSelection(self):
  
         timeFunc = None
@@ -470,9 +452,9 @@ class DigitalWatchGUI_Static(Frame):
         animationEvent = self.parent.after(1000, self.animateSelection)
 
         self.curSelectionInfo = [deleteEvent,
-                                                         creationEvent,
-                                                         animationEvent]        
-                
+                                 creationEvent,
+                                 animationEvent]
+
     def increaseTimeByOne(self):
         self.curTime[2] = self.curTime[2] + 1
         self.curTime[1] = (self.curTime[1] + self.curTime[2] // 60)
@@ -486,7 +468,7 @@ class DigitalWatchGUI_Static(Frame):
              self.curTime[1] == 0 and\
              self.curTime[2] == 0:
             self.increaseDateByOne()
-                                                        
+
     def increaseDateByOne(self):    
         month = self.curDate[0]
         day = self.curDate[1]
@@ -494,7 +476,7 @@ class DigitalWatchGUI_Static(Frame):
         
         numMonths = 12
         numDays = self.getNumDays()
-                                     
+
         self.curDate[1] = self.curDate[1] + 1
         self.curDate[0] = (self.curDate[0] + self.curDate[1] // (numDays + 1))
         self.curDate[2] = (self.curDate[2] + self.curDate[0] // (numMonths + 1))
@@ -543,16 +525,16 @@ class DigitalWatchGUI_Static(Frame):
         self.clearDisplay()         
             
         self.timeTag = self.displayCanvas.create_text((RECT_X0 + RECT_X1) / 2,
-                                                                                                 (RECT_Y0 + RECT_Y1) / 2 + 5,
-                                                                                                    font=FONT_TIME,
-                                                                                                    justify="center",
-                                                                                                    text=timeToDraw)
-                                                                                                    
+                                                     (RECT_Y0 + RECT_Y1) / 2 + 5,
+                                                        font=FONT_TIME,
+                                                        justify="center",
+                                                        text=timeToDraw)
+
     def hideTime(self):
         if self.timeTag != None:
             self.displayCanvas.delete(self.timeTag)
             self.timeTag = None
-                                                                                                                                 
+
     def drawChrono(self):
         chronoToDraw = self.__getChronoAsString()
 
@@ -562,11 +544,10 @@ class DigitalWatchGUI_Static(Frame):
             chronoToDraw = "88:88:88"
             
         self.chronoTag = self.displayCanvas.create_text((RECT_X0 + RECT_X1) / 2,
-                                                                                                     (RECT_Y0 + RECT_Y1) / 2 + 5,
-                                                                                                    font=FONT_TIME,
-                                                                                                    justify="center",
-                                                                                                    text=chronoToDraw)                                                                                                    
-                                                                                                                                                                                                                                    
+                                                         (RECT_Y0 + RECT_Y1) / 2 + 5,
+                                                        font=FONT_TIME,
+                                                        justify="center",
+                                                        text=chronoToDraw)
     def hideChrono(self):
         if self.chronoTag != None:
             self.displayCanvas.delete(self.chronoTag)
@@ -574,14 +555,14 @@ class DigitalWatchGUI_Static(Frame):
             
     def resetChrono(self):
         self.curChrono = [0, 0, 0]
-                                                                                     
+
     def drawDate(self, toDraw=["years", "months", "days"]):
         dateToDraw = self.__getDateAsString()
         
         if "months" not in toDraw:
             dateToDraw = "    " + dateToDraw[2:]            
         if "days" not in toDraw:
-            dateToDraw = dateToDraw[0:3] + "    " + dateToDraw[5:]                     
+            dateToDraw = dateToDraw[0:3] + "    " + dateToDraw[5:]
         if "years" not in toDraw:
             dateToDraw = dateToDraw[0:6] + "    "
 
@@ -591,11 +572,11 @@ class DigitalWatchGUI_Static(Frame):
         self.clearDate()
  
         self.dateTag = self.displayCanvas.create_text(RECT_X1 - 33,
-                                                                                                RECT_Y0 + 7,
-                                                                                                font=FONT_DATE,
-                                                                                                justify="center",
-                                                                                                text=dateToDraw)
-                                                                     
+                                                    RECT_Y0 + 7,
+                                                    font=FONT_DATE,
+                                                    justify="center",
+                                                    text=dateToDraw)
+
     def drawAlarm(self, toDraw=["hours", "minutes", "seconds"]):        
         alarmToDraw = self.__getAlarmAsString()
                 
@@ -612,15 +593,15 @@ class DigitalWatchGUI_Static(Frame):
         self.clearDisplay()
         
         self.alarmTag = self.displayCanvas.create_text((RECT_X0 + RECT_X1) / 2,
-                                                                                                    (RECT_Y0 + RECT_Y1) / 2 + 5,
-                                                                                                     font=FONT_TIME,
-                                                                                                     justify="center",
-                                                                                                     text=alarmToDraw)
-                                                                                                    
+                                                    (RECT_Y0 + RECT_Y1) / 2 + 5,
+                                                     font=FONT_TIME,
+                                                     justify="center",
+                                                     text=alarmToDraw)
+
     def hideAlarm(self):
         if self.alarmTag != None:
             self.displayCanvas.delete(self.alarmTag)
-            self.alarmTag = None                                                                     
+            self.alarmTag = None
 
     def setAlarm(self):
         if self.alarmNoteTag != None:
@@ -628,7 +609,7 @@ class DigitalWatchGUI_Static(Frame):
             self.alarmNoteTag = None
         else:
             self.alarmNoteTag = self.displayCanvas.create_image(RECT_X0 + 5, RECT_Y0 + 3, image=self.noteImage, anchor="nw")         
-                                                                                     
+
     def setIndiglo(self):
         self.displayCanvas.itemconfigure(self.display, fill='#96DCFA')
 

+ 59 - 15
examples/digitalwatch/run.py

@@ -1,25 +1,69 @@
 from DigitalWatchGUI import DigitalWatchGUI
 import tkinter
 from tkinter.constants import NO
-
+from time import perf_counter
 from sccd.controller.controller import *
-from sccd.statechart.parser.xml import parse_f, create_statechart_parser
+from sccd.statechart.parser.xml import parse_f, statechart_parser_rules
 from sccd.model.model import *
+import queue
+
+def now():
+    return int(perf_counter()*10000) # 100 us-precision, same as our model delta
+
+def main():
+    g = Globals()
+    sc_rules = statechart_parser_rules(g, "statechart_digitalwatch.xml")
+    statechart = parse_f("statechart_digitalwatch.xml", rules=sc_rules)
+    model = SingleInstanceModel(g, statechart)
+    g.set_delta(duration(100, Microsecond))
+    controller = Controller(model)
+
+    scheduled = None
+
+    def gui_event(event: str):
+        print(event)
+        controller.add_input(InputEvent(name=event, port="in", params=[], time_offset=duration(0)))
+        if scheduled:
+            tk.after_cancel(scheduled)
+        wakeup()
+
+    tk = tkinter.Tk()
+    tk.withdraw()
+    topLevel = tkinter.Toplevel(tk)
+    topLevel.resizable(width=NO, height=NO)
+    topLevel.title("DWatch")
+    gui = DigitalWatchGUI(topLevel)
+    gui.controller.send_event = gui_event
+
+    q = queue.Queue()
+    start_time = now()
 
-g = Globals()
-sc_parser = create_statechart_parser(g, "statechart_digitalwatch.xml")
-statechart = parse_f("statechart_digitalwatch.xml", rules=sc_parser)
-model = SingleInstanceModel(g, statechart)
-controller = Controller(model)
+    def wakeup():
+        nonlocal scheduled
+        # run controller - output will accumulate in 'q'
+        controller.run_until(now() - start_time, q)
 
+        # process output
+        try:
+            while True:
+                big_step_output = q.get_nowait()
+                for e in big_step_output:
+                    print("out:", e.name)
+                    # print("got output:", e.name)
+                    # the output event names happen to be functions on our GUI controller:
+                    method = getattr(gui.controller, e.name)
+                    # print(method)
+                    method()
+        except queue.Empty:
+            pass
 
-root = tkinter.Tk()
-root.withdraw()
-topLevel = tkinter.Toplevel(root)
-topLevel.resizable(width=NO, height=NO)
-topLevel.title("DWatch")
-gui = DigitalWatchGUI(topLevel)
+        # done enough for now, go to sleep
+        # convert our statechart's timestamp to tkinter's (100 us -> 1 ms)
+        sleep_duration = (controller.next_wakeup() - controller.simulated_time) // 10
+        scheduled = tk.after(sleep_duration, wakeup)
 
-gui.controller.
+    tk.after(0, wakeup)
+    tk.mainloop()
 
-root.mainloop()
+if __name__ == '__main__':
+    main()

+ 63 - 70
examples/digitalwatch/statechart_digitalwatch.xml

@@ -1,46 +1,37 @@
 <statechart>
+  <semantics big_step_maximality="take_many"/>
+  
   <datamodel>
-    # declare functions
-    set_alarm = func {};
-    set_indiglo = func {};
-    unset_indiglo = func {};
-    increase_time_by_one = func {};
-    refresh_time_display = func {};
-    check_time = func {};
-    start_selection = func {};
-    stop_selection = func {};
-    increase_selection = func {};
-    reset_chrono = func {};
-    increase_chrono_by_one = func {};
-    refresh_chrono_display = func {};
+    checkTime = func {
+      log("checkTime");
+    };
   </datamodel>
 
-  <outport name="out">
-    <event name="set_alarm"/>
-    <event name="set_indiglo"/>
-    <event name="unset_indiglo"/>
-    <event name="increase_time_by_one"/>
-    <event name="refresh_time_display"/>
-    <event name="check_time"/>
-    <event name="start_selection"/>
-    <event name="stop_selection"/>
-    <event name="increase_selection"/>
-    <event name="reset_chrono"/>
-    <event name="increase_chrono_by_one"/>
-    <event name="refresh_chrono_display"/>
-  </outport>
-
   <inport name="in">
-    <event name="bottom_left_pressed"/>
-    <event name="bottom_left_released"/>
-    <event name="bottom_right_pressed"/>
-    <event name="bottom_right_released"/>
-    <event name="top_left_pressed"/>
-    <event name="top_left_released"/>
-    <event name="top_right_pressed"/>
-    <event name="top_right_released"/>
+    <event name="bottomLeftPressed"/>
+    <event name="bottomLeftReleased"/>
+    <event name="bottomRightPressed"/>
+    <event name="bottomRightReleased"/>
+    <event name="topLeftPressed"/>
+    <event name="topLeftReleased"/>
+    <event name="topRightPressed"/>
+    <event name="topRightReleased"/>
   </inport>
 
+  <outport name="out">
+    <event name="setAlarm"/>
+    <event name="setIndiglo"/>
+    <event name="unsetIndiglo"/>
+    <event name="increaseTimeByOne"/>
+    <event name="refreshTimeDisplay"/>
+    <event name="startSelection"/>
+    <event name="stopSelection"/>
+    <event name="increaseSelection"/>
+    <event name="resetChrono"/>
+    <event name="increaseChronoByOne"/>
+    <event name="refreshChronoDisplay"/>
+  </outport>
+
   <outport name="out">
   </outport>
 
@@ -48,47 +39,48 @@
     <parallel id="P">
       <state id="Indiglo" initial="Off">
         <state id="Off">
-          <transition event="top_right_pressed" target="../Pushed">
-            <code> set_indiglo(); </code>
+          <transition event="topRightPressed" target="../Pushed">
+            <raise event="setIndiglo"/>
           </transition>
         </state>
 
         <state id="Pushed">
-          <transition event="top_right_released" target="../Released"/>
+          <transition event="topRightReleased" target="../Released"/>
         </state>
 
         <state id="Released">
-          <transition event="top_right_pressed" target="../Pushed"/>
+          <transition event="topRightPressed" target="../Pushed"/>
           <transition after="2 s" target="../Off">
-            <code> unset_indiglo(); </code>
+            <raise event="unsetIndiglo"/>
           </transition>
         </state>
       </state>
 
       <state id="Chrono" initial="Stopped">
         <state id="Stopped">
-          <transition event="bottom_left_pressed" cond='INSTATE(["/Display/ChronoUpdate"])' target=".">
-            <code> reset_chrono(); </code>
+          <transition event="bottomLeftPressed" cond='INSTATE(["/P/Display/ChronoUpdate"])' target=".">
+            <raise event="resetChrono"/>
           </transition>
-          <transition event="bottom_right_pressed" cond='INSTATE(["/Display/ChronoUpdate"])' target="../Running"/>
+          <transition event="bottomRightPressed" cond='INSTATE(["/P/Display/ChronoUpdate"])' target="../Running"/>
         </state>
 
         <state id="Running">
           <transition after="10 ms" target=".">
-            <code> increase_chrono_by_one(); </code>
+            <raise event="increaseChronoByOne"/>
           </transition>
-          <transition event="bottom_right_pressed" cond='INSTATE(["/Display/ChronoUpdate"])' target="../Stopped"/>
+          <transition event="bottomRightPressed" cond='INSTATE(["/P/Display/ChronoUpdate"])' target="../Stopped"/>
         </state>
       </state>
 
       <state id="Display" initial="TimeUpdate">
         <state id="TimeUpdate">
           <onentry>
-            <code> refresh_time_display(); </code>
+            <raise event="refreshTimeDisplay"/>
           </onentry>
-          <transition event="top_left_pressed" target="../ChronoUpdate"/>
-          <transition event="bottom_right_pressed" target="../WaitingToEdit"/>
-          <transition event="bottom_left_pressed" target="../WaitingForAlarm"/>
+          <transition after="1 s" target="."/>
+          <transition event="topLeftPressed" target="../ChronoUpdate"/>
+          <transition event="bottomRightPressed" target="../WaitingToEdit"/>
+          <transition event="bottomLeftPressed" target="../WaitingForAlarm"/>
         </state>
 
         <state id="WaitingToEdit">
@@ -99,22 +91,22 @@
 
         <state id="EditingTime" initial="Waiting">
           <onentry>
-            <code> start_selection(); </code>
+            <raise event="startSelection"/>
           </onentry>
           <onexit>
-            <code> stop_selection(); </code>
+            <raise event="stopSelection"/>
           </onexit>
 
           <state id="Waiting">
-            <transition event="bottom_left_pressed" target="../Increasing"/>
-            <transition event="bottom_right_pressed" target="../GoingToNext"/>
+            <transition event="bottomLeftPressed" target="../Increasing"/>
+            <transition event="bottomRightPressed" target="../GoingToNext"/>
             <transition after="5 s" target="../../TimeUpdate">
               <raise event="edit_done"/>
             </transition>
           </state>
 
           <state id="GoingToNext">
-            <transition event="bottom_right_released" target="../Waiting"/>
+            <transition event="bottomRightReleased" target="../Waiting"/>
             <transition after="2 s" target="../../TimeUpdate">
               <raise event="edit_done"/>
             </transition>
@@ -122,17 +114,17 @@
 
           <state id="Increasing">
             <onentry>
-              <code> increase_selection(); </code>
+              <raise event="increaseSelection"/>
             </onentry>
             <transition after="300 ms" target="."/>
-            <transition event="bottom_left_released" target="../Waiting"/>
+            <transition event="bottomLeftReleased" target="../Waiting"/>
           </state>
         </state>
 
         <state id="ChronoUpdate">
-          <transition event="top_left_pressed" target="../TimeUpdate"/>
+          <transition event="topLeftPressed" target="../TimeUpdate"/>
           <transition after="10 ms" target=".">
-            <code> refresh_chrono_display(); </code>
+            <raise event="refreshChronoDisplay"/>
           </transition>
         </state>
 
@@ -145,43 +137,44 @@
 
       <state id="Alarm" initial="Off">
         <state id="Off">
-          <transition event="bottom_left_pressed" cond='INSTATE(["/Display/TimeUpdate"]) or INSTATE(["/Display/WaitingForAlarm"])' target="../On">
-            <code> set_alarm(); </code>
+          <transition event="bottomLeftPressed" cond='INSTATE(["/P/Display/TimeUpdate"]) or INSTATE(["/P/Display/WaitingForAlarm"])' target="../On">
+            <raise event="setAlarm"/>
           </transition>
         </state>
 
         <state id="On">
-          <transition cond="check_time()" target="../Blinking"/>
+          <transition cond="checkTime()" target="../Blinking"/>
         </state>
 
         <state id="Blinking" initial="On">
           <onexit>
-            <code> set_alarm(); unset_indiglo(); </code>
+            <raise event="setAlarm"/>
+            <raise event="unsetIndiglo"/>
           </onexit>
           <state id="On">
             <onentry>
-              <code> set_indiglo(); </code>
+              <raise event="setIndiglo"/>
             </onentry>
             <transition after="500 ms" target="../Off"/>
           </state>
           <state id="Off">
             <onentry>
-              <code> unset_indiglo(); </code>
+              <raise event="unsetIndiglo"/>
             </onentry>
             <transition after="500 ms" target="../On"/>
           </state>
 
-          <transition event="top_right_pressed" target="../Off"/>
-          <transition event="top_left_pressed" target="../Off"/>
-          <transition event="bottom_right_pressed" target="../Off"/>
-          <transition event="bottom_left_pressed" target="../Off"/>
+          <transition event="topRightPressed" target="../Off"/>
+          <transition event="topLeftPressed" target="../Off"/>
+          <transition event="bottomRightPressed" target="../Off"/>
+          <transition event="bottomLeftPressed" target="../Off"/>
         </state>
       </state>
 
       <state id="Time" initial="Increasing">
         <state id="Increasing">
           <transition after="1 s" target=".">
-            <code> increase_time_by_one(); </code>
+            <raise event="increaseTimeByOne"/>
           </transition>
           <transition event="time_edit" target="../Editing"/>
         </state>

+ 36 - 29
src/sccd/controller/controller.py

@@ -37,6 +37,9 @@ class Controller:
         self.model.globals.assert_ready()
         # print_debug("model delta is %s" % str(self.model.globals.delta))
 
+        # First call to 'run_until' method initializes
+        self.run_until = self._initialize
+
     def _duration_to_time_offset(self, d: Duration) -> int:
         if self.model.globals.delta == duration(0):
             return 0
@@ -78,36 +81,40 @@ class Controller:
     def get_simulated_duration(self) -> Duration:
         return (self.model.globals.delta * self.simulated_time)
 
+    # Helper. Put big step output events in the event queue or add them to the right output listeners.
+    def _process_big_step_output(self, events: List[OutputEvent], pipe: queue.Queue):
+        pipe_events = []
+        for e in events:
+            if isinstance(e.target, InstancesTarget):
+                offset = self._duration_to_time_offset(e.time_offset)
+                self.queue.add(self.simulated_time + offset, Controller.EventQueueEntry(e.event, e.target.instances))
+            elif isinstance(e.target, OutputPortTarget):
+                assert (e.time_offset == duration(0)) # cannot combine 'after' with 'output port'
+                pipe_events.append(e.event)
+            else:
+                raise Exception("Unexpected type:", e.target)
+        if pipe_events:
+            pipe.put(pipe_events, block=True, timeout=None)
+
+
+    def _initialize(self, now: Optional[Timestamp], pipe: queue.Queue):
+        # first run...
+        # initialize the object manager, in turn initializing our default class
+        # and adding the generated events to the queue
+        for i in self.object_manager.instances:
+            events = i.initialize()
+            self._process_big_step_output(events, pipe)
+        print_debug("initialized. time is now %s" % str(self.get_simulated_duration()))
+
+        # Next call to 'run_until' will call '_run_until'
+        self.run_until = self._run_until
+
+        # Let's try it out :)
+        self.run_until(now, pipe)
+
     # Run until the event queue has no more due events wrt given timestamp and until all instances are stable.
     # If no timestamp is given (now = None), run until event queue is empty.
-    def run_until(self, now: Optional[Timestamp], pipe: queue.Queue):
-
-        # Helper. Put big step output events in the event queue or add them to the right output listeners.
-        def process_big_step_output(events: List[OutputEvent]):
-            pipe_events = []
-            for e in events:
-                if isinstance(e.target, InstancesTarget):
-                    offset = self._duration_to_time_offset(e.time_offset)
-                    self.queue.add(self.simulated_time + offset, Controller.EventQueueEntry(e.event, e.target.instances))
-                elif isinstance(e.target, OutputPortTarget):
-                    assert (e.time_offset == 0) # cannot combine 'after' with 'output port'
-                    pipe_events.append(e.event)
-                else:
-                    raise Exception("Unexpected type:", e.target)
-            if pipe_events:
-                pipe.put(pipe_events, block=True, timeout=None)
-
-        if not self.initialized:
-            self.initialized = True
-
-            # first run...
-            # initialize the object manager, in turn initializing our default class
-            # and adding the generated events to the queue
-            for i in self.object_manager.instances:
-                events = i.initialize()
-                process_big_step_output(events)
-            print_debug("initialized. time is now %s" % str(self.get_simulated_duration()))
-
+    def _run_until(self, now: Optional[Timestamp], pipe: queue.Queue):
         # Actual "event loop"
         for timestamp, entry in self.queue.due(now):
             if timestamp != self.simulated_time:
@@ -118,6 +125,6 @@ class Controller:
             for instance in entry.targets:
                 output = instance.big_step([entry.event])
                 # print_debug("completed big step (time = %s)" % str(self.model.globals.delta * self.simulated_time))
-                process_big_step_output(output)
+                self._process_big_step_output(output, pipe)
 
         self.simulated_time = now

+ 10 - 12
src/sccd/model/globals.py

@@ -8,10 +8,7 @@ from sccd.util.debug import *
 
 # @dataclass
 class Globals:
-  # max_delta: upper bound on model delta
-  def __init__(self, fixed_delta: Optional[Duration] = duration(100, Microsecond)):
-    self.fixed_delta = fixed_delta
-
+  def __init__(self):
     self.events = Namespace()
     self.inports = Namespace()
     self.outports = Namespace()
@@ -21,17 +18,18 @@ class Globals:
     # Calculated after all expressions have been parsed, based on all DurationLiterals.
     self.delta: Optional[Duration] = None
 
-  def process_durations(self):
-    self.delta = gcd(*self.durations)
-
-    # self.delta will be duration(0) now if there are no durations in the model, or all durations are 0.
+  # delta: if set, this will be the model delta. otherwise, model delta will be the GCD of all durations registered.
+  def set_delta(self, delta: Optional[Duration]):
+    gcd_delta = gcd(*self.durations)
 
     # Ensure delta not too big
-    if self.fixed_delta:
-      if duration(0) < self.delta < self.fixed_delta:
-        raise Exception("Model contains duration deltas (smallest = %s) not representable with fixed delta of %s." % (str(self.delta), str(self.fixed_delta)))
+    if delta:
+      if duration(0) < gcd_delta < delta:
+        raise Exception("Model contains duration deltas (smallest = %s) not representable with delta of %s." % (str(self.delta), str(delta)))
       else:
-        self.delta = self.fixed_delta
+        self.delta = delta
+    else:
+      self.delta = gcd_delta
 
   def assert_ready(self):
     if self.delta is None:

+ 1 - 1
src/sccd/statechart/dynamic/builtin_scope.py

@@ -25,7 +25,7 @@ def load_builtins(memory: MemoryInterface, state):
     return int(x)
 
   def log(memory: MemoryInterface, s: str) -> None:
-    print_debug(termcolor.colored("log: ",'blue')+s)
+    print(termcolor.colored("log: ",'blue')+s)
 
   def int_to_str(memory: MemoryInterface, i: int) -> str:
     return str(i)

+ 1 - 1
src/sccd/statechart/dynamic/statechart_instance.py

@@ -93,7 +93,7 @@ class StatechartInstance(Instance):
         raise_nextbs = lambda e, time_offset: self.execution.output.append(OutputEvent(e, InstancesTarget([self]), time_offset))
 
         raise_internal = {
-            InternalEventLifeline.QUEUE: lambda e: raise_nextbs(e, 0),
+            InternalEventLifeline.QUEUE: lambda e: raise_nextbs(e, duration(0)),
             InternalEventLifeline.NEXT_COMBO_STEP: combo_step.add_next_event,
             InternalEventLifeline.NEXT_SMALL_STEP: small_step.add_next_event,
 

+ 1 - 1
src/sccd/statechart/parser/text.py

@@ -16,7 +16,7 @@ class StatechartTransformer(action_lang.ExpressionTransformer):
     super().__init__()
     self.globals: Globals = None
 
-  # override: add all durations to 'globals'
+  # override
   def duration(self, node):
     d = action_lang.ExpressionTransformer.duration(self, node)
     self.globals.durations.append(d)

+ 5 - 2
src/sccd/statechart/parser/xml.py

@@ -105,7 +105,7 @@ def statechart_parser_rules(globals, path, load_external = True, parse_f = parse
             else:
               # output event - no ID in global namespace
               globals.outports.assign_id(port)
-              return RaiseOutputEvent(name=event_name, params=params, outport=port, time_offset=0)
+              return RaiseOutputEvent(name=event_name, params=params, outport=port, time_offset=duration(0))
           return ([("param*", parse_param)], finish_raise)
 
         def parse_code(el):
@@ -223,7 +223,10 @@ def statechart_parser_rules(globals, path, load_external = True, parse_f = parse
             after_expr = parse_expression(globals, after)
             after_type = after_expr.init_expr(scope)
             check_duration_type(after_type)
-            event_name = "_after%d" % after_id # transition gets unique event name
+            # after-events should only be generated by the runtime
+            # by putting a '+' in the event name (which isn't an allowed character in the parser),
+            # we can be certain that the user will never generate a valid after-event.
+            event_name = "+after%d" % after_id # transition gets unique event name
             transition.trigger = AfterTrigger(globals.events.assign_id(event_name), event_name, after_id, after_expr)
             after_id += 1
 

+ 2 - 2
src/sccd/test/parser.py

@@ -12,7 +12,7 @@ class TestVariant:
   output: list
 
 def test_parser_rules(statechart_parser_rules):
-  globals = Globals(fixed_delta=None)
+  globals = Globals()
   sc_rules = statechart_parser_rules(globals)
   input = []
   output = []
@@ -55,7 +55,7 @@ def test_parser_rules(statechart_parser_rules):
       return [("big_step+", parse_big_step)]
 
     def when_done(statechart):
-      globals.process_durations()
+      globals.set_delta(None)
       variants = statechart.semantics.generate_variants()
 
       def variant_description(i, variant) -> str:

+ 13 - 18
src/sccd/test/run.py

@@ -10,6 +10,12 @@ from sccd.controller.controller import *
 from sccd.test.parser import *
 from sccd.util import timer
 
+import sys
+if sys.version_info.minor >= 7:
+  QueueImplementation = queue.SimpleQueue
+else:
+  QueueImplementation = queue.Queue
+
 # A TestCase loading and executing a statechart test file.
 class Test(unittest.TestCase):
   def __init__(self, src: str):
@@ -35,7 +41,7 @@ class Test(unittest.TestCase):
 
     for test in test_variants:
       print_debug('\n'+test.name)
-      pipe = queue.Queue()
+      pipe = QueueImplementation()
       # interrupt = queue.Queue()
 
       controller = Controller(test.model)
@@ -65,9 +71,9 @@ class Test(unittest.TestCase):
 
       def fail(msg):
         thread.join()
-        def repr(output):
+        def pretty(output):
           return '\n'.join("%d: %s" % (i, str(big_step)) for i, big_step in enumerate(output))
-        self.fail('\n'+test.name + '\n'+msg + "\n\nActual:\n" + repr(actual) + "\n\nExpected:\n" + repr(expected))
+        self.fail('\n'+test.name + '\n'+msg + "\n\nActual:\n" + pretty(actual) + "\n\nExpected:\n" + pretty(expected))
 
       while True:
         data = pipe.get(block=True, timeout=None)
@@ -83,29 +89,18 @@ class Test(unittest.TestCase):
             break
 
         else:
-          big_step = data
           big_step_index = len(actual)
-          actual.append(big_step)
+          actual.append(data)
 
           if len(actual) > len(expected):
             fail("More output than expected.")
 
-          actual_bag = actual[big_step_index]
-          expected_bag = expected[big_step_index]
+          actual_big_step = actual[big_step_index]
+          expected_big_step = expected[big_step_index]
 
-          if len(actual_bag) != len(expected_bag):
+          if actual_big_step != expected_big_step:
             fail("Big step %d: output differs." % big_step_index)
 
-          # Sort both expected and actual lists of events before comparing.
-          # In theory the set of events at the end of a big step is unordered.
-          # key_f = lambda e: "%s.%s"%(e.port, e.name)
-          # actual_bag.sort(key=key_f)
-          # expected_bag.sort(key=key_f)
-
-          for (act_event, exp_event) in zip(actual_bag, expected_bag):
-            if act_event != exp_event:
-              fail("Big step %d: output differs." % big_step_index)
-
 
 class FailingTest(Test):
   @unittest.expectedFailure

+ 9 - 13
src/sccd/util/duration.py

@@ -62,27 +62,27 @@ class Duration(ABC):
   def __floordiv__(self, other: 'Duration') -> int:
     if other is _zero:
       raise ZeroDivisionError("duration floordiv by zero duration")
-    self_conv, other_conv, _ = _same_unit(self, other)
-    return self_conv // other_conv
+    self_val, other_val, _ = _same_unit(self, other)
+    return self_val // other_val
 
   def __mod__(self, other):
-      self_conv, other_conv, unit = _same_unit(self, other)
-      new_val = self_conv % other_conv
+      self_val, other_val, unit = _same_unit(self, other)
+      new_val = self_val % other_val
       if new_val == 0:
         return _zero
       else:
         return _NonZeroDuration(new_val, unit)
 
   def __lt__(self, other):
-    self_conv, other_conv = _same_unit(self, other)
-    return self_conv.val < other_conv.val
+    self_val, other_val, unit = _same_unit(self, other)
+    return self_val < other_val
 
 class _ZeroDuration(Duration):
   def _convert(self, unit: _Unit) -> int:
     return 0
 
   def __str__(self):
-    return '0'
+    return '0 d'
 
   def __eq__(self, other):
     return self is other
@@ -155,13 +155,9 @@ def _same_unit(x: Duration, y: Duration) -> Tuple[int, int, _Unit]:
     return (x.val, 0, x.unit)
 
   if x.unit.relative_size >= y.unit.relative_size:
-    x_conv = x._convert(y.unit)
-    y_conv = y.val
-    unit = y.unit
+    return (x._convert(y.unit), y.val, y.unit)
   else:
-    x_conv = x.val
-    y_conv = y._convert(x.unit)
-    unit = x.unit
+    return (x.val, y._convert(x.unit), x.unit)
   return (x_conv, y_conv, unit)
 
 def gcd_pair(x: Duration, y: Duration) -> Duration:

+ 7 - 4
src/sccd/util/timer.py

@@ -8,24 +8,27 @@ except KeyError:
 if TIMINGS:
   import time
   import atexit
+  import collections
 
   timers = {}
   timings = {}
-
+  counts = collections.Counter()
+    
   def start(what):
-    timers[what] = time.time()
+    timers[what] = time.perf_counter()
 
   def stop(what):
-    end = time.time()
+    end = time.perf_counter()
     begin = timers[what]
     duration = end - begin
     old_val = timings.setdefault(what, 0)
     timings[what] = old_val + duration
+    counts[what] += 1
 
   def _print_stats():
       print("\ntimings:")
       for key,val in timings.items():
-        print("  %s: %f ms" % (key,val*1000))
+        print("  %s: %.2f ms / %d = %.2f ms" % (key,val*1000,counts[key],val*1000/counts[key]))
 
   atexit.register(_print_stats)
 

+ 11 - 7
src/sccd/util/xml_parser.py

@@ -102,15 +102,14 @@ def parse(src_file, rules: RulesWDone, ignore_unmatched = False, decorate_except
 
   for event, el in etree.iterparse(src_file, events=("start", "end")):
     try:
+      when_done = None
+      pair = rules_stack[-1]
+      if isinstance(pair, tuple):
+        rules, when_done = pair
+      else:
+        rules = pair
       if event == "start":
         # print("start", el.tag)
-        allowed_tags = []
-        when_done = None
-        pair = rules_stack[-1]
-        if isinstance(pair, tuple):
-          rules, when_done = pair
-        else:
-          rules = pair
 
         parse_function = None
         if isinstance(rules, dict):
@@ -166,6 +165,11 @@ def parse(src_file, rules: RulesWDone, ignore_unmatched = False, decorate_except
         results_stack.append([])
 
       elif event == "end":
+        if isinstance(rules, list) and len(rules) > 1:
+          tag_w_suffix, func = rules[0]
+          tag, m = Multiplicity.parse_suffix(tag_w_suffix)
+          if m & Multiplicity.AT_LEAST_ONCE:
+            raise XmlError("Expected required element <%s> " % tag)
         children_results = results_stack.pop()
         pair = rules_stack.pop()
         if isinstance(pair, tuple):