Behavioural Models

From Modelling Finite Automata to Analysing Business Processes

Exercises

Bonus Exercise: Programming complex automata (Optional)

Notice: This task requires additional knowledge of programming in general and object-orientation in particular. It covers advanced topics beyond the scope of the book.

Imagine a simple digital camera. It has the button "power" to turn it off or on and the button "mode" to switch between taking photos, recording videos and viewing the results. There also are four other buttons "trigger", "set", "left", and "right". Each has different functions depending on the mode. Since the buttons have mode-specific behaviour, the control is implemented using an automaton.

Have a look at the following Python program (or dwonload it as a .zip file here). It is an implementation of the camera's control automaton. The program is written in Python3 and does not require any external libraries. You may run the file using the console command "python FILENAME.py". The final section of the file is a test script that sends a series of inputs to the automaton and prints the resulting outputs. You can experiment with it in order to find out, how the automaton behaviour changes depending on the control sequence.

Task:
  Extract the automaton structure from the program and design it as an UML state machine.

Hints:


class DigicamRecord():
    """File recorded by the digital camera.

    May be created as either a Photo or a video. Actual
    file contents are not stored for this simulation.
    """

    def is_video(self):
        """Return True if instance is video."""
        return self._is_video

    @classmethod
    def new_photo(cls):
        """Create new instance as photo."""
        photo = cls()
        photo._is_video = False
        return photo

    @classmethod
    def new_video(cls):
        """Create new instance as video."""
        video = cls()
        video._is_video = True
        return video


class DigicamAutomaton():
    """Automaton representing the digital camera controls.

    Automaton class accepts all inputs from the input alphabet (i.e. camera
    buttons) and internally handles them as state transition triggers at the
    current state.
    Output functions are also defined in this class to, and class is container
    for all automaton variables.
    """

    def __init__(self):
        """Set automaton variables and enter initial state."""
        # Variables:
        self.zoomlevel = 0  # may be between 0 and 10
        self.records = []
        self.current_record = 0
        # Initial state:
        self._state = PowerOffState(self)

    def power(self):
        """Accept input word "power" and pass to current state for handling."""
        print("INPUT: power")
        self._state = self._state.power()

    def trigger(self):
        """Accept input word "trigger" and pass to current state."""
        print("INPUT: trigger")
        self._state = self._state.trigger()

    def left(self):
        """Accept input word "left" and pass to current state for handling."""
        print("INPUT: left")
        self._state = self._state.left()

    def right(self):
        """Accept input word "left" and pass to current state for handling."""
        print("INPUT: right")
        self._state = self._state.right()

    def set(self):
        """Accept input word "set" and pass to current state for handling."""
        print("INPUT: set")
        self._state = self._state.set()

    def mode(self):
        """Accept input word "mode" and pass to current state for handling."""
        print("INPUT: mode")
        self._state = self._state.mode()

    def output_photo_mode(self):
        """Show on display that camera is now in photo mode."""
        print("OUTPUT: Photo Mode.")

    def output_video_mode(self):
        """Show on display that camera is now in video mode."""
        print("OUTPUT: Video Mode.")

    def output_view_mode(self):
        """Show on display that camera is now in view mode."""
        print("OUTPUT: View Mode.")

    def output_photo_taken(self):
        """Show on display that photo was taken successfully."""
        print("OUTPUT: Photo taken.")

    def output_video_start(self):
        """Show on display that video recording was started."""
        print("OUTPUT: Start recording video.")

    def output_video_finish(self):
        """Show on display that a video recording was finished."""
        print("OUTPUT: Recording video finished.")

    def output_zoom_in(self):
        """Show on display that zoom-level was changed (inwards)."""
        print("OUTPUT: Zoom in.")

    def output_zoom_out(self):
        """Show on display that zoom-level was changed (outwards)."""
        print("OUTPUT: Zoom out.")

    def output_set_focus(self):
        """Show on display that autofocus was set."""
        print("OUTPUT: Set autofocus.")

    def output_no_files(self):
        """Show message on display that there are no files to show."""
        print("OUTPUT: No files to show.")

    def output_show_file(self, i):
        """Show an image or video thumbnail on display (at given index)."""
        if self.records[i].is_video():
            print("OUTPUT: Video " + str(i + 1) + " thumbnail.")
        else:
            print("OUTPUT: Photo " + str(i + 1) + ".")

    def output_playback_video(self, i):
        """Start video playback on display."""
        if self.records[i].is_video():
            print("OUTPUT: Playback video " + str(i + 1) + ".")

    def output_confirm_delete(self, i):
        """Request manual confirmation before deleting an image or video."""
        if self.records[i].is_video():
            print("OUTPUT: Really want to delete video " + str(i + 1) + "?")
        else:
            print("OUTPUT: Really want to delete photo " + str(i + 1) + "?")

    def output_delete_success(self, i):
        """Show message that image or video was deleted on display."""
        if self.records[i].is_video():
            print("OUTPUT: Video " + str(i + 1) + " successfully deleted.")
        else:
            print("OUTPUT: Photo " + str(i + 1) + " successfully deleted.")

    def output_goodbye(self):
        """Show on goodbye message on display before camera turns off."""
        print("OUTPUT: Goodbye.")


