Browse Source

Cleaning up

Joeri Exelmans 5 years ago
parent
commit
7e9ac8f43d

+ 9 - 9
examples/digitalwatch/run.py

@@ -1,23 +1,17 @@
 from sccd.statechart.parser.xml import parse_f, statechart_parser_rules
 from sccd.cd.cd import *
-from sccd.controller.eventloop import *
+from sccd.realtime.eventloop import *
+from sccd.realtime.tkinter import TkInterImplementation
 import queue
 
-MODEL_DELTA = duration(100, Microsecond)
-
 def main():
     # Load statechart
     g = Globals()
     sc_rules = statechart_parser_rules(g, "statechart_digitalwatch.xml")
     statechart = parse_f("statechart_digitalwatch.xml", rules=sc_rules)
     cd = SingleInstanceCD(g, statechart)
-    g.set_delta(MODEL_DELTA)
+    g.set_delta(duration(100, Microsecond))
 
-    eventloop = None
-
-    def on_gui_event(event: str):
-        eventloop.add_input(InputEvent(name=event, port="in", params=[], time_offset=duration(0)))
-        eventloop.interrupt()
 
     import tkinter
     from tkinter.constants import NO
@@ -29,6 +23,11 @@ def main():
     topLevel.resizable(width=NO, height=NO)
     topLevel.title("DWatch")
     gui = DigitalWatchGUI(topLevel)
+
+    def on_gui_event(event: str):
+        eventloop.add_input(Event(id=-1, name=event, port="in", params=[]))
+        eventloop.interrupt()
+
     gui.controller.send_event = on_gui_event
 
     def on_big_step(output):
@@ -39,6 +38,7 @@ def main():
             method()
 
     eventloop = EventLoop(cd, TkInterImplementation(tk), on_big_step)
+
     eventloop.start()
     tk.mainloop()
 

+ 1 - 0
examples/digitalwatch/statechart_digitalwatch.xml

@@ -86,6 +86,7 @@
           <transition after="1500 ms" target="../EditingTime">
             <raise event="time_edit"/>
           </transition>
+          <transition event="bottomRightReleased" target="../TimeUpdate"/>
         </state>
 
         <state id="EditingTime" initial="Waiting">

+ 30 - 44
src/sccd/controller/controller.py

@@ -7,15 +7,6 @@ from sccd.controller.object_manager import *
 from sccd.util.debug import print_debug
 from sccd.cd.cd import *
 
-@dataclasses.dataclass
-class InputEvent:
-  name: str
-  port: str
-  params: List[Any]
-  time_offset: Duration
-
-Timestamp = int
-
 # The Controller class is a primitive that can be used to build backends of any kind:
 # Threads, integration with existing event loop, game loop, test framework, ...
 # The Controller class itself is NOT thread-safe.
@@ -29,7 +20,7 @@ class Controller:
     def __init__(self, cd: AbstractCD):
         self.cd = cd
         self.object_manager = ObjectManager(cd)
-        self.queue: EventQueue[Timestamp, EventQueueEntry] = EventQueue()
+        self.queue: EventQueue[int, EventQueueEntry] = EventQueue()
 
         self.simulated_time = 0 # integer
         self.initialized = False
@@ -38,14 +29,10 @@ class Controller:
         # print_debug("model delta is %s" % str(self.cd.globals.delta))
 
         # First call to 'run_until' method initializes
-        self.run_until = self._initialize
+        self.run_until = self._run_until_w_initialize
 
-    def _duration_to_time_offset(self, d: Duration) -> int:
-        if self.cd.globals.delta == duration(0):
-            return 0
-        return d // self.cd.globals.delta
 
-    def add_input(self, input: InputEvent):
+    def add_input(self, input: Event, time_offset: int):
             if input.name == "":
                 raise Exception("Input event can't have an empty name.")
         
@@ -59,45 +46,23 @@ class Controller:
             except KeyError as e:
                 raise Exception("No such event: '%s'" % input.name) from e
 
-            offset = self._duration_to_time_offset(input.time_offset)
-
-            e = Event(
-                id=event_id,
-                name=input.name,
-                port=input.port,
-                params=input.params)
+            input.id = event_id
 
             # For now, add events received on input ports to all instances.
             # In the future, we can optimize this by keeping a mapping from port name to a list of instances
             # potentially responding to the event
