Skip to content

Commit

Permalink
Merge pull request #139 from LaboratoireMecaniqueLille/feature/add_pa…
Browse files Browse the repository at this point in the history
…use_mechanism

Add a pause mechanism for the Blocks
  • Loading branch information
WeisLeDocto authored Nov 14, 2024
2 parents e54ff08 + 9d916c2 commit 4ba9acd
Show file tree
Hide file tree
Showing 11 changed files with 330 additions and 4 deletions.
6 changes: 6 additions & 0 deletions docs/source/crappy_docs/blocks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ Multiplexer
:members: loop
:special-members: __init__

Pause Block
+++++++++++
.. autoclass:: crappy.blocks.Pause
:members: prepare, loop
:special-members: __init__

PID
+++
.. autoclass:: crappy.blocks.PID
Expand Down
10 changes: 8 additions & 2 deletions docs/source/developers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ attributes. In addition to the synchronization and logging attributes, each
instance of Block also has :

- A few attributes managing its execution (target looping frequency, niceness,
flag for displaying the achieved looping frequency).
flag for displaying the achieved looping frequency, pausability).
- A few buffers storing values needed for trying to achieve and displaying the
looping frequency.
- A name, given by a :obj:`classmethod` to ensure it is unique.
Expand Down Expand Up @@ -446,7 +446,13 @@ specific to the first loop, and then the Block starts looping forever by
calling it :meth:`~crappy.blocks.Block.main` method. Under the hood, this
method calls the :meth:`~crappy.blocks.Block.loop` method, performing the main
task for which the Block was written. It also handles the regulation and the
display of the looping frequency, if requested by the user.
display of the looping frequency, if requested by the user. If a
:class:`~crappy.blocks.Pause` Block is used, all the Blocks having their
``pausable`` attribute set to :obj:`True` might be paused (most Blocks by
default). When paused, the :meth:`~crappy.blocks.Block.main` method keeps
looping at its target frequency, but the :meth:`~crappy.blocks.Block.loop`
method is never called. As soon as the pause ends, the normal behavior is
restored.

