You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
327 lines
9.1 KiB
327 lines
9.1 KiB
/*****************************************************************************
|
|
* Copyright (C) 2023 VLC authors and VideoLAN
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* ( at your option ) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
|
|
*****************************************************************************/
|
|
import QtQuick
|
|
|
|
import "qrc:///util/Helpers.js" as Helpers
|
|
|
|
/**
|
|
* @brief a pure QML hierarchical Finite State Machine implementation
|
|
*
|
|
* FSM {
|
|
* signal aSignal()
|
|
* signal anotherSignal(a, b, c)
|
|
*
|
|
* //map signals to a key
|
|
* signalMap: ({
|
|
* "aSignal": aSignal,
|
|
* "anotherSignal": anotherSignal
|
|
* })
|
|
*
|
|
* //define what is the initial sub state
|
|
* initialState: firstState
|
|
*
|
|
* FSMState {
|
|
* id: firstState
|
|
* //transitions defintions for this state
|
|
* transitions: ({
|
|
* "aSignal": finalState, //transition to finalState when receiving aSignal
|
|
* "anotherSignal": [{
|
|
* action: (a,b,c) => {
|
|
* //action is executed if transition is taken (anotherSignal is received and
|
|
* //guard returns true)
|
|
* },
|
|
* guard: (a,b,c) => a + b > c, //transition is taken if guard returns true
|
|
* target: subStateA //target state
|
|
* }, {
|
|
* target: subStateB
|
|
* }]
|
|
* })
|
|
* }
|
|
* FSMState {
|
|
* id: anotherState
|
|
*
|
|
* initialState: subStateA
|
|
* FSMState {
|
|
* id: subStateA
|
|
* //states may be nested
|
|
* }
|
|
* FSMState {
|
|
* id: subStateB
|
|
* }
|
|
* }
|
|
* FSMState {
|
|
* id: finalState
|
|
* }
|
|
* }
|
|
*/
|
|
FSMState {
|
|
id: fsm
|
|
|
|
//each signal is associated to a key, when a signal is received,
|
|
//transitions of active state for the given key are evaluated
|
|
property var signalMap: ({
|
|
})
|
|
|
|
property bool running: true
|
|
|
|
property bool started: false
|
|
|
|
/**
|
|
* @param {FSMState} state state handling the event
|
|
* @param {string} event name of the event
|
|
* @param {...*} args event arguments
|
|
* @param {Object} t transition definition
|
|
* @return {boolean} true if the state has handled the event
|
|
*/
|
|
function _evaluateTransition(state, event, t, ...args) {
|
|
if ("guard" in t) {
|
|
if (!(t.guard instanceof Function)) {
|
|
console.error(`guard property of ${state}::${event} is not a function`)
|
|
}
|
|
if (!t.guard(...args))
|
|
return false
|
|
}
|
|
|
|
if ("action" in t) {
|
|
if (!(t.action instanceof Function))
|
|
console.error(`action property of ${state}::${event} is not a function`)
|
|
t.action(...args)
|
|
}
|
|
|
|
if ("target" in t)
|
|
_changeState(t.target)
|
|
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* @param {FSMState} state state handling the event
|
|
* @param {string} event name of the event
|
|
* @param {...*} args event arguments
|
|
* @return {boolean} true if the state has handled the event
|
|
*/
|
|
function handleSignal(state, event, ...args) {
|
|
if (!running)
|
|
return false
|
|
|
|
if (!state)
|
|
return false
|
|
|
|
if (state._state) {
|
|
if (handleSignal(state._state, event, ...args))
|
|
return true
|
|
}
|
|
|
|
if (!(event in state.transitions)) {
|
|
return false
|
|
}
|
|
|
|
const transitions = state.transitions[event]
|
|
if (transitions === undefined) {
|
|
console.warn(`undefined transition for ${state}::${event}`)
|
|
//FIXME: comparing object to QML type with instanceof fails with 5.12
|
|
} else if (transitions === null || transitions.toString().startsWith("FSMState")) {
|
|
_changeState(transitions)
|
|
return true
|
|
} else if (Helpers.isArray(transitions)) {
|
|
for (const t of transitions) {
|
|
//stop at the first accepted transition
|
|
if (_evaluateTransition(state, event, t, ...args))
|
|
return true
|
|
}
|
|
return false
|
|
} else {
|
|
return _evaluateTransition(state, event, transitions, ...args)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {FSMState} state
|
|
*/
|
|
function _exitState(state) {
|
|
if (!state)
|
|
return
|
|
|
|
//exit sub states
|
|
if (state._state)
|
|
_exitState(state._state)
|
|
|
|
state._state = null
|
|
state.active = false
|
|
if (state.exit instanceof Function)
|
|
state.exit()
|
|
}
|
|
|
|
/**
|
|
* @brief mark the state as active, enter handler is evaluated
|
|
* @param {FSMState} state
|
|
*/
|
|
function _activateState(state) {
|
|
if (!state)
|
|
return
|
|
|
|
if (!state.active) {
|
|
state.active = true
|
|
if (state.enter instanceof Function)
|
|
state.enter()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief enter the target state, enter handler are evaluated
|
|
* inital sub-states are entered recursively
|
|
* @param {FSMState} state
|
|
*/
|
|
function _enterState(state) {
|
|
if (!state)
|
|
return
|
|
|
|
_activateState(state)
|
|
if (state.initialState) {
|
|
state._state = state.initialState
|
|
_enterState(state._state)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {FSMState} state
|
|
* @param {FSMState[]} parentStates
|
|
*/
|
|
function _resolveStatesHierarchy(state, parentStates) {
|
|
if (!state)
|
|
return
|
|
state._parentStates = parentStates
|
|
for (let i in state._children) {
|
|
const child = state._children[i]
|
|
if (child instanceof FSMState) {
|
|
state._subStates.push(child)
|
|
}
|
|
}
|
|
for (const s of state._subStates) {
|
|
_resolveStatesHierarchy(s, [...parentStates, state])
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {FSMState} state
|
|
*/
|
|
function _validateFSM(state) {
|
|
if (!state)
|
|
return
|
|
|
|
if (!(state instanceof FSMState)) {
|
|
console.warn(`invalid state machine: ${state} is not an FSMState node`)
|
|
}
|
|
|
|
for (const key of Object.keys(state.transitions)) {
|
|
if (!Object.keys(fsm.signalMap).includes(key)) {
|
|
console.warn(`transition ${key} ${state} match no signal`, Object.keys(fsm.signalMap))
|
|
}
|
|
}
|
|
|
|
for (const s of state._subStates) {
|
|
_validateFSM(s)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {FSMState[]} a state list
|
|
* @param {FSMState} a root state
|
|
* @return {FSMState} the common ancestor
|
|
*/
|
|
function _findCommonAncestorState(a, b) {
|
|
if (!a || !b)
|
|
return null
|
|
|
|
if (!b._parentStates.includes(a))
|
|
return null
|
|
|
|
const node = _findCommonAncestorState(a._state, b)
|
|
if (node !== null)
|
|
return node
|
|
|
|
return a
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {FSMState} state target state
|
|
*/
|
|
function _changeState(state) {
|
|
const ancestor = _findCommonAncestorState(fsm, state)
|
|
|
|
//exit uncommon states
|
|
if (ancestor) {
|
|
_exitState(ancestor._state)
|
|
}
|
|
|
|
if (!state) {
|
|
return
|
|
}
|
|
|
|
//activate parent state, but do not enter their initialState
|
|
let parentState = fsm
|
|
for (let i in state._parentStates) {
|
|
_activateState(state._parentStates[i])
|
|
parentState._state = state._parentStates[i]
|
|
parentState = state._parentStates[i]
|
|
}
|
|
|
|
//enter target state, then enter initial sub-states
|
|
parentState._state = state
|
|
_enterState(state)
|
|
}
|
|
|
|
|
|
/**
|
|
* reset the FSM to its initial state, exit handlers of the current state are not
|
|
* evaluated. enter hander of initial state will be evaluated
|
|
*/
|
|
function reset() {
|
|
function reset_rec(state) {
|
|
if (!state)
|
|
return
|
|
if (state._state) {
|
|
reset_rec(state._state)
|
|
state._state = null
|
|
}
|
|
state.active = false
|
|
}
|
|
reset_rec(fsm)
|
|
_changeState(initialState)
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
_resolveStatesHierarchy(fsm, [])
|
|
_validateFSM(fsm)
|
|
|
|
for (const signalName of Object.keys(signalMap)) {
|
|
signalMap[signalName].connect((...args) => {
|
|
//use callLater to ensure transitions are ordered.
|
|
//signal are not queued by default, this is an issue
|
|
//if an action/enter/exit function raise another signal
|
|
Qt.callLater(() => {
|
|
handleSignal(fsm, signalName, ...args)
|
|
})
|
|
})
|
|
}
|
|
|
|
_changeState(fsm)
|
|
fsm.started = true
|
|
}
|
|
}
|
|
|