-            self.queue.add(self.simulated_time+offset,
-                Controller.EventQueueEntry(e, self.object_manager.instances))
+            self.queue.add(self.simulated_time + time_offset,
+                Controller.EventQueueEntry(input, self.object_manager.instances))
 
     # Get timestamp of next entry in event queue
-    def next_wakeup(self) -> Optional[Timestamp]:
+    def next_wakeup(self) -> Optional[int]:
         return self.queue.earliest_timestamp()
 
     # Returns duration since start
     def get_simulated_duration(self) -> Duration:
         return (self.cd.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):
+    def _run_until_w_initialize(self, now: Optional[int], pipe: queue.Queue):
         # first run...
         # initialize the object manager, in turn initializing our default class
         # and adding the generated events to the queue
@@ -114,7 +79,7 @@ class Controller:
 
     # 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):
+    def _run_until(self, now: Optional[int], pipe: queue.Queue):
         # Actual "event loop"
         for timestamp, entry in self.queue.due(now):
             if timestamp != self.simulated_time:
@@ -128,3 +93,24 @@ class Controller:
                 self._process_big_step_output(output, pipe)
 
         self.simulated_time = now
+
+    # 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)
+
+    # Helper
+    def _duration_to_time_offset(self, d: Duration) -> int:
+        if self.cd.globals.delta == duration(0):
+            return 0
+        return d // self.cd.globals.delta

+ 22 - 31
src/sccd/controller/eventloop.py

@@ -1,5 +1,6 @@
-from sccd.controller.realtime import *
-import tkinter
+from abc import *
+from sccd.realtime.time import *
+from sccd.controller.controller import *
 
 ScheduledID = Any
 
@@ -17,21 +18,9 @@ class EventLoopImplementation(ABC):
     def cancel(self) -> Callable[[ScheduledID], None]:
         pass
 
-@dataclass
-class TkInterImplementation(EventLoopImplementation):
-    tk: tkinter.Tk
-
-    def time_unit(self) -> Duration:
-        return duration(1, Millisecond)
-
-    def schedule(self) -> Callable[[int, Callable[[],None]], ScheduledID]:
-        return self.tk.after
-
-    def cancel(self) -> Callable[[ScheduledID], None]:
-        return self.tk.after_cancel
 
 class EventLoop:
-    def __init__(self, cd, event_loop, output_callback, time_impl=DefaultTimeImplementation):
+    def __init__(self, cd: AbstractCD, event_loop: EventLoopImplementation, output_callback: Callable[[List[Event]],None], time_impl: TimeImplementation = DefaultTimeImplementation):
         self.timer = Timer(time_impl, unit=cd.globals.delta) # will give us timestamps in model unit
         self.controller = Controller(cd)
         self.event_loop = event_loop
@@ -44,7 +33,6 @@ class EventLoop:
         self.queue = queue.Queue()
 
     def _wakeup(self):
-        # run controller - output will accumulate in queue
         self.controller.run_until(self.timer.now(), self.queue)
 
         # process output
@@ -55,29 +43,32 @@ class EventLoop:
         except queue.Empty:
             pass
 
-        # go to sleep
-        # convert our statechart's timestamp to tkinter's (100 us -> 1 ms)
-        sleep_duration = self.event_loop_convert(
-            self.controller.next_wakeup() - self.controller.simulated_time)
-        self.scheduled = self.event_loop.schedule()(sleep_duration, self._wakeup)
-        # print("sleeping %d ms" % sleep_duration)
+        # back to sleep
+        next_wakeup = self.controller.next_wakeup()
+        if next_wakeup:
+            sleep_duration = self.event_loop_convert(next_wakeup - self.controller.simulated_time)
+            self.scheduled = self.event_loop.schedule()(sleep_duration, self._wakeup)
+        else:
+            self.scheduled = None
 
     def start(self):
         self.timer.start()
         self._wakeup()
 
-    def pause(self):
-        self.timer.pause()
-        self.event_loop.cancel()(self.scheduled)
+    # Uncomment if ever needed:
+    # Does not mix well with interrupt().
+    # def pause(self):
+    #     self.timer.pause()
+    #     self.event_loop.cancel()(self.scheduled)
 
     # Add input. Does not automatically 'wake up' the controller if it is sleeping.
     # If you want the controller to respond immediately, call 'interrupt'.
