diff --git a/README.md b/README.md index 1ba7aaf..ee1537c 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,15 @@ Normally a state machine is predefined by users. But advanced machine intelligen * By using NSM and DAR the transition logic of a state machine is totally decoupled from user action logic of an application; and by decoupling the transition logic from the user action logic, the framework improves the flexibility and reusability of a state machine. * With the State Machine Runtime Dynamic feature, the framework constructs a fundamental basis for the advanced machine learning and machine intelligence. -# Build +# Main Features +## Dynamic State Machine +Support create/change state machine at runtime. +## Dynamic Sub State Machine +Support attach sub state machine at runtime. +## Parallel State Machine +To be supported in future. +# Build * Go to project root directory * Run ./build.sh * After build is successfully done: @@ -55,5 +62,26 @@ Normally a state machine is predefined by users. But advanced machine intelligen * Run ./build-ut.sh * After build is successfully done, the ut applicatioin will be generated at ./ut_build/bin/imachine_ut, and can run it to check the UT result. - - +# SCXML Converter +By using the scxml-to-cpp converter, the standard SCXML state machine representation can be converted to C++ classes, which can then fit into the IMachine framework. +##### Tags supported according to the SCXML standard: +###### Core Constructs: +- <scxml> +- <state> +- <transiton> (targetless transition not supported) +- <event> +- <initial> +- <final> (Only one final not supported) + +##### Tags **NOT** supported according to the SCXML standard: +###### Core Constructs: +- <parallel> (to be supported in future) +- <history> (to be supported in future) +- <onentry> (intentionally not to be supported based on design) +- <onexit> (intentionally not to be supported based on design) + +###### Executable Content (intentionally not to be supported based on design) +###### Data Model and Data Manipulation (intentionally not to be supported based on design) +###### External Communications (Not considered for now) +#### How to use +python <repo_root>/tools/scxml_converter/scxml2cpp.py -h diff --git a/tools/scxml_converter/car.xml b/tools/scxml_converter/car.xml new file mode 100644 index 0000000..82847a6 --- /dev/null +++ b/tools/scxml_converter/car.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/scxml_converter/scxml2cpp.py b/tools/scxml_converter/scxml2cpp.py new file mode 100755 index 0000000..2a3a98f --- /dev/null +++ b/tools/scxml_converter/scxml2cpp.py @@ -0,0 +1,489 @@ +#!/usr/bin/python +#coding=utf-8 + +import getopt +import sys +import os + +from xml.dom.minidom import parse +import xml.dom.minidom +from inspect import isroutine + + +headerFileBegin = ''' +#ifndef $HEADER_MACRO +#define $HEADER_MACRO + +#include +#include +#include "org/jinsha/imachine/StateMachine.h" + +using namespace org::jinsha::imachine; +''' + +classDefBegin = ''' + +class $className : public StateMachine { + +public: +''' + +classDefEnd = ''' + $className(int id); + virtual ~$className(); + +private: + $className() = delete; + $className(const ATMStateMachine& instance) = delete; + $className& operator = (const ATMStateMachine& instance) = delete; + + std::list subMachines; +};''' + +headerFileEnd = ''' + +#endif +''' + +cppFileBegin = ''' +#include "$className.h" +''' +classImplBegin = ''' + +$className::$className(int id) : + StateMachine(id, "$className") +{ +''' + +classImplEnd = ''' +}; + +$className::~$className() +{ + for (auto machine : subMachines) { + delete machine; + } +}; +''' + +cppFileEnd = ''' + +''' + +def getFirstLevelElementsByTagName(node, tagName): + elementList = [] + for childNode in node.childNodes: + if childNode.nodeType == childNode.ELEMENT_NODE and childNode.tagName == tagName: + elementList.append(childNode) + return elementList +pass + +def isSubMachine(stateElement): + return False +pass + +def getTransitionElements(stateElement): + return [] +pass + +#If an element has sub state sub element +def hasSubState(element): + tmpList = getFirstLevelElementsByTagName(element, 'state') + if len(tmpList) > 0: + return True + else: + tmpList = getFirstLevelElementsByTagName(element, 'parallel') + if len(tmpList) > 0: + return True + else: + tmpList = getFirstLevelElementsByTagName(element, 'final') + if len(tmpList) > 0: + return True + + return False +pass + +#Get initial state element of a (scxml/state/parallel) element +def getInitialStateElement(element): + if not hasSubState(element): + return None + initialStateElement = None + initStateId = element.getAttribute('initial') #try to find initial as attribute + if initStateId != '': + foundInitState = True + else: #try to find initial as child element + tmpList = getFirstLevelElementsByTagName(element, 'initial') + if len(tmpList) > 0: + initStateId = tmpList[0].firstChild.data + foundInitState = True + else: #Then the initial shall be the first one + tmpNode = element.firstChild + while tmpNode != None: + if tmpNode.nodeType == tmpNode.ELEMENT_NODE and (tmpNode.tagName == 'state' or tmpNode.tagName == 'parallel'): +# initStateId = tmpNode.getAttribute('id') +# if initStateId == '': #id might not be present according the scxml specification +# initStateId = '_@_' +# tmpNode.setAttribute('id', '_@_') + initialStateElement = tmpNode; + foundInitState = True + return initialStateElement + tmpNode = tmpNode.nextSibling + + if not foundInitState: + #print '[ERROR] No initial state is found.' + return None + + tmpList = getFirstLevelElementsByTagName(element, "state") + if len(tmpList) == 0: + tmpList = getFirstLevelElementsByTagName(element, "parallel") + for item in tmpList: + if initStateId == item.getAttribute('id'): + initialStateElement = item + break + return initialStateElement +pass + +class State: + def __init__(self, id, name, parent, subMachine = None): + self.id = id + self.name = name + self.parent = parent + self.isInit = False + self.isFinal = False + self.subMachine = subMachine + if subMachine != None: + subMachine.parent = self + + self.eventMapList = [] +pass + +class Transition: + def __init__(self, id, fromState, toState): + self.id = id + self.name = fromState.name + '->' + toState.name + self.fromState = fromState + self.toState = toState +pass + +class Event: + def __init__(self, id, name = ''): + self.id = id + self.name = name +pass + +class StateMachine: + def __init__(self, className): + self.parent = None + self.className = className + self.stateList = [] + self.transitionList = [] + self.eventList = [] + self.initState = None + + def getStateByName(self, name): + for item in self.stateList: + if item.name == name: + return item + return None + + def getEventByName(self, name): + for item in self.eventList: + if item.name == name: + return item + return None + + def build(self, element, isRoot = False): + self.stateList = [] + self.transitionList = [] + self.eventList = [] + self.subMachineList = [] + + stateElementList = None + firstElement = None + foundInitState = False + + initStateElement = getInitialStateElement(element) + if initStateElement == None: + print '[ERROR] No initial state is found.' + return 4 + + ################################################################ + #Generate states + ################################################################ + stateElementList = getFirstLevelElementsByTagName(element, 'state') + stateCount = 0 + for stateElement in stateElementList: + stateCount += 1 + state = None + stateName = stateElement.getAttribute('id') + if stateName == '': + stateName = str(stateCount) + if hasSubState(stateElement): + subMachine = StateMachine(self.className + "_" + stateName) + subMachine.build(stateElement) + state = State(stateCount, stateName, self, subMachine) + self.subMachineList.append(subMachine) + else: + state = State(stateCount, stateName, self) + self.stateList.append(state) + if stateElement == initStateElement: + self.initState = state + state.isInit = True + + finalStateElementList = getFirstLevelElementsByTagName(element, 'final') + if len(finalStateElementList) > 1: + print '[ERROR] Multiple elements are current not supported. (Is multiple final really necessary?)' + exit(4) + for stateElement in finalStateElementList: + stateCount += 1 + stateName = stateElement.getAttribute('id') + if stateName == '': + stateName = str(stateCount) + state = State(-1, stateName, self) + state.isFinal = True + self.stateList.append(state) + + ################################################################ + #Generate transitions and event maps + ################################################################ + transitionCount = 0 + eventCount = 0 + stateIndex = -1 + for stateElement in stateElementList: + stateIndex += 1 + transitionElementList = getFirstLevelElementsByTagName(stateElement, 'transition') + for transitionElement in transitionElementList: + transitionCount += 1 + #Note only one target is allowed here + targetStateName = transitionElement.getAttribute('target') + if targetStateName == '': + print '[ERROR] transition without a target not support.' + exit(5) + sourceState = self.getStateByName(self.stateList[stateIndex].name) + targetState = self.getStateByName(targetStateName) + transition = Transition(transitionCount, sourceState, targetState) + self.transitionList.append(transition) + eventName = transitionElement.getAttribute('event') + if eventName != '': + event = self.getEventByName(eventName) + if event == None: + eventCount += 1 + event = Event(eventCount, eventName) + self.eventList.append(event) + sourceState.eventMapList.append({'event':event, 'transition': transition}) + else: #eventless transition + event = Event(-1) + sourceState.eventMapList.append({'event':event, 'transition': transition}) + + def outputClassDef(self, outputHeaderFile): + global classDefBegin + myClassDefBegin = classDefBegin.replace('$className', self.className) + outputHeaderFile.write(myClassDefBegin) + + for state in self.stateList: + tmpStr = ' /**\n' + tmpStr += ' * state %s\n' % (state.name) + tmpStr += ' */\n' + if state.isFinal: + tmpStr += ' static const int STATE_FINAL = State::FINAL_STATE_ID;\n' + else: + tmpStr += ' static const int STATE_%d = %d;\n\n' % (state.id, state.id) + outputHeaderFile.write(tmpStr) + outputHeaderFile.write('\n') + + for transition in self.transitionList: + tmpStr = ' /**\n' + tmpStr += ' * transition %s\n' % (transition.name) + tmpStr += ' */\n' + tmpStr += ' static const int TRAN_%d = %d;\n\n' % (transition.id, transition.id) + outputHeaderFile.write(tmpStr) + outputHeaderFile.write('\n') + + for event in self.eventList: + tmpStr = ' /**\n' + tmpStr += ' * event %s\n' % (event.name) + tmpStr += ' */\n' + tmpStr += ' static const int EVENT_%d = %d;\n\n' % (event.id, event.id) + outputHeaderFile.write(tmpStr) + outputHeaderFile.write('\n') + + global classDefEnd + myClassDefEnd = classDefEnd.replace('$className', self.className) + outputHeaderFile.write(myClassDefEnd) + outputHeaderFile.write('\n') + + for subMachine in self.subMachineList: + subMachine.outputClassDef(outputHeaderFile) + outputHeaderFile.write('\n') + + pass + + def outputClassImpl(self, outputCppFile): + global classImplBegin + myClassImplBegin = classImplBegin.replace('$className', self.className) + outputCppFile.write(myClassImplBegin) + + for state in self.stateList: + toStateIdStr = None + if state.isFinal: + stateIdStr = 'STATE_FINAL' + else: + stateIdStr = str(state.id) + tmpStr = ' addState(%s, "%s"); \n' % (stateIdStr, state.name) + outputCppFile.write(tmpStr) + outputCppFile.write('\n') + + tmpStr = ' setInitState(STATE_%d); \n' % (self.initState.id) + outputCppFile.write(tmpStr) + outputCppFile.write('\n') + + for state in self.stateList: + for eventMap in state.eventMapList: + event = eventMap['event'] + transition = eventMap['transition'] + toStateIdStr = None + if transition.toState.isFinal: + toStateIdStr = 'STATE_FINAL' + else: + toStateIdStr = 'STATE_' + str(transition.toState.id) + if event.name == '': #event less transition + tmpStr = ' addTransition(TRAN_%d, STATE_%d, %s, "%s"); \n' % (transition.id, transition.fromState.id, toStateIdStr, transition.name) + else: + tmpStr = ' addTransition(TRAN_%d, STATE_%d, %s, EVENT_%d, "%s"); \n' % (transition.id, transition.fromState.id, toStateIdStr, event.id, transition.name) + outputCppFile.write(tmpStr) + outputCppFile.write('\n') + + for subMachine in self.subMachineList: + tmpStr = ' {\n' + tmpStr += ' StateMachine* subMachine = new %s(0);\n' % (subMachine.className) + tmpStr += ' setSubMachine(STATE_%d, subMachine, true); \n' % (subMachine.parent.id) + tmpStr += ' subMachines.push_back(subMachine); \n' + tmpStr += ' };\n' + outputCppFile.write(tmpStr) + + global classImplEnd + myClassImplEnd = classImplEnd.replace('$className', self.className) + outputCppFile.write(myClassImplEnd) + outputCppFile.write('\n') + + for subMachine in self.subMachineList: + subMachine.outputClassImpl(outputCppFile) + + pass + +pass + + + +def main(argv): + + + scxmlFilePath = None + outputDir = None + namespace = None + className = 'NoNameStateMachine' + + usage = ''' +Usage: + [options] + +scxmlFile: The scxml file path +outputDir: The output directory path after conversion + +options: +-n, --ns: The base namespace of the classes converted to. +-c, --className: The state machine class name after conversion. +-h, --help: Usage +''' + + ################################################################ + #Process command line arguments + ################################################################ + if len(sys.argv) < 3: + print usage + return -1 + scxmlFilePath = sys.argv[1] + outputDir = sys.argv[2] + if outputDir.endswith('/') or outputDir.endswith('\\'): + outputDir = outputDir[:-1] + + try: + opts, args = getopt.getopt(sys.argv[3:], "n:c:h", ["ns=", "className=", "help"]) + for a,o in opts: + if a in ('-n', '--ns'): + namespace = str(o) + elif a in ('-h', '--help'): + print usage + return 0 + elif a in ('-c', '--className'): + className = str(o) + className = className.replace(' ', '') + except: + print "Invalid argument(s)." + print usage + return -2 + + if (not os.path.exists(scxmlFilePath)): + print "scxmlFile does not exist." + return 1 + + ################################################################ + #Parse input scxml file + ################################################################ + domTree = xml.dom.minidom.parse(scxmlFilePath) + rootElement = domTree.documentElement + initStateName = None + + #if rootElement.nodeType == rootElement.ELEMENT_NODE and rootElement.tagName == 'scxml': + if rootElement.tagName != 'scxml': +# initStateName = rootElement.getAttribute('initial') +# if initStateName == '': +# tmpNode = rootElement.firstChild +# while tmpNode != None: +# if tmpNode.nodeType == tmpNode.ELEMENT_NODE and (tmpNode.tagName == 'state' or tmpNode.tagName == 'parallel'): +# initStateName = tmpNode.getAttribute('id') +# break +# else: +# tmpNode = tmpNode.nextSibling +# if initStateName == '': +# print '[ERROR] No initial state is found.' +# return 4 +# else: + print 'This is not a scxml file.' + return 2 + + if (not os.path.exists(outputDir)): + os.mkdir(outputDir, 0755) + if (not os.path.exists(outputDir)): + print "Failed to create outputDir: " + outputDir + return 3 + + rootStateMachine = StateMachine(className) + rootStateMachine.build(rootElement, True) + + outputHeaderFileName = outputDir + '/' + className + '.h'; + outputCppFileName = outputDir + '/' + className + '.cpp'; + outputHeaderFile = open(outputHeaderFileName, 'w') + outputCppFile = open(outputCppFileName, 'w') + headerMacro = className.upper() + '_H_' + global headerFileBegin + headerFileBegin = headerFileBegin.replace('$HEADER_MACRO', headerMacro) + headerFileBegin = headerFileBegin.replace('$className', className) + outputHeaderFile.write(headerFileBegin) + rootStateMachine.outputClassDef(outputHeaderFile) + + global cppFileBegin + cppFileBegin = cppFileBegin.replace('$className', className) + outputCppFile.write(cppFileBegin) + rootStateMachine.outputClassImpl(outputCppFile) + global headerFileEnd + outputHeaderFile.write(headerFileEnd) + global cppFileEnd + outputHeaderFile.write(cppFileEnd) + + + +pass + +if __name__ == "__main__": + main(sys.argv) \ No newline at end of file diff --git a/tools/scxml_converter/stopwatch.xml b/tools/scxml_converter/stopwatch.xml new file mode 100644 index 0000000..3acc80f --- /dev/null +++ b/tools/scxml_converter/stopwatch.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/unit_test/src/CMakeLists.txt b/unit_test/src/CMakeLists.txt index fefcdb5..be1c860 100644 --- a/unit_test/src/CMakeLists.txt +++ b/unit_test/src/CMakeLists.txt @@ -11,34 +11,40 @@ set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin) set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib) set(GTEST_INCLUDE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../../3rdParty/gtest-1.8.0/googletest/include) set(GMOCK_INCLUDE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../../3rdParty/gtest-1.8.0/googlemock/include) -set(GMOCK_LIB_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../../3rdParty/gtest-1.8.0/googlemock) -set(GTEST_LIB_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../../3rdParty/gtest-1.8.0/googlemock/gtest) +set(GMOCK_LIB_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../../3rdParty/gtest-1.8.0/build/googlemock) +set(GTEST_LIB_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../../3rdParty/gtest-1.8.0/build/googlemock/gtest) add_definitions(-std=c++11) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fprofile-arcs -ftest-coverage") -set(src_dir ) -aux_source_directory(. src_dir) +set(ut_src_files ) +aux_source_directory(. ut_src_files) + +set(imachine_root_dir ${CMAKE_CURRENT_SOURCE_DIR}/../..) +aux_source_directory(${imachine_root_dir}/src/org/jinsha/imachine imachine_src_files) add_definitions(-DMOCK_UT) -include_directories(${GTEST_INCLUDE_PATH}) +include_directories(${GTEST_INCLUDE_PATH} ) include_directories(${GMOCK_INCLUDE_PATH}) +include_directories(${imachine_root_dir}/src) link_directories( ${GTEST_LIB_PATH} ${GMOCK_LIB_PATH} ) -add_executable(imachine_ut ${src_dir} +add_executable(imachine_ut + ${ut_src_files} + ${imachine_src_files} ) target_link_libraries(imachine_ut -libgtest.a -libgtest_main.a -libgmock.a -libgmock_main.a -pthread -gcov + libgtest.a + libgtest_main.a + libgmock.a + libgmock_main.a + pthread + gcov )