commit
This commit is contained in:
commit
8a24549cdb
21 changed files with 3478 additions and 0 deletions
80
StartingPoint/lib/controller.py
Normal file
80
StartingPoint/lib/controller.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# Author: Joeri Exelmans
|
||||
|
||||
class QueueEntry:
|
||||
__slots__ = ('timestamp', 'raise_method', 'value', 'canceled', 'event_name') # For MAXIMUM performance :)
|
||||
|
||||
def __init__(self, timestamp, raise_method, value, event_name):
|
||||
self.timestamp = timestamp
|
||||
self.raise_method = raise_method
|
||||
self.value = value
|
||||
self.event_name = event_name # name of the event - only needed for debugging
|
||||
self.canceled = False
|
||||
|
||||
def __repr__(self):
|
||||
return f"({self.timestamp}, {self.event_name}, {self.value})"
|
||||
|
||||
# The main primitive for discrete event simulation.
|
||||
# An event queue / event loop, using virtualized (simulated) time, independent of wall clock time.
|
||||
class Controller:
|
||||
def __init__(self):
|
||||
self.event_queue = []
|
||||
self.simulated_time = 0
|
||||
self.input_tracers = []
|
||||
|
||||
# timestamp = absolute value, in simulated time (since beginning of simulation)
|
||||
def add_input(self, sc, event_name, timestamp, value=None):
|
||||
if '.' in event_name:
|
||||
interface, short_event_name = event_name.split('.')
|
||||
raise_method = getattr(getattr(sc, interface), 'raise_' + short_event_name)
|
||||
else:
|
||||
raise_method = getattr(sc, 'raise_' + event_name)
|
||||
self.add_input_lowlevel(timestamp, raise_method, value, event_name)
|
||||
|
||||
# time_offset = relative to current simulated time
|
||||
def add_input_relative(self, sc, event_name, time_offset=0, value=None):
|
||||
timestamp = self.simulated_time + time_offset
|
||||
return self.add_input(sc, event_name, timestamp, value)
|
||||
|
||||
def add_input_lowlevel(self, timestamp, raise_method, value, event_name):
|
||||
e = QueueEntry(timestamp, raise_method, value, event_name)
|
||||
self.event_queue.append(e)
|
||||
# important to use a stable sorting algorithm here,
|
||||
# so the order between equally-timestamped events is preserved:
|
||||
self.event_queue.sort(key = lambda entry: entry.timestamp)
|
||||
return e
|
||||
|
||||
# difference here is that the added event will occur BEFORE equally-timestamped events that were already in the queue
|
||||
def add_input_lowlevel_interrupt(self, timestamp, raise_method, value, event_name):
|
||||
e = QueueEntry(timestamp, raise_method, value, event_name)
|
||||
self.event_queue.insert(0, e)
|
||||
self.event_queue.sort(key = lambda entry: entry.timestamp)
|
||||
return e
|
||||
|
||||
# Runs simulation as-fast-as-possible, until 'until'-timestamp (in simulated time)
|
||||
# blocking, synchronous function
|
||||
def run_until(self, until):
|
||||
# print('running until', pretty_time(until))
|
||||
while self.have_event() and self.get_earliest() <= until:
|
||||
e = self.event_queue[0]
|
||||
for sc, tracer in self.input_tracers:
|
||||
if sc == e.raise_method.__self__:
|
||||
tracer(e.timestamp, e.event_name, e.value)
|
||||
# e = self.event_queue.pop();
|
||||
self.event_queue = self.event_queue[1:]
|
||||
if not e.canceled:
|
||||
self.simulated_time = e.timestamp
|
||||
if e.value == None:
|
||||
e.raise_method()
|
||||
else:
|
||||
e.raise_method(e.value)
|
||||
|
||||
def have_event(self):
|
||||
return len(self.event_queue) > 0
|
||||
|
||||
def get_earliest(self):
|
||||
# return self.event_queue[-1].timestamp
|
||||
return self.event_queue[0].timestamp
|
||||
|
||||
|
||||
def pretty_time(time_ns):
|
||||
return f'{round(time_ns / 1000000000, 3)} s'
|
||||
67
StartingPoint/lib/realtime/event_loop.py
Normal file
67
StartingPoint/lib/realtime/event_loop.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
from lib.controller import Controller
|
||||
from lib.realtime.realtime import WallClock, AbstractRealTimeSimulation
|
||||
import time
|
||||
import abc
|
||||
|
||||
class AbstractEventLoop:
|
||||
# delay in nanoseconds
|
||||
# should be non-blocking
|
||||
# should return timer ID
|
||||
@abc.abstractmethod
|
||||
def schedule(self, delay, callback):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def cancel(self, timer_id):
|
||||
pass
|
||||
|
||||
# Runs virtual (simulated) time as close as possible to (scaled) wall-clock time.
|
||||
# Depending on how fast your computer is, simulated time will always run a tiny bit behind wall-clock time, but this error will NOT grow over time.
|
||||
class EventLoopRealTimeSimulation(AbstractRealTimeSimulation):
|
||||
|
||||
def __init__(self, controller: Controller, event_loop: AbstractEventLoop, wall_clock: WallClock, termination_condition=lambda: False, time_advance_callback=lambda simtime:None):
|
||||
self.controller = controller
|
||||
self.event_loop = event_loop
|
||||
|
||||
self.wall_clock = wall_clock
|
||||
|
||||
self.termination_condition = termination_condition
|
||||
|
||||
# Just a callback indicating that the current simulated time has changed.
|
||||
# Can be useful for displaying the simulated time in a GUI or something
|
||||
self.time_advance_callback = time_advance_callback
|
||||
|
||||
# At most one timer will be scheduled at the same time
|
||||
self.scheduled_id = None
|
||||
|
||||
def poke(self):
|
||||
if self.scheduled_id is not None:
|
||||
self.event_loop.cancel(self.scheduled_id)
|
||||
|
||||
self.controller.run_until(self.wall_clock.time_since_start()) # this call may actually consume some time
|
||||
|
||||
self.time_advance_callback(self.controller.simulated_time)
|
||||
|
||||
if self.termination_condition():
|
||||
print("Termination condition satisfied. Stop mainloop.")
|
||||
return
|
||||
|
||||
if self.controller.have_event():
|
||||
# schedule next wakeup
|
||||
sleep_duration = self.wall_clock.sleep_duration_until(self.controller.get_earliest())
|
||||
self.scheduled_id = self.event_loop.schedule(sleep_duration, self.poke)
|
||||
# print("sleeping for", pretty_time(sleep_duration))
|
||||
else:
|
||||
# print("sleeping until woken up")
|
||||
pass
|
||||
|
||||
# generate input event at the current wall clock time
|
||||
# this method should be used for generating events that represent e.g., button clicks, key presses
|
||||
def add_input_now(self, sc, event, value=None):
|
||||
self.controller.add_input(sc, event, timestamp=self.wall_clock.time_since_start(), value=value)
|
||||
self.poke()
|
||||
|
||||
# for events that need to happen immediately, at the current point in simulated time
|
||||
def add_input_sync(self, sc, event, value=None):
|
||||
self.controller.add_input_relative(sc, event, value=value)
|
||||
self.poke()
|
||||
35
StartingPoint/lib/realtime/realtime.py
Normal file
35
StartingPoint/lib/realtime/realtime.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import time
|
||||
import abc
|
||||
|
||||
# Use time_scale different from 1.0 for scaled real-time execution:
|
||||
# time_scale > 1 speeds up simulation
|
||||
# 0 < time_scale < 1 slows down simulation
|
||||
class WallClock:
|
||||
def __init__(self, time_scale=1.0):
|
||||
self.time_scale = time_scale
|
||||
self.purposefully_behind = 0
|
||||
|
||||
def record_start_time(self):
|
||||
self.start_time = time.perf_counter_ns()
|
||||
|
||||
def time_since_start(self):
|
||||
time_since_start = time.perf_counter_ns() - self.start_time
|
||||
return (time_since_start * self.time_scale) + self.purposefully_behind
|
||||
|
||||
def sleep_duration_until(self, earliest_event_time):
|
||||
now = self.time_since_start()
|
||||
sleep_duration = int((earliest_event_time - now) / self.time_scale)
|
||||
# sleep_duration can be negative, if the next event is in the past
|
||||
# This indicates that our computer is too slow, and cannot keep up with the simulation.
|
||||
# Like all things fate-related, we embrace this slowness, rather than fighting it:
|
||||
# We will temporarily run the simulation at a slower pace, which has the benefit of the simulation remaining responsive to user input.
|
||||
self.purposefully_behind = min(sleep_duration, 0) # see above comment
|
||||
actual_sleep_duration = max(sleep_duration, 0) # can never sleep less than 0
|
||||
return actual_sleep_duration
|
||||
|
||||
class AbstractRealTimeSimulation:
|
||||
# Generate input event at the current wall clock time (with time-scale applied, of course)
|
||||
# This method should be used for interactive simulation, for generating events that were caused by e.g., button clicks, key presses, ...
|
||||
@abc.abstractmethod
|
||||
def add_input_now(self, sc, event, value=None):
|
||||
pass
|
||||
43
StartingPoint/lib/realtime/threaded.py
Normal file
43
StartingPoint/lib/realtime/threaded.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import threading
|
||||
|
||||
from lib.realtime.realtime import WallClock, AbstractRealTimeSimulation
|
||||
from lib.controller import Controller, pretty_time
|
||||
|
||||
# Runs simulation, real-time, in its own thread
|
||||
#
|
||||
# Typical usage:
|
||||
# thread = threading.Thread(
|
||||
# target=ThreadedRealTimeSimulation(...).mainloop,
|
||||
# )
|
||||
# thread.start()
|
||||
class ThreadedRealTimeSimulation(AbstractRealTimeSimulation):
|
||||
def __init__(self, controller: Controller, wall_clock: WallClock, termination_condition = lambda: False):
|
||||
self.controller = controller
|
||||
self.wall_clock = wall_clock
|
||||
self.termination_condition = termination_condition
|
||||
self.condition = threading.Condition()
|
||||
|
||||
def mainloop(self):
|
||||
while True:
|
||||
self.controller.run_until(self.wall_clock.time_since_start())
|
||||
if self.termination_condition():
|
||||
print("Termination condition satisfied. Stop mainloop.")
|
||||
return
|
||||
if self.controller.have_event():
|
||||
earliest_event_time = self.controller.get_earliest()
|
||||
sleep_duration = self.wall_clock.sleep_duration_until(earliest_event_time)
|
||||
with self.condition:
|
||||
# print('thread sleeping for', pretty_time(sleep_duration), 'or until interrupted')
|
||||
self.condition.wait(sleep_duration / 1000000000)
|
||||
# print('thread woke up')
|
||||
else:
|
||||
with self.condition:
|
||||
# print('thread sleeping until interrupted')
|
||||
self.condition.wait()
|
||||
|
||||
def add_input_now(self, sc, event, value=None):
|
||||
with self.condition:
|
||||
self.controller.add_input(sc, event,
|
||||
timestamp=self.wall_clock.time_since_start(),
|
||||
value=value)
|
||||
self.condition.notify()
|
||||
13
StartingPoint/lib/realtime/tk_event_loop.py
Normal file
13
StartingPoint/lib/realtime/tk_event_loop.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
from lib.realtime.event_loop import AbstractEventLoop
|
||||
|
||||
# schedules calls in an existing tkinter eventloop
|
||||
class TkEventLoopAdapter(AbstractEventLoop):
|
||||
def __init__(self, tk):
|
||||
self.tk = tk
|
||||
|
||||
def schedule(self, delay, callback):
|
||||
return self.tk.after(int(delay / 1000000), # ns to ms
|
||||
callback)
|
||||
|
||||
def cancel(self, timer):
|
||||
self.tk.after_cancel(timer)
|
||||
136
StartingPoint/lib/test.py
Normal file
136
StartingPoint/lib/test.py
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
from difflib import ndiff
|
||||
|
||||
from lib.controller import Controller, pretty_time
|
||||
from lib.tracer import Tracer
|
||||
from lib.yakindu_helpers import YakinduTimerServiceAdapter, trace_output_events
|
||||
|
||||
# Can we ignore event in 'trace' at position 'idx' with respect to idempotency?
|
||||
def can_ignore(trace, idx, IDEMPOTENT):
|
||||
(timestamp, event_name, value) = trace[idx]
|
||||
if event_name in IDEMPOTENT:
|
||||
# If the same event occurred earlier, with the same parameter value, then this event can be ignored:
|
||||
for (earlier_timestamp, earlier_event_name, earlier_value) in reversed(trace[0:idx]):
|
||||
if (earlier_event_name, earlier_value) == (event_name, value):
|
||||
# same event name and same parameter value (timestamps allowed to differ)
|
||||
return True
|
||||
elif event_name == earlier_event_name:
|
||||
# same event name, but different parameter value:
|
||||
# stop looking into the past:
|
||||
break
|
||||
# If the same event occurs later event, but with the same timestamp, this event is overwritten and can be ignored:
|
||||
for (later_timestamp, later_event_name, later_value) in trace[idx+1:]:
|
||||
if (later_timestamp, later_event_name) == (timestamp, event_name):
|
||||
# if a later event with same name and timestamp occurs, ours will be overwritten:
|
||||
return True
|
||||
if later_timestamp != timestamp:
|
||||
# no need to look further into the future:
|
||||
break
|
||||
return False
|
||||
|
||||
def postprocess_trace(trace, INITIAL, IDEMPOTENT):
|
||||
# Prepend trace with events that set assumed initial state:
|
||||
result = [(0, event_name, value) for (event_name, value) in INITIAL] + trace
|
||||
# Remove events that have no effect:
|
||||
while True:
|
||||
filtered = [tup for (idx, tup) in enumerate(result) if not can_ignore(result, idx, IDEMPOTENT)]
|
||||
# Keep on filtering until no more events could be removed:
|
||||
if len(filtered) == len(result):
|
||||
return filtered
|
||||
result = filtered
|
||||
|
||||
def compare_traces(expected, actual):
|
||||
i = 0
|
||||
while i < len(expected) and i < len(actual):
|
||||
# Compare tuples:
|
||||
if expected[i] != actual[i]:
|
||||
print("Traces differ!")
|
||||
# print("expected: (%i, \"%s\", %s)" % expected[i])
|
||||
# print("actual: (%i, \"%s\", %s)" % actual[i])
|
||||
return False
|
||||
i += 1
|
||||
if len(expected) != len(actual):
|
||||
print("Traces have different length:")
|
||||
print("expected length: %i" % len(expected))
|
||||
print("actual length: %i" % len(actual))
|
||||
return False
|
||||
print("Traces match.")
|
||||
return True
|
||||
|
||||
def run_scenario(input_trace, expected_output_trace, statechart_class, INITIAL, IDEMPOTENT, verbose=False):
|
||||
controller = Controller()
|
||||
sc = statechart_class()
|
||||
tracer = Tracer(verbose=False)
|
||||
controller.input_tracers.append((sc, tracer.record_input_event))
|
||||
trace_output_events(controller, sc, callback=tracer.record_output_event)
|
||||
sc.timer_service = YakinduTimerServiceAdapter(controller)
|
||||
|
||||
# Put entire input trace in event queue, ready to go!
|
||||
for tup in input_trace:
|
||||
(timestamp, event_name, value) = tup
|
||||
controller.add_input(sc, event_name, timestamp, value)
|
||||
|
||||
sc.enter() # enter default state(s)
|
||||
|
||||
if len(expected_output_trace) > 0:
|
||||
last_output_event_timestamp = expected_output_trace[-1][0]
|
||||
else:
|
||||
last_output_event_timestamp = 0
|
||||
|
||||
# Blocking synchronous call:
|
||||
controller.run_until(last_output_event_timestamp)
|
||||
|
||||
actual_output_trace = tracer.output_events
|
||||
|
||||
clean_expected = postprocess_trace(expected_output_trace, INITIAL, IDEMPOTENT)
|
||||
clean_actual = postprocess_trace(actual_output_trace, INITIAL, IDEMPOTENT)
|
||||
|
||||
# clean_expected = expected_output_trace
|
||||
# clean_actual = actual_output_trace
|
||||
|
||||
def print_diff():
|
||||
# The diff printed will be a diff of the 'raw' traces, not of the cleaned up traces
|
||||
# A diff of the cleaned up traces would be confusing to the user.
|
||||
have_plus = False
|
||||
have_minus = False
|
||||
have_useless = False
|
||||
for diffline in ndiff(
|
||||
[str(tup)+'\n' for tup in expected_output_trace],
|
||||
[str(tup)+'\n' for tup in actual_output_trace],
|
||||
charjunk=None,
|
||||
):
|
||||
symbol = diffline[0]
|
||||
if symbol == '+':
|
||||
have_plus = True
|
||||
if symbol == '-':
|
||||
have_minus = True
|
||||
if symbol == '?':
|
||||
continue
|
||||
rest = diffline[2:-1] # drop last character (=newline)
|
||||
useless_line = (
|
||||
symbol == '-' and rest not in [str(tup) for tup in clean_expected]
|
||||
or symbol == '+' and rest not in [str(tup) for tup in clean_actual]
|
||||
# or symbol == ' ' and rest not in [str(tup) for tup in clean_actual]
|
||||
)
|
||||
if useless_line:
|
||||
print(" (%s) %s" % (symbol, rest))
|
||||
have_useless = True
|
||||
else:
|
||||
print(" %s %s" % (symbol, rest))
|
||||
|
||||
if have_minus or have_plus or have_useless:
|
||||
print("Legend:")
|
||||
if have_minus:
|
||||
print(" -: expected, but did not happen")
|
||||
if have_plus:
|
||||
print(" +: happened, but was not expected")
|
||||
if have_useless:
|
||||
print(" (-) or (+): indicates a \"useless event\" (because it has no effect), either in expected output (-) or in actual output (+).")
|
||||
print("\n\"Useless events\" are ignored by the comparison algorithm, and will never cause your test to fail. In this assignment, your solution is allowed to contain useless events.")
|
||||
|
||||
if not compare_traces(clean_expected, clean_actual):
|
||||
print("Raw diff between expected and actual output event trace:")
|
||||
print_diff()
|
||||
return False
|
||||
elif verbose:
|
||||
print_diff()
|
||||
return True
|
||||
36
StartingPoint/lib/tracer.py
Normal file
36
StartingPoint/lib/tracer.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
from lib.controller import pretty_time
|
||||
|
||||
# Records input/output events
|
||||
class Tracer:
|
||||
def __init__(self, verbose=True):
|
||||
self.verbose = verbose
|
||||
self.input_events = []
|
||||
self.output_events = []
|
||||
|
||||
def record_input_event(self, simtime, event_name, value):
|
||||
if self.verbose:
|
||||
print(f"time = {pretty_time(simtime)}, input event: {event_name}, value = {value}")
|
||||
if not event_name.startswith("__timer"):
|
||||
# we don't record timer events - they are specific to the statechart (not part of any fixed interface), and they are auto-generated by the timer interface
|
||||
self.input_events.append( (simtime, event_name, value) )
|
||||
|
||||
def record_output_event(self, simtime, event_name, value):
|
||||
if self.verbose:
|
||||
print(f"time = {pretty_time(simtime)}, output event: {event_name}, value = {value}")
|
||||
self.output_events.append( (simtime, event_name, value))
|
||||
|
||||
|
||||
def format_trace_as_python_code(trace, indent=0):
|
||||
txt = "[\n"
|
||||
for (timestamp, event_name, value) in trace:
|
||||
txt += (" "*indent)+" (%i, \"%s\", %s),\n" % (timestamp, event_name, value)
|
||||
txt += (" "*indent)+"],"
|
||||
return txt
|
||||
|
||||
# almost same as Python, but with arrays instead of tuples
|
||||
def format_trace_as_json(trace, indent=0):
|
||||
txt = "[\n"
|
||||
for (timestamp, event_name, value) in trace:
|
||||
txt += (" "*indent)+" [%i, \"%s\", %s],\n" % (timestamp, event_name, value)
|
||||
txt += (" "*indent)+"],"
|
||||
return txt
|
||||
5
StartingPoint/lib/yakindu/__init__.py
Normal file
5
StartingPoint/lib/yakindu/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
"""
|
||||
|
||||
Empty file that initializes the package it is contained in.
|
||||
|
||||
"""
|
||||
48
StartingPoint/lib/yakindu/rx.py
Normal file
48
StartingPoint/lib/yakindu/rx.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
"""Implementation for Observer and Observables used for out events.
|
||||
Generated by itemis CREATE code generator.
|
||||
"""
|
||||
|
||||
|
||||
class Observer():
|
||||
"""Observer implementation.
|
||||
"""
|
||||
|
||||
def next(self, value=None):
|
||||
"""Abstract next method, which must be implemented."""
|
||||
raise NotImplementedError('user must define next() to use this base class')
|
||||
|
||||
|
||||
class Observable():
|
||||
"""Observable implementation.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.observers = []
|
||||
|
||||
def next(self, value=None):
|
||||
"""Calls next function from every observer.
|
||||
"""
|
||||
for observer in self.observers:
|
||||
if observer is not None:
|
||||
if value is None:
|
||||
observer.next()
|
||||
else:
|
||||
observer.next(value)
|
||||
|
||||
def subscribe(self, observer):
|
||||
"""Subscribe on specified observer.
|
||||
"""
|
||||
if observer is not None:
|
||||
self.observers.append(observer)
|
||||
return True
|
||||
return False
|
||||
|
||||
def unsubscribe(self, observer):
|
||||
"""Unsubscribe from specified observer.
|
||||
"""
|
||||
if observer is None:
|
||||
return False
|
||||
if observer in self.observers:
|
||||
self.observers.remove(observer)
|
||||
return True
|
||||
return False
|
||||
74
StartingPoint/lib/yakindu_helpers.py
Normal file
74
StartingPoint/lib/yakindu_helpers.py
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# In this module, stuff that is specific to Yakindu's generated code
|
||||
# Author: Joeri Exelmans
|
||||
|
||||
from lib.controller import Controller, pretty_time
|
||||
|
||||
# for some stupid reason, we have to import the 'Observable' class like this, or `type(obj) == Observable` will fail:
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../lib')))
|
||||
from yakindu.rx import Observable, Observer
|
||||
|
||||
# Adapter to allow Yakindu generated code to (un)set timeouts
|
||||
# Uses event queue of the underlying Controller, making all timed transitions scheduled in simulated time (instead of wall-clock time as in Yakindu's own TimerService).
|
||||
class YakinduTimerServiceAdapter:
|
||||
def __init__(self, controller: Controller):
|
||||
self.controller = controller;
|
||||
self.timers = {}
|
||||
|
||||
# Duration: milliseconds
|
||||
def set_timer(self, sc, event_id, duration, periodic):
|
||||
self.unset_timer(None, event_id)
|
||||
|
||||
controller_duration = duration * 1000000 # ms to ns
|
||||
|
||||
e = self.controller.add_input_lowlevel(
|
||||
self.controller.simulated_time + controller_duration, # timestamp relative to simulated time
|
||||
raise_method=sc.time_elapsed,
|
||||
value=event_id,
|
||||
event_name="__timer"+str(event_id))
|
||||
|
||||
self.timers[event_id] = e
|
||||
|
||||
def unset_timer(self, _, event_id):
|
||||
try:
|
||||
e = self.timers[event_id]
|
||||
e.canceled = True
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
# Could not find a better way to get list of output events of a YAKINDU statechart
|
||||
def iter_output_observables(sc):
|
||||
for attr in dir(sc):
|
||||
obj = getattr(sc, attr)
|
||||
if type(obj) == Observable:
|
||||
yield (attr[0:-11], obj)
|
||||
|
||||
|
||||
# Useful for debugging
|
||||
class OutputEventTracer(Observer):
|
||||
def __init__(self, controller, event_name, callback):
|
||||
self.controller = controller
|
||||
self.event_name = event_name
|
||||
self.callback = callback
|
||||
def next(self, value=None):
|
||||
self.callback(self.controller.simulated_time, self.event_name, value)
|
||||
|
||||
def trace_output_events(controller, sc, callback, iface=None):
|
||||
if iface == None:
|
||||
for event_name, observable in iter_output_observables(sc):
|
||||
observable.subscribe(OutputEventTracer(controller, event_name, callback))
|
||||
else:
|
||||
for event_name, observable in iter_output_observables(getattr(sc, iface)):
|
||||
full_event_name = iface + '.' + event_name
|
||||
observable.subscribe(OutputEventTracer(controller, full_event_name, callback))
|
||||
|
||||
# Allows use of a simple callback to respond to an output event
|
||||
class CallbackObserver(Observer):
|
||||
def __init__(self, callback):
|
||||
self.callback = callback
|
||||
def next(self, value=None):
|
||||
if value == None:
|
||||
self.callback()
|
||||
else:
|
||||
self.callback(value)
|
||||
Loading…
Add table
Add a link
Reference in a new issue