Skip to content

Commit

Permalink
Merge pull request #69 from s4hri/dev-8.2.x
Browse files Browse the repository at this point in the history
Dev 8.2.x
  • Loading branch information
ddetommaso authored Oct 16, 2024
2 parents 26ca4f5 + 367b11c commit 40f8d3f
Show file tree
Hide file tree
Showing 14 changed files with 156 additions and 51 deletions.
43 changes: 43 additions & 0 deletions examples/3.Modules/3.runtimemodule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@

# BSD 2-Clause License
#
# Copyright (c) 2024, Social Cognition in Human-Robot Interaction,
# Istituto Italiano di Tecnologia, Genova
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


from pyicub.helper import iCub

class myCustomModule:

def test(self):
print("test message from my custom module")

icub = iCub()
icub.addRuntimeModule(name='test_module', module=myCustomModule())

input("PRESS A KEY TO CONTINUE")
icub.test_module.test()

32 changes: 16 additions & 16 deletions examples/7.FSM/1.semaphore/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,19 @@
from pyicub.fsm import FSM
import enum
import time
import random

def on_RED():
def on_RED(data):
print("Stop!")
time.sleep(1)
time.sleep(data)

def on_YELLOW():
def on_YELLOW(data):
print("Slow down!")
time.sleep(1)
time.sleep(data)

def on_GREEN():
def on_GREEN(data):
print("Go!")
time.sleep(1)
time.sleep(data)

fsm = FSM("Semaphore")

Expand All @@ -49,19 +50,18 @@ def on_GREEN():
fsm.addState(name="GREEN", on_enter_callback=on_GREEN)

# The initial state is always "init"
fsm.addTransition("start", "init", "RED")
fsm.addTransition("go", "RED", "GREEN")
fsm.addTransition("slowdown", "GREEN", "YELLOW")

# If a closed-loop is required, the final state has to be connected always with the "init"
fsm.addTransition("stop", "YELLOW", "init")
fsm.addTransition(trigger="start", source="init", dest="RED")
fsm.addTransition(trigger="go", source="RED", dest="GREEN")
fsm.addTransition(trigger="slowdown", source="GREEN", dest="YELLOW")
fsm.addTransition(trigger="stop", source="YELLOW", dest="init")

fsm.draw('diagram.png')

triggers = ["start", "go", "slowdown", "stop"]

for trigger in triggers:
fsm.runStep(trigger)
for i in range(3):
triggers = ["start", "go", "slowdown", "stop"]
for trigger in triggers:
fsm.runStep(trigger, data=random.randrange(1,2))
print("Session Count: ", fsm.getSessionCount())

print("\nSTATES: ", fsm.getStates())
print("\nTRANSITIONS: ", fsm.getTransitions())
Expand Down
2 changes: 1 addition & 1 deletion examples/7.FSM/1.semaphore/fsm.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,5 @@
],
"initial_state": "init",
"session_id": 0,
"session_count": 1
"session_count": 3
}
18 changes: 9 additions & 9 deletions examples/7.FSM/2.semaphore_subs/1.pub.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,17 @@ def __init__(self):
self.addTransition("slowdown", "GREEN", "YELLOW")
self.addTransition("stop", "YELLOW", "init")

def on_RED(self):
print("RED STATE: Stop!")
time.sleep(5)
def on_RED(self, msg='default', wait_time=6):
print("RED STATE: Stop! Received msg: %s" % msg)
time.sleep(wait_time)

def on_YELLOW(self):
print("YELLOW STATE: Slow down!")
time.sleep(1)
def on_YELLOW(self, msg='default', wait_time=1):
print("YELLOW STATE: Slow down! Received msg: %s" % msg)
time.sleep(wait_time)

def on_GREEN(self):
print("GREEN STATE: Go!")
time.sleep(3)
def on_GREEN(self, msg='default', wait_time=1):
print("GREEN STATE: Go! Received msg: %s" % msg)
time.sleep(wait_time)

class Publisher(PyiCubRESTfulServer):

Expand Down
5 changes: 4 additions & 1 deletion examples/7.FSM/2.semaphore_subs/3.cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@

from pyicub.rest import PyiCubRESTfulClient

import random

client = PyiCubRESTfulClient(host='localhost', port=9001)