class DigicamAbstractBaseState():
    """Base class for any automaton state.

    It is assumed that states only define input functions that result in
    meaningful transitions and that all other inputs are ignored and result in
    no state change (i.e. blank transitions to oneself). To this end, this
    class offers token functions for each input that let the automaton keep the
    current state without side effects. These method may be inherited as
    default.
    """

    def __init__(self, automaton):
        """Define reference to automaton for variable access."""
        self.automaton = automaton

    def power(self):
        """Remain in the same state on input "power"."""
        return self

    def trigger(self):
        """Remain in the same state on input "trigger"."""
        return self

    def left(self):
        """Remain in the same state on input "left"."""
        return self

    def right(self):
        """Remain in the same state on input "right"."""
        return self

    def set(self):
        """Remain in the same state on input "set"."""
        return self

    def mode(self):
        """Remain in the same state on input "mode"."""
        return self


class PowerOffState(DigicamAbstractBaseState):
    """State representing that the camera is powered off.

    Accepts only power button input (i.e. input word "power").
    """

    def power(self):
        """Start camera in input "power"."""
        return PowerOnState(self.automaton)


class PowerOnState(DigicamAbstractBaseState):
    """Hierarchical state representing that camera is turned on.

    Main state, all other states (except PowerOff) are a sub-state.
    Hence, all inputs (expect "power") are passed to current sub-state and
    result in local transition there.
    """

    def __init__(self, automaton):
        """Start sub-automaton with initial sub-state."""
        self.automaton = automaton
        # Start sub-automaton
        self.automaton.output_photo_mode()
        self._substate = PhotoState(self.automaton)

    def power(self):
        """Turn camera of on "power" input."""
        self.automaton.output_goodbye()
        self.automaton.zoomlevel = 0
        self.automaton.current_record = 0
        return PowerOffState(self.automaton)

    def trigger(self):
        """Accept input "trigger" but pass to sub-state for effect."""
        self._substate = self._substate.trigger()
        return self  # No state change on this hierarchy level

    def left(self):
        """Accept input "left" but pass to current sub-state for effect."""
        self._substate = self._substate.left()
        return self  # No state change on this hierarchy level

    def right(self):
        """Accept input "right" but pass to current sub-state for effect."""
        self._substate = self._substate.right()
        return self  # No state change on this hierarchy level

    def set(self):
        """Accept input "set" but pass to current sub-state for effect."""
        self._substate = self._substate.set()
        return self  # No state change on this hierarchy level

    def mode(self):
        """Accept input "mode" but pass to current sub-state for effect."""
        self._substate = self._substate.mode()
        return self  # No state change on this hierarchy level


class RecordingAbstractBaseState(DigicamAbstractBaseState):
    """Abstract state where something is recorded.

    Inputs "left" and "right" control zoom, input "set" autofocus sensor.
    Other input functions need to be defined specifically in respective
    inheriting classes.
    """

    def left(self):
        """Zoom in on input "left"."""
        if self.automaton.zoomlevel <= 9:
            self.automaton.output_zoom_in()
            self.automaton.zoomlevel += 1
        return self  # Return to same state after transition

    def right(self):
        """Zoom out on input "right"."""
        if self.automaton.zoomlevel >= 1:
            self.automaton.output_zoom_out()
            self.automaton.zoomlevel -= 1
        return self  # Return to same state after transition

    def set(self):
        """Set autofocus on input "set"."""
        self.automaton.output_set_focus()
        return self  # Return to same state after transition


