diff --git a/README.md b/README.md index d21654f..8dadea1 100644 --- a/README.md +++ b/README.md @@ -1,181 +1,45 @@ -app_manager [![Build Status](https://travis-ci.com/PR2/app_manager.svg?branch=kinetic-devel)](https://travis-ci.org/PR2/app_manager) -==================================================================================================================================== +# app_manager + +[![Build Status](https://travis-ci.com/PR2/app_manager.svg?branch=kinetic-devel)](https://travis-ci.org/PR2/app_manager) + +## app_manager A package for making launch file an application -## Installation +For detailed information, please read [app_manager](app_manager/README.md). + +## app_scheduler + +Scheduler for `app_manager` + +For detailed information, please read [app_scheduler](app_scheduler/README.md). + +## app_recorder + +Recorder plugin for `app_manager` -Run `sudo apt-get install ros-$ROS_DISTRO-app-manager` +For detailed information, please read [app_recorder](app_recorder/README.md). -## Usage +## app_uploader -The `app_manager` node loads information of available application from `.installed` files. -`.installed` file is a yaml file that defines installed applications in a package like below: +Uploader plugin for `app_manager` -```yaml -# package_root/apps/app.installed -apps: -- app: pkg_name/app_name1 - display: sample app -- app: pkg_name/app_name2 - display: another sample app -``` +For detailed information, please read [app_uploader](app_uploader/README.md). -Once `.installed` file is defined, you have to notify the location of the files to `app_manager` by either of two ways: +## app_notifier -1. Give the locations as arguments +Notifier plugin for `app_manager` -One way to notify the location is to add `--applist` argument with `rosrun`. +For detailed information, please read [app_notifier](app_notifier/README.md). -```bash -rosrun app_manager app_manager --applist `rospack find package_root`/apps -``` +## app_notification_saver -This is useful for testing one small `.installed` file or a demonstration. +Notification saver plugin for `app_manager` -2. Register as export attributes +For detailed information, please read [app_notification_saver](app_notification_saver/README.md). -Another way to notify the location is to define them in `` tag in `package.xml`. +## app_publisher -```xml - - - ... - app_manager - ... - - - - -``` +Publisher plugin for `app_manager` -And launch `app_manager` without any argument: - -```bash -rosrun app_manager app_manager -``` - -`app_manager` node automatically searches all `.installed` files and register as available applications. - -Applications can be filtered by platform defined in each `.app` file. -If you set the parameter `/robot/type` to `pr2`, then apps for platform `pr2` will be available. - -```bash -rosparam set /robot/type pr2 -``` - - -## APIs - -All topics/services are advertised under the namespace specified by the parameter `/robot/name`. - -### Publishing Topics - -- `app_list`: List available/running applications -- `application/app_status`: Current status of app manager - -### Services - -- `list_apps`: List available/running applications -- `start_app`: Start an available application -- `stop_app`: Stop a runniing application -- `reload_app_list`: Reload installed applications from `*.installed`) file. - - -## Examples - -Start default roscore -``` -$ roscore - -``` - -and start another roscore for app_manager from another Terminal - -``` -$ roscore -p 11312 -``` - -Start app_manager -``` -$ rosrun app_manager app_manager --applist `rospack find app_manager`/test/applist1 _interface_master:=http://localhost:11312 -``` -Make sure that it founds the apps -``` -[INFO] [1575604033.724035]: 1 apps found in /home/user/catkin_ws/src/app_manager/test/applist1/apps1.installed -``` - -Use service calls to list and start apps. -``` -$ rosservice call robot/list_apps -running_apps: [] -available_apps: - - - name: "app_manager/appA" - display_name: "Android Joystick" - icon: - format: '' - data: [] - client_apps: [] -$ rosservice call /robot/start_app app_manager/appA -started: True -error_code: 0 -message: "app [app_manager/appA] started" -namespace: "/robot/application" -``` - -## Plugins - -You can define `app_manager` plugins as below in app file such as `test.app`. - -```yaml -# app definitions -display: Test app -platform: all -launch: test_app_manager/test_app.xml -interface: test_app_manager/test_app.interface -# plugin definitions -plugins: - - name: mail_notifier_plugin # name to identify this plugin - type: app_notifier/mail_notifier_plugin # plugin type - launch_args: # arguments for plugin launch file - foo: hello - launch_arg_yaml: /etc/mail_notifier_launch_arg.yaml # argument yaml file for plugin launch file - # in this case, these arguments will be passed. - # {"hoge": 100, "fuga": 30, "bar": 10} will be passed to start plugin - # {"hoge": 50, "fuga": 30} will be passed to stop plugin - plugin_args: # arguments for plugin function - hoge: 10 - fuga: 30 - start_plugin_args: # arguments for start plugin function - hoge: 100 # arguments for start plugin function arguments (it overwrites plugin_args hoge: 10 -> 100) - bar: 10 - stop_plugin_args: # arguments for stop plugin function - hoge: 50 # arguments for stop plugin function arguments (it overwrites plugin_args hoge: 10 -> 50) - plugin_arg_yaml: /etc/mail_notifier_plugin_arg.yaml # argument yaml file for plugin function arguments - - name: rosbag_recorder_plugin # another plugin - type app_recorder/rosbag_recorder_plugin - launch_args: - rosbag_path: /tmp - rosbag_title: test.bag - compress: true - rosbag_topic_names: - - /rosout - - /tf - - /tf_static -plugin_order: # plugin running orders. if you don't set field, plugin will be run in order in plugins field - start_plugin_order: # start plugin running order - - rosbag_recorder_plugin # 1st plugin name - - mail_notifier_plugin #2nd plugin name - stop_plugin_order: # start plugin running order - - rosbag_recorder_plugin - - mail_notifier_plugin -``` - -Sample plugin repository is [knorth55/app_manager_utils](https://github.com/knorth55/app_manager_utils). - -For more detailed information, please read [#25](https://github.com/PR2/app_manager/pull/25). - -## Maintainer - -Yuki Furuta <> +For detailed information, please read [app_publisher](app_publisher/README.md). diff --git a/CHANGELOG.rst b/app_manager/CHANGELOG.rst similarity index 100% rename from CHANGELOG.rst rename to app_manager/CHANGELOG.rst diff --git a/CMakeLists.txt b/app_manager/CMakeLists.txt similarity index 100% rename from CMakeLists.txt rename to app_manager/CMakeLists.txt diff --git a/app_manager/README.md b/app_manager/README.md new file mode 100644 index 0000000..d21654f --- /dev/null +++ b/app_manager/README.md @@ -0,0 +1,181 @@ +app_manager [![Build Status](https://travis-ci.com/PR2/app_manager.svg?branch=kinetic-devel)](https://travis-ci.org/PR2/app_manager) +==================================================================================================================================== + +A package for making launch file an application + +## Installation + +Run `sudo apt-get install ros-$ROS_DISTRO-app-manager` + +## Usage + +The `app_manager` node loads information of available application from `.installed` files. +`.installed` file is a yaml file that defines installed applications in a package like below: + +```yaml +# package_root/apps/app.installed +apps: +- app: pkg_name/app_name1 + display: sample app +- app: pkg_name/app_name2 + display: another sample app +``` + +Once `.installed` file is defined, you have to notify the location of the files to `app_manager` by either of two ways: + +1. Give the locations as arguments + +One way to notify the location is to add `--applist` argument with `rosrun`. + +```bash +rosrun app_manager app_manager --applist `rospack find package_root`/apps +``` + +This is useful for testing one small `.installed` file or a demonstration. + +2. Register as export attributes + +Another way to notify the location is to define them in `` tag in `package.xml`. + +```xml + + + ... + app_manager + ... + + + + +``` + +And launch `app_manager` without any argument: + +```bash +rosrun app_manager app_manager +``` + +`app_manager` node automatically searches all `.installed` files and register as available applications. + +Applications can be filtered by platform defined in each `.app` file. +If you set the parameter `/robot/type` to `pr2`, then apps for platform `pr2` will be available. + +```bash +rosparam set /robot/type pr2 +``` + + +## APIs + +All topics/services are advertised under the namespace specified by the parameter `/robot/name`. + +### Publishing Topics + +- `app_list`: List available/running applications +- `application/app_status`: Current status of app manager + +### Services + +- `list_apps`: List available/running applications +- `start_app`: Start an available application +- `stop_app`: Stop a runniing application +- `reload_app_list`: Reload installed applications from `*.installed`) file. + + +## Examples + +Start default roscore +``` +$ roscore + +``` + +and start another roscore for app_manager from another Terminal + +``` +$ roscore -p 11312 +``` + +Start app_manager +``` +$ rosrun app_manager app_manager --applist `rospack find app_manager`/test/applist1 _interface_master:=http://localhost:11312 +``` +Make sure that it founds the apps +``` +[INFO] [1575604033.724035]: 1 apps found in /home/user/catkin_ws/src/app_manager/test/applist1/apps1.installed +``` + +Use service calls to list and start apps. +``` +$ rosservice call robot/list_apps +running_apps: [] +available_apps: + - + name: "app_manager/appA" + display_name: "Android Joystick" + icon: + format: '' + data: [] + client_apps: [] +$ rosservice call /robot/start_app app_manager/appA +started: True +error_code: 0 +message: "app [app_manager/appA] started" +namespace: "/robot/application" +``` + +## Plugins + +You can define `app_manager` plugins as below in app file such as `test.app`. + +```yaml +# app definitions +display: Test app +platform: all +launch: test_app_manager/test_app.xml +interface: test_app_manager/test_app.interface +# plugin definitions +plugins: + - name: mail_notifier_plugin # name to identify this plugin + type: app_notifier/mail_notifier_plugin # plugin type + launch_args: # arguments for plugin launch file + foo: hello + launch_arg_yaml: /etc/mail_notifier_launch_arg.yaml # argument yaml file for plugin launch file + # in this case, these arguments will be passed. + # {"hoge": 100, "fuga": 30, "bar": 10} will be passed to start plugin + # {"hoge": 50, "fuga": 30} will be passed to stop plugin + plugin_args: # arguments for plugin function + hoge: 10 + fuga: 30 + start_plugin_args: # arguments for start plugin function + hoge: 100 # arguments for start plugin function arguments (it overwrites plugin_args hoge: 10 -> 100) + bar: 10 + stop_plugin_args: # arguments for stop plugin function + hoge: 50 # arguments for stop plugin function arguments (it overwrites plugin_args hoge: 10 -> 50) + plugin_arg_yaml: /etc/mail_notifier_plugin_arg.yaml # argument yaml file for plugin function arguments + - name: rosbag_recorder_plugin # another plugin + type app_recorder/rosbag_recorder_plugin + launch_args: + rosbag_path: /tmp + rosbag_title: test.bag + compress: true + rosbag_topic_names: + - /rosout + - /tf + - /tf_static +plugin_order: # plugin running orders. if you don't set field, plugin will be run in order in plugins field + start_plugin_order: # start plugin running order + - rosbag_recorder_plugin # 1st plugin name + - mail_notifier_plugin #2nd plugin name + stop_plugin_order: # start plugin running order + - rosbag_recorder_plugin + - mail_notifier_plugin +``` + +Sample plugin repository is [knorth55/app_manager_utils](https://github.com/knorth55/app_manager_utils). + +For more detailed information, please read [#25](https://github.com/PR2/app_manager/pull/25). + +## Maintainer + +Yuki Furuta <> diff --git a/bin/appmaster b/app_manager/bin/appmaster similarity index 100% rename from bin/appmaster rename to app_manager/bin/appmaster diff --git a/bin/rosget b/app_manager/bin/rosget similarity index 100% rename from bin/rosget rename to app_manager/bin/rosget diff --git a/launch/app_manager.launch b/app_manager/launch/app_manager.launch similarity index 100% rename from launch/app_manager.launch rename to app_manager/launch/app_manager.launch diff --git a/msg/App.msg b/app_manager/msg/App.msg similarity index 100% rename from msg/App.msg rename to app_manager/msg/App.msg diff --git a/msg/AppInstallationState.msg b/app_manager/msg/AppInstallationState.msg similarity index 100% rename from msg/AppInstallationState.msg rename to app_manager/msg/AppInstallationState.msg diff --git a/msg/AppList.msg b/app_manager/msg/AppList.msg similarity index 100% rename from msg/AppList.msg rename to app_manager/msg/AppList.msg diff --git a/msg/AppStatus.msg b/app_manager/msg/AppStatus.msg similarity index 100% rename from msg/AppStatus.msg rename to app_manager/msg/AppStatus.msg diff --git a/msg/ClientApp.msg b/app_manager/msg/ClientApp.msg similarity index 100% rename from msg/ClientApp.msg rename to app_manager/msg/ClientApp.msg diff --git a/msg/ExchangeApp.msg b/app_manager/msg/ExchangeApp.msg similarity index 100% rename from msg/ExchangeApp.msg rename to app_manager/msg/ExchangeApp.msg diff --git a/msg/Icon.msg b/app_manager/msg/Icon.msg similarity index 100% rename from msg/Icon.msg rename to app_manager/msg/Icon.msg diff --git a/msg/KeyValue.msg b/app_manager/msg/KeyValue.msg similarity index 100% rename from msg/KeyValue.msg rename to app_manager/msg/KeyValue.msg diff --git a/msg/StatusCodes.msg b/app_manager/msg/StatusCodes.msg similarity index 100% rename from msg/StatusCodes.msg rename to app_manager/msg/StatusCodes.msg diff --git a/package.xml b/app_manager/package.xml similarity index 100% rename from package.xml rename to app_manager/package.xml diff --git a/scripts/app_manager b/app_manager/scripts/app_manager similarity index 100% rename from scripts/app_manager rename to app_manager/scripts/app_manager diff --git a/scripts/test_app.py b/app_manager/scripts/test_app.py similarity index 100% rename from scripts/test_app.py rename to app_manager/scripts/test_app.py diff --git a/setup.py b/app_manager/setup.py similarity index 100% rename from setup.py rename to app_manager/setup.py diff --git a/src/app_manager/__init__.py b/app_manager/src/app_manager/__init__.py similarity index 100% rename from src/app_manager/__init__.py rename to app_manager/src/app_manager/__init__.py diff --git a/src/app_manager/app.py b/app_manager/src/app_manager/app.py similarity index 100% rename from src/app_manager/app.py rename to app_manager/src/app_manager/app.py diff --git a/src/app_manager/app_list.py b/app_manager/src/app_manager/app_list.py similarity index 100% rename from src/app_manager/app_list.py rename to app_manager/src/app_manager/app_list.py diff --git a/src/app_manager/app_manager.py b/app_manager/src/app_manager/app_manager.py similarity index 100% rename from src/app_manager/app_manager.py rename to app_manager/src/app_manager/app_manager.py diff --git a/src/app_manager/app_manager_plugin.py b/app_manager/src/app_manager/app_manager_plugin.py similarity index 100% rename from src/app_manager/app_manager_plugin.py rename to app_manager/src/app_manager/app_manager_plugin.py diff --git a/src/app_manager/exceptions.py b/app_manager/src/app_manager/exceptions.py similarity index 100% rename from src/app_manager/exceptions.py rename to app_manager/src/app_manager/exceptions.py diff --git a/src/app_manager/exchange.py b/app_manager/src/app_manager/exchange.py similarity index 100% rename from src/app_manager/exchange.py rename to app_manager/src/app_manager/exchange.py diff --git a/src/app_manager/master_sync.py b/app_manager/src/app_manager/master_sync.py similarity index 100% rename from src/app_manager/master_sync.py rename to app_manager/src/app_manager/master_sync.py diff --git a/srv/GetAppDetails.srv b/app_manager/srv/GetAppDetails.srv similarity index 100% rename from srv/GetAppDetails.srv rename to app_manager/srv/GetAppDetails.srv diff --git a/srv/GetInstallationState.srv b/app_manager/srv/GetInstallationState.srv similarity index 100% rename from srv/GetInstallationState.srv rename to app_manager/srv/GetInstallationState.srv diff --git a/srv/InstallApp.srv b/app_manager/srv/InstallApp.srv similarity index 100% rename from srv/InstallApp.srv rename to app_manager/srv/InstallApp.srv diff --git a/srv/ListApps.srv b/app_manager/srv/ListApps.srv similarity index 100% rename from srv/ListApps.srv rename to app_manager/srv/ListApps.srv diff --git a/srv/StartApp.srv b/app_manager/srv/StartApp.srv similarity index 100% rename from srv/StartApp.srv rename to app_manager/srv/StartApp.srv diff --git a/srv/StopApp.srv b/app_manager/srv/StopApp.srv similarity index 100% rename from srv/StopApp.srv rename to app_manager/srv/StopApp.srv diff --git a/srv/UninstallApp.srv b/app_manager/srv/UninstallApp.srv similarity index 100% rename from srv/UninstallApp.srv rename to app_manager/srv/UninstallApp.srv diff --git a/test/appA.app b/app_manager/test/appA.app similarity index 100% rename from test/appA.app rename to app_manager/test/appA.app diff --git a/test/applist0/.gitignore b/app_manager/test/applist0/.gitignore similarity index 100% rename from test/applist0/.gitignore rename to app_manager/test/applist0/.gitignore diff --git a/test/applist1/apps1.installed b/app_manager/test/applist1/apps1.installed similarity index 100% rename from test/applist1/apps1.installed rename to app_manager/test/applist1/apps1.installed diff --git a/test/applistbad/bad.installed b/app_manager/test/applistbad/bad.installed similarity index 100% rename from test/applistbad/bad.installed rename to app_manager/test/applistbad/bad.installed diff --git a/test/applistbad/bad2.installed b/app_manager/test/applistbad/bad2.installed similarity index 100% rename from test/applistbad/bad2.installed rename to app_manager/test/applistbad/bad2.installed diff --git a/test/empty.interface b/app_manager/test/empty.interface similarity index 100% rename from test/empty.interface rename to app_manager/test/empty.interface diff --git a/test/plugin/__init__.py b/app_manager/test/plugin/__init__.py similarity index 100% rename from test/plugin/__init__.py rename to app_manager/test/plugin/__init__.py diff --git a/test/plugin/package.xml b/app_manager/test/plugin/package.xml similarity index 100% rename from test/plugin/package.xml rename to app_manager/test/plugin/package.xml diff --git a/test/plugin/plugin.installed b/app_manager/test/plugin/plugin.installed similarity index 100% rename from test/plugin/plugin.installed rename to app_manager/test/plugin/plugin.installed diff --git a/test/plugin/plugin.yaml b/app_manager/test/plugin/plugin.yaml similarity index 100% rename from test/plugin/plugin.yaml rename to app_manager/test/plugin/plugin.yaml diff --git a/test/plugin/sample_node.py b/app_manager/test/plugin/sample_node.py similarity index 100% rename from test/plugin/sample_node.py rename to app_manager/test/plugin/sample_node.py diff --git a/test/plugin/sample_node.xml b/app_manager/test/plugin/sample_node.xml similarity index 100% rename from test/plugin/sample_node.xml rename to app_manager/test/plugin/sample_node.xml diff --git a/test/plugin/test_plugin.app b/app_manager/test/plugin/test_plugin.app similarity index 100% rename from test/plugin/test_plugin.app rename to app_manager/test/plugin/test_plugin.app diff --git a/test/plugin/test_plugin.interface b/app_manager/test/plugin/test_plugin.interface similarity index 100% rename from test/plugin/test_plugin.interface rename to app_manager/test/plugin/test_plugin.interface diff --git a/test/plugin/test_plugin.py b/app_manager/test/plugin/test_plugin.py similarity index 100% rename from test/plugin/test_plugin.py rename to app_manager/test/plugin/test_plugin.py diff --git a/test/resources/example-min.launch b/app_manager/test/resources/example-min.launch similarity index 100% rename from test/resources/example-min.launch rename to app_manager/test/resources/example-min.launch diff --git a/test/test1.interface b/app_manager/test/test1.interface similarity index 100% rename from test/test1.interface rename to app_manager/test/test1.interface diff --git a/test/test_app.py b/app_manager/test/test_app.py similarity index 100% rename from test/test_app.py rename to app_manager/test/test_app.py diff --git a/test/test_app.test b/app_manager/test/test_app.test similarity index 100% rename from test/test_app.test rename to app_manager/test/test_app.test diff --git a/test/test_app_list.py b/app_manager/test/test_app_list.py similarity index 100% rename from test/test_app_list.py rename to app_manager/test/test_app_list.py diff --git a/test/test_plugin.py b/app_manager/test/test_plugin.py similarity index 100% rename from test/test_plugin.py rename to app_manager/test/test_plugin.py diff --git a/test/test_plugin.test b/app_manager/test/test_plugin.test similarity index 100% rename from test/test_plugin.test rename to app_manager/test/test_plugin.test diff --git a/test/test_start_fail.py b/app_manager/test/test_start_fail.py similarity index 100% rename from test/test_start_fail.py rename to app_manager/test/test_start_fail.py diff --git a/test/test_start_fail.test b/app_manager/test/test_start_fail.test similarity index 100% rename from test/test_start_fail.test rename to app_manager/test/test_start_fail.test diff --git a/test/test_stop_app.py b/app_manager/test/test_stop_app.py similarity index 100% rename from test/test_stop_app.py rename to app_manager/test/test_stop_app.py diff --git a/app_notification_saver/CMakeLists.txt b/app_notification_saver/CMakeLists.txt new file mode 100644 index 0000000..ced2c9c --- /dev/null +++ b/app_notification_saver/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 2.8.3) +project(app_notification_saver) + +find_package(catkin REQUIRED COMPONENTS + message_generation + rospy +) + +add_service_files( + FILES SaveAppNotification.srv +) + +catkin_python_setup() + +generate_messages() + +catkin_package( + CATKIN_DEPENDS message_runtime +) diff --git a/app_notification_saver/README.md b/app_notification_saver/README.md new file mode 100644 index 0000000..5f82958 --- /dev/null +++ b/app_notification_saver/README.md @@ -0,0 +1,180 @@ +# app_notification_saver + +Plugins and nodes to save notification to json file and pass it to `app_notifier` + +## `app_manager` plugins + +### `app_notification_saver/service_notification_saver`: General notification saver plugin + +This plugin saves notification via service call. + +#### `plugin_args`: Plugin arguments + +`None` + +#### `launch_args`: Plugin launch arguments + +- `json_path` : JSON path + +#### Sample plugin description + +```yaml +plugins: + - name: service_notification_saver + type: app_notification_saver/service_notification_saver + launch_args: + json_path: /tmp/app_notification.json +``` + +#### Save app notification + +You can save app notification with service call. + +```bash +rosservice call /service_notification_saver/save_app_notification "title: 'object recognition' +stamp: + secs: 1627467479 + nsecs: 13279914 +location: 'kitchen' +message: 'Dish is found'" +``` + +#### Clear app notification + +You can also clear app notification. + +```bash +rosservice call /service_notification_saver/clear_app_notification "{}" +``` + +### `app_notification_saver/smach_notification_saver`: SMACH notification saver plugin + +This plugin saves notification via service call. + +#### `plugin_args`: Plugin arguments + +`None` + +#### `launch_args`: Plugin launch arguments + +- `json_path` : JSON path +- `smach_status_topic`: SMACH status topic name + +#### Sample plugin description + +```yaml +plugins: + - name: smach_notification_saver + type: app_notification_saver/smach_notification_saver + launch_args: + json_path: /tmp/app_notification.json + smach_status_topic: /server_name/smach/container_status +``` + +## Nodes + +### `service_notification_saver_node.py`: Node for general notification saver + +Save notification node via service call. + +#### Services + +- `~save_app_notification` (`app_notification_saver/SaveAppNotification`) + + Service to save app notification to JSON. + +- `~clear_app_notification` (`std_srvs/Empty`) + + Service to clear app notification in JSON. + +#### Parameters + +- `~json_path` (`String`, default: `/tmp/app_notification.json`) + + Path to json file which contains app notification + +#### Sample + +##### Launch service_notification_saver node + +```bash +roslaunch app_notification_saver service_notification_saver.launch +``` + +##### Save app notification + +You can save app notification with service call. + +```bash +rosservice call /service_notification_saver/save_app_notification "title: 'object recognition' +stamp: + secs: 1627467479 + nsecs: 13279914 +location: 'kitchen' +message: 'Dish is found'" +``` + +##### Clear app notification + +You can also clear app notification. + +```bash +rosservice call /service_notification_saver/clear_app_notification "{}" +``` + +##### Check output JSON + +The sample output of the json file is like below: + +```json +{ + "object recognition": [ + { + "date": "2021-07-28T19:17:59", + "message": "Dish is found", + "location": "kitchen" + }, + { + "date": "2021-07-28T19:18:09", + "message": "Cup is found", + "location": "kitchen" + } + ], + "navigation failure": [ + { + "date": "2021-07-28T19:18:29", + "message": "Stucked in front of the chair", + "location": "living room" + } + ] +} +``` + +### `smach_notification_saver_node.py`: Node for SMACH notification saver + +Save notification of smach state. + +#### Subscribe topics + +- `~smach/container_status` (`smach_msgs/SmachContainerStatus`, default: `/server_name/smach/container_status`) + + Smach status topic + +#### Parameters + +- `~json_path` (`String`, default: `/tmp/app_notification.json`) + + Path to json file which contains app notification + +#### Sample + +##### Launch smach_notification_saver node + +```bash +# Launch only smach_notification_saver node +roslaunch app_notification_saver smach_notification_saver.launch + +# Sample +# Launch smach_notification_saver node and rosbag +roslaunch app_notification_saver sample_smach_notification_saver.launch --screen +``` diff --git a/app_notification_saver/app_notification_saver_plugin.yaml b/app_notification_saver/app_notification_saver_plugin.yaml new file mode 100644 index 0000000..4536ba0 --- /dev/null +++ b/app_notification_saver/app_notification_saver_plugin.yaml @@ -0,0 +1,6 @@ +- name: app_notification_saver/service_notification_saver + launch: app_notification_saver/service_notification_saver.launch + module: null +- name: app_notification_saver/smach_notification_saver + launch: app_notification_saver/smach_notification_saver.launch + module: null diff --git a/app_notification_saver/data/smach.bag b/app_notification_saver/data/smach.bag new file mode 100644 index 0000000..32b24c4 Binary files /dev/null and b/app_notification_saver/data/smach.bag differ diff --git a/app_notification_saver/launch/service_notification_saver.launch b/app_notification_saver/launch/service_notification_saver.launch new file mode 100644 index 0000000..88ba2d3 --- /dev/null +++ b/app_notification_saver/launch/service_notification_saver.launch @@ -0,0 +1,12 @@ + + + + + + + + json_path: $(arg json_path) + + + diff --git a/app_notification_saver/launch/smach_notification_saver.launch b/app_notification_saver/launch/smach_notification_saver.launch new file mode 100644 index 0000000..e7b729e --- /dev/null +++ b/app_notification_saver/launch/smach_notification_saver.launch @@ -0,0 +1,14 @@ + + + + + + + + + + json_path: $(arg json_path) + + + diff --git a/app_notification_saver/node_scripts/service_notification_saver_node.py b/app_notification_saver/node_scripts/service_notification_saver_node.py new file mode 100755 index 0000000..490100a --- /dev/null +++ b/app_notification_saver/node_scripts/service_notification_saver_node.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python + +import rospy + +from app_notification_saver import ServiceNotificationSaver + + +if __name__ == '__main__': + rospy.init_node('service_notification_saver_node') + ServiceNotificationSaver() + rospy.spin() diff --git a/app_notification_saver/node_scripts/smach_notification_saver_node.py b/app_notification_saver/node_scripts/smach_notification_saver_node.py new file mode 100755 index 0000000..ac58ccf --- /dev/null +++ b/app_notification_saver/node_scripts/smach_notification_saver_node.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python + +import rospy + +from app_notification_saver import SmachNotificationSaver + +if __name__ == '__main__': + rospy.init_node('smach_notification_saver_node') + SmachNotificationSaver() + rospy.spin() diff --git a/app_notification_saver/package.xml b/app_notification_saver/package.xml new file mode 100644 index 0000000..c64da11 --- /dev/null +++ b/app_notification_saver/package.xml @@ -0,0 +1,24 @@ + + + app_notification_saver + 1.3.0 + The app_notification_saver package + + Shingo Kitagawa + Naoya Yamaguchi + BSD + + catkin + python-setuptools + python3-setuptools + + message_generation + app_manager + message_runtime + rospy + smach_msgs + + + + + diff --git a/app_notification_saver/sample/sample_smach_notification_saver.launch b/app_notification_saver/sample/sample_smach_notification_saver.launch new file mode 100644 index 0000000..fad9d8a --- /dev/null +++ b/app_notification_saver/sample/sample_smach_notification_saver.launch @@ -0,0 +1,9 @@ + + + + + + + diff --git a/app_notification_saver/setup.py b/app_notification_saver/setup.py new file mode 100644 index 0000000..a0be37b --- /dev/null +++ b/app_notification_saver/setup.py @@ -0,0 +1,11 @@ +from catkin_pkg.python_setup import generate_distutils_setup +from setuptools import find_packages +from setuptools import setup + + +d = generate_distutils_setup( + packages=find_packages('src'), + package_dir={'': 'src'}, +) + +setup(**d) diff --git a/app_notification_saver/src/app_notification_saver/__init__.py b/app_notification_saver/src/app_notification_saver/__init__.py new file mode 100644 index 0000000..68dc4f2 --- /dev/null +++ b/app_notification_saver/src/app_notification_saver/__init__.py @@ -0,0 +1,3 @@ +from app_notification_saver.app_notification_saver_base import AppNotificationSaver # NOQA +from app_notification_saver.service_notification_saver import ServiceNotificationSaver # NOQA +from app_notification_saver.smach_notification_saver import SmachNotificationSaver # NOQA diff --git a/app_notification_saver/src/app_notification_saver/app_notification_saver_base.py b/app_notification_saver/src/app_notification_saver/app_notification_saver_base.py new file mode 100644 index 0000000..f4ff587 --- /dev/null +++ b/app_notification_saver/src/app_notification_saver/app_notification_saver_base.py @@ -0,0 +1,43 @@ +import datetime +import json +import os +import rospy + + +class AppNotificationSaver(object): + def __init__(self): + self.json_path = rospy.get_param( + '~json_path', '/tmp/app_notification.json') + + def save_app_notification(self, title, stamp, location, message): + """ + Save app notification to json file. + + Args: + title (str) : Notification title (e.g. object detection, navigation faliure ...) # NOQA + stamp (float) : UNIX time when the event occurred + location (str): The location where the event occurred + message (str) : Notification message + + Returns: + Result of whether the json was saved. (bool) + """ + # Load notification json + if os.path.exists(self.json_path): + with open(self.json_path, 'r') as j: + notification = json.load(j) + else: + notification = {} + # Append notification + stamp = datetime.datetime.fromtimestamp(stamp) + new_notification = {'date': stamp.isoformat(), + 'location': location, + 'message': message} + if title in notification: + notification[title].append(new_notification) + else: + notification[title] = [new_notification] + # Dump json + with open(self.json_path, 'w') as j: + json.dump(notification, j, indent=4) + rospy.loginfo(json.dumps(notification, indent=4)) diff --git a/app_notification_saver/src/app_notification_saver/service_notification_saver.py b/app_notification_saver/src/app_notification_saver/service_notification_saver.py new file mode 100644 index 0000000..f34bd23 --- /dev/null +++ b/app_notification_saver/src/app_notification_saver/service_notification_saver.py @@ -0,0 +1,33 @@ +import os +import rospy + +from app_notification_saver import AppNotificationSaver + +from app_notification_saver.srv import SaveAppNotification +from app_notification_saver.srv import SaveAppNotificationResponse +from std_srvs.srv import Empty +from std_srvs.srv import EmptyResponse + + +class ServiceNotificationSaver(AppNotificationSaver): + def __init__(self): + super(ServiceNotificationSaver, self).__init__() + rospy.Service( + '~save_app_notification', + SaveAppNotification, + self.save_service_notification_cb) + rospy.Service( + '~clear_app_notification', + Empty, + self.clear_app_notification_cb) + + def save_service_notification_cb(self, req): + self.save_app_notification( + req.title, float(req.stamp.secs), req.location, req.message) + return SaveAppNotificationResponse(True) + + def clear_app_notification_cb(self, req): + if os.path.exists(self.json_path): + os.remove(self.json_path) + rospy.loginfo('Remove file {}'.format(self.json_path)) + return EmptyResponse() diff --git a/app_notification_saver/src/app_notification_saver/smach_notification_saver.py b/app_notification_saver/src/app_notification_saver/smach_notification_saver.py new file mode 100644 index 0000000..2e63e3b --- /dev/null +++ b/app_notification_saver/src/app_notification_saver/smach_notification_saver.py @@ -0,0 +1,20 @@ +import rospy + +from app_notification_saver import AppNotificationSaver + +from smach_msgs.msg import SmachContainerStatus + + +class SmachNotificationSaver(AppNotificationSaver): + def __init__(self): + super(SmachNotificationSaver, self).__init__() + rospy.Subscriber( + "~smach/container_status", SmachContainerStatus, self.smach_cb) + + def smach_cb(self, msg): + if len(msg.active_states) == 0: + return + states_str = "Active states is " + states_str += ', '.join(msg.active_states) + self.save_app_notification( + "smach", float(msg.header.stamp.secs), "", states_str) diff --git a/app_notification_saver/srv/SaveAppNotification.srv b/app_notification_saver/srv/SaveAppNotification.srv new file mode 100644 index 0000000..eefa9e6 --- /dev/null +++ b/app_notification_saver/srv/SaveAppNotification.srv @@ -0,0 +1,6 @@ +string title # Notification title (e.g. object detection, navigation faliure ...) +time stamp # UNIX time when the event occurred +string location # The location where the event occurred +string message # Notification message +--- +bool result # Result of whether the json was saved diff --git a/app_notifier/CMakeLists.txt b/app_notifier/CMakeLists.txt new file mode 100644 index 0000000..145c10f --- /dev/null +++ b/app_notifier/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 2.8.3) +project(app_notifier) + +find_package(catkin REQUIRED) + +catkin_python_setup() + +catkin_package() + +include_directories() diff --git a/app_notifier/README.md b/app_notifier/README.md new file mode 100644 index 0000000..48ee1a9 --- /dev/null +++ b/app_notifier/README.md @@ -0,0 +1,115 @@ +# app_notifier + +Notifier plugin for `app_manager` + +## `app_manager` plugins + +### `app_notifier/mail_notifier_plugin`: Mail notifier plugin + +This plugin notifies app results by sending an e-mail. + +#### `plugin_args`: Plugin arguments + +- `mail_title`: mail title +- `sender_address`: mail sender address +- `receiver_address`: mail receiver address +- `use_timestamp_title` (default: `False`) : Use timestamp in title or not + +#### `launch_args`: Plugin launch arguments + +`None` + +#### Sample plugin description + +```yaml +plugins: + - name: mail_notifier_plugin + type: app_notifier/mail_notifier_plugin + plugin_args: + mail_title: Test app + use_timestamp_title: true + sender_address: hoge + receiver_address: hoge +``` + +#### Settings for sending from gmail address + +Please see this [link](https://kifarunix.com/configure-postfix-to-use-gmail-smtp-on-ubuntu-18-04/) to configure properly. + +### `app_notifier/speech_notifier_plugin`: Speech notifier plugin + +This plugin notifies app results by speaking. + +#### `plugin_args`: Plugin arguments + +- `client_name`: client name for `sound_play` +- `lang` (default: `None`): language, if `None`, a robot speaks English. + +#### `launch_args`: Plugin launch arguments + +`None` + +#### Sample plugin description + +```yaml +plugins: + - name: speech_notifier_plugin + type: app_notifier/speech_notifier_plugin + plugin_args: + client_name: /sound_play_jp + lang: jp +``` + +### `app_notifier/tweet_notifier_plugin`: Tweet notifier plugin + +This plugin notifies app results by tweeting. + +#### `plugin_args`: Plugin arguments + +- `client_name`: client name for `sound_play` +- `image` (default: `False`): whether tweet with image or not. +- `image_topic_name` (default: `None`): tweet image topic. this argument is used only when `image` is `true`. +- `warning` (default: `False`): whether warn unknown user or not. + +#### `launch_args`: Plugin launch arguments + +`None` + +#### Sample plugin description + +```yaml +plugins: + - name: tweet_notifier_plugin + type: app_notifier/tweet_notifier_plugin + plugin_args: + client_name: /tweet_image_server/tweet + image: true + image_topic_name: /head_camera/rgb/image_rect_color + warning: true +``` + +### `app_notifier/user_speech_notifier_plugin`: User speech notifier plugin + +This plugin notifies which user is running the app by speaking. + +#### `plugin_args`: Plugin arguments + +- `client_name`: client name for `sound_play` +- `lang` (default: `None`): language, if `None`, a robot speaks English. +- `warning` (default: `False`): whether warn unknown user or not. + +#### `launch_args`: Plugin launch arguments + +`None` + +#### Sample plugin description + +```yaml +plugins: + - name: user_speech_notifier_plugin + type: app_notifier/user_speech_notifier_plugin + plugin_args: + client_name: /sound_play_jp + lang: jp + warning: true +``` diff --git a/app_notifier/app_notifier_plugin.yaml b/app_notifier/app_notifier_plugin.yaml new file mode 100644 index 0000000..ff3dbcc --- /dev/null +++ b/app_notifier/app_notifier_plugin.yaml @@ -0,0 +1,12 @@ +- name: app_notifier/mail_notifier_plugin + launch: null + module: app_notifier.mail_notifier_plugin.MailNotifierPlugin +- name: app_notifier/speech_notifier_plugin + launch: null + module: app_notifier.speech_notifier_plugin.SpeechNotifierPlugin +- name: app_notifier/tweet_notifier_plugin + launch: null + module: app_notifier.tweet_notifier_plugin.TweetNotifierPlugin +- name: app_notifier/user_speech_notifier_plugin + launch: null + module: app_notifier.user_speech_notifier_plugin.UserSpeechNotifierPlugin diff --git a/app_notifier/package.xml b/app_notifier/package.xml new file mode 100644 index 0000000..eb75f2c --- /dev/null +++ b/app_notifier/package.xml @@ -0,0 +1,24 @@ + + + app_notifier + 1.3.0 + The app_notifier package + Shingo Kitagawa + Shingo Kitagawa + BSD + + catkin + python-setuptools + python3-setuptools + + actionlib + app_manager + app_notification_saver + python-dateutil + rostwitter + sound_play + + + + + diff --git a/app_notifier/setup.py b/app_notifier/setup.py new file mode 100644 index 0000000..a0be37b --- /dev/null +++ b/app_notifier/setup.py @@ -0,0 +1,11 @@ +from catkin_pkg.python_setup import generate_distutils_setup +from setuptools import find_packages +from setuptools import setup + + +d = generate_distutils_setup( + packages=find_packages('src'), + package_dir={'': 'src'}, +) + +setup(**d) diff --git a/app_notifier/src/app_notifier/__init__.py b/app_notifier/src/app_notifier/__init__.py new file mode 100644 index 0000000..aafc7fd --- /dev/null +++ b/app_notifier/src/app_notifier/__init__.py @@ -0,0 +1,4 @@ +from app_notifier.mail_notifier_plugin import MailNotifierPlugin # NOQA +from app_notifier.speech_notifier_plugin import SpeechNotifierPlugin # NOQA +from app_notifier.tweet_notifier_plugin import TweetNotifierPlugin # NOQA +from app_notifier.user_speech_notifier_plugin import UserSpeechNotifierPlugin # NOQA diff --git a/app_notifier/src/app_notifier/mail_notifier_plugin.py b/app_notifier/src/app_notifier/mail_notifier_plugin.py new file mode 100644 index 0000000..20ff001 --- /dev/null +++ b/app_notifier/src/app_notifier/mail_notifier_plugin.py @@ -0,0 +1,83 @@ +import datetime +import subprocess + +from app_manager import AppManagerPlugin +import rospy + +from app_notifier.util import check_timestamp_before_start +from app_notifier.util import get_notification_json_paths +from app_notifier.util import load_notification_jsons + + +class MailNotifierPlugin(AppManagerPlugin): + def __init__(self): + super(MailNotifierPlugin, self).__init__() + + def app_manager_start_plugin(self, app, ctx, plugin_args): + self.start_time = rospy.Time.now() + + def app_manager_stop_plugin(self, app, ctx, plugin_args): + mail_title = plugin_args['mail_title'] + sender_address = plugin_args['sender_address'] + receiver_address = plugin_args['receiver_address'] + use_timestamp_title = False + if 'use_timestamp_title' in plugin_args: + use_timestamp_title = plugin_args['use_timestamp_title'] + + if use_timestamp_title: + timestamp = '{0:%Y/%m/%d (%H:%M:%S)}'.format( + datetime.datetime.now()) + mail_title += ': {}'.format(timestamp) + + display_name = app.display_name + mail_content = "Hi, \\n" + if ctx['exit_code'] == 0 and not ctx['stopped']: + mail_content += "I succeeded in doing {}.\\n".format(display_name) + elif ctx['stopped']: + mail_content += "I stopped doing {}.\\n".format(display_name) + else: + mail_content += "I failed to do {}.\\n".format(display_name) + if 'upload_successes' in ctx: + if all(ctx['upload_successes']): + mail_content += "I succeeded to upload data.\\n" + else: + mail_content += "I failed to upload data.\\n" + mail_content += "\\n" + for success, file_url in zip( + ctx['upload_successes'], ctx['upload_file_urls']): + if success: + mail_content += "URL: {}\\n".format(file_url) + mail_content += "\\n" + + json_paths = get_notification_json_paths() + notification = load_notification_jsons(json_paths) + for n_type in notification.keys(): + mail_content += "Following {} is reported.\\n".format(n_type) + for event in notification[n_type]: + if check_timestamp_before_start( + event['date'], self.start_time): + continue + if event['location'] == "": + mail_content += " - At {}, {}.\\n".format( + event['date'], event['message']) + else: + mail_content += " - At {}, {} in {}.\\n".format( + event['date'], event['message'], event['location']) + mail_content += "\\n" + + cmd = "LC_CTYPE=en_US.UTF-8 /bin/echo -e \"{}\"".format(mail_content) + cmd += " | /usr/bin/mail -s \"{}\" -r {} {}".format( + mail_title, sender_address, receiver_address) + exit_code = subprocess.call(cmd, shell=True) + rospy.loginfo('Title: {}'.format(mail_title)) + if exit_code > 0: + rospy.logerr( + 'Failed to send e-mail: {} -> {}'.format( + sender_address, receiver_address)) + rospy.logerr("You may need to do '$ sudo apt install mailutils'") + else: + rospy.loginfo( + 'Succeeded to send e-mail: {} -> {}'.format( + sender_address, receiver_address)) + ctx['mail_notifier_exit_code'] = exit_code + return ctx diff --git a/app_notifier/src/app_notifier/speech_notifier_plugin.py b/app_notifier/src/app_notifier/speech_notifier_plugin.py new file mode 100644 index 0000000..ec9a493 --- /dev/null +++ b/app_notifier/src/app_notifier/speech_notifier_plugin.py @@ -0,0 +1,68 @@ +import actionlib +from app_manager import AppManagerPlugin +import rospy + +from app_notifier.util import check_timestamp_before_start +from app_notifier.util import get_notification_json_paths +from app_notifier.util import load_notification_jsons +from app_notifier.util import speak + +from sound_play.msg import SoundRequestAction + + +class SpeechNotifierPlugin(AppManagerPlugin): + def __init__(self): + super(SpeechNotifierPlugin, self).__init__() + self.client = None + + def app_manager_start_plugin(self, app, ctx, plugin_args): + self.start_time = rospy.Time.now() + client_name = plugin_args['client_name'] + lang = None + if 'lang' in plugin_args: + lang = plugin_args['lang'] + + display_name = app.display_name + display_name = display_name.replace('_', ' ') + display_name = display_name.replace('-', ' ') + client = actionlib.SimpleActionClient(client_name, SoundRequestAction) + speech_text = "I'm starting {} app.".format(display_name) + speak(client, speech_text, lang=lang) + return ctx + + def app_manager_stop_plugin(self, app, ctx, plugin_args): + client_name = plugin_args['client_name'] + lang = None + if 'lang' in plugin_args: + lang = plugin_args['lang'] + + display_name = app.display_name + display_name = display_name.replace('_', ' ') + display_name = display_name.replace('-', ' ') + client = actionlib.SimpleActionClient(client_name, SoundRequestAction) + if ctx['exit_code'] == 0 and not ctx['stopped']: + speech_text = "I succeeded in doing {} app.".format(display_name) + elif ctx['stopped']: + speech_text = "I stopped doing {} app.".format(display_name) + else: + speech_text = "I failed to do {} app.".format(display_name) + if 'upload_successes' in ctx: + if all(ctx['upload_successes']): + speech_text += " I succeeded to upload data." + else: + speech_text += " I failed to upload data." + + # only speak about object recognition + json_paths = get_notification_json_paths() + notification = load_notification_jsons(json_paths) + if 'object recognition' in notification: + for event in notification['object recognition']: + if check_timestamp_before_start( + event['date'], self.start_time): + continue + time = event['date'].split('T')[1] + speech_text += " At {}, {} in {}.".format( + time, event['message'], event['location']) + + speak(client, speech_text, lang=lang) + return ctx diff --git a/app_notifier/src/app_notifier/tweet_notifier_plugin.py b/app_notifier/src/app_notifier/tweet_notifier_plugin.py new file mode 100644 index 0000000..3773f42 --- /dev/null +++ b/app_notifier/src/app_notifier/tweet_notifier_plugin.py @@ -0,0 +1,85 @@ +import actionlib +from app_manager import AppManagerPlugin +import rospy + +from app_notifier.util import check_timestamp_before_start +from app_notifier.util import get_notification_json_paths +from app_notifier.util import load_notification_jsons +from app_notifier.util import tweet + +from rostwitter.msg import TweetAction + + +class TweetNotifierPlugin(AppManagerPlugin): + def __init__(self): + super(TweetNotifierPlugin, self).__init__() + self.client = None + + def app_manager_start_plugin(self, app, ctx, plugin_args): + self.start_time = rospy.Time.now() + client_name = plugin_args['client_name'] + image = False + if 'image' in plugin_args: + image = plugin_args['image'] + if image and 'image_topic_name': + image_topic_name = plugin_args['image_topic_name'] + + if 'warning' in plugin_args: + warning = plugin_args['warning'] + else: + warning = False + + display_name = app.display_name + username = rospy.get_param('/app_manager/running_user_name', None) + tweet_text = None + if username: + tweet_text = "{} is starting {} app".format(username, display_name) + elif warning: + tweet_text = "Unknown user is starting {} app".format(display_name) + + if tweet_text is not None: + client = actionlib.SimpleActionClient( + client_name, TweetAction) + tweet( + client, tweet_text, image=image, + image_topic_name=image_topic_name) + return ctx + + def app_manager_stop_plugin(self, app, ctx, plugin_args): + client_name = plugin_args['client_name'] + image = False + if 'image' in plugin_args: + image = plugin_args['image'] + if image and 'image_topic_name': + image_topic_name = plugin_args['image_topic_name'] + + display_name = app.display_name + client = actionlib.SimpleActionClient(client_name, TweetAction) + if ctx['exit_code'] == 0 and not ctx['stopped']: + tweet_text = "I succeeded in doing {} app.".format(display_name) + elif ctx['stopped']: + tweet_text = "I stopped doing {} app.".format(display_name) + else: + tweet_text = "I failed to do {} app.".format(display_name) + if 'upload_successes' in ctx: + if all(ctx['upload_successes']): + tweet_text += " I succeeded to upload data." + else: + tweet_text += " I failed to upload data." + + # only tweet about object recognition + json_paths = get_notification_json_paths() + notification = load_notification_jsons(json_paths) + if 'object recognition' in notification: + for event in notification['object recognition']: + if check_timestamp_before_start( + event['date'], self.start_time): + continue + time = event['date'].split('T')[1] + tweet_text += " At {}, {} in {}.".format( + time, event['message'], event['location']) + + tweet( + client, tweet_text[:280], image=image, + image_topic_name=image_topic_name) + return ctx diff --git a/app_notifier/src/app_notifier/user_speech_notifier_plugin.py b/app_notifier/src/app_notifier/user_speech_notifier_plugin.py new file mode 100644 index 0000000..de76928 --- /dev/null +++ b/app_notifier/src/app_notifier/user_speech_notifier_plugin.py @@ -0,0 +1,70 @@ +import actionlib +from app_manager import AppManagerPlugin +import rospy + +from app_notifier.util import speak + +from sound_play.msg import SoundRequestAction + + +class UserSpeechNotifierPlugin(AppManagerPlugin): + def __init__(self): + super(UserSpeechNotifierPlugin, self).__init__() + self.client = None + self.username = rospy.get_param('/app_manager/running_user_name', None) + if self.username is not None: + self.username = self.username.replace('_', ' ') + self.username = self.username.replace('-', ' ') + + def app_manager_start_plugin(self, app, ctx, plugin_args): + client_name = plugin_args['client_name'] + if 'warning' in plugin_args: + warning = plugin_args['warning'] + else: + warning = False + display_name = app.display_name + display_name = display_name.replace('_', ' ') + display_name = display_name.replace('-', ' ') + speech_text = None + if self.username: + speech_text = "{} is starting {} app".format( + self.username, display_name) + elif warning: + speech_text = "Unknown user is starting {} app".format( + display_name) + + if speech_text is not None: + lang = None + if 'lang' in plugin_args: + lang = plugin_args['lang'] + client = actionlib.SimpleActionClient( + client_name, SoundRequestAction) + speak(client, speech_text, lang=lang) + return ctx + + def app_manager_stop_plugin(self, app, ctx, plugin_args): + client_name = plugin_args['client_name'] + if 'warning' in plugin_args: + warning = plugin_args['warning'] + else: + warning = False + + display_name = app.display_name + display_name = display_name.replace('_', ' ') + display_name = display_name.replace('-', ' ') + speech_text = None + if self.username: + speech_text = "{} is stopping {} app".format( + self.username, display_name) + elif warning: + speech_text = "Unknown user is stopping {} app".format( + display_name) + + if speech_text is not None: + lang = None + if 'lang' in plugin_args: + lang = plugin_args['lang'] + client = actionlib.SimpleActionClient( + client_name, SoundRequestAction) + speak(client, speech_text, lang=lang) + return ctx diff --git a/app_notifier/src/app_notifier/util.py b/app_notifier/src/app_notifier/util.py new file mode 100644 index 0000000..3b77454 --- /dev/null +++ b/app_notifier/src/app_notifier/util.py @@ -0,0 +1,100 @@ +from __future__ import print_function + +import datetime +import json +import os +import rospy +import sys + +from rostwitter.msg import TweetGoal +from sound_play.msg import SoundRequestGoal + +if ((sys.version_info.major == 3 and sys.version_info.minor >= 7) + or (sys.version_info.major > 3)): + from datetime import date + fromisoformat = date.fromisoformat +else: + try: + import dateutil.parser + fromisoformat = dateutil.parser.isoparse + except AttributeError as e: + print(''' + We need python-dateutil>=2.7.0 for timestamp check. + Please try the following command. + pip install python-dateutil==2.7.0 + +''', file=sys.stderr) + print(e, file=sys.stderr) + fromisoformat = None + + +def speak(client, speech_text, lang=None): + client.wait_for_server(timeout=rospy.Duration(1.0)) + sound_goal = SoundRequestGoal() + sound_goal.sound_request.sound = -3 + sound_goal.sound_request.command = 1 + sound_goal.sound_request.volume = 1.0 + if lang is not None: + sound_goal.sound_request.arg2 = lang + sound_goal.sound_request.arg = speech_text + client.send_goal(sound_goal) + client.wait_for_result() + return client.get_result() + + +def tweet(client, tweet_text, image=False, image_topic_name=None): + client.wait_for_server(timeout=rospy.Duration(1.0)) + tweet_goal = TweetGoal() + tweet_goal.text = tweet_text + tweet_goal.image = image + if image and image_topic_name: + tweet_goal.image_topic_name = image_topic_name + tweet_goal.speak = False + tweet_goal.warning = False + tweet_goal.warning_time = 0 + client.send_goal(tweet_goal) + client.wait_for_result() + return client.get_result() + + +def get_notification_json_paths(): + notification_json_path = rospy.get_param( + '/service_notification_saver/json_path', None) + smach_json_path = rospy.get_param( + '/smach_notification_saver/json_path', None) + if notification_json_path and smach_json_path: + if notification_json_path == smach_json_path: + json_paths = [notification_json_path] + else: + json_paths = [notification_json_path, smach_json_path] + elif notification_json_path: + json_paths = [notification_json_path] + elif smach_json_path: + json_paths = [smach_json_path] + else: + json_paths = ['/tmp/app_notification.json'] + return json_paths + + +def load_notification_jsons(json_paths): + notification = {} + for json_path in json_paths: + if not os.path.exists(json_path): + continue + with open(json_path, 'r') as f: + n_data = json.load(f) + for n_type in n_data.keys(): + if n_type in notification: + notification[n_type].append(n_data[n_type]) + else: + notification[n_type] = n_data[n_type] + return notification + + +def check_timestamp_before_start(timestamp, start_time): + if fromisoformat is None: + print('Please install python-dateutil >= 2.7.0', file=sys.stderr) + print('Skip timestap checking', file=sys.stderr) + return False + start_date = datetime.datetime.fromtimestamp(start_time.to_sec()) + return fromisoformat(timestamp) < start_date diff --git a/app_publisher/CMakeLists.txt b/app_publisher/CMakeLists.txt new file mode 100644 index 0000000..842149d --- /dev/null +++ b/app_publisher/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 2.8.3) +project(app_publisher) + +find_package(catkin REQUIRED) + +catkin_python_setup() + +catkin_package() + +include_directories() diff --git a/app_publisher/README.md b/app_publisher/README.md new file mode 100644 index 0000000..c87f20f --- /dev/null +++ b/app_publisher/README.md @@ -0,0 +1,64 @@ +# app_publisher + +ROS topic publisher plugin for `app_manager` + +## `app_manager` plugins + +### `app_notifier/rostopic_publisher_plugin`: ROS topic publisher plugin + +This plugin publishes ROS topic at the beginning or end of the app. + +#### `plugin_args`: Plugin arguments + +- `start_topics`: topic which is published at the beginning of app + - `name`: name of the topic + - `pkg`: package name of the message + - `type`: type of the message + - `field`: content of the message field +- `stop_topics`: topic which is published at the end of app + - `name`: name of the topic + - `pkg`: package name of the message + - `type`: type of the message + - `field`: content of the message field + - `cond`: Decide to publish a topic depending on the app exit condition. `success`, `failure`, `stop` and `timeout` can be set. + +#### `launch_args`: Plugin launch arguments + +`None` + +#### Sample plugin description + +```yaml +plugins + - name: rostopic_publisher_plugin + type: app_publisher/rostopic_publisher_plugin + plugin_args: + start_topics: + - name: /test_bool + pkg: std_msgs + type: Bool + - name: /test_polygon_stamped + pkg: geometry_msgs + type: PolygonStamped + field: + header: + seq: 0 + stamp: now + frame_id: test + polygon: + points: + - x: 1 + y: 0 + z: 0 + - x: 0 + y: 1 + z: 0 + - x: 0 + y: 0 + z: 1 + stop_topics: + - name: /test_cancel + pkg: actionlib_msgs + type: GoalID + cond: success +``` diff --git a/app_publisher/app_publisher_plugin.yaml b/app_publisher/app_publisher_plugin.yaml new file mode 100644 index 0000000..98083f1 --- /dev/null +++ b/app_publisher/app_publisher_plugin.yaml @@ -0,0 +1,3 @@ +- name: app_publisher/rostopic_publisher_plugin + launch: null + module: app_publisher.rostopic_publisher_plugin.RostopicPublisherPlugin diff --git a/app_publisher/package.xml b/app_publisher/package.xml new file mode 100644 index 0000000..78218c3 --- /dev/null +++ b/app_publisher/package.xml @@ -0,0 +1,20 @@ + + + app_publisher + 1.3.0 + The app_publisher package + Shingo Kitagawa + Naoya Yamaguchi + BSD + + catkin + python-setuptools + python3-setuptools + + app_manager + rospy_message_converter + + + + + diff --git a/app_publisher/setup.py b/app_publisher/setup.py new file mode 100644 index 0000000..a0be37b --- /dev/null +++ b/app_publisher/setup.py @@ -0,0 +1,11 @@ +from catkin_pkg.python_setup import generate_distutils_setup +from setuptools import find_packages +from setuptools import setup + + +d = generate_distutils_setup( + packages=find_packages('src'), + package_dir={'': 'src'}, +) + +setup(**d) diff --git a/app_publisher/src/app_publisher/__init__.py b/app_publisher/src/app_publisher/__init__.py new file mode 100644 index 0000000..efa2aca --- /dev/null +++ b/app_publisher/src/app_publisher/__init__.py @@ -0,0 +1 @@ +from app_publisher.rostopic_publisher_plugin import RostopicPublisherPlugin # NOQA diff --git a/app_publisher/src/app_publisher/rostopic_publisher_plugin.py b/app_publisher/src/app_publisher/rostopic_publisher_plugin.py new file mode 100644 index 0000000..9b63263 --- /dev/null +++ b/app_publisher/src/app_publisher/rostopic_publisher_plugin.py @@ -0,0 +1,47 @@ +import importlib + +from app_manager import AppManagerPlugin +import rospy +from rospy_message_converter import message_converter + + +class RostopicPublisherPlugin(AppManagerPlugin): + def __init__(self): + super(RostopicPublisherPlugin, self).__init__() + + def publish_topic(self, topic, ctx): + if 'cond' in topic: + if ((topic['cond'] == 'success' and ctx['exit_code'] == 0) + or (topic['cond'] == 'failure' and ctx['exit_code'] != 0) + or (topic['cond'] == 'stop' and ctx['stopped'] is True) + or (topic['cond'] == 'timeout' and ctx['stopped'] is True + and ctx['timeout'] is True)): + pass + else: + return + msg = getattr( + importlib.import_module( + '{}.msg'.format(topic['pkg'])), topic['type']) + pub = rospy.Publisher(topic['name'], msg, queue_size=1) + rospy.sleep(1) + if 'field' in topic: + pub_msg = message_converter.convert_dictionary_to_ros_message( + '{}/{}'.format(topic['pkg'], topic['type']), + topic['field']) + else: + pub_msg = msg() + pub.publish(pub_msg) + + def app_manager_start_plugin(self, app, ctx, plugin_args): + if 'start_topics' not in plugin_args: + return + topics = plugin_args['start_topics'] + for topic in topics: + self.publish_topic(topic, ctx) + + def app_manager_stop_plugin(self, app, ctx, plugin_args): + if 'stop_topics' not in plugin_args: + return + topics = plugin_args['stop_topics'] + for topic in topics: + self.publish_topic(topic, ctx) diff --git a/app_recorder/CMakeLists.txt b/app_recorder/CMakeLists.txt new file mode 100644 index 0000000..5d98436 --- /dev/null +++ b/app_recorder/CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 2.8.3) +project(app_recorder) + +find_package(catkin REQUIRED) + +catkin_python_setup() + +catkin_package() + +include_directories() + +install(DIRECTORY launch + DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION} + USE_SOURCE_PERMISSIONS +) diff --git a/app_recorder/README.md b/app_recorder/README.md new file mode 100644 index 0000000..17c5911 --- /dev/null +++ b/app_recorder/README.md @@ -0,0 +1,214 @@ +# app_recorder + +Recorder plugin for `app_manager` + +## `app_manager` plugins + +### `app_recorder/audio_video_recorder_plugin`: Audio-video recorder plugin + +This plugin records video data with audio during when an app is running. + +#### `plugin_args`: Plugin arguments + +`None` + +#### `launch_args`: Plugin launch arguments + +- `video_path`: + - video file directory path +- `video_title`: + - video file name +- `audio_topic_name`: + - image topic name for audio +- `audio_channels`: + - audio channels +- `audio_sample_rate`: + - audio sample rate +- `audio_format`: + - audio format +- `audio_sample_format`: + - audio sample format +- `video_topic_name`: + - image topic name for video +- `video_height`: + - video height +- `video_width`: + - video width +- `video_framerate`: + - video framerate +- `video_encoding`: + - video encoding +- `use_comrpressed`: (default: `False`) + - Use compressed image topic or not +- `video_decompressed_topic_name`: + - decompressed image topic name when `use_comrpressed` is `True` +- `use_machine`: (default: `False`) + - Use machine tag or not +- `machine_name`: + - machine name when `use_machine` is `True` +- `machine_file`: + - machine file path when `use_machine` is `True` + +#### Sample plugin description + +```yaml +plugins: + - name: audio_video_recorder_plugin + type: app_recorder/audio_video_recorder_plugin + launch_args: + video_path: /tmp + video_title: test.avi + audio_topic_name: /audio + audio_channels: 1 + audio_sample_rate: 16000 + audio_format: wave + audio_sample_format: S16LE + video_topic_name: /head_camera/rgb/image_rect_color + video_height: 480 + video_width: 640 + video_framerate: 30 + video_encoding: RGB +``` + +### `app_recorder/video_recorder_plugin`: Video recorder plugin + +This plugin records video data during when an app is running. + +#### `plugin_args`: Plugin arguments + +`None` + +#### `launch_args`: Plugin launch arguments + +- `video_path`: + - video file directory path +- `video_title`: + - video file name +- `video_topic_name`: + - image topic name for video +- `video_fps`: + - video fps +- `video_codec`: (default: `XVID`) + - video codec +- `use_comrpressed`: (default: `False`) + - Use compressed image topic or not +- `video_decompressed_topic_name`: + - decompressed image topic name when `use_comrpressed` is `True` +- `use_machine`: (default: `False`) + - Use machine tag or not +- `machine_name`: + - machine name when `use_machine` is `True` +- `machine_file`: + - machine file path when `use_machine` is `True` + +#### Sample plugin description + +```yaml +plugins: + - name: video_recorder_plugin + type: app_recorder/video_recorder_plugin + launch_args: + video_path: /tmp + video_title: test.avi + video_topic_name: /wide_stereo/right/image_rect_color + video_fps: 30 +``` + +### `app_recorder/audio_recorder_plugin`: Audio recorder plugin + +This plugin records audio data during when an app is running. + +#### `plugin_args`: Plugin arguments + +`None` + +#### `launch_args`: Plugin launch arguments + +- `audio_path`: + - audio file directory path +- `audio_title`: + - audio file name +- `audio_topic_name`: + - image topic name for audio +- `audio_format`: (default: `wave`) + - audio format +- `use_machine`: (default: `False`) + - Use machine tag or not +- `machine_name`: + - machine name when `use_machine` is `True` +- `machine_file`: + - machine file path when `use_machine` is `True` + +#### Sample plugin description + +```yaml +plugins: + - name: audio_recorder_plugin + type: app_recorder/audio_recorder_plugin + launch_args: + audio_path: /tmp + audio_title: test.mp3 + audio_topic_name: /audio + audio_format: mp3 +``` + +### `app_recorder/rosbag_recorder_plugin`: Rosbag recorder plugin + +This plugin records rosbag data during when an app is running. + +#### `plugin_args`: Plugin arguments + +`None` + +#### `launch_args`: Plugin launch arguments + +- `rosbag_path`: + - rosbag file directory path +- `rosbag_title`: + - rosbag file name +- `rosbag_topic_names`: + - topic names for rosbag +- `compress`: (default: `False`) + - compress rosbag or not +- `use_machine`: (default: `False`) + - Use machine tag or not +- `machine_name`: + - machine name when `use_machine` is `True` +- `machine_file`: + - machine file path when `use_machine` is `True` + +#### Sample plugin description + +```yaml +plugins: + - name: rosbag_recorder_plugin + type: app_recorder/rosbag_recorder_plugin + launch_args: + rosbag_path: /tmp + rosbag_title: test.bag + rosbag_topic_names: /tf /joint_states +``` + +### `app_recorder/result_recorder_plugin`: Result recorder plugin + +This plugin records app result in yaml when app finishs. + +#### `plugin_args`: Plugin arguments + +- `result_path`: result file directory path +- `result_title`: result file name + +#### `launch_args`: Plugin launch arguments + +`None` + +#### Sample plugin description + +```yaml +plugins: + - name: result_recorder_plugin + type: app_recorder/result_recorder_plugin + plugin_args: + result_path: /tmp + result_title: test.yaml +``` diff --git a/app_recorder/app_recorder_plugin.yaml b/app_recorder/app_recorder_plugin.yaml new file mode 100644 index 0000000..4251fad --- /dev/null +++ b/app_recorder/app_recorder_plugin.yaml @@ -0,0 +1,15 @@ +- name: app_recorder/video_recorder_plugin + launch: app_recorder/video_recorder.launch + module: null +- name: app_recorder/audio_recorder_plugin + launch: app_recorder/audio_recorder.launch + module: null +- name: app_recorder/audio_video_recorder_plugin + launch: app_recorder/audio_video_recorder.launch + module: null +- name: app_recorder/rosbag_recorder_plugin + launch: app_recorder/rosbag_recorder.launch + module: null +- name: app_recorder/result_recorder_plugin + launch: null + module: app_recorder.result_recorder_plugin.ResultRecorderPlugin diff --git a/app_recorder/launch/audio_recorder.launch b/app_recorder/launch/audio_recorder.launch new file mode 100644 index 0000000..427a931 --- /dev/null +++ b/app_recorder/launch/audio_recorder.launch @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + dst: $(arg audio_path)/$(arg audio_title) + format: $(arg audio_format) + + + diff --git a/app_recorder/launch/audio_video_recorder.launch b/app_recorder/launch/audio_video_recorder.launch new file mode 100644 index 0000000..1a1655f --- /dev/null +++ b/app_recorder/launch/audio_video_recorder.launch @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + queue_size: 100 + file_name: $(arg video_path)/$(arg video_title) + file_format: avi + audio_channels: $(arg audio_channels) + audio_sample_rate: $(arg audio_sample_rate) + audio_format: $(arg audio_format) + audio_sample_format: $(arg audio_sample_format) + video_height: $(arg video_height) + video_width: $(arg video_width) + video_framerate: $(arg video_framerate) + video_encoding: $(arg video_encoding) + + + + + + + + + queue_size: 100 + file_name: $(arg video_path)/$(arg video_title) + file_format: avi + audio_channels: $(arg audio_channels) + audio_sample_rate: $(arg audio_sample_rate) + audio_format: $(arg audio_format) + audio_sample_format: $(arg audio_sample_format) + video_height: $(arg video_height) + video_width: $(arg video_width) + video_framerate: $(arg video_framerate) + video_encoding: $(arg video_encoding) + + + + + diff --git a/app_recorder/launch/rosbag_recorder.launch b/app_recorder/launch/rosbag_recorder.launch new file mode 100644 index 0000000..f7fc2cc --- /dev/null +++ b/app_recorder/launch/rosbag_recorder.launch @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app_recorder/launch/video_recorder.launch b/app_recorder/launch/video_recorder.launch new file mode 100644 index 0000000..2dd67ff --- /dev/null +++ b/app_recorder/launch/video_recorder.launch @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + filename: $(arg video_path)/$(arg video_title) + stamped_filename: false + fps: $(arg video_fps) + codec: $(arg video_codec) + + + + + + + + filename: $(arg video_path)/$(arg video_title) + stamped_filename: false + fps: $(arg video_fps) + codec: $(arg video_codec) + + + + + diff --git a/app_recorder/package.xml b/app_recorder/package.xml new file mode 100644 index 0000000..6aa4d12 --- /dev/null +++ b/app_recorder/package.xml @@ -0,0 +1,22 @@ + + + app_recorder + 1.3.0 + The app_recorder package + Shingo Kitagawa + Shingo Kitagawa + BSD + + catkin + python-setuptools + python3-setuptools + app_manager + audio_play + audio_video_recorder + image_transport + image_view + + + + + diff --git a/app_recorder/setup.py b/app_recorder/setup.py new file mode 100644 index 0000000..a0be37b --- /dev/null +++ b/app_recorder/setup.py @@ -0,0 +1,11 @@ +from catkin_pkg.python_setup import generate_distutils_setup +from setuptools import find_packages +from setuptools import setup + + +d = generate_distutils_setup( + packages=find_packages('src'), + package_dir={'': 'src'}, +) + +setup(**d) diff --git a/app_recorder/src/app_recorder/__init__.py b/app_recorder/src/app_recorder/__init__.py new file mode 100644 index 0000000..d7da087 --- /dev/null +++ b/app_recorder/src/app_recorder/__init__.py @@ -0,0 +1,5 @@ +from app_recorder.audio_recorder_plugin import AudioRecorderPlugin # NOQA +from app_recorder.audio_video_recorder_plugin import AudioVideoRecorderPlugin # NOQA +from app_recorder.result_recorder_plugin import ResultRecorderPlugin # NOQA +from app_recorder.rosbag_recorder_plugin import RosbagRecorderPlugin # NOQA +from app_recorder.video_recorder_plugin import VideoRecorderPlugin # NOQA diff --git a/app_recorder/src/app_recorder/audio_recorder_plugin.py b/app_recorder/src/app_recorder/audio_recorder_plugin.py new file mode 100644 index 0000000..e8825b6 --- /dev/null +++ b/app_recorder/src/app_recorder/audio_recorder_plugin.py @@ -0,0 +1,6 @@ +from app_manager import AppManagerPlugin + + +class AudioRecorderPlugin(AppManagerPlugin): + def __init__(self): + super(AudioRecorderPlugin, self).__init__() diff --git a/app_recorder/src/app_recorder/audio_video_recorder_plugin.py b/app_recorder/src/app_recorder/audio_video_recorder_plugin.py new file mode 100644 index 0000000..786b819 --- /dev/null +++ b/app_recorder/src/app_recorder/audio_video_recorder_plugin.py @@ -0,0 +1,6 @@ +from app_manager import AppManagerPlugin + + +class AudioVideoRecorderPlugin(AppManagerPlugin): + def __init__(self): + super(AudioVideoRecorderPlugin, self).__init__() diff --git a/app_recorder/src/app_recorder/result_recorder_plugin.py b/app_recorder/src/app_recorder/result_recorder_plugin.py new file mode 100644 index 0000000..a2d7e19 --- /dev/null +++ b/app_recorder/src/app_recorder/result_recorder_plugin.py @@ -0,0 +1,31 @@ +import os +import rospy +import yaml + +from app_manager import AppManagerPlugin + + +class ResultRecorderPlugin(AppManagerPlugin): + def __init__(self): + super(ResultRecorderPlugin, self).__init__() + + @classmethod + def app_manager_stop_plugin(cls, app, ctx, plugin_args): + result = { + 'exit_code': ctx['exit_code'], + 'stopped': ctx['stopped'], + } + result_path = '/tmp' + if 'result_path' in plugin_args: + result_path = plugin_args['result_path'] + result_title = 'result.yaml' + if 'result_title' in plugin_args: + result_title = plugin_args['result_title'] + try: + with open(os.path.join(result_path, result_title), 'w') as f: + yaml.safe_dump(result, f) + except Exception as e: + rospy.logerr( + 'failed to write result in {}: {}'.format( + os.path.join(result_path, result_title), e)) + return ctx diff --git a/app_recorder/src/app_recorder/rosbag_recorder_plugin.py b/app_recorder/src/app_recorder/rosbag_recorder_plugin.py new file mode 100644 index 0000000..9ea5fd4 --- /dev/null +++ b/app_recorder/src/app_recorder/rosbag_recorder_plugin.py @@ -0,0 +1,6 @@ +from app_manager import AppManagerPlugin + + +class RosbagRecorderPlugin(AppManagerPlugin): + def __init__(self): + super(RosbagRecorderPlugin, self).__init__() diff --git a/app_recorder/src/app_recorder/video_recorder_plugin.py b/app_recorder/src/app_recorder/video_recorder_plugin.py new file mode 100644 index 0000000..91ab33a --- /dev/null +++ b/app_recorder/src/app_recorder/video_recorder_plugin.py @@ -0,0 +1,6 @@ +from app_manager import AppManagerPlugin + + +class VideoRecorderPlugin(AppManagerPlugin): + def __init__(self): + super(VideoRecorderPlugin, self).__init__() diff --git a/app_scheduler/CMakeLists.txt b/app_scheduler/CMakeLists.txt new file mode 100644 index 0000000..8958690 --- /dev/null +++ b/app_scheduler/CMakeLists.txt @@ -0,0 +1,33 @@ +cmake_minimum_required(VERSION 2.8.3) +project(app_scheduler) + +find_package(catkin REQUIRED COMPONENTS + roscpp + rospy + message_generation +) + +catkin_python_setup() + +add_message_files( + FILES + AppSchedule.msg + AppScheduleEntry.msg + AppScheduleEntries.msg +) + +add_service_files( + FILES + AddEntry.srv + RemoveEntry.srv +) + +generate_messages() + +catkin_package( + CATKIN_DEPENDS message_runtime +) + +install(DIRECTORY apps launch sample scripts + DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION} + USE_SOURCE_PERMISSIONS) diff --git a/app_scheduler/README.md b/app_scheduler/README.md new file mode 100644 index 0000000..93b141d --- /dev/null +++ b/app_scheduler/README.md @@ -0,0 +1,35 @@ +# app_scheduler + +Scheduler for `app_manager` with `schedule` python package + +## Sample launch + +```bash +roslaunch app_scheduler sample_app_scheduler.launch +``` + +## Sample schedule yaml + +You can set schedule with `schedule` python package syntax. + +```yaml +- name: sample0 + app_name: app_scheduler/sample0 + app_args: + - hoge: fuga + app_schedule: + start: every(2).minutes.at(":00") +- name: sample1 + app_name: app_scheduler/sample1 + app_schedule: + start: every(60).seconds +- name: sample2 + app_name: app_scheduler/sample2 + app_schedule: + start: every(1).hour.at(":00") +- name: sample3 + app_name: app_scheduler/sample3 + app_schedule: + start: every(1).day.at("10:00:00") + stop: every(1).day.at("10:00:03") +``` diff --git a/app_scheduler/apps/apps.installed b/app_scheduler/apps/apps.installed new file mode 100644 index 0000000..1ca8f05 --- /dev/null +++ b/app_scheduler/apps/apps.installed @@ -0,0 +1,9 @@ +apps: + - app: app_scheduler/sample0 + display: sample0 + - app: app_scheduler/sample1 + display: sample1 + - app: app_scheduler/sample2 + display: sample2 + - app: app_scheduler/sample3 + display: sample3 diff --git a/app_scheduler/apps/sample0/sample0.app b/app_scheduler/apps/sample0/sample0.app new file mode 100644 index 0000000..6b88d07 --- /dev/null +++ b/app_scheduler/apps/sample0/sample0.app @@ -0,0 +1,5 @@ +display: sample0 +description: sample0 +platform: turtlebot +launch: app_scheduler/sample0.xml +interface: app_scheduler/sample0.interface diff --git a/app_scheduler/apps/sample0/sample0.interface b/app_scheduler/apps/sample0/sample0.interface new file mode 100644 index 0000000..044105d --- /dev/null +++ b/app_scheduler/apps/sample0/sample0.interface @@ -0,0 +1,2 @@ +published_topics: {} +subscribed_topics: {} diff --git a/app_scheduler/apps/sample0/sample0.l b/app_scheduler/apps/sample0/sample0.l new file mode 100644 index 0000000..13d8942 --- /dev/null +++ b/app_scheduler/apps/sample0/sample0.l @@ -0,0 +1,11 @@ +#!/usr/bin/env roseus + +(ros::roseus-add-msgs "std_msgs") + +(ros::roseus "sample0") +(ros::ros-info "sample0 start") +(ros::ros-info "publishing /app_scheduler/sample0 ...") +(one-shot-publish "/app_scheduler/sample0" + (instance std_msgs::String :init :data "sample0")) +(ros::ros-info "sample0 finish") +(exit) diff --git a/app_scheduler/apps/sample0/sample0.xml b/app_scheduler/apps/sample0/sample0.xml new file mode 100644 index 0000000..44a53e2 --- /dev/null +++ b/app_scheduler/apps/sample0/sample0.xml @@ -0,0 +1,4 @@ + + + diff --git a/app_scheduler/apps/sample1/sample1.app b/app_scheduler/apps/sample1/sample1.app new file mode 100644 index 0000000..d286dc9 --- /dev/null +++ b/app_scheduler/apps/sample1/sample1.app @@ -0,0 +1,5 @@ +display: sample1 +description: sample1 +platform: turtlebot +launch: app_scheduler/sample1.xml +interface: app_scheduler/sample1.interface diff --git a/app_scheduler/apps/sample1/sample1.interface b/app_scheduler/apps/sample1/sample1.interface new file mode 100644 index 0000000..044105d --- /dev/null +++ b/app_scheduler/apps/sample1/sample1.interface @@ -0,0 +1,2 @@ +published_topics: {} +subscribed_topics: {} diff --git a/app_scheduler/apps/sample1/sample1.l b/app_scheduler/apps/sample1/sample1.l new file mode 100644 index 0000000..c4464be --- /dev/null +++ b/app_scheduler/apps/sample1/sample1.l @@ -0,0 +1,11 @@ +#!/usr/bin/env roseus + +(ros::roseus-add-msgs "std_msgs") + +(ros::roseus "sample1") +(ros::ros-info "sample1 start") +(ros::ros-info "publishing /app_scheduler/sample1 ...") +(one-shot-publish "/app_scheduler/sample1" + (instance std_msgs::String :init :data "sample1")) +(ros::ros-info "sample1 finish") +(exit) diff --git a/app_scheduler/apps/sample1/sample1.xml b/app_scheduler/apps/sample1/sample1.xml new file mode 100644 index 0000000..087e243 --- /dev/null +++ b/app_scheduler/apps/sample1/sample1.xml @@ -0,0 +1,4 @@ + + + diff --git a/app_scheduler/apps/sample2/sample2.app b/app_scheduler/apps/sample2/sample2.app new file mode 100644 index 0000000..469e424 --- /dev/null +++ b/app_scheduler/apps/sample2/sample2.app @@ -0,0 +1,5 @@ +display: sample2 +description: sample2 +platform: turtlebot +launch: app_scheduler/sample2.xml +interface: app_scheduler/sample2.interface diff --git a/app_scheduler/apps/sample2/sample2.interface b/app_scheduler/apps/sample2/sample2.interface new file mode 100644 index 0000000..044105d --- /dev/null +++ b/app_scheduler/apps/sample2/sample2.interface @@ -0,0 +1,2 @@ +published_topics: {} +subscribed_topics: {} diff --git a/app_scheduler/apps/sample2/sample2.l b/app_scheduler/apps/sample2/sample2.l new file mode 100644 index 0000000..7d6897c --- /dev/null +++ b/app_scheduler/apps/sample2/sample2.l @@ -0,0 +1,11 @@ +#!/usr/bin/env roseus + +(ros::roseus-add-msgs "std_msgs") + +(ros::roseus "sample2") +(ros::ros-info "sample2 start") +(ros::ros-info "publishing /app_scheduler/sample2 ...") +(one-shot-publish "/app_scheduler/sample2" + (instance std_msgs::String :init :data "sample2")) +(ros::ros-info "sample2 finish") +(exit) diff --git a/app_scheduler/apps/sample2/sample2.xml b/app_scheduler/apps/sample2/sample2.xml new file mode 100644 index 0000000..d85bc86 --- /dev/null +++ b/app_scheduler/apps/sample2/sample2.xml @@ -0,0 +1,4 @@ + + + diff --git a/app_scheduler/apps/sample3/sample3.app b/app_scheduler/apps/sample3/sample3.app new file mode 100644 index 0000000..d751c10 --- /dev/null +++ b/app_scheduler/apps/sample3/sample3.app @@ -0,0 +1,5 @@ +display: sample3 +description: sample3 +platform: turtlebot +launch: app_scheduler/sample3.xml +interface: app_scheduler/sample3.interface diff --git a/app_scheduler/apps/sample3/sample3.interface b/app_scheduler/apps/sample3/sample3.interface new file mode 100644 index 0000000..044105d --- /dev/null +++ b/app_scheduler/apps/sample3/sample3.interface @@ -0,0 +1,2 @@ +published_topics: {} +subscribed_topics: {} diff --git a/app_scheduler/apps/sample3/sample3.l b/app_scheduler/apps/sample3/sample3.l new file mode 100644 index 0000000..e698c78 --- /dev/null +++ b/app_scheduler/apps/sample3/sample3.l @@ -0,0 +1,13 @@ +#!/usr/bin/env roseus + +(ros::roseus-add-msgs "std_msgs") + +(ros::roseus "sample3") +(setq input (ros::get-param "~input" "default")) +(ros::ros-info "sample3 start") +(ros::ros-info "input: ~A" input) +(ros::ros-info "publishing /app_scheduler/sample3 ...") +(one-shot-publish "/app_scheduler/sample3" + (instance std_msgs::String :init :data input)) +(ros::ros-info "sample3 finish") +(exit) diff --git a/app_scheduler/apps/sample3/sample3.xml b/app_scheduler/apps/sample3/sample3.xml new file mode 100644 index 0000000..13534c4 --- /dev/null +++ b/app_scheduler/apps/sample3/sample3.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app_scheduler/launch/app_scheduler.launch b/app_scheduler/launch/app_scheduler.launch new file mode 100644 index 0000000..ea884b2 --- /dev/null +++ b/app_scheduler/launch/app_scheduler.launch @@ -0,0 +1,14 @@ + + + + + + + + + duration: $(arg duration) + update_duration: $(arg update_duration) + yaml_path: $(arg yaml_path) + + + diff --git a/app_scheduler/msg/AppSchedule.msg b/app_scheduler/msg/AppSchedule.msg new file mode 100644 index 0000000..257772f --- /dev/null +++ b/app_scheduler/msg/AppSchedule.msg @@ -0,0 +1,2 @@ +string start +string stop diff --git a/app_scheduler/msg/AppScheduleEntries.msg b/app_scheduler/msg/AppScheduleEntries.msg new file mode 100644 index 0000000..1854039 --- /dev/null +++ b/app_scheduler/msg/AppScheduleEntries.msg @@ -0,0 +1 @@ +app_scheduler/AppScheduleEntry[] entries diff --git a/app_scheduler/msg/AppScheduleEntry.msg b/app_scheduler/msg/AppScheduleEntry.msg new file mode 100644 index 0000000..27cbac0 --- /dev/null +++ b/app_scheduler/msg/AppScheduleEntry.msg @@ -0,0 +1,4 @@ +string name +string app_name +app_scheduler/AppSchedule app_schedule +string[] app_args diff --git a/app_scheduler/package.xml b/app_scheduler/package.xml new file mode 100644 index 0000000..b75330b --- /dev/null +++ b/app_scheduler/package.xml @@ -0,0 +1,24 @@ + + + app_scheduler + 1.3.0 + The scheduler for app_manager package + Shingo Kitagawa + Shingo Kitagawa + BSD + + catkin + python-setuptools + python3-setuptools + message_generation + app_manager + message_runtime + python-schedule + python3-schedule + roseus + std_msgs + + + + + diff --git a/app_scheduler/sample/sample_app_schedule.yaml b/app_scheduler/sample/sample_app_schedule.yaml new file mode 100644 index 0000000..a245569 --- /dev/null +++ b/app_scheduler/sample/sample_app_schedule.yaml @@ -0,0 +1,23 @@ +- name: sample0 + app_name: app_scheduler/sample0 + app_schedule: + start: every(2).minutes.at(":00") +- name: sample1 + app_name: app_scheduler/sample1 + app_schedule: + start: every(60).seconds +- name: sample2 + app_name: app_scheduler/sample2 + app_schedule: + start: every(1).hour.at(":00") +- name: sample3 + app_name: app_scheduler/sample3 + app_schedule: + start: every().day.at("10:00:00") + stop: every().day.at("10:00:03") +- name: sample3_with_args + app_name: app_scheduler/sample3 + app_schedule: + start: every(60).seconds + app_args: + input: sample3_with_args diff --git a/app_scheduler/sample/sample_app_scheduler.launch b/app_scheduler/sample/sample_app_scheduler.launch new file mode 100644 index 0000000..aa1e0bb --- /dev/null +++ b/app_scheduler/sample/sample_app_scheduler.launch @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app_scheduler/scripts/app_scheduler b/app_scheduler/scripts/app_scheduler new file mode 100755 index 0000000..d9b2df9 --- /dev/null +++ b/app_scheduler/scripts/app_scheduler @@ -0,0 +1,15 @@ +#!/usr/bin/env python + +import rospy + +from app_scheduler import AppScheduler + + +if __name__ == '__main__': + rospy.init_node('app_scheduler') + robot_name = rospy.get_param('/robot/name', 'robot') + yaml_path = rospy.get_param('~yaml_path') + duration = rospy.get_param('~duration', 1) + update_duration = rospy.get_param('~update_duration', 60) + scheduler = AppScheduler(robot_name, yaml_path, duration, update_duration) + rospy.spin() diff --git a/app_scheduler/setup.py b/app_scheduler/setup.py new file mode 100644 index 0000000..a0be37b --- /dev/null +++ b/app_scheduler/setup.py @@ -0,0 +1,11 @@ +from catkin_pkg.python_setup import generate_distutils_setup +from setuptools import find_packages +from setuptools import setup + + +d = generate_distutils_setup( + packages=find_packages('src'), + package_dir={'': 'src'}, +) + +setup(**d) diff --git a/app_scheduler/src/app_scheduler/__init__.py b/app_scheduler/src/app_scheduler/__init__.py new file mode 100644 index 0000000..de991c9 --- /dev/null +++ b/app_scheduler/src/app_scheduler/__init__.py @@ -0,0 +1 @@ +from app_scheduler.app_scheduler_lib import AppScheduler # NOQA diff --git a/app_scheduler/src/app_scheduler/app_scheduler.py b/app_scheduler/src/app_scheduler/app_scheduler.py new file mode 100644 index 0000000..57361bb --- /dev/null +++ b/app_scheduler/src/app_scheduler/app_scheduler.py @@ -0,0 +1,146 @@ +import yaml + +import rospy + +import schedule + +from app_manager.msg import AppList +from app_manager.msg import AppStatus +from app_manager.srv import StartApp +from app_manager.srv import StartAppRequest +from app_manager.srv import StopApp +from app_manager.srv import StopAppRequest + + +class AppScheduler(object): + + def __init__(self, robot_name, yaml_path, duration, update_duration): + self.robot_name = robot_name + self.yaml_path = yaml_path + self.running_app_names = [] + self.running_jobs = {} + self.app_list_topic_name = '/{}/app_list'.format(self.robot_name) + self.start_app = rospy.ServiceProxy( + '/{}/start_app'.format(self.robot_name), StartApp) + self.stop_app = rospy.ServiceProxy( + '/{}/stop_app'.format(self.robot_name), StopApp) + self.job_timer = rospy.Timer(rospy.Duration(duration), self._timer_cb) + self.update_timer = rospy.Timer( + rospy.Duration(update_duration), self._update_timer_cb) + self.sub = rospy.Subscriber( + '/{}/application/app_status'.format(self.robot_name), + AppStatus, self._sub_cb) + self._load_yaml() + self._register_apps() + + def _load_yaml(self): + with open(self.yaml_path, 'r') as yaml_f: + self.apps = yaml.load(yaml_f) + + def _register_apps(self): + for app in self.apps: + rospy.loginfo( + 'register app schedule => name: {0}, app_name: {1}'.format( + app['name'], app['app_name'])) + self._register_app(app) + + def _register_app(self, app): + app_schedule = app['app_schedule'] + name = app['name'] + app_name = app['app_name'] + # default app_args is [] + if 'app_args' in app: + app_args = app['app_args'] + else: + app_args = [] + start_job = self._create_start_job(name, app_name, app_args) # NOQA + try: + eval('schedule.{}.do(start_job)'.format(app_schedule['start'])) + except (AssertionError, ValueError) as e: + rospy.logerr(e) + rospy.logerr('Cannot register start app') + rospy.logerr('Please upgrade schedule module. $ pip install schedule==0.6.0 --user') # NOQA + if 'stop' in app_schedule: + stop_job = self._create_stop_job(name, app_name) # NOQA + try: + eval('schedule.{}.do(stop_job)'.format(app_schedule['stop'])) + except ValueError as e: + rospy.logerr(e) + rospy.logerr('Cannot register stop app') + rospy.logerr('Please upgrade schedule module. $ pip install schedule==0.6.0 --user') # NOQA + + def _create_start_job(self, name, app_name, app_args): + def start_job(): + start_req = StartAppRequest( + name=app_name, args=app_args) + start_res = self.start_app(start_req) + if not start_res.started: + rospy.logerr('Failed to start app: {}, {}, {}'.format( + name, app_name, app_args)) + rospy.logerr('StartApp error code: {}'.format( + start_res.error_code)) + rospy.logerr('StartApp error message: {}'.format( + start_res.message)) + self.running_jobs[name] = { + 'app_name': app_name, + 'running': start_res.started + } + return start_job + + def _create_stop_job(self, name, app_name): + def stop_job(): + if app_name in self.running_app_names: + stop_req = StopAppRequest(name=app_name) + stop_res = self.stop_app(stop_req) + if not stop_res.stopped: + rospy.logerr('Failed to stop app: {}, {}'.format( + name, app_name)) + rospy.logerr('StopApp error code: {}'.format( + stop_res.error_code)) + rospy.logerr('StopApp error message: {}'.format( + stop_res.message)) + self.running_jobs[name] = { + 'app_name': app_name, + 'running': not stop_res.stopped + } + return stop_job + + def _update_running_app_names(self): + try: + msg = rospy.wait_for_message( + self.app_list_topic_name, AppList, timeout=1) + except Exception as e: + rospy.logwarn( + 'Failed to subscribe {}: {}'.format( + self.app_list_topic_name, e)) + return + self.running_app_names = [x.name for x in msg.running_apps] + + def _update_running_jobs(self): + for name, job_data in self.running_jobs.items(): + if (job_data['running'] + and job_data['app_name'] not in self.running_app_names): + self.running_jobs[name]['running'] = False + + def _timer_cb(self, event): + try: + schedule.run_pending() + except TypeError as e: + rospy.logerr(e) + rospy.logerr('Cannot run pending app') + rospy.logerr('Please upgrade schedule module. $ pip install schedule==0.6.0 --user') # NOQA + + def _update_timer_cb(self, event): + self._update_running_app_names() + self._update_running_jobs() + + def _sub_cb(self, msg): + if msg.type == AppStatus.INFO: + # INFO + rospy.loginfo('app_scheduler: {}'.format(msg.status)) + elif msg.type == AppStatus.WARN: + # WARN + rospy.logwarn('app_scheduler: {}'.format(msg.status)) + else: + # ERROR + rospy.logerr('app_scheduler: {}'.format(msg.status)) diff --git a/app_scheduler/src/app_scheduler/app_scheduler_lib.py b/app_scheduler/src/app_scheduler/app_scheduler_lib.py new file mode 100644 index 0000000..59fd807 --- /dev/null +++ b/app_scheduler/src/app_scheduler/app_scheduler_lib.py @@ -0,0 +1,221 @@ +import yaml + +import rospy + +import schedule +import threading + +from app_manager.msg import AppList +from app_manager.msg import AppStatus +from app_manager.msg import KeyValue +from app_manager.srv import StartApp +from app_manager.srv import StartAppRequest +from app_manager.srv import StopApp +from app_manager.srv import StopAppRequest +from app_scheduler.msg import AppScheduleEntries +from app_scheduler.msg import AppScheduleEntry +from app_scheduler.srv import AddEntry +from app_scheduler.srv import AddEntryResponse +from app_scheduler.srv import RemoveEntry +from app_scheduler.srv import RemoveEntryResponse + + +class AppScheduler(object): + + def __init__(self, robot_name, yaml_path, duration, update_duration): + self.robot_name = robot_name + self.yaml_path = yaml_path + self.running_app_names = [] + self.running_jobs = {} + self.app_list_topic_name = '/{}/app_list'.format(self.robot_name) + self.start_app = rospy.ServiceProxy( + '/{}/start_app'.format(self.robot_name), StartApp) + self.stop_app = rospy.ServiceProxy( + '/{}/stop_app'.format(self.robot_name), StopApp) + self.app_lock = threading.Lock() + self.job_timer = rospy.Timer(rospy.Duration(duration), self._timer_cb) + self.update_timer = rospy.Timer( + rospy.Duration(update_duration), self._update_timer_cb) + self.pub_schedules = rospy.Publisher( + '~app_schedules', AppScheduleEntries, queue_size=1) + self.sub = rospy.Subscriber( + '/{}/application/app_status'.format(self.robot_name), + AppStatus, self._sub_cb) + self.srv_add_entry = rospy.Service( + '~add_entry', AddEntry, self._srv_add_entry_cb) + self.srv_remove_entry = rospy.Service( + '~remove_entry', RemoveEntry, self._srv_remove_entry_cb) + self._load_yaml() + self._register_apps() + + def _add_entry(self, entry): + app = { + 'name': entry.name, + 'app_name': entry.app_name, + 'app_schedule': {} + } + if entry.app_schedule.start != '': + app['app_schedule']['start'] = entry.app_schedule.start + if entry.app_schedule.stop != '': + app['app_schedule']['stop'] = entry.app_schedule.stop + rospy.loginfo( + 'register app schedule => name: {0}, app_name: {1}'.format( + app['name'], app['app_name'])) + with self.app_lock: + self.apps.append(app) + self._register_app(app) + + def _remove_entry(self, name): + with self.app_lock: + self.apps = [app for app in self.apps if app['name'] != name] + self._unregister_app(name) + + def _publish_app_schedules(self): + msg = AppScheduleEntries() + with self.app_lock: + for app in self.apps: + entry = AppScheduleEntry() + entry.name = app['name'] + entry.app_name = app['app_name'] + if 'start' in app['app_schedule']: + entry.app_schedule.start = app['app_schedule']['start'] + if 'stop' in app['app_schedule']: + entry.app_schedule.stop = app['app_schedule']['stop'] + if 'app_args' in app: + for key, value in app['app_args'].items(): + entry.app_args.append('{}: {}'.format(key, value)) + msg.entries.append(entry) + self.pub_schedules.publish(msg) + + def _load_yaml(self): + with open(self.yaml_path, 'r') as yaml_f: + self.apps = yaml.load(yaml_f) + + def _register_apps(self): + for app in self.apps: + rospy.loginfo( + 'register app schedule => name: {0}, app_name: {1}'.format( + app['name'], app['app_name'])) + self._register_app(app) + + def _register_app(self, app): + app_schedule = app['app_schedule'] + name = app['name'] + app_name = app['app_name'] + # default app_args is [] + if 'app_args' in app and isinstance(app['app_args'], dict): + app_args = app['app_args'] + else: + app_args = {} + start_job = self._create_start_job(name, app_name, app_args) # NOQA + try: + eval('schedule.{}.do(start_job).tag(\'{}\')'.format( + app_schedule['start'], app['name'])) + except (AssertionError, ValueError, AttributeError) as e: + rospy.logerr(e) + rospy.logerr('Cannot register start app') + rospy.logerr('Please upgrade schedule module. $ pip install schedule==0.6.0 --user') # NOQA + if 'stop' in app_schedule: + stop_job = self._create_stop_job(name, app_name) # NOQA + try: + eval('schedule.{}.do(stop_job).tag(\'{}\')'.format( + app_schedule['stop'], app['name'])) + except ValueError as e: + rospy.logerr(e) + rospy.logerr('Cannot register stop app') + rospy.logerr('Please upgrade schedule module. $ pip install schedule==0.6.0 --user') # NOQA + + def _unregister_app(self, name): + schedule.clear(name) + + def _create_start_job(self, name, app_name, app_args): + def start_job(): + start_req = StartAppRequest(name=app_name) + for key, value in app_args.items(): + start_req.args.append(KeyValue(key=key, value=value)) + start_res = self.start_app(start_req) + if not start_res.started: + rospy.logerr('Failed to start app: {}, {}, {}'.format( + name, app_name, app_args)) + rospy.logerr('StartApp error code: {}'.format( + start_res.error_code)) + rospy.logerr('StartApp error message: {}'.format( + start_res.message)) + self.running_jobs[name] = { + 'app_name': app_name, + 'running': start_res.started + } + return start_job + + def _create_stop_job(self, name, app_name): + def stop_job(): + if app_name in self.running_app_names: + stop_req = StopAppRequest(name=app_name) + stop_res = self.stop_app(stop_req) + if not stop_res.stopped: + rospy.logerr('Failed to stop app: {}, {}'.format( + name, app_name)) + rospy.logerr('StopApp error code: {}'.format( + stop_res.error_code)) + rospy.logerr('StopApp error message: {}'.format( + stop_res.message)) + self.running_jobs[name] = { + 'app_name': app_name, + 'running': not stop_res.stopped + } + return stop_job + + def _update_running_app_names(self): + try: + msg = rospy.wait_for_message( + self.app_list_topic_name, AppList, timeout=1) + except Exception as e: + rospy.logwarn( + 'Failed to subscribe {}: {}'.format( + self.app_list_topic_name, e)) + return + self.running_app_names = [x.name for x in msg.running_apps] + + def _update_running_jobs(self): + for name, job_data in self.running_jobs.items(): + if (job_data['running'] + and job_data['app_name'] not in self.running_app_names): + self.running_jobs[name]['running'] = False + + def _timer_cb(self, event): + try: + schedule.run_pending() + except TypeError as e: + rospy.logerr(e) + rospy.logerr('Cannot run pending app') + rospy.logerr('Please upgrade schedule module. $ pip install schedule==0.6.0 --user') # NOQA + + def _update_timer_cb(self, event): + self._update_running_app_names() + self._update_running_jobs() + self._publish_app_schedules() + + def _sub_cb(self, msg): + if msg.type == AppStatus.INFO: + # INFO + rospy.loginfo('app_scheduler: {}'.format(msg.status)) + elif msg.type == AppStatus.WARN: + # WARN + rospy.logwarn('app_scheduler: {}'.format(msg.status)) + else: + # ERROR + rospy.logerr('app_scheduler: {}'.format(msg.status)) + + def _srv_add_entry_cb(self, req): + self._add_entry(req.entry) + self._publish_app_schedules() + res = AddEntryResponse() + res.success = True + return res + + def _srv_remove_entry_cb(self, req): + self._remove_entry(req.name) + self._publish_app_schedules() + res = RemoveEntryResponse() + res.success = True + return res diff --git a/app_scheduler/srv/AddEntry.srv b/app_scheduler/srv/AddEntry.srv new file mode 100644 index 0000000..0ef3863 --- /dev/null +++ b/app_scheduler/srv/AddEntry.srv @@ -0,0 +1,4 @@ +app_scheduler/AppScheduleEntry entry +--- +bool success +string message diff --git a/app_scheduler/srv/RemoveEntry.srv b/app_scheduler/srv/RemoveEntry.srv new file mode 100644 index 0000000..9fa6a16 --- /dev/null +++ b/app_scheduler/srv/RemoveEntry.srv @@ -0,0 +1,4 @@ +string name +--- +bool success +string message diff --git a/app_uploader/CMakeLists.txt b/app_uploader/CMakeLists.txt new file mode 100644 index 0000000..13f9b26 --- /dev/null +++ b/app_uploader/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 2.8.3) +project(app_uploader) + +find_package(catkin REQUIRED) + +catkin_python_setup() + +catkin_package() + +include_directories() diff --git a/app_uploader/README.md b/app_uploader/README.md new file mode 100644 index 0000000..65436b7 --- /dev/null +++ b/app_uploader/README.md @@ -0,0 +1,37 @@ +# app_uploader + +Uploader plugin for `app_manager` + +## `app_manager` plugins + +### `app_uploader/gdrive_uploader_plugin`: Google drive uploader plugin + +This plugin depends on [gdrive_ros](https://github.com/jsk-ros-pkg/jsk_3rdparty/tree/master/gdrive_ros). + +#### `plugin_args`: Plugin arguments + +- `upload_file_paths`: upload file directory paths +- `upload_file_titles`: upload file names +- `upload_parents_path`: google drive upload path +- `upload_server_name`: `gdrive_ros/gdriver_serve_node` server name + +#### `launch_args`: Plugin launch arguments + +`None` + +#### Sample plugin description + +```yaml +plugins: + - name: gdrive_uploader_plugin + type: app_uploader/gdrive_uploader_plugin + plugin_args: + upload_file_paths: + - /tmp/test.avi + - /tmp/test.bag + upload_file_titles: + - test.avi + - test.bag + upload_parents_path: logs + upload_server_name: /gdrive_server +``` diff --git a/app_uploader/app_uploader_plugin.yaml b/app_uploader/app_uploader_plugin.yaml new file mode 100644 index 0000000..2c59700 --- /dev/null +++ b/app_uploader/app_uploader_plugin.yaml @@ -0,0 +1,3 @@ +- name: app_uploader/gdrive_uploader_plugin + launch: null + module: app_uploader.gdrive_uploader_plugin.GdriveUploaderPlugin diff --git a/app_uploader/package.xml b/app_uploader/package.xml new file mode 100644 index 0000000..86b19fc --- /dev/null +++ b/app_uploader/package.xml @@ -0,0 +1,19 @@ + + + app_uploader + 1.3.0 + The app_uploader package + Shingo Kitagawa + Shingo Kitagawa + BSD + + catkin + python-setuptools + python3-setuptools + app_manager + gdrive_ros + + + + + diff --git a/app_uploader/setup.py b/app_uploader/setup.py new file mode 100644 index 0000000..a0be37b --- /dev/null +++ b/app_uploader/setup.py @@ -0,0 +1,11 @@ +from catkin_pkg.python_setup import generate_distutils_setup +from setuptools import find_packages +from setuptools import setup + + +d = generate_distutils_setup( + packages=find_packages('src'), + package_dir={'': 'src'}, +) + +setup(**d) diff --git a/app_uploader/src/app_uploader/__init__.py b/app_uploader/src/app_uploader/__init__.py new file mode 100644 index 0000000..3560a13 --- /dev/null +++ b/app_uploader/src/app_uploader/__init__.py @@ -0,0 +1 @@ +from app_uploader.gdrive_uploader_plugin import GdriveUploaderPlugin # NOQA diff --git a/app_uploader/src/app_uploader/gdrive_uploader_plugin.py b/app_uploader/src/app_uploader/gdrive_uploader_plugin.py new file mode 100644 index 0000000..390356f --- /dev/null +++ b/app_uploader/src/app_uploader/gdrive_uploader_plugin.py @@ -0,0 +1,38 @@ +import rospy + +from app_manager import AppManagerPlugin + +from gdrive_ros.srv import MultipleUpload +from gdrive_ros.srv import MultipleUploadRequest + + +class GdriveUploaderPlugin(AppManagerPlugin): + def __init__(self): + super(GdriveUploaderPlugin, self).__init__() + + @classmethod + def app_manager_stop_plugin(cls, app, ctx, plugin_args): + req = MultipleUploadRequest() + req.file_paths = plugin_args['upload_file_paths'] + req.file_titles = plugin_args['upload_file_titles'] + req.parents_path = plugin_args['upload_parents_path'] + req.use_timestamp_folder = True + req.use_timestamp_file_title = True + gdrive_upload = rospy.ServiceProxy( + plugin_args['upload_server_name'] + '/upload_multi', + MultipleUpload + ) + res = gdrive_upload(req) + if all(res.successes): + rospy.loginfo('Upload succeeded.') + else: + rospy.logerr('Upload failed') + if 'upload_successes' in ctx: + ctx['upload_successes'] += res.successes + else: + ctx['upload_successes'] = res.successes + if 'upload_file_urls' in ctx: + ctx['upload_file_urls'] += res.file_urls + else: + ctx['upload_file_urls'] = res.file_urls + return ctx