There are several ways the Block can stop. First, the stop
:obj:`~multiprocessing.Event` might be set in another Process, which conducts
Expand Down
14 changes: 14 additions & 0 deletions docs/source/features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,20 @@ Hardware control
<https://github.com/LaboratoireMecaniqueLille/crappy/tree/master/examples/
blocks/ucontroller>`_.

Test management
+++++++++++++++

- :ref:`Pause Block`

Pauses the current Crappy script if the received data meets one of the given
criteria. Useful when human intervention on hardware is needed during a test,
but has some strong limitations. Refer to the documentation of this Block for
more details.

The examples folder on GitHub contains `one example of the Pause Block
<https://github.com/LaboratoireMecaniqueLille/crappy/blob/master/examples/
blocks/pause_block.py>`_.

Others
++++++

Expand Down
4 changes: 4 additions & 0 deletions docs/source/tutorials/custom_objects.rst
Original file line number Diff line number Diff line change
Expand Up @@ -945,6 +945,10 @@ meaning :
- :py:`name` contains the unique name attributed to the Block by Crappy. It can
be read at any time, and even modified. This name is only used for logging,
and appears in the log messages for identifying where a message comes from.
- :py:`pausable` is a :obj:`bool` indicating whether the Block is affected when
a pause is started by a :class:`~crappy.blocks.Pause` Block. By default,
most Blocks are affected except for the ones managing the test flow (like the
:class:`~crappy.blocks.StopButton` Block).

In the presented example, you may have recognized a few of the presented
attributes. They are highlighted here for convenience :
Expand Down
93 changes: 93 additions & 0 deletions examples/blocks/pause_block.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# coding: utf-8

"""
This example demonstrates the use of the Pause Block. It does not require
any hardware to run, but necessitates the Python module psutil to be installed.
This Block allows to pause other Blocks during a test based on a given set of
conditions, and to later resume the paused Blocks. This can be useful when
human intervention is needed on a setup while a test is running, to make sure
no command is sent to hardware during that time.
Here, a Generator Block generates a signal, based on which a Pause Block
decides to pause or resume the other Blocks. The Generator is of course
configured to be insensitive to the pauses. In parallel, an IOBlock monitors
the current RAM usage, and sends it to a LinkReader Block for display.
After starting this script, the values acquired by the IOBlock start appearing
in the console. After 8s, they should stop appearing, as the IOBlock is put in
pause. After 12s, it is resumed and the values appear again. Same goes after
28s, except the pause never ends due to a second pause condition being
satisfied for t>30s. To end this demo, click on the stop button that appears.
You can also hit CTRL+C, but it is not a clean way to stop Crappy.
"""

import crappy

if __name__ == '__main__':

# This Generator Block generates a cyclic ramp signal, and that is sent to
# the Pause Block
gen = crappy.blocks.Generator(
# Generating a cyclic ramp signal, oscillating in a linear way between 0
# and 10 with a period of 20s
({'type': 'CyclicRamp', 'init_value': 0, 'cycles': 0,
'condition1': 'delay=10', 'condition2': 'delay=10',
'speed1': 1, 'speed2': -1},),
cmd_label='value', # The labels carrying the generated value
freq=10, # Setting a low frequency because we don't need more

# Sticking to default for the other arguments
)
# Extremely important line, prevents the Generator from being paused
# Otherwise, the signal checked by the Pause Block ceases to be generated and
# the pause therefore never ends
gen.pausable = False

# This Block checks if any of the pause criteria are met, and if so puts all
# the pausable Blocks in pause
pause = crappy.blocks.Pause(
# The pause lasts as long as the "value" label is higher than 8, or when
# the time reaches 30s
criteria=('value>8', 't(s)>30'),
freq=20, # Setting a low frequency because we don't need more

# Sticking to default for the other arguments
)

# This IOBlock reads the current memory usage of the system, and sends it to
# the LinkReader
io = crappy.blocks.IOBlock(
'FakeInOut', # The name of the InOut object to drive
labels=('t(s)', 'memory'), # The names of the labels to output
freq=5, # Low frequency to avoid spamming the console
display_freq=True, # Display the looping frequency to show that there
# are still loops, although no data is acquired

# Sticking to default for the other arguments
)

# This LinkReader Block displays in the console the data it receives from the
# IOBlock
reader = crappy.blocks.LinkReader(
name='Reader', # A name for identifying the Block in the console
freq=5, # Useless to set a frequency higher than the labels to display

# Sticking to default for the other arguments
)
# During the pause, no data is displayed because the IOBlock is on hold and
# not because the LinkReader is paused
reader.pausable = False

# This Block allows the user to properly exit the script
# By default, it is not affected by the pause mechanism
stop = crappy.blocks.StopButton(
# No specific argument to give for this Block
)

# Linking the Block so that the information is correctly sent and received
crappy.link(gen, pause)
crappy.link(io, reader)

# Mandatory line for starting the test, this call is blocking
crappy.start()
1 change: 1 addition & 0 deletions src/crappy/blocks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from .machine import Machine
from .mean import MeanBlock
from .multiplexer import Multiplexer
from .pause import Pause
from .pid import PID
from .link_reader import LinkReader
from .recorder import Recorder
Expand Down
16 changes: 14 additions & 2 deletions src/crappy/blocks/meta_block/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class Block(Process, metaclass=MetaBlock):
shared_t0: Optional[Synchronized] = None
ready_barrier: Optional[synchronize.Barrier] = None
start_event: Optional[synchronize.Event] = None
pause_event: Optional[synchronize.Event] = None
stop_event: Optional[synchronize.Event] = None
raise_event: Optional[synchronize.Event] = None
kbi_event: Optional[synchronize.Event] = None
Expand Down Expand Up @@ -79,11 +80,13 @@ def __init__(self) -> None:
self.freq = None
self.display_freq = False
self.name = self.get_name(type(self).__name__)
self.pausable: bool = True

# The synchronization objects will be set later
self._instance_t0: Optional[Synchronized] = None
self._ready_barrier: Optional[synchronize.Barrier] = None
self._start_event: Optional[synchronize.Event] = None
self._pause_event: Optional[synchronize.Event] = None
self._stop_event: Optional[synchronize.Event] = None
self._raise_event: Optional[synchronize.Event] = None
self._kbi_event: Optional[synchronize.Event] = None
Expand Down Expand Up @@ -239,6 +242,7 @@ def prepare_all(cls, log_level: Optional[int] = logging.DEBUG) -> None:
cls.ready_barrier = Barrier(len(cls.instances) + 1)
cls.shared_t0 = Value('d', -1.0)
cls.start_event = Event()
cls.pause_event = Event()
cls.stop_event = Event()
cls.raise_event = Event()
cls.kbi_event = Event()
Expand Down Expand Up @@ -266,6 +270,7 @@ def prepare_all(cls, log_level: Optional[int] = logging.DEBUG) -> None:
instance._ready_barrier = cls.ready_barrier
instance._instance_t0 = cls.shared_t0
instance._stop_event = cls.stop_event
instance._pause_event = cls.pause_event
instance._start_event = cls.start_event
instance._raise_event = cls.raise_event
instance._kbi_event = cls.kbi_event
Expand Down Expand Up @@ -736,6 +741,7 @@ def reset(cls) -> None:
cls.shared_t0 = None
cls.ready_barrier = None
cls.start_event = None
cls.pause_event = None
cls.stop_event = None
cls.raise_event = None
cls.kbi_event = None
Expand Down Expand Up @@ -947,8 +953,14 @@ def main(self) -> None:

# Looping until told to stop or an error occurs
while not self._stop_event.is_set():
self.log(logging.DEBUG, "Looping")
self.loop()
# Only looping if the Block is not paused
if not self._pause_event.is_set() or not self.pausable:
self.log(logging.DEBUG, "Looping")
self.loop()
else:
self.log(logging.DEBUG, "Block currently paused, not calling loop()")
# Handling the frequency in all cases to avoid hyperactive Blocks when
# "paused"
self.log(logging.DEBUG, "Handling freq")
self._handle_freq()

Expand Down
Loading

0 comments on commit 4ba9acd

Please sign in to comment.