diff --git a/jsk_robot_common/jsk_robot_startup/launch/robot_database_mongo_server.launch b/jsk_robot_common/jsk_robot_startup/launch/robot_database_mongo_server.launch new file mode 100644 index 0000000000..9a826b441d --- /dev/null +++ b/jsk_robot_common/jsk_robot_startup/launch/robot_database_mongo_server.launch @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jsk_robot_common/jsk_robot_startup/lifelog/README.md b/jsk_robot_common/jsk_robot_startup/lifelog/README.md index 7ac192515a..bd7e71515e 100644 --- a/jsk_robot_common/jsk_robot_startup/lifelog/README.md +++ b/jsk_robot_common/jsk_robot_startup/lifelog/README.md @@ -85,3 +85,45 @@ Save object detection result to database * `~robot_frame` (String, default: `base_footprint`) robot base tf frame + + +## Examples + +You can start a mongodb logging system using `lifelog/mongodb.launch` and `lifelog/common_logger.launch` files. + See [jsk_pr2_lifelog/db_client.launch](https://github.com/jsk-ros-pkg/jsk_robot/blob/5d90d483aaa674d33968f34db83f53cd3d018bd4/jsk_pr2_robot/jsk_pr2_startup/jsk_pr2_lifelog/db_client.launch), [fetch_lifelog.xml](https://github.com/jsk-ros-pkg/jsk_robot/blob/921097b7a9c16cd99d0eb8f9f271bda4784dadc5/jsk_fetch_robot/jsk_fetch_startup/launch/fetch_lifelog.xml) and [jsk_baxter_lifelog/db_client.launch](https://github.com/jsk-ros-pkg/jsk_robot/blob/c03dc5af06d8b7786b4212b132047acaa229eb0e/jsk_baxter_robot/jsk_baxter_startup/jsk_baxter_lifelog/db_client.launch) for examples. + +## Sample client + +Sample program to retrieve stored data can be found at [jsk_robot_startup/scripts/robot_database_monogo_example.py](./scripts/robot_database_monogo_example.py). + +To connect replicator server (musca), you need to start [jsk_robot_startup/launch/robot_database_monogo_server.launch](./launch/robot_database_monogo_server.launch). Please be careful about `ROS_MASTER_URI`, because `robot_database_monogo_server.launch` will starts same node name as robot system nodes, thus it may corrupt some settings. + +## Troubleshooting + +If you encounter following error +``` +[ERROR] [1666693339.502586]: Could not get message store services. Maybe the message store has not been started? Retrying.. +[ERROR] [1666693339.502586]: Could not get message store services. Maybe the message store has not been started? Retrying.. +``` + +1) Is mongodb service working correctly? +``` +sudo systemctl status mongodb.service +sudo systemctl start mongodb.service +``` + +2) Is `mongo` command works? +``` +mongo localhost:27017 +``` + +3) Check /etc/mongodb.conf +``` +$ cat /etc/mongodb.conf | grep -v ^# +dbpath=/var/lib/mongodb +logpath=/var/log/mongodb/mongodb.log +logappend=true +bind_ip = 0.0.0.0 +journal=true +``` +Default `bind_jp` is `127.0.0.1`, and it not work on some machines, see [#1706](https://github.com/jsk-ros-pkg/jsk_robot/issues/1706) for more info. diff --git a/jsk_robot_common/jsk_robot_startup/scripts/robot_database_mongo_dump.py b/jsk_robot_common/jsk_robot_startup/scripts/robot_database_mongo_dump.py new file mode 100755 index 0000000000..547bfeaee1 --- /dev/null +++ b/jsk_robot_common/jsk_robot_startup/scripts/robot_database_mongo_dump.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# +# Copied from https://github.com/strands-project/mongodb_store/blob/melodic-devel/mongodb_store/scripts/mongodb_play.py +# +from __future__ import print_function + +import argparse +import calendar +import cv2 +import datetime +import json +import mongodb_store.util as mg_util +import os +import pymongo +import rospy +import sys +import yaml + +# https://answers.ros.org/question/196365/is-there-a-general-way-to-convert-ros-messages-into-json-format/ +import rosbridge_library.internal.message_conversion +from rosbridge_library.util import string_types, bson +rosbridge_library.internal.message_conversion.bson_only_mode = False +rosbridge_library.internal.message_conversion.binary_encoder = bson.Binary + +# https://stackoverflow.com/questions/10971033/backporting-python-3-openencoding-utf-8-to-python-2 +from io import open + + +from cv_bridge import CvBridge +from dateutil import tz +from sensor_msgs.msg import Image, CompressedImage + +UTC = tz.gettz('UTC') +JST = tz.gettz('Asia/Tokyo') + +MongoClient = mg_util.import_MongoClient() + +TIME_KEY = '_meta.inserted_at' + +def max_time(collection): + return collection.find_one(sort=[(TIME_KEY, pymongo.DESCENDING)])['_meta']['inserted_at'] + +def min_time(collection): + return collection.find_one(sort=[(TIME_KEY, pymongo.ASCENDING)])['_meta']['inserted_at'] + +def to_ros_time(dt): + return rospy.Time(calendar.timegm(dt.utctimetuple()), dt.microsecond * 1000) + +def to_datetime(rt): + return datetime.datetime.utcfromtimestamp(rt.secs) + datetime.timedelta(microseconds = rt.nsecs / 1000) + +def ros_time_strftime(rt, format): + """ converts a ros time to a datetime and calls strftime on it with the given format """ + return to_datetime(rt).strftime(format) + +def mkdatetime(date_string): + return datetime.datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S') + +def main(argv): + parser = argparse.ArgumentParser() + parser.add_argument("--host", default="musca.jsk.imi.i.u-tokyo.ac.jp") + parser.add_argument("--port", "-p", default=27017) + parser.add_argument("--database", "-d", default="jsk_robot_lifelog") + parser.add_argument("--collection", "-c", default="basil") + parser.add_argument("--start", "-s", default="", help='start datetime of query, defaults to the earliest date stored in db, across all requested collections. Formatted "Y-m-d H:M" e.g. "2022-07-14 06:38:00"') + parser.add_argument("--end", "-e", default="", help='end datetime of query, defaults to the latest date stored in db, across all requested collections. Formatted "Y-m-d H:M" e.g. "2022-07-14 06:39:00"') + args = parser.parse_args() + bridge = CvBridge() + + print("Connecting ... {}:{}".format(args.host, args.port)) + mongo_client=MongoClient(args.host, args.port) + collection = mongo_client[args.database][args.collection] + print("Selected ... {}/{}".format(args.database,args.collection)) + + # make sure there's an index on time in the collection so the sort operation doesn't require the whole collection to be loaded + collection.ensure_index(TIME_KEY) + + # get the min and max time across all collections, conver to ros time + if args.start: + start_time = mkdatetime(args.start).replace(tzinfo=JST) + else: + start_time = min_time(collection).replace(tzinfo=UTC).astimezone(JST) + + if args.end: + end_time = mkdatetime(args.end).replace(tzinfo=JST) + else: + end_time = max_time(collection).replace(tzinfo=UTC).astimezone(JST) + + print(" From : {}".format(start_time.strftime('%Y-%m-%d %H:%M:%S'))) + print(" To : {}".format(end_time.strftime('%Y-%m-%d %H:%M:%S'))) + + documents = collection.find({TIME_KEY: { '$gte': start_time, '$lte': end_time}}, sort=[(TIME_KEY, pymongo.ASCENDING)]) + documents_count = documents.count() + print("This document contains {} messages".format(documents_count)) + # print(documents[0]['_meta']['inserted_at']) + # print(documents[documents_count-1]['_meta']['inserted_at']) + + dirname = collection.full_name + start_time.strftime('_%Y-%m-%d_%H:%M:%S_') + end_time.strftime('_%Y-%m-%d_%H:%M:%S') + if not os.path.exists(dirname): + os.mkdir(dirname) + msg_classes = {} + for d in documents: + stored_class = d['_meta']['stored_class'] + input_topic = d['_meta']['input_topic'] + inserted_at = d['_meta']['inserted_at'] + ros_time = to_ros_time(inserted_at) + if stored_class in msg_classes: + msg_class = msg_classes[stored_class] + else: + try: + msg_class = msg_classes[stored_class] = mg_util.load_class(stored_class) + except ImportError as e: + print(";;") + print(";; ImportError: {}".format(e)) + print(";;") + print(";; try install ros-{}-{}".format(os.environ['ROS_DISTRO'], stored_class.split('.')[0].replace('_','-'))) + print(";;") + sys.exit(-1) + message = mg_util.dictionary_to_message(d, msg_class) + + if type(message) == Image: + filename = "{}{}.jpg".format(ros_time.to_nsec(), input_topic.replace('/','-')) + image = bridge.imgmsg_to_cv2(message) + cv2.imwrite(os.path.join(dirname, filename), image) + elif type(message) == CompressedImage: + filename = "{}{}.jpg".format(ros_time.to_nsec(), input_topic.replace('/','-')) + image = bridge.compressed_imgmsg_to_cv2(message) + cv2.imwrite(os.path.join(dirname, filename), image) + else: + filename = "{}{}.json".format(ros_time.to_nsec(), input_topic.replace('/','-')) + with open(os.path.join(dirname, filename), "w", encoding="utf-8") as f: + # f.write(yaml.dump(yaml.load(str(message)), allow_unicode=True)) + ## + # data = yaml.load(str(message)) does not work because of containing ':' in value. + # > yaml.scanner.ScannerError: mapping values are not allowed here + # > in "", line 26, column 41: ... meters: b'{\n "sentence-ending": "\\u305f\\u3088\\u306d", \n " ... + yaml_data = rosbridge_library.internal.message_conversion.extract_values(message) + json_data = json.dumps(yaml_data, default=lambda o: o.decode('utf-8') if isinstance(o, bytes) else o, indent=4, ensure_ascii=False) + try: + json_data = json_data.decode('utf-8') # Python2 need to decode to use write + except: + pass # Python3 does not have decode() + # convert bytes objects to strings before serializing to JSON + f.write(json_data) + print("Writing.. {} ({})".format(filename, inserted_at)) + + +# processes load main so move init_node out +if __name__ == "__main__": + main(sys.argv) diff --git a/jsk_robot_common/jsk_robot_startup/scripts/robot_database_mongo_example.py b/jsk_robot_common/jsk_robot_startup/scripts/robot_database_mongo_example.py new file mode 100755 index 0000000000..e23e079a49 --- /dev/null +++ b/jsk_robot_common/jsk_robot_startup/scripts/robot_database_mongo_example.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import base64 +import numpy as np +import pickle +import rospy +import sys + +# date +from datetime import datetime, timedelta, tzinfo +from dateutil import tz +import calendar +import pytz + +# mongo +from mongodb_store.message_store import MessageStoreProxy +import mongodb_store.util as MU +import pymongo + +# message +from jsk_robot_startup.lifelog.logger_base import LoggerBase +from sensor_msgs.msg import CompressedImage +from smach_msgs.msg import SmachContainerStatus + +# image processing +from cv_bridge import CvBridge +import cv2 + +# global variabels +JST = tz.gettz('Asia/Tokyo') +bridge = CvBridge() + +# sample functions +def query_latest_smach(): + try: + rospy.loginfo("Loading last smach execution...") + last_msg, _ = msg_store.query( + SmachContainerStatus._type, + {"info": "START"}, + single=True, + sort_query=[("_meta.inserted_at", pymongo.DESCENDING)] + ) + msgs = msg_store.query( + SmachContainerStatus._type, + {"header.stamp.secs": {"$gt": last_msg.header.stamp.secs}}, + sort_query=[("_meta.inserted_at", pymongo.ASCENDING)] + ) + + def status_to_img(msg): + if sys.version_info.major < 3: + local_data_str = pickle.loads(msg.local_data) + else: + local_data_str = pickle.loads( + msg.local_data.encode('utf-8'), encoding='utf-8') + print("{} @{}".format(local_data_str['DESCRIPTION'], + datetime.fromtimestamp(msg.header.stamp.to_sec(), JST))) + imgmsg = None + if 'IMAGE' in local_data_str and local_data_str['IMAGE']: + imgmsg = CompressedImage() + imgmsg.deserialize(base64.b64decode(local_data_str['IMAGE'])) + return imgmsg + + return filter(lambda x: x is not None, map(lambda x: status_to_img(x[0]), msgs)) + except Exception as e: + rospy.logerr('failed to load images from db: %s' % e) + return None + + +def query_images(now = datetime.now(JST)-timedelta(hours=0), + then = datetime.now(JST)-timedelta(hours=1)): + try: + rospy.loginfo("Loading images...") + msgs = msg_store.query( + CompressedImage._type, + {"_meta.inserted_at": {"$lt": now, "$gte": then}}, + sort_query=[("_meta.inserted_at", pymongo.ASCENDING)] + ) + return map(lambda x: x[0], msgs) + except Exception as e: + rospy.logerr('failed to load images from db: %s' % e) + return None + +if __name__ == '__main__': + rospy.init_node('sample_robot_database') + db_name = 'jsk_robot_lifelog' + col_name = 'basil' # pr1012, fetch17 etc.. + msg_store = MessageStoreProxy(database=db_name, collection=col_name) + + print("> sample program for robot database") + print("> 1: get latest smach data") + print("> 2: get last 1 hours image data") + key = int(input("> {1, 2, 3..} : ")) + + if key == 1: + msgs = query_latest_smach() + elif key == 2: + msgs = query_images(then = datetime(2022, 11, 1, 17, 0, tzinfo=JST), + now = datetime(2022, 11, 1, 20, 10, tzinfo=JST)) + else: + print("unknown inputs...") + + # show data.. + for msg in msgs: + print(" @{}".format(datetime.fromtimestamp(msg.header.stamp.to_sec(), JST))) + cv_image = bridge.compressed_imgmsg_to_cv2(msg, "bgr8") + cv2.imshow('image', cv_image) + cv2.waitKey(50)