controller.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. import queue
  2. import dataclasses
  3. from typing import Dict, List, Optional
  4. from sccd.controller.event_queue import *
  5. from sccd.statechart.dynamic.event import *
  6. from sccd.controller.object_manager import *
  7. from sccd.util.debug import print_debug
  8. from sccd.cd.static.cd import *
  9. def _dummy_output_callback(output_event):
  10. pass
  11. # The Controller class' sole responsibility is running a model.
  12. # Its interface is a primitive that can be used to build backends of any kind:
  13. # Threads, integration with existing event loop, game loop, test framework, ...
  14. # All methods take control of the current thread and are synchronous (blocking).
  15. # The Controller class itself is NOT thread-safe.
  16. class Controller:
  17. __slots__ = ["cd", "object_manager", "queue", "simulated_time", "run_until"]
  18. @dataclasses.dataclass(eq=False, frozen=True)
  19. class EventQueueEntry:
  20. __slots__ = ["events", "targets"]
  21. events: List[InternalEvent]
  22. targets: List[Instance]
  23. def __repr__(self):
  24. return "QueueEntry("+str(self.event)+")"
  25. def __init__(self, cd: AbstractCD, output_callback: Callable[[OutputEvent],None] = _dummy_output_callback):
  26. cd.globals.assert_ready()
  27. self.cd = cd
  28. self.simulated_time = 0 # integer
  29. # Our instances should not have 'full access' to the global event queue (e.g. they shouldn't pop due events, that's the controller's task!). They are only allowed to schedule and cancel scheduled events. Hence we pass them 2 callbacks:
  30. def schedule_after(after, event, instances):
  31. entry = Controller.EventQueueEntry([event], instances)
  32. return self.queue.add(self.simulated_time + after, entry)
  33. def cancel_after(entry):
  34. self.queue.remove(entry)
  35. self.object_manager = ObjectManager(cd, output_callback, schedule_after, cancel_after)
  36. self.queue: EventQueue[int, EventQueueEntry] = EventQueue()
  37. if DEBUG:
  38. self.cd.print()
  39. print("Model delta is %s" % str(self.cd.get_delta()))
  40. # This is a 'hack', the attribute run_until First call to 'run_until' method initializes
  41. self.run_until = self._run_until_w_initialize
  42. # Lower-level way of adding an event to the queue
  43. # See also method 'add_input'
  44. def schedule(self, timestamp: int, events: List[InternalEvent], instances: List[Instance]):
  45. self.queue.add(timestamp, Controller.EventQueueEntry(events, instances))
  46. # Low-level utility function, intended to map a port name to a list of instances
  47. # For now, all known ports map to all instances (i.e. all ports are broadcast ports)
  48. def inport_to_instances(self, port: str) -> List[Instance]:
  49. # try:
  50. # self.cd.globals.inports.get_id(port)
  51. # except KeyError as e:
  52. # raise Exception("No such port: '%s'" % port) from e
  53. # For now, we just broadcast all input events.
  54. # We don't even check if the event is allowed on the input port.
  55. # TODO: multicast event only to instances that subscribe to this port.
  56. return self.object_manager.instances
  57. def all_instances(self) -> List[Instance]:
  58. return self.object_manager.instances
  59. # Higher-level way of adding an event to the queue.
  60. # See also method 'schedule'
  61. def add_input(self, timestamp: int, port: str, event_name: str, params = []):
  62. # try:
  63. # event_id = self.cd.globals.events.get_id(event_name)
  64. # except KeyError as e:
  65. # raise Exception("No such event: '%s'" % event_name) from e
  66. instances = self.inport_to_instances(port)
  67. # event = InternalEvent(event_id, event_name, params)
  68. event = InternalEvent(event_name, params)
  69. self.schedule(timestamp, [event], instances)
  70. # Get timestamp of earliest entry in event queue
  71. def next_wakeup(self) -> Optional[int]:
  72. return self.queue.earliest_timestamp()
  73. # Before calling _run_until, this method should be called instead, exactly once.
  74. def _run_until_w_initialize(self, now: Optional[int]):
  75. # first run...
  76. # initialize the object manager, in turn initializing our default class
  77. # and adding the generated events to the queue
  78. for i in self.object_manager.instances:
  79. i.initialize()
  80. if DEBUG:
  81. print("initialized.")
  82. # Next call to 'run_until' will call '_run_until'
  83. self.run_until = self._run_until
  84. # Let's try it out :)
  85. self.run_until(now)
  86. # Run until the event queue has no more events smaller than given timestamp.
  87. # If no timestamp is given (now = None), the "given timestamp" is considered to be +infinity,
  88. # meaning the controller will run until event queue is empty.
  89. # This method is blocking.
  90. def _run_until(self, now: Optional[int]):
  91. # Actual "event loop"
  92. for timestamp, entry in self.queue.due(now):
  93. if timestamp != self.simulated_time:
  94. # make time leap
  95. self.simulated_time = timestamp
  96. if DEBUG:
  97. print("\ntime is now %s" % str(self.cd.globals.delta * self.simulated_time))
  98. # print("popped", entry)
  99. # print("remaining", self.queue)
  100. # run all instances for whom there are events
  101. for instance in entry.targets:
  102. instance.big_step(entry.events)
  103. # print_debug("completed big step (time = %s)" % str(self.cd.globals.delta * self.simulated_time))