class PhotoState(RecordingAbstractBaseState):
    """State representing the photo mode.

    Photos can be taken using "trigger" input.
    Behaviour for zoom and autofocus is inherited.
    """

    def trigger(self):
        """Take photo on input "trigger"."""
        # Display output
        self.automaton.output_photo_taken()
        # Append new photo record to record list
        self.automaton.records.append(DigicamRecord.new_photo())
        return self  # Return to same state after transition

    def mode(self):
        """Toggle to video mode on input "mode"."""
        self.automaton.output_video_mode()
        return VideoState(self.automaton)


class VideoState(RecordingAbstractBaseState):
    """State representing the video recording mode (when no recording is active).

    Video recording can be started using "trigger" input.
    Behaviour for zoom and autofocus is inherited.
    """

    def trigger(self):
        """Start recording, switch to respective state on input "trigger"."""
        self.automaton.output_video_start()
        return VideoRecordingState(self.automaton)

    def mode(self):
        """Toggle to view mode on input "mode"."""
        self.automaton.output_view_mode()
        return ViewState(self.automaton)


class VideoRecordingState(RecordingAbstractBaseState):
    """State representing a running video recording.

    Video recording can be stopped using "trigger" input.
    Behaviour for zoom and autofocus is inherited.
    Mode changes are ignored while recording.
    """

    def trigger(self):
        """Finish video recording on input "trigger"."""
        self.automaton.output_video_finish()
        self.automaton.records.append(DigicamRecord.new_video())
        return VideoState(self.automaton)


class ViewState(DigicamAbstractBaseState):
    """Hierarchical state representing that camera is in view mode.

    In view mode, videos can be played and files delete. The required steps
    are implemented using sub-states. Hence, most inputs are not considered on
    this hierarchy level and passed to current sub-state for a local transition
    there.
    """

    def __init__(self, automaton):
        """Initialize sub-automaton by entering initial sub-state."""
        self.automaton = automaton
        # Select fitting initial substrate and ensure transition goes there.
        if len(self.automaton.records) > 0:
            self._substate = NormalViewState(self.automaton)
        else:
            self._substate = EmptyViewState(self.automaton)

    def trigger(self):
        """Directly switch to photo mode on input "trigger"."""
        self.automaton.current_record = 0  # Reset view position for next time
        self.automaton.output_photo_mode()
        return PhotoState(self.automaton)

    def left(self):
        """Accept input "left" but pass to current sub-state for effect."""
        self._substate = self._substate.left()
        return self  # No state change in this hierarchy level

    def right(self):
        """Accept input "right" but pass to current sub-state for effect."""
        self._substate = self._substate.right()
        return self  # No state change in this hierarchy level

    def set(self):
        """Accept input "set" but pass to current sub-state for effect."""
        self._substate = self._substate.set()
        return self  # No state change in this hierarchy level

    def mode(self):
        """Toggle to photo mode on input "mode"."""
        self.automaton.current_record = 0  # Reset view position for next time
        self.automaton.output_photo_mode()
        return PhotoState(self.automaton)


class EmptyViewState(DigicamAbstractBaseState):
    """State that represents the camera in view mode when there are no files.

    All inputs are ignored, since there are no files to iterate or delete.
    """

    def __init__(self, automaton):
        """Show message upon entering state."""
        self.automaton = automaton
        self.automaton.output_no_files()


class NormalViewState(DigicamAbstractBaseState):
    """State representing the camera in view mode when a file is shown."""

    def __init__(self, automaton):
        """Show file at current index upon entering (or re-entering) state."""
        self.automaton = automaton
        self.automaton.output_show_file(self.automaton.current_record)

    def left(self):
        """Navigate backwards in file list on input "left"."""
        if self.automaton.current_record == 0:
            # If already at first file, navigate to last
            self.automaton.current_record = len(self.automaton.records) - 1
        else:
            self.automaton.current_record -= 1
        # Remain in same state, but ensure that new object is initialized to
        # trigger state-entry behaviour
        return NormalViewState(self.automaton)

    def right(self):
        """Navigate forward in file list on input "right"."""
        if self.automaton.current_record == len(self.automaton.records) - 1:
            # If already at last file, jump to first
            self.automaton.current_record = 0
        else:
            self.automaton.current_record += 1
        # Remain in same state, but ensure that new object is initialized to
        # trigger state-entry behaviour
        return NormalViewState(self.automaton)

    def set(self):
        """Start video playback or photo delete dialogue on input "set"."""
        if self.automaton.records[self.automaton.current_record].is_video():
            self.automaton.output_playback_video(self.automaton.current_record)
            return PlayingViewState(self.automaton)
        else:
            return ConfirmDeleteState(self.automaton)