-    def add_input(self, event):
-        offset = self.controller.simulated_time - self.timer.now()
-        event.time_offset += offset * self.timer.unit
-        self.controller.add_input(event)
+    def add_input(self, event: Event):
+        # If the controller is sleeping, it's simulated time value may be in the past, but we want to make it look like the event arrived NOW, so from the controller's point of view, in the future:
+        offset = self.timer.now() - self.controller.simulated_time
+        self.controller.add_input(event, offset)
 
-    # Do NOT call while paused!
     def interrupt(self):
-        self.event_loop.cancel()(self.scheduled)
+        if self.scheduled:
+            self.event_loop.cancel()(self.scheduled)
         self._wakeup()

+ 2 - 2
src/sccd/controller/realtime.py

@@ -1,6 +1,6 @@
-from abc import *
-from sccd.controller.controller import *
+from dataclasses import dataclass
 from numbers import Real # superclass for 'int' and 'float'
+from sccd.util.duration import *
 
 # The only requirement for a TimeImplementation is that the diffs between get_time call results are real durations of a non-complex number type (int, float).
 @dataclass

+ 17 - 0
src/sccd/realtime/tkinter.py

@@ -0,0 +1,17 @@
+from dataclasses import dataclass
+from sccd.realtime.eventloop import EventLoopImplementation
+import tkinter
+from sccd.util.duration import *
+
+@dataclass
+class TkInterImplementation(EventLoopImplementation):
+    tk: tkinter.Tk
+
+    def time_unit(self):
+        return duration(1, Millisecond)
+
+    def schedule(self):
+        return self.tk.after
+
+    def cancel(self):
+        return self.tk.after_cancel

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

@@ -5,7 +5,7 @@ from typing import *
 from sccd.util.duration import *
 
 # A raised event.
-@dataclass(frozen=True)
+@dataclass(eq=True)
 class Event:
     id: int
     name: str

+ 11 - 6
src/sccd/test/parser.py

@@ -1,15 +1,19 @@
 from sccd.statechart.parser.xml import *
 from sccd.cd.globals import *
-from sccd.controller.controller import InputEvent
 from sccd.statechart.dynamic.event import Event
 from sccd.cd.cd import *
 
+@dataclass
+class TestInputEvent:
+  event: Event
+  at: Duration
+
 @dataclass
 class TestVariant:
   name: str
-  model: Any
-  input: list
-  output: list
+  cd: AbstractCD
+  input: List[TestInputEvent]
+  output: List[List[Event]]
 
 def test_parser_rules(statechart_parser_rules):
   globals = Globals()
@@ -27,7 +31,8 @@ def test_parser_rules(statechart_parser_rules):
         time_type = time_expr.init_expr(scope=None)
         check_duration_type(time_type)
         time_val = time_expr.eval(memory=None)
-        input.append(InputEvent(name=name, port=port, params=[], time_offset=time_val))
+        input.append(TestInputEvent(
+          event=Event(id=-1, name=name, port=port, params=[]), at=time_val))
 
       return [("event+", parse_input_event)]
 
@@ -68,7 +73,7 @@ def test_parser_rules(statechart_parser_rules):
 
       return [TestVariant(
         name=variant_description(i, variant),
-        model=SingleInstanceCD(
+        cd=SingleInstanceCD(
           globals,
           Statechart(
             semantics=variant,

+ 2 - 3
src/sccd/test/run.py

@@ -42,12 +42,11 @@ class Test(unittest.TestCase):
     for test in test_variants:
       print_debug('\n'+test.name)
       pipe = QueueImplementation()
-      # interrupt = queue.Queue()
 
-      controller = Controller(test.model)
+      controller = Controller(test.cd)
 
       for i in test.input:
-        controller.add_input(i)
+        controller.add_input(i.event, controller._duration_to_time_offset(i.at))
 
       def controller_thread():
         try:

+ 4 - 1
src/sccd/util/duration.py

@@ -180,8 +180,11 @@ def gcd_pair(x: Duration, y: Duration) -> Duration:
 def gcd(*iterable: Iterable[Duration]) -> Duration:
   return functools.reduce(gcd_pair, iterable, _zero)
 
-# Useful for efficiently converting many values from the same unit to another same unit.
+# Useful for efficiently converting many values from some fixed unit to some other fixed unit.
 def get_conversion_f(from_unit: Duration, to_unit: Duration):
+  if from_unit is _zero or to_unit is _zero:
+    raise Exception("Cannot convert between zero-duration units")
+    
   if from_unit > to_unit:
     factor = from_unit // to_unit
     return lambda x: x * factor