print("PyiCub ver: ", client.get_version())
Expand All @@ -37,5 +39,6 @@
while True:
input("Press ENTER to run the FSM (CTRL+C to exit): ")
for trigger in triggers:
res = client.fsm_runStep(robot_name='generic', app_name='Publisher', trigger=trigger)
msg = input("Type a message and press ENTER to send the trigger %s : " % trigger)
res = client.fsm_runStep(robot_name='generic', app_name='Publisher', trigger=trigger, msg=msg, wait_time=random.randrange(1,2))
print(res)
Binary file modified examples/7.FSM/3.icub_rest/diagram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified examples/7.FSM/4.icub_multiple/FSM_A.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified examples/7.FSM/4.icub_multiple/FSM_B.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion pyicub/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,5 @@
__authors__ = 'Davide De Tommaso, Adam Lukomski, Nicola Russi'
__emails__ = '[email protected], [email protected], [email protected]'
__license__ = 'BSD-2'
__version__ = '8.1.1'
__version__ = '8.2.0'
__description__ = 'Developing iCub applications using Python'
53 changes: 36 additions & 17 deletions pyicub/fsm.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,28 @@
from pyicub.utils import importFromJSONFile, exportJSONFile

import json
import threading
import io
import time
import queue

import logging
logging.basicConfig(level=logging.DEBUG)
logging.getLogger('transitions').setLevel(logging.INFO)

class FSM:

INIT_STATE = "init"

def __init__(self, name="", JSON_dict=None, JSON_file=None, session_id=0):
def __init__(self, name="", JSON_dict=None, JSON_file=None, session_id=0, auto_transitions=False):
self._name_ = name
self._states_ = []
self._triggers_ = {}
self._transitions_ = []
self._session_id_ = session_id
self._session_count_ = 0
self._root_state_ = None
self._machine_ = GraphMachine(model=self, states=[], initial=FSM.INIT_STATE, auto_transitions=False)
self._machine_ = GraphMachine(model=self, states=[], initial=FSM.INIT_STATE, auto_transitions=auto_transitions)

if JSON_dict or JSON_file:
if JSON_dict:
self.importFromJSONDict(JSON_dict)
Expand All @@ -27,22 +35,25 @@ def __init__(self, name="", JSON_dict=None, JSON_file=None, session_id=0):
def name(self):
return self._name_

def addState(self, name, description='', on_enter_callback=None):
s = State(name=name, on_enter=on_enter_callback)
def addState(self, name, description='', on_enter_callback=None, on_exit_callback=None):
s = State(name=name, on_enter=on_enter_callback, on_exit=on_exit_callback)
self._machine_.add_state(s)
self._states_.append({"name": name, "description": description})
return s

def addTransition(self, trigger, source, dest):
self._transitions_.append({'trigger': trigger, 'source': source, 'dest': dest})
self._triggers_[trigger] = dest
self._machine_.add_transition(trigger=trigger, source=source, dest=dest)
def addTransition(self, trigger="", source=INIT_STATE, dest="", conditions=None, unless=None, before=None, after=None, prepare=None):
if not dest:
dest = source
if source == FSM.INIT_STATE:
self._root_state_ = dest
if not trigger:
trigger = "{}>{}".format(source, dest)
self._transitions_.append({'trigger': trigger, 'source': source, 'dest': dest})
self._machine_.add_transition(trigger=trigger, source=source, dest=dest, conditions=conditions, unless=unless, before=before, after=after, prepare=prepare)

def draw(self, filepath):
self.get_graph().draw(filepath, prog='dot')

self.get_graph().draw(filepath, prog='dot')
def exportJSONFile(self, filepath):
data = json.dumps(self.toJSON(), default=lambda o: o.__dict__, indent=4)
exportJSONFile(filepath, data)
Expand Down Expand Up @@ -80,7 +91,7 @@ def importFromJSONDict(self, data):
transitions = data.get("transitions", [])

for state_data in states:
self.addState(name=state_data["name"], description=state_data["description"], on_enter_callback=self.__on_enter_action__)
self.addState(name=state_data["name"], description=state_data["description"])

for transition_data in transitions:
self.addTransition(trigger=transition_data["trigger"], source=transition_data["source"], dest=transition_data["dest"])
Expand All @@ -92,19 +103,27 @@ def importFromJSONFile(self, filepath):
data = importFromJSONFile(filepath)
self.importFromJSONDict(data)

def runStep(self, trigger):
state = self._triggers_[trigger]
if state == self._root_state_:
self._session_count_ += 1
self.trigger(trigger)
def runStep(self, trigger, **kargs):
if kargs:
self.trigger(trigger_name=trigger, **kargs)
else:
self.trigger(trigger_name=trigger)
triggers = self.getCurrentTriggers()
if not triggers:
self._machine_.set_state(FSM.INIT_STATE)
if self.getCurrentState() == self._root_state_:
self._session_count_ += 1
return triggers

def setSessionID(self, session_id):
self._session_id_ = session_id

def getSessionID(self):
return self._session_id_

def getSessionCount(self):
return self._session_count_

