diff --git a/app_manager_utils/README.md b/app_manager_utils/README.md new file mode 100644 index 000000000..a69b9e2f3 --- /dev/null +++ b/app_manager_utils/README.md @@ -0,0 +1,42 @@ +# app_manager_utils + +## Dependency + +- [PR2/app_manager kinetic-devel branch](https://github.com/PR2/app_manager) +- [python-dateutil >= 2.7.0](https://github.com/dateutil/dateutil) + +## app_scheduler + +Scheduler for `app_manager` + +For detailed information, please read [app_scheduler](app_scheduler/README.md). + +## app_recorder + +Recorder plugin for `app_manager` + +For detailed information, please read [app_recorder](app_recorder/README.md). + +## app_uploader + +Uploader plugin for `app_manager` + +For detailed information, please read [app_uploader](app_uploader/README.md). + +## app_notifier + +Notifier plugin for `app_manager` + +For detailed information, please read [app_notifier](app_notifier/README.md). + +## app_notification_saver + +Notification saver plugin for `app_manager` + +For detailed information, please read [app_notification_saver](app_notification_saver/README.md). + +## app_publisher + +Publisher plugin for `app_manager` + +For detailed information, please read [app_publisher](app_publisher/README.md). diff --git a/app_manager_utils/app_manager_utils/CHANGELOG.rst b/app_manager_utils/app_manager_utils/CHANGELOG.rst new file mode 100644 index 000000000..2d8c40a4f --- /dev/null +++ b/app_manager_utils/app_manager_utils/CHANGELOG.rst @@ -0,0 +1,33 @@ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Changelog for package app_manager_utils +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +0.0.6 (2022-07-29) +------------------ + +0.0.5 (2022-02-08) +------------------ + +0.0.4 (2021-12-27) +------------------ + +0.0.3 (2021-12-17) +------------------ + +0.0.2 (2021-12-11) +------------------ + +0.0.1 (2021-10-06) +------------------ +* Merge pull request `#26 `_ from 708yamaguchi/app-publisher + Add rostopic_publisher_plugin +* Add rostopic_publisher_plugin +* remove app_manager_plugin package +* Merge pull request `#7 `_ from knorth55/add-plugins +* update app_manager_utils package.xml +* Merge pull request `#3 `_ from knorth55/add-app-manager-plugin +* add test_app_manager package +* use exec_depend +* add build_depend +* initial commit +* Contributors: Naoya Yamaguchi, Shingo Kitagawa diff --git a/app_manager_utils/app_manager_utils/CMakeLists.txt b/app_manager_utils/app_manager_utils/CMakeLists.txt new file mode 100644 index 000000000..db8fc96d0 --- /dev/null +++ b/app_manager_utils/app_manager_utils/CMakeLists.txt @@ -0,0 +1,4 @@ +cmake_minimum_required(VERSION 2.8.3) +project(app_manager_utils) +find_package(catkin REQUIRED) +catkin_metapackage() diff --git a/app_manager_utils/app_manager_utils/package.xml b/app_manager_utils/app_manager_utils/package.xml new file mode 100644 index 000000000..b4e4f77f1 --- /dev/null +++ b/app_manager_utils/app_manager_utils/package.xml @@ -0,0 +1,21 @@ + + + app_manager_utils + 2.2.12 + The app_manager_utils meta package + Shingo Kitagawa + Shingo Kitagawa + BSD + + catkin + app_notification_saver + app_notifier + app_publisher + app_recorder + app_scheduler + app_uploader + + + + + diff --git a/app_manager_utils/app_notification_saver/CHANGELOG.rst b/app_manager_utils/app_notification_saver/CHANGELOG.rst new file mode 100644 index 000000000..a2f858bb8 --- /dev/null +++ b/app_manager_utils/app_notification_saver/CHANGELOG.rst @@ -0,0 +1,53 @@ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Changelog for package app_notification_saver +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +0.0.6 (2022-07-29) +------------------ + +0.0.5 (2022-02-08) +------------------ + +0.0.4 (2021-12-27) +------------------ + +0.0.3 (2021-12-17) +------------------ + +0.0.2 (2021-12-11) +------------------ + +0.0.1 (2021-10-06) +------------------ +* add python-dateutil in package.xml +* update app_notification_saver/README.md +* update app_notification_saver/README.md +* update app_notification_saver/README.md +* add json_path arg in smach_notification_saver.launch +* add json_path arg in service_notification_saver.launch +* Merge pull request `#18 `_ from 708yamaguchi/add-smach-notification +* Do not update json if active_states is empty +* Move sample_smach_notification_saver.launch to sample directory +* Update README.md using sample_smach_notification_saver +* Split sample_smach_notification_server and smach_notification_server +* Add smach_notification_saver plugin to yaml +* add usage of smach_notification_saver +* Add arg to use rosbag in smach_notification_saver.launch +* Add sample rosbag which contains smach topics +* Add smach_notification_saver to notify smach states +* Use roslaunch instead of rosrun in README usage +* flake8 +* import from __init\_\_ +* rename to app_notification_saver_base.py +* add app_notification_saver_plugin +* add executable in node_scripts +* app_notification_saver as python package +* Fix typo in README.md +* Set language for code block +* Add location field to notification +* Update README.md +* Use title field instead of type field because of python reserved word +* Make base class AppNotificationSaver +* Add json sample to README +* Add app_notification_saver to pass messages to app_notifier +* Contributors: Naoya Yamaguchi, Shingo Kitagawa diff --git a/app_manager_utils/app_notification_saver/CMakeLists.txt b/app_manager_utils/app_notification_saver/CMakeLists.txt new file mode 100644 index 000000000..ced2c9cad --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_notification_saver/README.md b/app_manager_utils/app_notification_saver/README.md new file mode 100644 index 000000000..5f8295882 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_notification_saver/app_notification_saver_plugin.yaml b/app_manager_utils/app_notification_saver/app_notification_saver_plugin.yaml new file mode 100644 index 000000000..4536ba099 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_notification_saver/data/smach.bag b/app_manager_utils/app_notification_saver/data/smach.bag new file mode 100644 index 000000000..32b24c47d Binary files /dev/null and b/app_manager_utils/app_notification_saver/data/smach.bag differ diff --git a/app_manager_utils/app_notification_saver/launch/service_notification_saver.launch b/app_manager_utils/app_notification_saver/launch/service_notification_saver.launch new file mode 100644 index 000000000..88ba2d38e --- /dev/null +++ b/app_manager_utils/app_notification_saver/launch/service_notification_saver.launch @@ -0,0 +1,12 @@ + + + + + + + + json_path: $(arg json_path) + + + diff --git a/app_manager_utils/app_notification_saver/launch/smach_notification_saver.launch b/app_manager_utils/app_notification_saver/launch/smach_notification_saver.launch new file mode 100644 index 000000000..e7b729ece --- /dev/null +++ b/app_manager_utils/app_notification_saver/launch/smach_notification_saver.launch @@ -0,0 +1,14 @@ + + + + + + + + + + json_path: $(arg json_path) + + + diff --git a/app_manager_utils/app_notification_saver/node_scripts/service_notification_saver_node.py b/app_manager_utils/app_notification_saver/node_scripts/service_notification_saver_node.py new file mode 100755 index 000000000..490100a05 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_notification_saver/node_scripts/smach_notification_saver_node.py b/app_manager_utils/app_notification_saver/node_scripts/smach_notification_saver_node.py new file mode 100755 index 000000000..ac58ccf78 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_notification_saver/package.xml b/app_manager_utils/app_notification_saver/package.xml new file mode 100644 index 000000000..329031e67 --- /dev/null +++ b/app_manager_utils/app_notification_saver/package.xml @@ -0,0 +1,24 @@ + + + app_notification_saver + 2.2.12 + 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_manager_utils/app_notification_saver/sample/sample_smach_notification_saver.launch b/app_manager_utils/app_notification_saver/sample/sample_smach_notification_saver.launch new file mode 100644 index 000000000..fad9d8a93 --- /dev/null +++ b/app_manager_utils/app_notification_saver/sample/sample_smach_notification_saver.launch @@ -0,0 +1,9 @@ + + + + + + + diff --git a/app_manager_utils/app_notification_saver/setup.py b/app_manager_utils/app_notification_saver/setup.py new file mode 100644 index 000000000..a0be37b04 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_notification_saver/src/app_notification_saver/__init__.py b/app_manager_utils/app_notification_saver/src/app_notification_saver/__init__.py new file mode 100644 index 000000000..68dc4f29b --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_notification_saver/src/app_notification_saver/app_notification_saver_base.py b/app_manager_utils/app_notification_saver/src/app_notification_saver/app_notification_saver_base.py new file mode 100644 index 000000000..f4ff587a2 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_notification_saver/src/app_notification_saver/service_notification_saver.py b/app_manager_utils/app_notification_saver/src/app_notification_saver/service_notification_saver.py new file mode 100644 index 000000000..f34bd2319 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_notification_saver/src/app_notification_saver/smach_notification_saver.py b/app_manager_utils/app_notification_saver/src/app_notification_saver/smach_notification_saver.py new file mode 100644 index 000000000..2e63e3b4c --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_notification_saver/srv/SaveAppNotification.srv b/app_manager_utils/app_notification_saver/srv/SaveAppNotification.srv new file mode 100644 index 000000000..eefa9e6e7 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_notifier/CHANGELOG.rst b/app_manager_utils/app_notifier/CHANGELOG.rst new file mode 100644 index 000000000..60efba909 --- /dev/null +++ b/app_manager_utils/app_notifier/CHANGELOG.rst @@ -0,0 +1,113 @@ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Changelog for package app_notifier +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +0.0.6 (2022-07-29) +------------------ +* Merge pull request `#53 `_ from knorth55/app-notifier-fix-typo +* fix init +* return None when postfix version is low +* Merge pull request `#52 `_ from tkmtnt7000/display-file-name +* Fix dependent on flake8 +* Display upload succeeded files in email +* Display failing upload data in email +* Contributors: JSK fetch user, Naoto Tsukamoto, Shingo Kitagawa + +0.0.5 (2022-02-08) +------------------ +* fix indent +* Merge pull request `#50 `_ from 708yamaguchi/wait-queue-clear +* Add timeout to postfix queue operations +* [app_notifier] Wait until the postfix queue is empty +* Merge pull request `#49 `_ from knorth55/notify-timeout +* notify whether timeout or not +* Contributors: Naoya Yamaguchi, Shingo Kitagawa + +0.0.4 (2021-12-27) +------------------ + +0.0.3 (2021-12-17) +------------------ + +0.0.2 (2021-12-11) +------------------ + +0.0.1 (2021-10-06) +------------------ +* do not check if python-dateutil version is low +* fix bug when username is None +* add stopping notifier for speech notifier +* escape _ and - in speech notifier +* add check_timestamp_before_start +* support python3.7 and above +* refactor mail_notifier_plugin +* support for python3.7 and above +* add python-dateutil in package.xml +* Merge pull request `#22 `_ from 708yamaguchi/use-instance + Filter notification using timestamp +* Use python-dateutil to compare datetime.datetime +* fix typo in app_notifier/README.md +* update app_notifier README +* set default use_timestamp_title +* Merge pull request `#23 `_ from 708yamaguchi/add-timestamp-usage + Add use_timestamp_title usage to README.md +* Add use_timestamp_title usage to README.md +* [tweet_notifier] Filtering notification using timestamp +* [speech_notifier] Filtering notification using timestamp +* [mail_notifier] Filtering notification using timestamp +* Merge pull request `#19 `_ from knorth55/json-speak-tweet + Speak and tweet about object recognition result +* add space to avoid concat +* fix typo in util.py +* tweet in 280 words +* speak and tweet about object recognition +* use util functions +* add get_notification_json_paths and load_jsons +* update README.md +* format mail_notifier_plugin.py +* Merge pull request `#18 `_ from 708yamaguchi/add-smach-notification +* Add newline between json notifications +* Do not notify location if location string is empty +* Add location information in the mail +* Add app_notification_saver dependency to app_notifier +* Use app_notification_saver in mail_notifier_plugin +* Add error message to prompt installing mailutils +* update for noetic +* update readme +* add e-mail settings link +* fix app_notifier conditions +* refactor app_notifier +* flake8 +* refactor notifier english +* add stopped conditions for app notifiers +* add start plugin in TweetNotifierPlugin +* add descriptions +* update readme +* update readme in app_notifier +* add speak in tweet_notifier +* update package.xml +* add TweetNotifierPlugin +* add warning in UserSpeechNotifierPlugin +* update __init_\_.py in app_notifier +* add UserSpeechNotifierPlugin +* add util in app_notifier +* Merge pull request `#12 `_ from knorth55/add-superlinter +* fix markdown lint +* remove app_manager_plugin package +* remove debug line in mail_notifier_plugin.py +* add use_timestamp in mail title +* update README +* refactor speech text +* fix typo +* fix typo in speech_notifier_plugin +* add SpeechNotifierPlugin +* fix typo in mail_notifier_plugin +* update readme +* add README.md +* Merge pull request `#7 `_ from knorth55/add-plugins +* use package forma=2 +* update mail_notifier_plugin +* updata plugin_args +* add mail notifier plugin +* add app_notifier package +* Contributors: Naoya Yamaguchi, Shingo Kitagawa diff --git a/app_manager_utils/app_notifier/CMakeLists.txt b/app_manager_utils/app_notifier/CMakeLists.txt new file mode 100644 index 000000000..145c10fe4 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_notifier/README.md b/app_manager_utils/app_notifier/README.md new file mode 100644 index 000000000..48ee1a9ee --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_notifier/app_notifier_plugin.yaml b/app_manager_utils/app_notifier/app_notifier_plugin.yaml new file mode 100644 index 000000000..ff3dbccaf --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_notifier/package.xml b/app_manager_utils/app_notifier/package.xml new file mode 100644 index 000000000..cc4f7db74 --- /dev/null +++ b/app_manager_utils/app_notifier/package.xml @@ -0,0 +1,24 @@ + + + app_notifier + 2.2.12 + 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_manager_utils/app_notifier/setup.py b/app_manager_utils/app_notifier/setup.py new file mode 100644 index 000000000..a0be37b04 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_notifier/src/app_notifier/__init__.py b/app_manager_utils/app_notifier/src/app_notifier/__init__.py new file mode 100644 index 000000000..aafc7fd20 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_notifier/src/app_notifier/mail_notifier_plugin.py b/app_manager_utils/app_notifier/src/app_notifier/mail_notifier_plugin.py new file mode 100644 index 000000000..6b265f706 --- /dev/null +++ b/app_manager_utils/app_notifier/src/app_notifier/mail_notifier_plugin.py @@ -0,0 +1,131 @@ +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 count_postfix_queued_mail +from app_notifier.util import get_notification_json_paths +from app_notifier.util import load_notification_jsons +from app_notifier.util import parse_context + + +class MailNotifierPlugin(AppManagerPlugin): + def __init__(self): + super(MailNotifierPlugin, self).__init__() + self.use_app_start_time = False + + def app_manager_start_plugin(self, app, ctx, plugin_args): + self.start_time = rospy.Time.now() + # Set mail title first if use_app_start_time is true. + # This can combine many emails into a single email thread. + if 'use_app_start_time' in plugin_args: + self.use_app_start_time = plugin_args['use_app_start_time'] + if self.use_app_start_time: + mail_title = plugin_args['mail_title'] + timestamp = '{0:%Y/%m/%d (%H:%M:%S)}'.format( + datetime.datetime.fromtimestamp(self.start_time.to_sec())) + mail_title += ': {}'.format(timestamp) + rospy.set_param('/email_topic/mail_title', mail_title) + + 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: + if self.use_app_start_time: + date_time = datetime.datetime.fromtimestamp( + self.start_time.to_sec()) + else: + date_time = datetime.datetime.now() + timestamp = '{0:%Y/%m/%d (%H:%M:%S)}'.format(date_time) + mail_title += ': {}'.format(timestamp) + + exit_code, stopped, timeout,\ + upload_successes, upload_file_urls, request_file_titles = \ + parse_context(ctx) + + display_name = app.display_name + mail_content = "Hi, \\n" + if exit_code == 0 and not stopped: + mail_content += "I succeeded in doing {}.\\n".format(display_name) + elif stopped: + mail_content += "I stopped doing {}".format(display_name) + if timeout: + mail_content += " because of timeout" + mail_content += ".\\n" + else: + mail_content += "I failed to do {}.\\n".format(display_name) + if upload_successes: + if all(upload_successes): + mail_content += "I succeeded to upload data.\\n" + else: + mail_content += "I failed to upload the following data:\\n" + for success, file_title in zip( + upload_successes, request_file_titles): + if not success: + mail_content += " {}\\n".format(file_title) + mail_content += "\\n" + for success, file_url, file_title in zip( + upload_successes, upload_file_urls, request_file_titles): + if success: + mail_content += "{}: {} \\n".format(file_title, 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" + + queued_mail_num = count_postfix_queued_mail() + 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) + + # Wait for mail to be added in postfix queue + timeout = 10 + start_time = rospy.Time.now() + while (queued_mail_num is not None + and queued_mail_num == count_postfix_queued_mail() + or (rospy.Time.now() - start_time).to_sec() > timeout): + rospy.sleep(0.1) + # Wait for mail to be send from queue + start_time = rospy.Time.now() + while (queued_mail_num is not None + and queued_mail_num < count_postfix_queued_mail() + or (rospy.Time.now() - start_time).to_sec() > timeout): + rospy.sleep(0.1) + + 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 + # delete ~mail_title rosparam for next + # It is assumed that this is the last one sent in a series of emails. + if rospy.has_param('~mail_title'): + rospy.delete_param('~mail_title') + return ctx diff --git a/app_manager_utils/app_notifier/src/app_notifier/speech_notifier_plugin.py b/app_manager_utils/app_notifier/src/app_notifier/speech_notifier_plugin.py new file mode 100644 index 000000000..4a141313e --- /dev/null +++ b/app_manager_utils/app_notifier/src/app_notifier/speech_notifier_plugin.py @@ -0,0 +1,75 @@ +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 parse_context +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'] + + exit_code, stopped, timeout, upload_successes, _, _ = \ + parse_context(ctx) + + display_name = app.display_name + display_name = display_name.replace('_', ' ') + display_name = display_name.replace('-', ' ') + client = actionlib.SimpleActionClient(client_name, SoundRequestAction) + if exit_code == 0 and not stopped: + speech_text = "I succeeded in doing {} app.".format(display_name) + elif stopped: + speech_text = "I stopped doing {} app".format(display_name) + if timeout: + speech_text += " because of timeout" + speech_text += "." + else: + speech_text = "I failed to do {} app.".format(display_name) + if upload_successes: + if all(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_manager_utils/app_notifier/src/app_notifier/tweet_notifier_plugin.py b/app_manager_utils/app_notifier/src/app_notifier/tweet_notifier_plugin.py new file mode 100644 index 000000000..b0e4155a0 --- /dev/null +++ b/app_manager_utils/app_notifier/src/app_notifier/tweet_notifier_plugin.py @@ -0,0 +1,92 @@ +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 parse_context +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'] + + exit_code, stopped, timeout, upload_successes, _, _ = \ + parse_context(ctx) + + display_name = app.display_name + client = actionlib.SimpleActionClient(client_name, TweetAction) + if exit_code == 0 and not stopped: + tweet_text = "I succeeded in doing {} app.".format(display_name) + elif stopped: + tweet_text = "I stopped doing {} app".format(display_name) + if timeout: + tweet_text += " because of timeout." + tweet_text += "." + else: + tweet_text = "I failed to do {} app.".format(display_name) + if upload_successes: + if all(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_manager_utils/app_notifier/src/app_notifier/user_speech_notifier_plugin.py b/app_manager_utils/app_notifier/src/app_notifier/user_speech_notifier_plugin.py new file mode 100644 index 000000000..de76928de --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_notifier/src/app_notifier/util.py b/app_manager_utils/app_notifier/src/app_notifier/util.py new file mode 100644 index 000000000..3f3293e3d --- /dev/null +++ b/app_manager_utils/app_notifier/src/app_notifier/util.py @@ -0,0 +1,127 @@ +from __future__ import print_function + +import datetime +import json +import os +import rospy +import subprocess +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 + + +def parse_context(ctx): + exit_code = ctx['exit_code'] if 'exit_code' in ctx else None + stopped = ctx['stopped'] if 'stopped' in ctx else None + timeout = ctx['timeout'] if 'timeout' in ctx else None + upload_successes = None + if 'upload_successes' in ctx: + upload_successes = ctx['upload_successes'] + upload_file_urls = None + if 'upload_file_urls' in ctx: + upload_file_urls = ctx['upload_file_urls'] + request_file_titles = None + if 'request_file_titles' in ctx: + request_file_titles = ctx['request_file_titles'] + return exit_code, stopped, timeout, upload_successes, upload_file_urls, \ + request_file_titles + + +def count_postfix_queued_mail(): + try: + postqueue = subprocess.check_output(['postqueue', '-j']) + queued_mail_num = postqueue.count('queue_name') + except Exception: + return None + return queued_mail_num diff --git a/app_manager_utils/app_publisher/CHANGELOG.rst b/app_manager_utils/app_publisher/CHANGELOG.rst new file mode 100644 index 000000000..0abb6b9e8 --- /dev/null +++ b/app_manager_utils/app_publisher/CHANGELOG.rst @@ -0,0 +1,58 @@ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Changelog for package app_publisher +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +0.0.6 (2022-07-29) +------------------ +* Merge pull request `#51 `_ from Affonso-Gui/fix_publisher_readme + Fix plugin name in app_publisher/README.md +* Fix plugin name in app_publisher/README.md +* Contributors: Guilherme Affonso, Shingo Kitagawa + +0.0.5 (2022-02-08) +------------------ + +0.0.4 (2021-12-27) +------------------ +* Merge pull request `#48 `_ from 708yamaguchi/multiple-cond +* [app_publisher] Rename variable can_publish -> do_publish +* Update app_publisher/src/app_publisher/rostopic_publisher_plugin.py + Co-authored-by: Shingo Kitagawa +* Update app_publisher/src/app_publisher/rostopic_publisher_plugin.py + Co-authored-by: Shingo Kitagawa +* Merge pull request `#47 `_ from 708yamaguchi/fix-condition +* [app_publisher] Use multiple condition in README +* [app_publisher] Enable multiple cond +* [app_publisher] Publish topic when _check_condition() is True +* Merge pull request `#45 `_ from knorth55/refactor-pub-plugin +* refactor rostopic_publisher_plugin +* Merge pull request `#44 `_ from knorth55/check-cond +* refactor rostopic_publisher_plugin +* Merge pull request `#40 `_ from 708yamaguchi/exit-code-app-publisher +* fix indentation +* [app_publisher] fix for linter +* [app_publisher] Fix format for linter +* [app_publisher] Use noun as cond keyword +* [app_publisher] Add timeout condition +* Publish topic depending on app end condition in app_publisher +* Contributors: Naoya Yamaguchi, Shingo Kitagawa + +0.0.3 (2021-12-17) +------------------ + +0.0.2 (2021-12-11) +------------------ + +0.0.1 (2021-10-06) +------------------ +* Merge pull request `#28 `_ from 708yamaguchi/topic-field + Use topic field in rostopic_publisher_plugin.py +* update app_publisher/README +* Enable message field in app_publisher +* Merge pull request `#27 `_ from knorth55/update-readme + Update README.md +* Update README.md +* Merge pull request `#26 `_ from 708yamaguchi/app-publisher + Add rostopic_publisher_plugin +* Add rostopic_publisher_plugin +* Contributors: Naoya Yamaguchi, Shingo Kitagawa diff --git a/app_manager_utils/app_publisher/CMakeLists.txt b/app_manager_utils/app_publisher/CMakeLists.txt new file mode 100644 index 000000000..842149d15 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_publisher/README.md b/app_manager_utils/app_publisher/README.md new file mode 100644 index 000000000..2ce693f86 --- /dev/null +++ b/app_manager_utils/app_publisher/README.md @@ -0,0 +1,66 @@ +# app_publisher + +ROS topic publisher plugin for `app_manager` + +## `app_manager` plugins + +### `app_publisher/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 + - stopped +``` diff --git a/app_manager_utils/app_publisher/app_publisher_plugin.yaml b/app_manager_utils/app_publisher/app_publisher_plugin.yaml new file mode 100644 index 000000000..98083f150 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_publisher/package.xml b/app_manager_utils/app_publisher/package.xml new file mode 100644 index 000000000..aea6adc80 --- /dev/null +++ b/app_manager_utils/app_publisher/package.xml @@ -0,0 +1,20 @@ + + + app_publisher + 2.2.12 + The app_publisher package + Shingo Kitagawa + Naoya Yamaguchi + BSD + + catkin + python-setuptools + python3-setuptools + + app_manager + rospy_message_converter + + + + + diff --git a/app_manager_utils/app_publisher/setup.py b/app_manager_utils/app_publisher/setup.py new file mode 100644 index 000000000..a0be37b04 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_publisher/src/app_publisher/__init__.py b/app_manager_utils/app_publisher/src/app_publisher/__init__.py new file mode 100644 index 000000000..efa2aca89 --- /dev/null +++ b/app_manager_utils/app_publisher/src/app_publisher/__init__.py @@ -0,0 +1 @@ +from app_publisher.rostopic_publisher_plugin import RostopicPublisherPlugin # NOQA diff --git a/app_manager_utils/app_publisher/src/app_publisher/rostopic_publisher_plugin.py b/app_manager_utils/app_publisher/src/app_publisher/rostopic_publisher_plugin.py new file mode 100644 index 000000000..fd64dc8f4 --- /dev/null +++ b/app_manager_utils/app_publisher/src/app_publisher/rostopic_publisher_plugin.py @@ -0,0 +1,84 @@ +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 _check_condition(self, topic_cond, ctx): + exit_code = ctx['exit_code'] if 'exit_code' in ctx else None + stopped = ctx['stopped'] if 'stopped' in ctx else None + timeout = ctx['timeout'] if 'timeout' in ctx else None + if topic_cond == 'success': + return exit_code == 0 + elif topic_cond == 'failure': + return exit_code != 0 + elif topic_cond == 'stop': + if stopped is None: + rospy.logerr( + 'stopped is not set in app_manager plugin ctx.') + rospy.logerr( + 'Please check app_manager version.') + rospy.logerr('Skipping rostopic_publisher_plugin') + return False + else: + return stopped + elif topic_cond == 'timeout': + if stopped is None: + rospy.logerr( + 'stopped is not set in app_manager plugin ctx.' + 'Please check app_manager version.') + rospy.logerr('Skipping rostopic_publisher_plugin') + return False + elif timeout is None: + rospy.logerr( + 'timeout is not set in app_manager plugin ctx.' + 'Please check app_manager version.') + rospy.logerr('Skipping rostopic_publisher_plugin') + return False + else: + return stopped and timeout + else: + rospy.logerr('Invalid topic cond: {}'.format(topic_cond)) + return False + + def _publish_topic(self, topic, ctx, check_cond=False): + if check_cond and 'cond' in topic: + conditions = topic['cond'] + if not isinstance(conditions, list): + conditions = [conditions] + do_publish = False + for cond in conditions: + do_publish = do_publish or self._check_condition(cond, ctx) + if do_publish is False: + 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, check_cond=False) + + 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, check_cond=True) diff --git a/app_manager_utils/app_recorder/CHANGELOG.rst b/app_manager_utils/app_recorder/CHANGELOG.rst new file mode 100644 index 000000000..4f886d147 --- /dev/null +++ b/app_manager_utils/app_recorder/CHANGELOG.rst @@ -0,0 +1,78 @@ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Changelog for package app_recorder +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +0.0.6 (2022-07-29) +------------------ +* Merge pull request `#54 `_ from 708yamaguchi/rosbag-converter +* [app_recorder] Exit if jsk_rosbag_tools is not found +* Avoid linter error: E501 line too long (80 > 79 characters) +* [app_recorder] Use rospy.logerr() instead of print() +* Add README.md +* [app_recorder] Add rosbag converter plugins +* Contributors: Naoya Yamaguchi, Shingo Kitagawa + +0.0.5 (2022-02-08) +------------------ + +0.0.4 (2021-12-27) +------------------ +* Merge pull request `#46 `_ from 708yamaguchi/save-ctx-as-result +* [app_recorder] Save ctx directly as app result +* Contributors: Naoya Yamaguchi, Shingo Kitagawa + +0.0.3 (2021-12-17) +------------------ + +0.0.2 (2021-12-11) +------------------ + +0.0.1 (2021-10-06) +------------------ +* update readme +* add machine tag in rosbag_recorder +* add machine tag in audio_recorder.launch +* set default machine_file +* Merge pull request `#34 `_ from knorth55/use-decompressed-name + use decompressed topic name +* use decompressed topic name +* Merge pull request `#32 `_ from 708yamaguchi/machine-name +* load machine file +* Add machine to audio_video_recorder +* Add machine to video_recorder +* Merge pull request `#31 `_ from 708yamaguchi/compressed-video-recorder +* Do not change video_topic_name +* Use anon to node name for multiple use +* Add default value to video_compressed_topic_name in audio_video_recorder +* Add default value to video_compressed_topic_name +* Add image_transport dependency +* Add option to record compressed audio video +* Add option to record compressed video +* update app_recorder/README.md +* update README.md +* update for noetic +* remove do_timestamp param +* update file_name +* linter +* update app_recorder readme +* add stopped in result_recorder_plugin +* add audio_video_recorder plugin +* add ResultRecorderPlugin +* Merge pull request `#8 `_ from 708yamaguchi/add-audio-recorder + Add audio record plugin +* Add audio record plugin +* add video_codec in video_recorder.launch +* remove app_manager_plugin package +* add compress option in rosbag_recorder +* use module: null syntax +* update readme +* add README.md +* Merge pull request `#7 `_ from knorth55/add-plugins +* use package forma=2 +* refactor app_recorder app_uploader package.xml +* fix app_recorder plugin names +* fix typo +* add plugin name +* add RosbagRecorderPlugin and VideoRecorderPlugin +* add app_recorder package +* Contributors: Naoya Yamaguchi, Shingo Kitagawa diff --git a/app_manager_utils/app_recorder/CMakeLists.txt b/app_manager_utils/app_recorder/CMakeLists.txt new file mode 100644 index 000000000..5d98436c1 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_recorder/README.md b/app_manager_utils/app_recorder/README.md new file mode 100644 index 000000000..701c942b7 --- /dev/null +++ b/app_manager_utils/app_recorder/README.md @@ -0,0 +1,296 @@ +# 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 +``` + +### `app_recorder/rosbag_audio_converter_plugin`: Rosbag audio converter plugin + +This plugin converts rosbag to audio file when app finishs. + +#### `plugin_args`: Plugin arguments + +- `rosbag_path`: + - rosbag file directory path +- `rosbag_title`: + - rosbag file name +- `audio_path`: + - Output audio file (.wav) +- `audio_topic_name`: + - Input audio topic name +- `audio_sample_rate`: + - Input audio sample rate +- `audio_channels`: + - Input audio the number of channels + +#### `launch_args`: Plugin launch arguments + +`None` + +#### Sample plugin description + +```yaml +plugins: + - name: respeaker_audio_converter_plugin + type: app_recorder/rosbag_audio_converter_plugin + plugin_args: + rosbag_path: /tmp + rosbag_title: go_to_kitchen_rosbag.bag + audio_path: /tmp/go_to_kitchen_audio.wav + audio_topic_name: /audio + audio_sample_rate: 16000 + audio_channels: 1 +``` + +### `app_recorder/rosbag_video_converter_plugin`: Rosbag video converter plugin + +This plugin converts rosbag to video file when app finishs. If `audio_topic_name` is given, video with audio is generated. + +#### `plugin_args`: Plugin arguments + +- `rosbag_path`: + - rosbag file directory path +- `rosbag_title`: + - rosbag file name +- `video_path`: + - Output video file (.mp4) +- `image_topic_name`: + - Input image topic name +- `image_fps`: + - Input image frame rate +- `audio_topic_name`: + - Input audio topic name +- `audio_sample_rate`: + - Input audio sample rate +- `audio_channels`: + - Input audio the number of channels + +#### `launch_args`: Plugin launch arguments + +`None` + +#### Sample plugin description + +```yaml +plugins: + - name: head_camera_converter_plugin + type: app_recorder/rosbag_video_converter_plugin + plugin_args: + rosbag_path: /tmp + rosbag_title: go_to_kitchen_rosbag.bag + video_path: /tmp/go_to_kitchen_head_camera.mp4 + image_topic_name: /head_camera/rgb/throttled/image_rect_color/compressed + image_fps: 5 + audio_topic_name: /audio + audio_sample_rate: 16000 + audio_channels: 1 +``` diff --git a/app_manager_utils/app_recorder/app_recorder_plugin.yaml b/app_manager_utils/app_recorder/app_recorder_plugin.yaml new file mode 100644 index 000000000..f92cc5ec6 --- /dev/null +++ b/app_manager_utils/app_recorder/app_recorder_plugin.yaml @@ -0,0 +1,21 @@ +- 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 +- name: app_recorder/rosbag_audio_converter_plugin + launch: null + module: app_recorder.rosbag_audio_converter_plugin.RosbagAudioConverterPlugin +- name: app_recorder/rosbag_video_converter_plugin + launch: null + module: app_recorder.rosbag_video_converter_plugin.RosbagVideoConverterPlugin diff --git a/app_manager_utils/app_recorder/launch/audio_recorder.launch b/app_manager_utils/app_recorder/launch/audio_recorder.launch new file mode 100644 index 000000000..427a931b2 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_recorder/launch/audio_video_recorder.launch b/app_manager_utils/app_recorder/launch/audio_video_recorder.launch new file mode 100644 index 000000000..1a1655f77 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_recorder/launch/rosbag_recorder.launch b/app_manager_utils/app_recorder/launch/rosbag_recorder.launch new file mode 100644 index 000000000..f7fc2ccd2 --- /dev/null +++ b/app_manager_utils/app_recorder/launch/rosbag_recorder.launch @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app_manager_utils/app_recorder/launch/video_recorder.launch b/app_manager_utils/app_recorder/launch/video_recorder.launch new file mode 100644 index 000000000..2dd67ff4a --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_recorder/package.xml b/app_manager_utils/app_recorder/package.xml new file mode 100644 index 000000000..389d6a073 --- /dev/null +++ b/app_manager_utils/app_recorder/package.xml @@ -0,0 +1,23 @@ + + + app_recorder + 2.2.12 + 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 + jsk_rosbag_tools + + + + + diff --git a/app_manager_utils/app_recorder/setup.py b/app_manager_utils/app_recorder/setup.py new file mode 100644 index 000000000..a0be37b04 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_recorder/src/app_recorder/__init__.py b/app_manager_utils/app_recorder/src/app_recorder/__init__.py new file mode 100644 index 000000000..f7b3b8dbf --- /dev/null +++ b/app_manager_utils/app_recorder/src/app_recorder/__init__.py @@ -0,0 +1,7 @@ +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 +from app_recorder.rosbag_audio_converter_plugin import RosbagAudioConverterPlugin # NOQA +from app_recorder.rosbag_video_converter_plugin import RosbagVideoConverterPlugin # NOQA diff --git a/app_manager_utils/app_recorder/src/app_recorder/audio_recorder_plugin.py b/app_manager_utils/app_recorder/src/app_recorder/audio_recorder_plugin.py new file mode 100644 index 000000000..e8825b6b9 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_recorder/src/app_recorder/audio_video_recorder_plugin.py b/app_manager_utils/app_recorder/src/app_recorder/audio_video_recorder_plugin.py new file mode 100644 index 000000000..786b81919 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_recorder/src/app_recorder/result_recorder_plugin.py b/app_manager_utils/app_recorder/src/app_recorder/result_recorder_plugin.py new file mode 100644 index 000000000..b4fd64028 --- /dev/null +++ b/app_manager_utils/app_recorder/src/app_recorder/result_recorder_plugin.py @@ -0,0 +1,27 @@ +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_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(ctx, 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_manager_utils/app_recorder/src/app_recorder/rosbag_audio_converter_plugin.py b/app_manager_utils/app_recorder/src/app_recorder/rosbag_audio_converter_plugin.py new file mode 100644 index 000000000..71adbcc5c --- /dev/null +++ b/app_manager_utils/app_recorder/src/app_recorder/rosbag_audio_converter_plugin.py @@ -0,0 +1,42 @@ +import distutils.spawn +import os +import rospy + +from app_manager import AppManagerPlugin + +# check if ffmpeg is found +if distutils.spawn.find_executable('ffmpeg'): + try: + from jsk_rosbag_tools.bag_to_video import bag_to_audio + except ImportError as e: + rospy.logerr('{}'.format(e)) + rospy.logerr('Skip using rosbag_audio_converter_plugin') + bag_to_audio = None +else: + rospy.logerr('ffpmeg is not found.') + rospy.logerr('Skip using rosbag_audio_converter_plugin') + bag_to_audio = None + + +class RosbagAudioConverterPlugin(AppManagerPlugin): + def __init__(self): + super(RosbagAudioConverterPlugin, self).__init__() + + @classmethod + def app_manager_stop_plugin(cls, app, ctx, plugin_args): + if bag_to_audio: + rosbag_file_path = os.path.join( + str(plugin_args['rosbag_path']), + str(plugin_args['rosbag_title'])) + kwargs = {'wav_outpath': str(plugin_args['audio_path']), + 'topic_name': str(plugin_args['audio_topic_name']), + 'samplerate': int(plugin_args['audio_sample_rate']), + 'channels': int(plugin_args['audio_channels'])} + try: + bag_to_audio(rosbag_file_path, **kwargs) + except ValueError as e: + # topic is not included in bagfile + rospy.logerr('{}'.format(e)) + else: + rospy.logerr('Skipping rosbag_audio_converter_plugin') + return ctx diff --git a/app_manager_utils/app_recorder/src/app_recorder/rosbag_recorder_plugin.py b/app_manager_utils/app_recorder/src/app_recorder/rosbag_recorder_plugin.py new file mode 100644 index 000000000..9ea5fd459 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_recorder/src/app_recorder/rosbag_video_converter_plugin.py b/app_manager_utils/app_recorder/src/app_recorder/rosbag_video_converter_plugin.py new file mode 100644 index 000000000..e375d2345 --- /dev/null +++ b/app_manager_utils/app_recorder/src/app_recorder/rosbag_video_converter_plugin.py @@ -0,0 +1,48 @@ +import distutils.spawn +import os +import rospy + +# check if ffmpeg is found +if distutils.spawn.find_executable('ffmpeg'): + try: + from jsk_rosbag_tools.bag_to_video import bag_to_video + except ImportError as e: + rospy.logerr('{}'.format(e)) + rospy.logerr('Skip using rosbag_video_converter_plugin') + bag_to_video = None +else: + rospy.logerr('ffpmeg is not found.') + rospy.logerr('Skip using rosbag_video_converter_plugin') + bag_to_video = None + +from app_manager import AppManagerPlugin + + +class RosbagVideoConverterPlugin(AppManagerPlugin): + def __init__(self): + super(RosbagVideoConverterPlugin, self).__init__() + + @classmethod + def app_manager_stop_plugin(cls, app, ctx, plugin_args): + if bag_to_video: + rosbag_file_path = os.path.join( + str(plugin_args['rosbag_path']), + str(plugin_args['rosbag_title'])) + kwargs = {'output_filepath': str(plugin_args['video_path']), + 'image_topic': str(plugin_args['image_topic_name']), + 'fps': int(plugin_args['image_fps'])} + # If audio_topic_name is given, video with audio is converted + if 'audio_topic_name' in plugin_args: + audio_kwargs = { + 'audio_topic': str(plugin_args['audio_topic_name']), + 'samplerate': int(plugin_args['audio_sample_rate']), + 'channels': int(plugin_args['audio_channels'])} + kwargs.update(audio_kwargs) + try: + bag_to_video(rosbag_file_path, **kwargs) + except ValueError as e: + # topic is not included in bagfile + rospy.logerr('{}'.format(e)) + else: + rospy.logerr('Skipping rosbag_video_converter_plugin') + return ctx diff --git a/app_manager_utils/app_recorder/src/app_recorder/video_recorder_plugin.py b/app_manager_utils/app_recorder/src/app_recorder/video_recorder_plugin.py new file mode 100644 index 000000000..91ab33a27 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_scheduler/CHANGELOG.rst b/app_manager_utils/app_scheduler/CHANGELOG.rst new file mode 100644 index 000000000..ec71ff5e5 --- /dev/null +++ b/app_manager_utils/app_scheduler/CHANGELOG.rst @@ -0,0 +1,91 @@ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Changelog for package app_scheduler +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +0.0.6 (2022-07-29) +------------------ + +0.0.5 (2022-02-08) +------------------ + +0.0.4 (2021-12-27) +------------------ +* Merge pull request `#43 `_ from knorth55/commentout-app-dir +* comment out app_dir in app_scheduler +* Merge pull request `#42 `_ from knorth55/use-rospy +* remove roseus dependencies +* Contributors: Shingo Kitagawa + +0.0.3 (2021-12-17) +------------------ +* [app_scheduler] fix bugs for app_args +* [app_scheduler] add a sample app with app_args +* Merge pull request `#38 `_ from sktometometo/PR/bugfix +* [app_scheduler] add app_args to message and fix bugs +* Contributors: Koki Shinjo, Shingo Kitagawa + +0.0.2 (2021-12-11) +------------------ +* Merge pull request `#37 `_ from sktometometo/PR/update-app-scheduler-error-handling +* [app_scheduler] add AttributeError handling +* use absolute import in app_scheduler +* Merge pull request `#36 `_ from sktometometo/PR/add-ros-interface +* [app_scheduler] fix format +* [app_scheduler] fix format +* [app_scheduler] make service return to True +* [app_scheduler] Update app_scheduler/src/app_scheduler/app_scheduler_lib.py + Co-authored-by: Shingo Kitagawa +* [app_scheduler] Update app_scheduler/src/app_scheduler/app_scheduler_lib.py + Co-authored-by: Shingo Kitagawa +* [app_scheduler] Update app_scheduler/src/app_scheduler/app_scheduler_lib.py + Co-authored-by: Shingo Kitagawa +* [app_scheduler] Update app_scheduler/src/app_scheduler/app_scheduler_lib.py + Co-authored-by: Shingo Kitagawa +* [app_scheduler] Update app_scheduler/src/app_scheduler/app_scheduler_lib.py + Co-authored-by: Shingo Kitagawa +* [app_scheduler] Update app_scheduler/src/app_scheduler/app_scheduler_lib.py + Co-authored-by: Shingo Kitagawa +* [app_scheduler] add Service Interface +* [app_scheduler] rename library script +* [app_scheduler] add ROS message +* support startapp args in app_scheduler +* use request msg in app_scheduler +* Contributors: Koki Shinjo, Shingo Kitagawa + +0.0.1 (2021-10-06) +------------------ +* Merge pull request `#25 `_ from knorth55/app-scheduler-logerr + [app_scheduler] add more logerr in app_scheduler +* add more logerr in app_scheduler +* Merge pull request `#24 `_ from 708yamaguchi/schedule-version + Add error message to upgrade schedule module +* Add error message to upgrade schedule module +* update for noetic +* set lower update duration +* fix typo +* use app_list topic +* update readme +* update package.xml +* update sample_app_schedule.yaml +* add app_scheduler.launch +* add update_duration +* refactor app_scheduler.py +* update readme +* improve loginfo +* update sample +* update duration +* add start and stop in schedule +* add status subscriber +* add output=screen in sample launch +* update readme +* add exec_depend +* update sample +* fix typo in app_scheduler +* update CMakeLists.txt +* add sample +* add app_scheduler +* add app_scheduler/README +* instal scripts +* add catkin_python_setup +* initial commit +* Contributors: Naoya Yamaguchi, Shingo Kitagawa diff --git a/app_manager_utils/app_scheduler/CMakeLists.txt b/app_manager_utils/app_scheduler/CMakeLists.txt new file mode 100644 index 000000000..8958690fe --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_scheduler/README.md b/app_manager_utils/app_scheduler/README.md new file mode 100644 index 000000000..93b141d5d --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_scheduler/apps/apps.installed b/app_manager_utils/app_scheduler/apps/apps.installed new file mode 100644 index 000000000..1ca8f050e --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_scheduler/apps/sample0/sample0.app b/app_manager_utils/app_scheduler/apps/sample0/sample0.app new file mode 100644 index 000000000..6b88d07d7 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_scheduler/apps/sample0/sample0.interface b/app_manager_utils/app_scheduler/apps/sample0/sample0.interface new file mode 100644 index 000000000..044105d64 --- /dev/null +++ b/app_manager_utils/app_scheduler/apps/sample0/sample0.interface @@ -0,0 +1,2 @@ +published_topics: {} +subscribed_topics: {} diff --git a/app_manager_utils/app_scheduler/apps/sample0/sample0.py b/app_manager_utils/app_scheduler/apps/sample0/sample0.py new file mode 100755 index 000000000..9bd43eee6 --- /dev/null +++ b/app_manager_utils/app_scheduler/apps/sample0/sample0.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +import sys +import time + +import rospy + +from std_msgs.msg import String + + +def main(): + rospy.loginfo("sample0 start") + pub = rospy.Publisher( + '/app_scheduler/sample0', String, queue_size=1) + time.sleep(0.5) + rospy.loginfo("publishing /app_scheduler/sample0 ...") + pub.publish(String(data='sample0')) + rospy.loginfo('sample0 finish') + sys.exit(0) + + +if __name__ == '__main__': + rospy.init_node('sample0') + main() diff --git a/app_manager_utils/app_scheduler/apps/sample0/sample0.xml b/app_manager_utils/app_scheduler/apps/sample0/sample0.xml new file mode 100644 index 000000000..737efdc22 --- /dev/null +++ b/app_manager_utils/app_scheduler/apps/sample0/sample0.xml @@ -0,0 +1,3 @@ + + + diff --git a/app_manager_utils/app_scheduler/apps/sample1/sample1.app b/app_manager_utils/app_scheduler/apps/sample1/sample1.app new file mode 100644 index 000000000..d286dc9a1 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_scheduler/apps/sample1/sample1.interface b/app_manager_utils/app_scheduler/apps/sample1/sample1.interface new file mode 100644 index 000000000..044105d64 --- /dev/null +++ b/app_manager_utils/app_scheduler/apps/sample1/sample1.interface @@ -0,0 +1,2 @@ +published_topics: {} +subscribed_topics: {} diff --git a/app_manager_utils/app_scheduler/apps/sample1/sample1.py b/app_manager_utils/app_scheduler/apps/sample1/sample1.py new file mode 100755 index 000000000..525f4c589 --- /dev/null +++ b/app_manager_utils/app_scheduler/apps/sample1/sample1.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +import sys +import time + +import rospy + +from std_msgs.msg import String + + +def main(): + rospy.loginfo("sample1 start") + pub = rospy.Publisher( + '/app_scheduler/sample1', String, queue_size=1) + time.sleep(0.5) + rospy.loginfo("publishing /app_scheduler/sample1 ...") + pub.publish(String(data='sample1')) + rospy.loginfo('sample1 finish') + sys.exit(0) + + +if __name__ == '__main__': + rospy.init_node('sample1') + main() diff --git a/app_manager_utils/app_scheduler/apps/sample1/sample1.xml b/app_manager_utils/app_scheduler/apps/sample1/sample1.xml new file mode 100644 index 000000000..2eaae7596 --- /dev/null +++ b/app_manager_utils/app_scheduler/apps/sample1/sample1.xml @@ -0,0 +1,3 @@ + + + diff --git a/app_manager_utils/app_scheduler/apps/sample2/sample2.app b/app_manager_utils/app_scheduler/apps/sample2/sample2.app new file mode 100644 index 000000000..469e42489 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_scheduler/apps/sample2/sample2.interface b/app_manager_utils/app_scheduler/apps/sample2/sample2.interface new file mode 100644 index 000000000..044105d64 --- /dev/null +++ b/app_manager_utils/app_scheduler/apps/sample2/sample2.interface @@ -0,0 +1,2 @@ +published_topics: {} +subscribed_topics: {} diff --git a/app_manager_utils/app_scheduler/apps/sample2/sample2.py b/app_manager_utils/app_scheduler/apps/sample2/sample2.py new file mode 100755 index 000000000..daf5b0aec --- /dev/null +++ b/app_manager_utils/app_scheduler/apps/sample2/sample2.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +import sys +import time + +import rospy + +from std_msgs.msg import String + + +def main(): + rospy.loginfo("sample2 start") + pub = rospy.Publisher( + '/app_scheduler/sample2', String, queue_size=1) + time.sleep(0.5) + rospy.loginfo("publishing /app_scheduler/sample2 ...") + pub.publish(String(data='sample2')) + rospy.loginfo('sample2 finish') + sys.exit(0) + + +if __name__ == '__main__': + rospy.init_node('sample2') + main() diff --git a/app_manager_utils/app_scheduler/apps/sample2/sample2.xml b/app_manager_utils/app_scheduler/apps/sample2/sample2.xml new file mode 100644 index 000000000..809ce4c7e --- /dev/null +++ b/app_manager_utils/app_scheduler/apps/sample2/sample2.xml @@ -0,0 +1,3 @@ + + + diff --git a/app_manager_utils/app_scheduler/apps/sample3/sample3.app b/app_manager_utils/app_scheduler/apps/sample3/sample3.app new file mode 100644 index 000000000..d751c1048 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_scheduler/apps/sample3/sample3.interface b/app_manager_utils/app_scheduler/apps/sample3/sample3.interface new file mode 100644 index 000000000..044105d64 --- /dev/null +++ b/app_manager_utils/app_scheduler/apps/sample3/sample3.interface @@ -0,0 +1,2 @@ +published_topics: {} +subscribed_topics: {} diff --git a/app_manager_utils/app_scheduler/apps/sample3/sample3.py b/app_manager_utils/app_scheduler/apps/sample3/sample3.py new file mode 100755 index 000000000..95073350f --- /dev/null +++ b/app_manager_utils/app_scheduler/apps/sample3/sample3.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python + +import sys +import time + +import rospy + +from std_msgs.msg import String + + +def main(): + rospy.loginfo("sample3 start") + input_value = rospy.get_param('~input', 'default') + rospy.loginfo('input: {}'.format(input_value)) + pub = rospy.Publisher( + '/app_scheduler/sample3', String, queue_size=1) + time.sleep(0.5) + rospy.loginfo("publishing /app_scheduler/sample3 ...") + pub.publish(String(data=input_value)) + rospy.loginfo('sample3 finish') + sys.exit(0) + + +if __name__ == '__main__': + rospy.init_node('sample3') + main() diff --git a/app_manager_utils/app_scheduler/apps/sample3/sample3.xml b/app_manager_utils/app_scheduler/apps/sample3/sample3.xml new file mode 100644 index 000000000..7f09a9e79 --- /dev/null +++ b/app_manager_utils/app_scheduler/apps/sample3/sample3.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app_manager_utils/app_scheduler/launch/app_scheduler.launch b/app_manager_utils/app_scheduler/launch/app_scheduler.launch new file mode 100644 index 000000000..ea884b2b8 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_scheduler/msg/AppSchedule.msg b/app_manager_utils/app_scheduler/msg/AppSchedule.msg new file mode 100644 index 000000000..257772f23 --- /dev/null +++ b/app_manager_utils/app_scheduler/msg/AppSchedule.msg @@ -0,0 +1,2 @@ +string start +string stop diff --git a/app_manager_utils/app_scheduler/msg/AppScheduleEntries.msg b/app_manager_utils/app_scheduler/msg/AppScheduleEntries.msg new file mode 100644 index 000000000..1854039f5 --- /dev/null +++ b/app_manager_utils/app_scheduler/msg/AppScheduleEntries.msg @@ -0,0 +1 @@ +app_scheduler/AppScheduleEntry[] entries diff --git a/app_manager_utils/app_scheduler/msg/AppScheduleEntry.msg b/app_manager_utils/app_scheduler/msg/AppScheduleEntry.msg new file mode 100644 index 000000000..27cbac065 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_scheduler/package.xml b/app_manager_utils/app_scheduler/package.xml new file mode 100644 index 000000000..d1cb715d0 --- /dev/null +++ b/app_manager_utils/app_scheduler/package.xml @@ -0,0 +1,23 @@ + + + app_scheduler + 2.2.12 + 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 + std_msgs + + + + + diff --git a/app_manager_utils/app_scheduler/sample/sample_app_schedule.yaml b/app_manager_utils/app_scheduler/sample/sample_app_schedule.yaml new file mode 100644 index 000000000..a24556955 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_scheduler/sample/sample_app_scheduler.launch b/app_manager_utils/app_scheduler/sample/sample_app_scheduler.launch new file mode 100644 index 000000000..aa1e0bb26 --- /dev/null +++ b/app_manager_utils/app_scheduler/sample/sample_app_scheduler.launch @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app_manager_utils/app_scheduler/scripts/app_scheduler b/app_manager_utils/app_scheduler/scripts/app_scheduler new file mode 100755 index 000000000..d9b2df9c5 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_scheduler/setup.py b/app_manager_utils/app_scheduler/setup.py new file mode 100644 index 000000000..a0be37b04 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_scheduler/src/app_scheduler/__init__.py b/app_manager_utils/app_scheduler/src/app_scheduler/__init__.py new file mode 100644 index 000000000..de991c9a7 --- /dev/null +++ b/app_manager_utils/app_scheduler/src/app_scheduler/__init__.py @@ -0,0 +1 @@ +from app_scheduler.app_scheduler_lib import AppScheduler # NOQA diff --git a/app_manager_utils/app_scheduler/src/app_scheduler/app_scheduler_lib.py b/app_manager_utils/app_scheduler/src/app_scheduler/app_scheduler_lib.py new file mode 100644 index 000000000..59fd80763 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_scheduler/srv/AddEntry.srv b/app_manager_utils/app_scheduler/srv/AddEntry.srv new file mode 100644 index 000000000..0ef386311 --- /dev/null +++ b/app_manager_utils/app_scheduler/srv/AddEntry.srv @@ -0,0 +1,4 @@ +app_scheduler/AppScheduleEntry entry +--- +bool success +string message diff --git a/app_manager_utils/app_scheduler/srv/RemoveEntry.srv b/app_manager_utils/app_scheduler/srv/RemoveEntry.srv new file mode 100644 index 000000000..9fa6a16fb --- /dev/null +++ b/app_manager_utils/app_scheduler/srv/RemoveEntry.srv @@ -0,0 +1,4 @@ +string name +--- +bool success +string message diff --git a/app_manager_utils/app_uploader/CHANGELOG.rst b/app_manager_utils/app_uploader/CHANGELOG.rst new file mode 100644 index 000000000..19936f017 --- /dev/null +++ b/app_manager_utils/app_uploader/CHANGELOG.rst @@ -0,0 +1,40 @@ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Changelog for package app_uploader +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +0.0.6 (2022-07-29) +------------------ +* Merge pull request `#52 `_ from tkmtnt7000/display-file-name +* Display failing upload data in email +* Contributors: Naoto Tsukamoto, Shingo Kitagawa + +0.0.5 (2022-02-08) +------------------ + +0.0.4 (2021-12-27) +------------------ + +0.0.3 (2021-12-17) +------------------ + +0.0.2 (2021-12-11) +------------------ + +0.0.1 (2021-10-06) +------------------ +* update README.md +* update for noetic +* Merge pull request `#12 `_ from knorth55/add-superlinter +* fix markdown lint +* remove app_manager_plugin package +* update readme +* add README.md +* Merge pull request `#7 `_ from knorth55/add-plugins +* use package forma=2 +* add plugin_args in app_manager_plugin +* refactor app_recorder app_uploader package.xml +* fix GdriveUploaderPlugin +* add plugin name +* add GdriveUploderPlugin +* add app_uploader package +* Contributors: Shingo Kitagawa diff --git a/app_manager_utils/app_uploader/CMakeLists.txt b/app_manager_utils/app_uploader/CMakeLists.txt new file mode 100644 index 000000000..13f9b2616 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_uploader/README.md b/app_manager_utils/app_uploader/README.md new file mode 100644 index 000000000..65436b75b --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_uploader/app_uploader_plugin.yaml b/app_manager_utils/app_uploader/app_uploader_plugin.yaml new file mode 100644 index 000000000..2c5970026 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_uploader/package.xml b/app_manager_utils/app_uploader/package.xml new file mode 100644 index 000000000..2f7d7b5c8 --- /dev/null +++ b/app_manager_utils/app_uploader/package.xml @@ -0,0 +1,19 @@ + + + app_uploader + 2.2.12 + The app_uploader package + Shingo Kitagawa + Shingo Kitagawa + BSD + + catkin + python-setuptools + python3-setuptools + app_manager + gdrive_ros + + + + + diff --git a/app_manager_utils/app_uploader/setup.py b/app_manager_utils/app_uploader/setup.py new file mode 100644 index 000000000..a0be37b04 --- /dev/null +++ b/app_manager_utils/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_manager_utils/app_uploader/src/app_uploader/__init__.py b/app_manager_utils/app_uploader/src/app_uploader/__init__.py new file mode 100644 index 000000000..3560a1394 --- /dev/null +++ b/app_manager_utils/app_uploader/src/app_uploader/__init__.py @@ -0,0 +1 @@ +from app_uploader.gdrive_uploader_plugin import GdriveUploaderPlugin # NOQA diff --git a/app_manager_utils/app_uploader/src/app_uploader/gdrive_uploader_plugin.py b/app_manager_utils/app_uploader/src/app_uploader/gdrive_uploader_plugin.py new file mode 100644 index 000000000..83d0c3edf --- /dev/null +++ b/app_manager_utils/app_uploader/src/app_uploader/gdrive_uploader_plugin.py @@ -0,0 +1,42 @@ +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 + if 'request_file_titles' in ctx: + ctx['request_file_titles'] += req.file_titles + else: + ctx['request_file_titles'] = req.file_titles + return ctx