class PlayingViewState(NormalViewState):
    """State representing camera in view mode while video is played.

    Inputs "left" and "right" navigate files as in normal view mode, hence
    transition functions are inherited to avoid code duplication.
    """

    def __init__(self, automaton):
        """Default constructor behaviour, prevent inheriting entry output."""
        self.automaton = automaton

    def set(self):
        """Start file deletion dialogue upon input "set"."""
        return ConfirmDeleteState(self.automaton)


class ConfirmDeleteState(DigicamAbstractBaseState):
    """State that represents the delete confirmation state."""

    def __init__(self, automaton):
        """Output delete confirmation request."""
        self.automaton = automaton
        self.automaton.output_confirm_delete(self.automaton.current_record)

    def left(self):
        """Abort deletion and return to view on input "left"."""
        return NormalViewState(self.automaton)

    def right(self):
        """Abort deletion and return to view on input "right"."""
        return NormalViewState(self.automaton)

    def set(self):
        """Actually delete file on input "left"."""
        self.automaton.output_delete_success(self.automaton.current_record)

        # Delete element at index from list
        self.automaton.records.pop(self.automaton.current_record)

        # If images left, return to normal view sate.
        if len(self.automaton.records) > 0:
            if self.automaton.current_record == len(self.automaton.records):
                self.automaton.current_record = 0  # Prevent invalid index
            return NormalViewState(self.automaton)

        # If no images are left, go to EmptyView state.
        else:
            return EmptyViewState(self.automaton)


# Test program below.

# Initialize automaton.
a = DigicamAutomaton()

# Turn on, take a few photos and turn off again.
a.power()
a.trigger()
a.trigger()
a.trigger()
a.trigger()
a.power()

# Try a few buttons that should have no effect.
a.trigger()
a.right()
a.set()
a.left()
a.set()

# Turn on again and toggle to view mode.
a.power()
a.mode()
a.mode()

# Go left 4 times. We have 4 images, so we should return to the first again.
a.left()
a.left()
a.left()
a.left()

# Toggle to video mode and start recording a video.
a.mode()
a.mode()
a.trigger()

# Zoom in exactly 10 times, which should be the maximum allowed.
a.left()
a.left()
a.left()
a.left()
a.left()
a.left()
a.left()
a.left()
a.left()
a.left()

# Now zoom in again, which again should have no effect.
a.left()
a.left()

# Now stop video recording and play a bit with autofocus.
a.set()
a.trigger()
a.set()
a.set()
a.set()

# Start recording again, but turn off in the middle of it.
a.trigger()
a.power()

# Turn on again and toggle to view mode
a.power()
a.mode()
a.mode()

# Go right 5 times. We have 4 images and 1 video, so we should return to
# the first one again.
a.right()
a.right()
a.right()
a.right()
a.right()

# Delete the first 2 elements which should be photos.
a.set()
a.set()
a.set()
a.set()

# Trigger deletion of the next element but aboard.
a.set()
a.left()

# Trigger deletion again, now aboard by jumping to photo mode though "trigger".
a.set()
a.trigger()

# Take 2 Photos and zoom in a bit which should work again after the restart.
a.trigger()
a.left()
a.left()
a.left()
a.trigger()
a.left()

# Go to view mode again and navigate to 3rd file, which should be the video.
a.mode()
a.mode()
a.right()
a.right()

# Playback video and aboard playback by going left and right again. Then
# delete it.
a.set()
a.left()
a.right()
a.set()
a.set()
a.set()

# Go right 6 times. We should start at image 3 out of 4 and hence finish
# with viewing image 1.
a.right()
a.right()
a.right()
a.right()
a.right()
a.right()

# Turn the camera off.
a.power()