def toJSON(self):
data = {
"name": self._name_,
Expand Down
6 changes: 6 additions & 0 deletions pyicub/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,12 @@ def parts(self):
def robot_name(self):
return self._robot_name_

def addRuntimeModule(self, name, module):
if not name in self.__dict__.keys():
self.__dict__[name] = module
else:
self._logger_.error('You are trying to add a runtime module with a name %s that already exists in the helper class' % name)

def addAction(self, action: iCubFullbodyAction, action_id=None):
action_id = self.actions_manager.addAction(action, action_id=action_id)
return action_id
Expand Down
2 changes: 1 addition & 1 deletion pyicub/modules/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def __init__(self, robot, side="right", proxy_host=None):
self.__portCamera__ = []
self._proxy_host_ = proxy_host
if robot == "icubSim":
self.__portCamera__ = "/" + robot + "/cam/" + side
self.__portCamera__ = "/" + robot + "/cam/" + side + "/rgbImage:o"
elif robot == "icub":
self.__portCamera__ = "/" + robot + "/camcalib/" + side + "/out"
yarp.Network.connect(self.__portCamera__, self.__portImg__.getName())
Expand Down
35 changes: 34 additions & 1 deletion pyicub/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from flask import Flask, jsonify, request
from flask_cors import CORS
from urllib.parse import urlparse, urlsplit
from typing import Any

import requests
import json
Expand All @@ -51,6 +52,17 @@
import functools
import importlib

# A sample class that might represent a generic type
class GenericType:
def __init__(self, data):
self.data = data

# Custom serialization function
def custom_serializer(obj: Any) -> Any:
if isinstance(obj, GenericType):
return {'__GenericType__': True, 'data': obj.data}
raise TypeError(f"Object of type {obj.__class__.__name__} is not JSON serializable")

class RESTJSON:

def __init__(self, json_dict=None):
Expand Down Expand Up @@ -315,9 +327,11 @@ def __init__(self, icubrequestmanager, rule_prefix, host, port, proxy_host, prox
self._proxy_host_ = proxy_host
self._proxy_port_ = proxy_port
self._requests_ = {}
self._processes_ = {}
self._subscribers_ = {}
self._request_manager_ = icubrequestmanager
self._flaskapp_.add_url_rule("/%s/requests" % self._rule_prefix_, methods=['GET'], view_func=self.requests)
self._flaskapp_.add_url_rule("/%s/processes" % self._rule_prefix_, methods=['GET'], view_func=self.processes)
self._flaskapp_.add_url_rule("/%s/<robot_name>/<app_name>/<target_name>/<local_id>" % (self._rule_prefix_), methods=['GET'], view_func=self.single_req_info)

def __del__(self):
Expand Down Expand Up @@ -354,6 +368,16 @@ def requests(self):
reqs.append(req['request'].info())
return jsonify(reqs)

def processes(self):
if 'name' in request.args:
return self.proc_info(name=request.args['name'])
return jsonify(self._processes_)

def proc_info(self, name):
if name in self._processes_.keys():
return jsonify(self._processes_[name])
return jsonify("")

def req_info(self, req_id):
if req_id in self._requests_.keys():
return jsonify(self._requests_[req_id]['request'].info())
Expand Down Expand Up @@ -406,6 +430,9 @@ def process_target(self, service):
self._requests_[req.req_id] = {'robot_name': service.robot_name,
'app_name': service.app_name,
'request': req}

self._processes_[service.name] = req.req_id

self.request_manager.run_request(req, wait_for_completed, **kwargs)
if 'sync' in request.args:
wait_for_completed=True
Expand Down Expand Up @@ -876,6 +903,10 @@ def __init__(self, JSON_dict=None, JSON_file=None):
@property
def actions(self):
return self._actions_

@property
def icub(self):
return self._app_.icub

def __on_enter_action__(self):
current_state = self.getCurrentState()
Expand Down Expand Up @@ -941,9 +972,11 @@ def __run__(self, robot_name, app_name, target_name, sync, *args, **kwargs):
res = requests.post(url=url, json=data)
return res.json()

def fsm_runStep(self, robot_name, app_name, trigger):
def fsm_runStep(self, robot_name, app_name, trigger, **kargs):
data = {}
data['trigger'] = trigger
for key, value in kargs.items():
data[key] = value
res = requests.post(url=self._header_ + '/' + robot_name + '/' + app_name + '/fsm.runStep?sync', json=data)
return res.json()

Expand Down
9 changes: 5 additions & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
PyYAML==6.0.1
Flask==3.0.2
Flask-Cors==3.0.10
transitions==0.9.0
graphviz==0.20.1
requests==2.32.0
Flask-Cors==5.0.0
transitions==0.9.2
graphviz==0.20.3
requests==2.32.0
pygraphviz==1.11

0 comments on commit 40f8d3f

Please sign in to comment.