From 9f8323b222424fcc858aca9277adf6c6eca882d3 Mon Sep 17 00:00:00 2001 From: JiaqiangZhang Date: Wed, 1 Mar 2023 13:43:32 +0200 Subject: [PATCH 01/16] modify index and viewer --- rosboard/html/js/index.js | 4 +++- rosboard/html/js/viewers/meta/Viewer.js | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/rosboard/html/js/index.js b/rosboard/html/js/index.js index 87722115..f2e30ca4 100644 --- a/rosboard/html/js/index.js +++ b/rosboard/html/js/index.js @@ -57,6 +57,8 @@ setInterval(() => { } }, 5000); + + function updateStoredSubscriptions() { if(window.localStorage) { let storedSubscriptions = {}; @@ -71,7 +73,7 @@ function updateStoredSubscriptions() { function newCard() { // creates a new card, adds it to the grid, and returns it. - let card = $("
").addClass('card') + let card = $("
").addClass('card') .appendTo($('.grid')); return card; } diff --git a/rosboard/html/js/viewers/meta/Viewer.js b/rosboard/html/js/viewers/meta/Viewer.js index b0c4db51..8864e37d 100644 --- a/rosboard/html/js/viewers/meta/Viewer.js +++ b/rosboard/html/js/viewers/meta/Viewer.js @@ -28,7 +28,7 @@ class Viewer { card.title = $('
').addClass('card-title').text("Waiting for data ...").appendTo(card); // card content div - card.content = $('
').addClass('card-content').text('').appendTo(card); + card.content = $('
').addClass('card-content').text('').appendTo(card); // card pause button let menuId = 'menu-' + Math.floor(Math.random() * 1e6); @@ -56,11 +56,14 @@ class Viewer { for(let i in viewers) { let item = $('
  • ' + viewers[i].friendlyName + '
  • ').appendTo(this.card.menu); let that = this; - item.click(() => { Viewer.onSwitchViewer(that, viewers[i]); }); + item.click(() => { Viewer.onSwitchViewer(that, viewers[i]); console.log(viewers[i]); }); } componentHandler.upgradeAllRegistered(); + // card pin it button + // card.pinitButton + // card pause button card.pauseButton = $('') .addClass('mdl-button') From f1bc9857a79dc3f8ccd5262f15962de84909a71c Mon Sep 17 00:00:00 2001 From: Minh Nguyen Date: Tue, 7 Mar 2023 14:58:59 +0200 Subject: [PATCH 02/16] custom pointcloud sampling rate --- rosboard/compression.py | 16 ++++++++++++---- rosboard/serialization.py | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/rosboard/compression.py b/rosboard/compression.py index 3f365900..c1d0647c 100644 --- a/rosboard/compression.py +++ b/rosboard/compression.py @@ -237,7 +237,8 @@ def compress_occupancy_grid(msg, output): 8: np.float64, } -def compress_point_cloud2(msg, output): +def compress_point_cloud2(msg, output, sample_size = 65535): + # sample_size (int): custom sample size, default 65535 # assuming fields are ('x', 'y', 'z', ...), # compression scheme is: # msg['_data_uint16'] = { @@ -249,6 +250,13 @@ def compress_point_cloud2(msg, output): # 65535 represents the max value in the dataset, and bounds: [...] holds information on those bounds so the # client can decode back to a float + ### EDIT: CUSTOM COMPRESSION + # choose point cloud sample size + + if (sample_size < 0): + output["_warn"] = "sample size < 0, set to default 65535" + sample_size = 65535 + output["data"] = [] output["__comp"] = ["data"] @@ -268,9 +276,9 @@ def compress_point_cloud2(msg, output): except AssertionError as e: output["_error"] = "PointCloud2 error: %s" % str(e) - if points.size > 65536: - output["_warn"] = "Point cloud too large, randomly subsampling to 65536 points." - idx = np.random.randint(points.size, size=65536) + if points.size > sample_size: + output["_warn"] = "Point cloud too large, randomly subsampling to sample_size points." + idx = np.random.randint(points.size, size=sample_size) points = points[idx] xpoints = points['x'].astype(np.float32) diff --git a/rosboard/serialization.py b/rosboard/serialization.py index 80294e66..10e53d89 100644 --- a/rosboard/serialization.py +++ b/rosboard/serialization.py @@ -63,7 +63,7 @@ def ros2dict(msg): if (msg.__module__ == "sensor_msgs.msg._PointCloud2" or \ msg.__module__ == "sensor_msgs.msg._point_cloud2") \ and field == "data": - rosboard.compression.compress_point_cloud2(msg, output) + rosboard.compression.compress_point_cloud2(msg, output, sample_size=1000) continue value = getattr(msg, field) From cfa145df84afe4a5b10037d72b7d29c262ee233d Mon Sep 17 00:00:00 2001 From: JiaqiangZhang Date: Tue, 7 Mar 2023 16:18:12 +0200 Subject: [PATCH 03/16] Add prefixed panels image and pointcloud --- rosboard/html/js/index.js | 17 ++++++++++++++++- .../html/js/transports/WebSocketV1Transport.js | 12 ++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/rosboard/html/js/index.js b/rosboard/html/js/index.js index f2e30ca4..0023dcba 100644 --- a/rosboard/html/js/index.js +++ b/rosboard/html/js/index.js @@ -73,11 +73,25 @@ function updateStoredSubscriptions() { function newCard() { // creates a new card, adds it to the grid, and returns it. - let card = $("
    ").addClass('card') + let card = $("
    ").addClass('card') .appendTo($('.grid')); return card; } +// add prefixed card +let onPrefixedCard = function() { + let preSubscriptions = { + "/camera/rgb/image_raw": { topicType: "sensor_msgs/Image" }, + "/camera/depth/points": { topicType: "sensor_msgs/PointCloud2" }, + }; + // console.log("preSubscriptions", preSubscriptions); + + for(let topic_name in preSubscriptions){ + // console.log("topic_name", topic_name) + initSubscribe({topicName: topic_name, topicType: preSubscriptions[topic_name].topicType}); + } +} + let onOpen = function() { for(let topic_name in subscriptions) { console.log("Re-subscribing to " + topic_name); @@ -224,6 +238,7 @@ function initDefaultTransport() { onMsg: onMsg, onTopics: onTopics, onSystem: onSystem, + onPrefixedCard: onPrefixedCard, }); currentTransport.connect(); } diff --git a/rosboard/html/js/transports/WebSocketV1Transport.js b/rosboard/html/js/transports/WebSocketV1Transport.js index 58046980..3c34d7e5 100644 --- a/rosboard/html/js/transports/WebSocketV1Transport.js +++ b/rosboard/html/js/transports/WebSocketV1Transport.js @@ -1,11 +1,12 @@ class WebSocketV1Transport { - constructor({path, onOpen, onClose, onRosMsg, onTopics, onSystem}) { + constructor({path, onOpen, onClose, onRosMsg, onTopics, onSystem, onPrefixedCard}) { this.path = path; this.onOpen = onOpen ? onOpen.bind(this) : null; this.onClose = onClose ? onClose.bind(this) : null; this.onMsg = onMsg ? onMsg.bind(this) : null; this.onTopics = onTopics ? onTopics.bind(this) : null; this.onSystem = onSystem ? onSystem.bind(this) : null; + this.onPrefixedCard = onPrefixedCard ? onPrefixedCard.bind(this) : null; this.ws = null; this.joystickX = 0.0; this.joystickY = 0.0; @@ -19,9 +20,16 @@ class WebSocketV1Transport { this.ws = new WebSocket(abspath); + // this.ws.onopen = function(){ + // console.log("connected"); + // // console.log("that", that.onPrefixedCard(that)) + // // if(that.onPrefixedCard) that.onPrefixedCard(that); + // if(that.onOpen) that.onOpen(that); + // } + this.ws.onopen = function(){ console.log("connected"); - if(that.onOpen) that.onOpen(that); + if(that.onPrefixedCard) that.onPrefixedCard(that); } this.ws.onclose = function(){ From 888a8f5b4aea369b0394caffa29c882953e78114 Mon Sep 17 00:00:00 2001 From: JiaqiangZhang Date: Tue, 7 Mar 2023 17:25:26 +0200 Subject: [PATCH 04/16] find out how and where to change image quality --- rosboard/compression.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/rosboard/compression.py b/rosboard/compression.py index c1d0647c..848d9b11 100644 --- a/rosboard/compression.py +++ b/rosboard/compression.py @@ -28,31 +28,33 @@ def decode_jpeg(input_bytes): return np.asarray(Image.open(io.BytesIO(input_bytes))) def encode_jpeg(img): + # use img_quality to change image quality between 0 and 100 + img_quality = 50 if simplejpeg: if len(img.shape) == 2: img = np.expand_dims(img, axis=2) if not img.flags['C_CONTIGUOUS']: img = img.copy(order='C') - return simplejpeg.encode_jpeg(img, colorspace = "GRAY", quality = 50) + return simplejpeg.encode_jpeg(img, colorspace = "GRAY", quality = img_quality) elif len(img.shape) == 3: if not img.flags['C_CONTIGUOUS']: img = img.copy(order='C') if img.shape[2] == 1: - return simplejpeg.encode_jpeg(img, colorspace = "GRAY", quality = 50) + return simplejpeg.encode_jpeg(img, colorspace = "GRAY", quality = img_quality) elif img.shape[2] == 4: - return simplejpeg.encode_jpeg(img, colorspace = "RGBA", quality = 50) + return simplejpeg.encode_jpeg(img, colorspace = "RGBA", quality = img_quality) elif img.shape[2] == 3: - return simplejpeg.encode_jpeg(img, colorspace = "RGB", quality = 50) + return simplejpeg.encode_jpeg(img, colorspace = "RGB", quality = img_quality) else: return b'' elif cv2: if len(img.shape) == 3 and img.shape[2] == 3: img = img[:,:,::-1] - return cv2.imencode('.jpg', img, [cv2.IMWRITE_JPEG_QUALITY, 50])[1].tobytes() + return cv2.imencode('.jpg', img, [cv2.IMWRITE_JPEG_QUALITY, img_quality])[1].tobytes() elif PIL: pil_img = Image.fromarray(img) buffered = io.BytesIO() - pil_img.save(buffered, format="JPEG", quality = 50) + pil_img.save(buffered, format="JPEG", quality = img_quality) return buffered.getvalue() _PCL2_DATATYPES_NUMPY_MAP = { From 602dbace45a4789df38b4a510ff371b017e9f5e9 Mon Sep 17 00:00:00 2001 From: JiaqiangZhang Date: Tue, 7 Mar 2023 17:50:09 +0200 Subject: [PATCH 05/16] format point cloud panel warning --- rosboard/compression.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rosboard/compression.py b/rosboard/compression.py index 848d9b11..b091947e 100644 --- a/rosboard/compression.py +++ b/rosboard/compression.py @@ -279,7 +279,7 @@ def compress_point_cloud2(msg, output, sample_size = 65535): output["_error"] = "PointCloud2 error: %s" % str(e) if points.size > sample_size: - output["_warn"] = "Point cloud too large, randomly subsampling to sample_size points." + output["_warn"] = "Point cloud too large, randomly subsampling to {} points.".format(sample_size) idx = np.random.randint(points.size, size=sample_size) points = points[idx] From a6e53cec476abbc016e16481f63315d226025988 Mon Sep 17 00:00:00 2001 From: Minh Nguyen Date: Fri, 10 Mar 2023 17:53:04 +0200 Subject: [PATCH 06/16] custom image encoding dimesion --- README.md | 9 +++++++++ rosboard/compression.py | 18 +++++++++++++----- rosboard/html/js/viewers/ImageViewer.js | 3 ++- rosboard/serialization.py | 2 +- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d1b4f159..4d8ddb88 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,14 @@ # ROSboard +## Customize +- Custom image encoding +Choose the encoding image dimension within the `compress_image()` and the crispy image within the `encode_jpeg()` function + +- Custom point cloud subsample size +Choose the number of sub-samples every scan within the `compress_point_cloud2()` function. + +-------- + ROS node that runs a web server on your robot. Run the node, point your web browser at http://your-robot-ip:8888/ and you get nice visualizations. diff --git a/rosboard/compression.py b/rosboard/compression.py index b091947e..9656a87e 100644 --- a/rosboard/compression.py +++ b/rosboard/compression.py @@ -2,7 +2,7 @@ import io import numpy as np from rosboard.cv_bridge import imgmsg_to_cv2 - +import time try: import simplejpeg except ImportError: @@ -152,9 +152,8 @@ def compress_compressed_image(msg, output): output["_error"] = "Error: %s" % str(e) output["_data_jpeg"] = base64.b64encode(img_jpeg).decode() output["_data_shape"] = list(original_shape) - -def compress_image(msg, output): +def compress_image(msg, output, max_heigh = 800, max_width = 800): output["data"] = [] output["__comp"] = ["data"] @@ -175,8 +174,9 @@ def compress_image(msg, output): cv2_img = np.stack((cv2_img[:,:,0], cv2_img[:,:,1], np.zeros(cv2_img[:,:,0].shape)), axis = -1) # enforce <800px max dimension, and do a stride-based resize - if cv2_img.shape[0] > 800 or cv2_img.shape[1] > 800: - stride = int(np.ceil(max(cv2_img.shape[0] / 800.0, cv2_img.shape[1] / 800.0))) + # Edit: choose a smaller dimension for quicker encoding and larger dimension vice versa. + if cv2_img.shape[0] > max_heigh or cv2_img.shape[1] > max_width: + stride = int(np.ceil(max(cv2_img.shape[0] / max_heigh*1.0, cv2_img.shape[1] / max_width*1.0))) cv2_img = cv2_img[::stride,::stride] # if image format isn't already uint8, make it uint8 for visualization purposes @@ -195,8 +195,13 @@ def compress_image(msg, output): cv2_img = np.clip(cv2_img * 255, 0, 255).astype(np.uint8) try: + start = time.time() img_jpeg = encode_jpeg(cv2_img) + + end = time.time() + output["_warn"] = "encodejpeg elapsed = " + str(end - start) + "sec" output["_data_jpeg"] = base64.b64encode(img_jpeg).decode() + output["_warn"] += "\nlength of encoded jpeg string = " + str(len(base64.b64encode(img_jpeg).decode())) output["_data_shape"] = original_shape except OSError as e: output["_error"] = str(e) @@ -223,7 +228,10 @@ def compress_occupancy_grid(msg, output): except Exception as e: output["_error"] = str(e) try: + start = time.time() img_jpeg = encode_jpeg(cv2_img) + end = time.time() + output["_warn"] = (end - start) output["_data_jpeg"] = base64.b64encode(img_jpeg).decode() except OSError as e: output["_error"] = str(e) diff --git a/rosboard/html/js/viewers/ImageViewer.js b/rosboard/html/js/viewers/ImageViewer.js index d5b8dfde..6a7a7cd0 100644 --- a/rosboard/html/js/viewers/ImageViewer.js +++ b/rosboard/html/js/viewers/ImageViewer.js @@ -55,12 +55,13 @@ class ImageViewer extends Viewer { onData(msg) { this.card.title.text(msg._topic_name); - + console.time("image Viewer") if(msg.__comp) { this.decodeAndRenderCompressed(msg); } else { this.decodeAndRenderUncompressed(msg); } + console.timeEnd("image Viewer") } decodeAndRenderCompressed(msg) { diff --git a/rosboard/serialization.py b/rosboard/serialization.py index 10e53d89..fc250ab5 100644 --- a/rosboard/serialization.py +++ b/rosboard/serialization.py @@ -38,7 +38,7 @@ def ros2dict(msg): if (msg.__module__ == "sensor_msgs.msg._Image" or \ msg.__module__ == "sensor_msgs.msg._image") \ and field == "data": - rosboard.compression.compress_image(msg, output) + rosboard.compression.compress_image(msg, output, max_height=800, max_width=800) continue # OccupancyGrid: render and compress to jpeg From 530fc52ec01b21820d229f473ff2851067750690 Mon Sep 17 00:00:00 2001 From: Minh Nguyen Date: Tue, 28 Mar 2023 13:51:29 +0300 Subject: [PATCH 07/16] ignore vscode settings --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index bee8a64b..0c232bb0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ __pycache__ +.vscode/* From 83cc45131d73afb3fbf53c880ea64882c5c20452 Mon Sep 17 00:00:00 2001 From: JiaqiangZhang Date: Tue, 28 Mar 2023 15:12:43 +0300 Subject: [PATCH 08/16] add api for image quailty (not finished) --- rosboard/__init__.py | 4 +++- rosboard/handlers.py | 17 ++++++++++++++++- rosboard/html/js/index.js | 4 +++- .../html/js/transports/WebSocketV1Transport.js | 3 ++- rosboard/rosboard.py | 2 +- 5 files changed, 25 insertions(+), 5 deletions(-) diff --git a/rosboard/__init__.py b/rosboard/__init__.py index 42cf7cd5..24ee297f 100644 --- a/rosboard/__init__.py +++ b/rosboard/__init__.py @@ -1 +1,3 @@ -__version__ = "1.2.1" \ No newline at end of file +__version__ = "1.2.1" + +__img_quality__ = 50 \ No newline at end of file diff --git a/rosboard/handlers.py b/rosboard/handlers.py index e919453e..8fe3369f 100644 --- a/rosboard/handlers.py +++ b/rosboard/handlers.py @@ -8,7 +8,15 @@ import types import uuid -from . import __version__ +from . import __version__, __img_quality__ + +# class PostHandler(tornado.web.RequestHandler): +# def post(self): +# data = json.loads(self.request.body) +# self.write("Set image quality to: ", data) + +# def set_img_quality(img_quality): +# __img_quality__ = img_quality class NoCacheStaticFileHandler(tornado.web.StaticFileHandler): def set_extra_headers(self, path): @@ -18,6 +26,7 @@ def set_extra_headers(self, path): class ROSBoardSocketHandler(tornado.websocket.WebSocketHandler): sockets = set() joy_msg = None + img_quality_msg = 50 def initialize(self, node): # store the instance of the ROS node that created this WebSocketHandler so we can access it later @@ -193,6 +202,11 @@ def on_message(self, message): elif argv[0] == ROSBoardSocketHandler.JOY_MSG: ROSBoardSocketHandler.joy_msg = argv[1] + # image quality + elif argv[0] == ROSBoardSocketHandler.IMG_QUALITY: + # ROSBoardSocketHandler.img_quality_msg = argv[1] + __img_quality__ = argv[1] + ROSBoardSocketHandler.MSG_PING = "p"; ROSBoardSocketHandler.MSG_PONG = "q"; @@ -207,3 +221,4 @@ def on_message(self, message): ROSBoardSocketHandler.PONG_TIME = "t"; ROSBoardSocketHandler.JOY_MSG = "j"; +ROSBoardSocketHandler.IMG_QUALITY = "i"; \ No newline at end of file diff --git a/rosboard/html/js/index.js b/rosboard/html/js/index.js index 0023dcba..6b093a5d 100644 --- a/rosboard/html/js/index.js +++ b/rosboard/html/js/index.js @@ -81,8 +81,10 @@ function newCard() { // add prefixed card let onPrefixedCard = function() { let preSubscriptions = { - "/camera/rgb/image_raw": { topicType: "sensor_msgs/Image" }, "/camera/depth/points": { topicType: "sensor_msgs/PointCloud2" }, + "/odom": { topicType: "nav_msgs/msg/Odometry" }, + "/camera/rgb/image_raw": { topicType: "sensor_msgs/Image" }, + }; // console.log("preSubscriptions", preSubscriptions); diff --git a/rosboard/html/js/transports/WebSocketV1Transport.js b/rosboard/html/js/transports/WebSocketV1Transport.js index 3c34d7e5..c0438a33 100644 --- a/rosboard/html/js/transports/WebSocketV1Transport.js +++ b/rosboard/html/js/transports/WebSocketV1Transport.js @@ -97,4 +97,5 @@ class WebSocketV1Transport { WebSocketV1Transport.PONG_SEQ = "s"; WebSocketV1Transport.PONG_TIME = "t"; - WebSocketV1Transport.JOY_MSG = "j"; \ No newline at end of file + WebSocketV1Transport.JOY_MSG = "j"; + WebSocketV1Transport.IMG_QUALITY = "i"; \ No newline at end of file diff --git a/rosboard/rosboard.py b/rosboard/rosboard.py index 4f44aa0d..00063d03 100755 --- a/rosboard/rosboard.py +++ b/rosboard/rosboard.py @@ -24,7 +24,7 @@ from rosboard.subscribers.processes_subscriber import ProcessesSubscriber from rosboard.subscribers.system_stats_subscriber import SystemStatsSubscriber from rosboard.subscribers.dummy_subscriber import DummySubscriber -from rosboard.handlers import ROSBoardSocketHandler, NoCacheStaticFileHandler +from rosboard.handlers import ROSBoardSocketHandler, NoCacheStaticFileHandler #, PostHandler class ROSBoardNode(object): instance = None From 90c7bd70d44089dd1d7864bd5e6f66c1feba1397 Mon Sep 17 00:00:00 2001 From: JiaqiangZhang Date: Wed, 29 Mar 2023 19:49:17 +0300 Subject: [PATCH 09/16] Add point cloud, odom, and img prefixed card --- rosboard/html/css/index.css | 28 ++++-- rosboard/html/index.html | 2 + rosboard/html/js/index.js | 44 ++++++++-- rosboard/html/js/viewers/StatsViewer.js | 108 ++++++++++++++++++++++++ 4 files changed, 170 insertions(+), 12 deletions(-) create mode 100644 rosboard/html/js/viewers/StatsViewer.js diff --git a/rosboard/html/css/index.css b/rosboard/html/css/index.css index c1bd474e..11a585dd 100644 --- a/rosboard/html/css/index.css +++ b/rosboard/html/css/index.css @@ -111,17 +111,32 @@ input, textarea, *[contenteditable=true] { background:#202020; } +/* .grid::after { + content: ''; + display: block; + clear: both; +} */ + +.grid-sizer, +.card { + width: 30%; +} + .card { border-radius:5pt; overflow:hidden; - margin-left:20pt; + margin-left:5pt; margin-top:20pt; background:#303030; color:#c0c0c0; box-shadow:3pt 3pt 3pt rgba(0,0,0,0.2); - width:calc(100% - 40pt); + /* width:100%; */ + /* width:calc(100% - 10pt); */ } +.card--width2 { width:calc(60% - 10pt); } +.card--width3 { width:calc(30% - 10pt); } + .card-title { font-family:Titillium Web; font-size:12pt; @@ -153,6 +168,7 @@ input, textarea, *[contenteditable=true] { .card-buttons { position:absolute; + /* position:auto; */ display:flex; top:0; right:5pt; @@ -187,21 +203,21 @@ input, textarea, *[contenteditable=true] { @media screen and (min-width: 900px) { .card { display:inline-block; - width:calc(50% - 40pt); + /* width:calc(50% - 40pt); */ } } @media screen and (min-width: 1200px) { .card { display:inline-block; - width:calc(33% - 40pt); + /* width:calc(33% - 40pt); */ } } @media screen and (min-width: 1800px) { .card { display:inline-block; - width:calc(25% - 40pt); + /* width:calc(25% - 40pt); */ } } @@ -331,7 +347,7 @@ input, textarea, *[contenteditable=true] { /* width */ ::-webkit-scrollbar { - width: 10px; + width: 20px; } /* Track */ diff --git a/rosboard/html/index.html b/rosboard/html/index.html index 47d3f071..fbf28967 100644 --- a/rosboard/html/index.html +++ b/rosboard/html/index.html @@ -46,6 +46,8 @@
    +
    +
    diff --git a/rosboard/html/js/index.js b/rosboard/html/js/index.js index 6b093a5d..b88e762b 100644 --- a/rosboard/html/js/index.js +++ b/rosboard/html/js/index.js @@ -44,7 +44,8 @@ let $grid = null; $(() => { $grid = $('.grid').masonry({ itemSelector: '.card', - gutter: 10, + gutter: 0, + columnWidth: '.grid-sizer', percentPosition: true, }); $grid.masonry("layout"); @@ -71,10 +72,35 @@ function updateStoredSubscriptions() { } } -function newCard() { - // creates a new card, adds it to the grid, and returns it. - let card = $("
    ").addClass('card') - .appendTo($('.grid')); +function newCard({topicName, topicType}) { + var card = null; //document.createElement('div'); + // creates a new card, adds it to the grid, and returns it. style='width: 40%' + // let card = $("
    ").addClass('card') + // .appendTo($('.grid')); + console.log("topicType", topicType) + if(topicType == "sensor_msgs/PointCloud2"){ + // card.className = 'card-pc' + card = $("
    ")//.appendTo($('.grid'));//.addClass('card-pc') + // .appendTo($('.grid')); + console.log("pc card", card) + } + else if(topicType == "nav_msgs/msg/Odometry"){ + card = $("
    ")//.appendTo($('.grid'));//.addClass('card-odom') + // .appendTo($('.grid')); card-odom--width + console.log("odom card", card) + } + else if(topicType == "sensor_msgs/Image"){ + card = $("
    ")//.appendTo($('.grid'));//.addClass('card-img') + // .appendTo($('.grid')); card-img--width + console.log("img card", card) + } + else{ + card = $("
    ")//.appendTo($('.grid'));//.addClass('card-pc') + // .appendTo($('.grid')); card-pc--width + console.log("else card", card) + } + card = card.appendTo($('.grid')) + return card; } @@ -84,6 +110,11 @@ let onPrefixedCard = function() { "/camera/depth/points": { topicType: "sensor_msgs/PointCloud2" }, "/odom": { topicType: "nav_msgs/msg/Odometry" }, "/camera/rgb/image_raw": { topicType: "sensor_msgs/Image" }, + "/camera/rgb/image_raw1": { topicType: "sensor_msgs/Image" }, + "/camera/rgb/image_raw2": { topicType: "sensor_msgs/Image" }, + "/camera/rgb/image_raw3": { topicType: "sensor_msgs/Image" }, + "/camera/rgb/image_raw4": { topicType: "sensor_msgs/Image" }, + "/camera/rgb/image_raw5": { topicType: "sensor_msgs/Image" }, }; // console.log("preSubscriptions", preSubscriptions); @@ -217,7 +248,8 @@ function initSubscribe({topicName, topicType}) { } currentTransport.subscribe({topicName: topicName}); if(!subscriptions[topicName].viewer) { - let card = newCard(); + console.log('initSubscribe topicType', topicType) + let card = newCard({topicName, topicType}); let viewer = Viewer.getDefaultViewerForType(topicType); try { subscriptions[topicName].viewer = new viewer(card, topicName, topicType); diff --git a/rosboard/html/js/viewers/StatsViewer.js b/rosboard/html/js/viewers/StatsViewer.js new file mode 100644 index 00000000..cb4bf360 --- /dev/null +++ b/rosboard/html/js/viewers/StatsViewer.js @@ -0,0 +1,108 @@ +"use strict"; + +// Viewer for /rosout and other logs that can be expressed in +// rcl_interfaces/msgs/Log format. + +class StatsViewer extends Viewer { + /** + * Gets called when Viewer is first initialized. + * @override + **/ + onCreate() { + this.card.title.text("StatsViewer"); + + // wrapper and wrapper2 are css BS that are necessary to + // have something that is 100% width but fixed aspect ratio + this.wrapper = $('
    ') + .css({ + "position": "relative", + "width": "100%", + }) + .appendTo(this.card.content); + + this.wrapper2 = $('
    ') + .css({ + "width": "100%", + "padding-bottom": "80%", + "background": "#101010", + "position": "relative", + "overflow": "hidden", + }) + .appendTo(this.wrapper); + + // actual log container, put it inside wrapper2 + this.logContainer = $('
    ') + .addClass("monospace") + .css({ + "position": "absolute", + "width": "100%", + "height": "100%", + "font-size": "7pt", + "line-height": "1.4em", + "overflow-y": "hidden", + "overflow-x": "hidden", + }) + .appendTo(this.wrapper2); + + // add the first log + $('
    ') + .text("Logs will appear here.") + .appendTo(this.logContainer); + + super.onCreate(); + + let that = this; + this.logScrollTimeout = setTimeout(() => { + that.logContainer[0].scrollTop = that.logContainer[0].scrollHeight; + }, 1000); + } + + onData(msg) { + while(this.logContainer.children().length > 30) { + this.logContainer.children()[0].remove(); + } + + this.card.title.text(msg._topic_name); + + let color = "#c0c0c0"; // default color + let level_text = ""; + + // set colors based on log level, if defined + // 10-20-30-40-50 is ROS2, 1-2-4-8-16 is ROS1 + if(msg.level === 10 || msg.level === 1) { level_text = "DEBUG"; color = "#00a000"; } + if(msg.level === 20 || msg.level === 2) { level_text = "INFO"; color = "#a0a0a0"; } + if(msg.level === 30 || msg.level === 4) { level_text = "WARN"; color = "#c0c000"; } + if(msg.level === 40 || msg.level === 8) { level_text = "ERROR"; color = "#ff4040"; } + if(msg.level === 50 || msg.level === 16) { level_text = "FATAL"; color = "#ff0000"; } + + let text = ""; + if(level_text !== "") text += "[" + level_text + "] " + if(msg.name) text += "[" + msg.name + "] "; + text += msg.msg; + + text = text + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'") + .replace(/\n/g, "
    \n") + .replace(/(\[[0-9\.]*\])/g, '$1'); + + $('
    ') + .html(text) + .css({ "color": color }) + .appendTo(this.logContainer); + + this.logContainer[0].scrollTop = this.logContainer[0].scrollHeight; + } +} + +StatsViewer.friendlyName = "Log view"; + +StatsViewer.supportedTypes = [ + "rcl_interfaces/msg/Log", + "rosgraph_msgs/msg/Log", +]; + +Viewer.registerViewer(StatsViewer); \ No newline at end of file From 09d4d7828759e416da81b0408535ea7ca761db60 Mon Sep 17 00:00:00 2001 From: JiaqiangZhang Date: Thu, 30 Mar 2023 13:35:23 +0300 Subject: [PATCH 10/16] Add fixed 'standup' and 'sitdown' button (function unfinished) --- rosboard/html/css/index.css | 17 ++++++++++++++--- rosboard/html/index.html | 6 ++++++ rosboard/html/js/index.js | 6 +++--- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/rosboard/html/css/index.css b/rosboard/html/css/index.css index 11a585dd..e883efec 100644 --- a/rosboard/html/css/index.css +++ b/rosboard/html/css/index.css @@ -111,6 +111,12 @@ input, textarea, *[contenteditable=true] { background:#202020; } +.float-button { + position: fixed; + bottom: 90px; + right: 50px; +} + /* .grid::after { content: ''; display: block; @@ -119,7 +125,7 @@ input, textarea, *[contenteditable=true] { .grid-sizer, .card { - width: 30%; + width: 20%; } .card { @@ -134,8 +140,13 @@ input, textarea, *[contenteditable=true] { /* width:calc(100% - 10pt); */ } -.card--width2 { width:calc(60% - 10pt); } -.card--width3 { width:calc(30% - 10pt); } +.card--width-pc { width:calc(60% - 10pt); } +.card--width-odom { width:calc(40% - 10pt); } +.card--width-img { width:calc(20% - 10pt); } + +.card--height-pc { height:500px; } +.card--height-odom { height:200px; } +.card--height-img { height:200px; } .card-title { font-family:Titillium Web; diff --git a/rosboard/html/index.html b/rosboard/html/index.html index fbf28967..c914bf2a 100644 --- a/rosboard/html/index.html +++ b/rosboard/html/index.html @@ -49,6 +49,12 @@
    +
    + + + +
    + diff --git a/rosboard/html/js/index.js b/rosboard/html/js/index.js index b88e762b..63f6ca30 100644 --- a/rosboard/html/js/index.js +++ b/rosboard/html/js/index.js @@ -80,17 +80,17 @@ function newCard({topicName, topicType}) { console.log("topicType", topicType) if(topicType == "sensor_msgs/PointCloud2"){ // card.className = 'card-pc' - card = $("
    ")//.appendTo($('.grid'));//.addClass('card-pc') + card = $("
    ")//.appendTo($('.grid'));//.addClass('card-pc') // .appendTo($('.grid')); console.log("pc card", card) } else if(topicType == "nav_msgs/msg/Odometry"){ - card = $("
    ")//.appendTo($('.grid'));//.addClass('card-odom') + card = $("
    ")//.appendTo($('.grid'));//.addClass('card-odom') // .appendTo($('.grid')); card-odom--width console.log("odom card", card) } else if(topicType == "sensor_msgs/Image"){ - card = $("
    ")//.appendTo($('.grid'));//.addClass('card-img') + card = $("
    ")//.appendTo($('.grid'));//.addClass('card-img') // .appendTo($('.grid')); card-img--width console.log("img card", card) } From 42195048cc89b15accb61f7fd53e4257cdb358d6 Mon Sep 17 00:00:00 2001 From: Minh Nguyen Date: Fri, 31 Mar 2023 09:34:11 +0300 Subject: [PATCH 11/16] typo in compression_image() --- rosboard/compression.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rosboard/compression.py b/rosboard/compression.py index 9656a87e..1651376a 100644 --- a/rosboard/compression.py +++ b/rosboard/compression.py @@ -153,7 +153,7 @@ def compress_compressed_image(msg, output): output["_data_jpeg"] = base64.b64encode(img_jpeg).decode() output["_data_shape"] = list(original_shape) -def compress_image(msg, output, max_heigh = 800, max_width = 800): +def compress_image(msg, output, max_height = 800, max_width = 800): output["data"] = [] output["__comp"] = ["data"] @@ -175,8 +175,8 @@ def compress_image(msg, output, max_heigh = 800, max_width = 800): # enforce <800px max dimension, and do a stride-based resize # Edit: choose a smaller dimension for quicker encoding and larger dimension vice versa. - if cv2_img.shape[0] > max_heigh or cv2_img.shape[1] > max_width: - stride = int(np.ceil(max(cv2_img.shape[0] / max_heigh*1.0, cv2_img.shape[1] / max_width*1.0))) + if cv2_img.shape[0] > max_height or cv2_img.shape[1] > max_width: + stride = int(np.ceil(max(cv2_img.shape[0] / max_height*1.0, cv2_img.shape[1] / max_width*1.0))) cv2_img = cv2_img[::stride,::stride] # if image format isn't already uint8, make it uint8 for visualization purposes From 9645090cac4cb08325b74183bb8fa41e4c9b71bf Mon Sep 17 00:00:00 2001 From: Minh Nguyen Date: Tue, 18 Apr 2023 12:23:58 +0300 Subject: [PATCH 12/16] prefixed card for /high_state topic --- rosboard/compression.py | 2 +- rosboard/html/css/index.css | 4 +++- rosboard/html/js/index.js | 23 ++++++++++++++--------- rosboard/html/js/viewers/StatsViewer.js | 1 + rosboard/rosboard.py | 2 +- setup.py | 4 ++-- 6 files changed, 22 insertions(+), 14 deletions(-) diff --git a/rosboard/compression.py b/rosboard/compression.py index 1651376a..6f92f9eb 100644 --- a/rosboard/compression.py +++ b/rosboard/compression.py @@ -29,7 +29,7 @@ def decode_jpeg(input_bytes): def encode_jpeg(img): # use img_quality to change image quality between 0 and 100 - img_quality = 50 + __img_quality__ = 50 if simplejpeg: if len(img.shape) == 2: img = np.expand_dims(img, axis=2) diff --git a/rosboard/html/css/index.css b/rosboard/html/css/index.css index e883efec..60e52d9a 100644 --- a/rosboard/html/css/index.css +++ b/rosboard/html/css/index.css @@ -130,7 +130,7 @@ input, textarea, *[contenteditable=true] { .card { border-radius:5pt; - overflow:hidden; + overflow:scroll; margin-left:5pt; margin-top:20pt; background:#303030; @@ -143,10 +143,12 @@ input, textarea, *[contenteditable=true] { .card--width-pc { width:calc(60% - 10pt); } .card--width-odom { width:calc(40% - 10pt); } .card--width-img { width:calc(20% - 10pt); } +.card--width-state { width:calc(30% - 10pt); } .card--height-pc { height:500px; } .card--height-odom { height:200px; } .card--height-img { height:200px; } +.card--height-state { height: 400px; scroll-behavior: smooth;} .card-title { font-family:Titillium Web; diff --git a/rosboard/html/js/index.js b/rosboard/html/js/index.js index 63f6ca30..c15c4f43 100644 --- a/rosboard/html/js/index.js +++ b/rosboard/html/js/index.js @@ -15,6 +15,7 @@ importJsOnce("js/viewers/DiagnosticViewer.js"); importJsOnce("js/viewers/TimeSeriesPlotViewer.js"); importJsOnce("js/viewers/PointCloud2Viewer.js"); importJsOnce("js/viewers/JoystickController.js"); +importJsOnce("js/viewers/StatsViewer.js") // GenericViewer must be last importJsOnce("js/viewers/GenericViewer.js"); @@ -94,6 +95,10 @@ function newCard({topicName, topicType}) { // .appendTo($('.grid')); card-img--width console.log("img card", card) } + else if(topicType == "unitree_legged_msgs/HighState"){ + card = $("
    ") + console.log("state card", card) + } else{ card = $("
    ")//.appendTo($('.grid'));//.addClass('card-pc') // .appendTo($('.grid')); card-pc--width @@ -107,15 +112,15 @@ function newCard({topicName, topicType}) { // add prefixed card let onPrefixedCard = function() { let preSubscriptions = { - "/camera/depth/points": { topicType: "sensor_msgs/PointCloud2" }, - "/odom": { topicType: "nav_msgs/msg/Odometry" }, - "/camera/rgb/image_raw": { topicType: "sensor_msgs/Image" }, - "/camera/rgb/image_raw1": { topicType: "sensor_msgs/Image" }, - "/camera/rgb/image_raw2": { topicType: "sensor_msgs/Image" }, - "/camera/rgb/image_raw3": { topicType: "sensor_msgs/Image" }, - "/camera/rgb/image_raw4": { topicType: "sensor_msgs/Image" }, - "/camera/rgb/image_raw5": { topicType: "sensor_msgs/Image" }, - + // "/camera/depth/points": { topicType: "sensor_msgs/PointCloud2" }, + // "/odom": { topicType: "nav_msgs/msg/Odometry" }, + // "/camera/rgb/image_raw": { topicType: "sensor_msgs/Image" }, + // "/camera/rgb/image_raw1": { topicType: "sensor_msgs/Image" }, + // "/camera/rgb/image_raw2": { topicType: "sensor_msgs/Image" }, + // "/camera/rgb/image_raw3": { topicType: "sensor_msgs/Image" }, + // "/camera/rgb/image_raw4": { topicType: "sensor_msgs/Image" }, + // "/camera/rgb/image_raw5": { topicType: "sensor_msgs/Image" }, + "/high_state": { topicType: "unitree_legged_msgs/HighState"}, }; // console.log("preSubscriptions", preSubscriptions); diff --git a/rosboard/html/js/viewers/StatsViewer.js b/rosboard/html/js/viewers/StatsViewer.js index cb4bf360..289d7a7a 100644 --- a/rosboard/html/js/viewers/StatsViewer.js +++ b/rosboard/html/js/viewers/StatsViewer.js @@ -101,6 +101,7 @@ class StatsViewer extends Viewer { StatsViewer.friendlyName = "Log view"; StatsViewer.supportedTypes = [ + "b1_legged_msgs/HighState", "rcl_interfaces/msg/Log", "rosgraph_msgs/msg/Log", ]; diff --git a/rosboard/rosboard.py b/rosboard/rosboard.py index 00063d03..310e5bb0 100755 --- a/rosboard/rosboard.py +++ b/rosboard/rosboard.py @@ -236,7 +236,7 @@ def sync_subs(self): { "_topic_name": topic_name, # special non-ros topics start with _ "_topic_type": topic_type, - "_error": "Could not load message type '%s'. Are the .msg files for it source-bashed?" % topic_type, + "_error": "Could not load message '{}' of type '{}'. Are the .msg files for it source-bashed?".format(topic_name,topic_type), }, ] ) diff --git a/setup.py b/setup.py index a2ca5d77..12c88a5c 100644 --- a/setup.py +++ b/setup.py @@ -29,8 +29,8 @@ ] }, #zip_safe=True, - maintainer='dheera', - maintainer_email='dheera.r.e.m.o.v.e.t.h.i.s@dheera.net', + maintainer=['jiaqiang', 'minh'], + maintainer_email=['jizhan@utu.fi', 'mhnguy@utu.fi'], description='ROS node that turns your robot into a web server to visualize ROS topics', license='BSD', tests_require=['pytest'], From 5e9c16031eddc9fbcd1bc150945b9f7b4d1d5b96 Mon Sep 17 00:00:00 2001 From: Minh Nguyen Date: Tue, 18 Apr 2023 17:10:27 +0300 Subject: [PATCH 13/16] selective field from /high_state messages --- rosboard/html/css/index.css | 4 +- rosboard/html/js/viewers/HighStateViewer.js | 98 +++++++++++++++++++++ rosboard/html/js/viewers/StatsViewer.js | 2 +- 3 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 rosboard/html/js/viewers/HighStateViewer.js diff --git a/rosboard/html/css/index.css b/rosboard/html/css/index.css index 60e52d9a..ffe1ea7a 100644 --- a/rosboard/html/css/index.css +++ b/rosboard/html/css/index.css @@ -143,12 +143,12 @@ input, textarea, *[contenteditable=true] { .card--width-pc { width:calc(60% - 10pt); } .card--width-odom { width:calc(40% - 10pt); } .card--width-img { width:calc(20% - 10pt); } -.card--width-state { width:calc(30% - 10pt); } +.card--width-state { width:calc(25% - 10pt); } .card--height-pc { height:500px; } .card--height-odom { height:200px; } .card--height-img { height:200px; } -.card--height-state { height: 400px; scroll-behavior: smooth;} +.card--height-state { height: 600px; scroll-behavior: smooth;} .card-title { font-family:Titillium Web; diff --git a/rosboard/html/js/viewers/HighStateViewer.js b/rosboard/html/js/viewers/HighStateViewer.js new file mode 100644 index 00000000..0756b51c --- /dev/null +++ b/rosboard/html/js/viewers/HighStateViewer.js @@ -0,0 +1,98 @@ +"use strict"; + +// GenericViewer just displays message fields and values in a table. +// It can be used on any ROS type. + +class HighStateViewer extends Viewer { + /** + * Gets called when Viewer is first initialized. + * @override + **/ + onCreate() { + this.viewerNode = $('
    ') + .css({'font-size': '11pt'}) + .appendTo(this.card.content); + + this.viewerNodeFadeTimeout = null; + + this.expandFields = { }; + this.fieldNodes = { }; + this.dataTable = $('
    ') + .addClass('mdl-data-table') + .addClass('mdl-js-data-table') + .css({'width': '100%', 'min-height': '30pt', 'table-layout': 'fixed' }) + .appendTo(this.viewerNode); + + console.log("High State viewer created") + super.onCreate(); + } + + onData(data) { + this.card.title.text(data._topic_name); + console.log(data) + for(let field in data) { + // if(field[0] === "_") continue; + // if(field === "header") continue; + // if(field === "name") continue; + if(field != "imu" && field != "bms" && field != "bodyHeight"){ // only show specific field of /high_state messages + continue; + } + if(!this.fieldNodes[field]) { + let tr = $('') + .appendTo(this.dataTable); + $('') + .addClass('mdl-data-table__cell--non-numeric') + .text(field) + .css({'width': '10%', 'font-weight': 'bold', 'overflow': 'hidden', 'text-overflow': 'ellipsis'}) + .appendTo(tr); + this.fieldNodes[field] = $('') + .addClass('mdl-data-table__cell--non-numeric') + .addClass('monospace') + .css({'overflow': 'hidden', 'text-overflow': 'ellipsis'}) + .appendTo(tr); + let that = this; + this.fieldNodes[field].click(() => {that.expandFields[field] = !that.expandFields[field]; }); + } + + if(data[field].uuid) { + this.fieldNodes[field].text(data[field].uuid.map((byte) => ((byte<16) ? "0": "") + (byte & 0xFF).toString(16)).join('')); + this.fieldNodes[field].css({"color": "#808080"}); + continue; + } + + if(typeof(data[field])==="boolean") { + if(data[field] === true) { + this.fieldNodes[field].text("true"); + this.fieldNodes[field].css({"color": "#80ff80"}); + } else { + this.fieldNodes[field].text("false"); + this.fieldNodes[field].css({"color": "#ff8080"}); + } + continue; + } + + if(data.__comp && data.__comp.includes(field)) { + this.fieldNodes[field][0].innerHTML = "(compressed)"; + continue; + } + + if(this.expandFields[field]) { + this.fieldNodes[field][0].innerHTML = ( + JSON.stringify(data[field], null, ' ') + .replace(/\n/g, "
    ") + .replace(/ /g, " ") + ); + } else { + this.fieldNodes[field][0].innerHTML = JSON.stringify(data[field], null, ' '); + } + } + } +} + +HighStateViewer.friendlyName = "High State message"; + +HighStateViewer.supportedTypes = [ + "unitree_legged_msgs/msg/HighState", +]; + +Viewer.registerViewer(HighStateViewer); \ No newline at end of file diff --git a/rosboard/html/js/viewers/StatsViewer.js b/rosboard/html/js/viewers/StatsViewer.js index 289d7a7a..c650dcc5 100644 --- a/rosboard/html/js/viewers/StatsViewer.js +++ b/rosboard/html/js/viewers/StatsViewer.js @@ -101,9 +101,9 @@ class StatsViewer extends Viewer { StatsViewer.friendlyName = "Log view"; StatsViewer.supportedTypes = [ - "b1_legged_msgs/HighState", "rcl_interfaces/msg/Log", "rosgraph_msgs/msg/Log", + "rosboard_msgs/msg/SystemStats", ]; Viewer.registerViewer(StatsViewer); \ No newline at end of file From 8aae1a195f3b9bf519a9c096a040c3854b0f6061 Mon Sep 17 00:00:00 2001 From: Minh Nguyen Date: Wed, 19 Apr 2023 18:25:13 +0300 Subject: [PATCH 14/16] button card for publishing /high_cmd --- rosboard/handlers.py | 6 ++- rosboard/html/css/index.css | 23 ++++++++ rosboard/html/index.html | 8 +-- rosboard/html/js/index.js | 7 +++ .../js/transports/WebSocketV1Transport.js | 20 +++++-- rosboard/html/js/viewers/HighCmdViewer.js | 53 +++++++++++++++++++ rosboard/html/js/viewers/HighStateViewer.js | 1 - rosboard/rosboard.py | 20 +++++++ 8 files changed, 129 insertions(+), 9 deletions(-) create mode 100644 rosboard/html/js/viewers/HighCmdViewer.js diff --git a/rosboard/handlers.py b/rosboard/handlers.py index 8fe3369f..14c95ce3 100644 --- a/rosboard/handlers.py +++ b/rosboard/handlers.py @@ -26,6 +26,7 @@ def set_extra_headers(self, path): class ROSBoardSocketHandler(tornado.websocket.WebSocketHandler): sockets = set() joy_msg = None + highcmd_msg = None img_quality_msg = 50 def initialize(self, node): @@ -201,7 +202,9 @@ def on_message(self, message): # Joy elif argv[0] == ROSBoardSocketHandler.JOY_MSG: ROSBoardSocketHandler.joy_msg = argv[1] - + # HighCmd + elif argv[0] == ROSBoardSocketHandler.HIGHCMD_MSG: + ROSBoardSocketHandler.highcmd_msg = argv[1] # image quality elif argv[0] == ROSBoardSocketHandler.IMG_QUALITY: # ROSBoardSocketHandler.img_quality_msg = argv[1] @@ -221,4 +224,5 @@ def on_message(self, message): ROSBoardSocketHandler.PONG_TIME = "t"; ROSBoardSocketHandler.JOY_MSG = "j"; +ROSBoardSocketHandler.HIGHCMD_MSG = "h"; ROSBoardSocketHandler.IMG_QUALITY = "i"; \ No newline at end of file diff --git a/rosboard/html/css/index.css b/rosboard/html/css/index.css index ffe1ea7a..810123ba 100644 --- a/rosboard/html/css/index.css +++ b/rosboard/html/css/index.css @@ -144,11 +144,13 @@ input, textarea, *[contenteditable=true] { .card--width-odom { width:calc(40% - 10pt); } .card--width-img { width:calc(20% - 10pt); } .card--width-state { width:calc(25% - 10pt); } +.card--width-highcmd { width:200px; } .card--height-pc { height:500px; } .card--height-odom { height:200px; } .card--height-img { height:200px; } .card--height-state { height: 600px; scroll-behavior: smooth;} +.card--height-highcmd { height: 100px;} .card-title { font-family:Titillium Web; @@ -205,6 +207,27 @@ input, textarea, *[contenteditable=true] { cursor:pointer; } +#stand-up-button{ + background-color: white; + color: black; + border: 2px solid #4CAF50; +} + +#stand-up-button:hover{ + background-color: #4CAF50; + color: white; +} + +#sit-down-button{ + background-color: white; + color: black; + border: 2px solid #555555; +} + +#sit-down-button:hover{ + background-color: #555555; + color: white; +} .card-content { -webkit-transition: opacity 0.3s ease; -moz-transition: opacity 0.3s ease; diff --git a/rosboard/html/index.html b/rosboard/html/index.html index c914bf2a..d37c0fbb 100644 --- a/rosboard/html/index.html +++ b/rosboard/html/index.html @@ -49,11 +49,11 @@
    -
    - - +
    diff --git a/rosboard/html/js/index.js b/rosboard/html/js/index.js index c15c4f43..10768733 100644 --- a/rosboard/html/js/index.js +++ b/rosboard/html/js/index.js @@ -16,6 +16,8 @@ importJsOnce("js/viewers/TimeSeriesPlotViewer.js"); importJsOnce("js/viewers/PointCloud2Viewer.js"); importJsOnce("js/viewers/JoystickController.js"); importJsOnce("js/viewers/StatsViewer.js") +importJsOnce("js/viewers/HighStateViewer.js") +importJsOnce("js/viewers/HighCmdViewer.js") // GenericViewer must be last importJsOnce("js/viewers/GenericViewer.js"); @@ -99,6 +101,10 @@ function newCard({topicName, topicType}) { card = $("
    ") console.log("state card", card) } + else if(topicType == "unitree_legged_msgs/HighCmd"){ + card = $("
    ") + console.log("state card", card) + } else{ card = $("
    ")//.appendTo($('.grid'));//.addClass('card-pc') // .appendTo($('.grid')); card-pc--width @@ -121,6 +127,7 @@ let onPrefixedCard = function() { // "/camera/rgb/image_raw4": { topicType: "sensor_msgs/Image" }, // "/camera/rgb/image_raw5": { topicType: "sensor_msgs/Image" }, "/high_state": { topicType: "unitree_legged_msgs/HighState"}, + "/high_cmd": { topicType: "unitree_legged_msgs/HighCmd"}, }; // console.log("preSubscriptions", preSubscriptions); diff --git a/rosboard/html/js/transports/WebSocketV1Transport.js b/rosboard/html/js/transports/WebSocketV1Transport.js index c0438a33..e522b0fc 100644 --- a/rosboard/html/js/transports/WebSocketV1Transport.js +++ b/rosboard/html/js/transports/WebSocketV1Transport.js @@ -10,6 +10,8 @@ class WebSocketV1Transport { this.ws = null; this.joystickX = 0.0; this.joystickY = 0.0; + this.mode = 0; + this.bodyHeight = 0.0; } connect() { @@ -59,11 +61,13 @@ class WebSocketV1Transport { else if(wsMsgType === WebSocketV1Transport.MSG_MSG && that.onMsg) that.onMsg(data[1]); else if(wsMsgType === WebSocketV1Transport.MSG_TOPICS && that.onTopics) that.onTopics(data[1]); else if(wsMsgType === WebSocketV1Transport.MSG_SYSTEM && that.onSystem) that.onSystem(data[1]); + else console.log("received unknown message: " + wsmsg.data); - this.send(JSON.stringify([WebSocketV1Transport.JOY_MSG, { - ["x"]: that.joystickX.toFixed(3), - ["y"]: that.joystickY.toFixed(3),}])); + // this.send(JSON.stringify([WebSocketV1Transport.JOY_MSG, { + // ["x"]: that.joystickX.toFixed(3), + // ["y"]: that.joystickY.toFixed(3),}])); + } } @@ -83,6 +87,15 @@ class WebSocketV1Transport { this.joystickX = joystickX; this.joystickY = joystickY; } + update_highcmd({mode, bodyHeight}) { + console.log("sending high_cmd ...") + this.mode = mode; + this.bodyHeight = bodyHeight; + this.ws.send(JSON.stringify([WebSocketV1Transport.HIGHCMD_MSG, { + ["mode"]: mode, + ["bodyHeight"]: bodyHeight + }])); + } } WebSocketV1Transport.MSG_PING = "p"; @@ -98,4 +111,5 @@ class WebSocketV1Transport { WebSocketV1Transport.PONG_TIME = "t"; WebSocketV1Transport.JOY_MSG = "j"; + WebSocketV1Transport.HIGHCMD_MSG = "h" WebSocketV1Transport.IMG_QUALITY = "i"; \ No newline at end of file diff --git a/rosboard/html/js/viewers/HighCmdViewer.js b/rosboard/html/js/viewers/HighCmdViewer.js new file mode 100644 index 00000000..641e3bc4 --- /dev/null +++ b/rosboard/html/js/viewers/HighCmdViewer.js @@ -0,0 +1,53 @@ +"use strict"; + +class HighCmdController extends Viewer { + /** + * Gets called when Viewer is first initialized. + * @override + **/ + onCreate() { + this.viewer = $('
    ') + .css({'class': 'card-buttons'}) + .appendTo(this.card.content); + + this.btnStandUpId = "stand-up-button" ; + this.btnSitDownId = "sit-down-button" ; + this.btnStandUp = $('') + .css({ "class": "card-button"}) + .text("Stand Up") + .appendTo(this.viewer); + this.btnSitDown = $('') + .css({"class": "card-button"}) + .text("Sit Down") + .appendTo(this.viewer); + + this.btnStandUp.on("click", + function(event){ + let mode = 6 // mode 6 = position stand up + let bodyHeight = 0.5 + currentTransport.update_highcmd({mode, bodyHeight}); + }); + this.btnSitDown.on("click", + function(event){ + let mode = 5 // mode 5 = position stand down + let bodyHeight = 0.0 + currentTransport.update_highcmd({mode, bodyHeight}); + }); + console.log("HighCmd Viewer created"); + } + onData(msg) { + this.card.title.text(msg._topic_name); + } + +} + +HighCmdController.friendlyName = "High Cmd Controller"; + +HighCmdController.supportedTypes = [ + "unitree_legged_msgs/msg/HighCmd", +]; + + +HighCmdController.maxUpdateRate = 0.5; + +Viewer.registerViewer(HighCmdController); \ No newline at end of file diff --git a/rosboard/html/js/viewers/HighStateViewer.js b/rosboard/html/js/viewers/HighStateViewer.js index 0756b51c..068249a8 100644 --- a/rosboard/html/js/viewers/HighStateViewer.js +++ b/rosboard/html/js/viewers/HighStateViewer.js @@ -29,7 +29,6 @@ class HighStateViewer extends Viewer { onData(data) { this.card.title.text(data._topic_name); - console.log(data) for(let field in data) { // if(field[0] === "_") continue; // if(field === "header") continue; diff --git a/rosboard/rosboard.py b/rosboard/rosboard.py index 310e5bb0..e2b23462 100755 --- a/rosboard/rosboard.py +++ b/rosboard/rosboard.py @@ -18,6 +18,7 @@ from geometry_msgs.msg import Twist from rosgraph_msgs.msg import Log +from unitree_legged_msgs.msg import HighCmd from rosboard.serialization import ros2dict from rosboard.subscribers.dmesg_subscriber import DMesgSubscriber @@ -60,6 +61,8 @@ def __init__(self, node_name = "rosboard_node"): self.twist_pub = rospy.Publisher('/cmd_vel', Twist, queue_size=100) + self.highcmd_pub = rospy.Publisher('/high_cmd', HighCmd, queue_size=1) + tornado_settings = { 'debug': True, 'static_path': os.path.join(os.path.dirname(os.path.realpath(__file__)), 'html') @@ -97,6 +100,9 @@ def __init__(self, node_name = "rosboard_node"): # loop to send client joy message to ros topic threading.Thread(target = self.joy_loop, daemon = True).start() + # loop to send command message to ros topic /high_cmd + threading.Thread(target = self.highcmd_loop, daemon = True).start() + self.lock = threading.Lock() rospy.loginfo("ROSboard listening on :%d" % self.port) @@ -142,6 +148,20 @@ def joy_loop(self): twist.angular.z = -float(ROSBoardSocketHandler.joy_msg['x']) * 2.0 self.twist_pub.publish(twist) + def highcmd_loop(self): + """ + Sending HighCmd message from client + """ + highcmd = HighCmd() + while True: + time.sleep(0.1) + if not isinstance(ROSBoardSocketHandler.highcmd_msg, dict): + continue + else: + highcmd.mode = ROSBoardSocketHandler.highcmd_msg['mode'] + highcmd.bodyHeight = ROSBoardSocketHandler.highcmd_msg['bodyHeight'] + self.highcmd_pub.publish(highcmd) + def pingpong_loop(self): """ Loop to send pings to all active sockets every 5 seconds. From bf86866d25b8e6d05a7b541b1fe9bec3ff8bd58e Mon Sep 17 00:00:00 2001 From: Minh Nguyen Date: Fri, 21 Apr 2023 14:11:59 +0300 Subject: [PATCH 15/16] /high_cmd state update once per button click --- rosboard/html/js/transports/WebSocketV1Transport.js | 6 +++--- rosboard/rosboard.py | 10 ++++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/rosboard/html/js/transports/WebSocketV1Transport.js b/rosboard/html/js/transports/WebSocketV1Transport.js index e522b0fc..3948ab21 100644 --- a/rosboard/html/js/transports/WebSocketV1Transport.js +++ b/rosboard/html/js/transports/WebSocketV1Transport.js @@ -68,6 +68,7 @@ class WebSocketV1Transport { // ["x"]: that.joystickX.toFixed(3), // ["y"]: that.joystickY.toFixed(3),}])); + } } @@ -88,12 +89,11 @@ class WebSocketV1Transport { this.joystickY = joystickY; } update_highcmd({mode, bodyHeight}) { - console.log("sending high_cmd ...") this.mode = mode; this.bodyHeight = bodyHeight; this.ws.send(JSON.stringify([WebSocketV1Transport.HIGHCMD_MSG, { - ["mode"]: mode, - ["bodyHeight"]: bodyHeight + ["mode"]: this.mode, + ["bodyHeight"]: this.bodyHeight }])); } } diff --git a/rosboard/rosboard.py b/rosboard/rosboard.py index e2b23462..41c55b8d 100755 --- a/rosboard/rosboard.py +++ b/rosboard/rosboard.py @@ -141,7 +141,7 @@ def joy_loop(self): twist = Twist() while True: time.sleep(0.1) - if not isinstance(ROSBoardSocketHandler.joy_msg, dict): + if not (ROSBoardSocketHandler.joy_msg): # return False if joymsg is empty continue if 'x' in ROSBoardSocketHandler.joy_msg and 'y' in ROSBoardSocketHandler.joy_msg: twist.linear.x = -float(ROSBoardSocketHandler.joy_msg['y']) * 3.0 @@ -155,11 +155,13 @@ def highcmd_loop(self): highcmd = HighCmd() while True: time.sleep(0.1) - if not isinstance(ROSBoardSocketHandler.highcmd_msg, dict): + if not (ROSBoardSocketHandler.highcmd_msg): # return False if highcmd_msg is empty continue else: - highcmd.mode = ROSBoardSocketHandler.highcmd_msg['mode'] - highcmd.bodyHeight = ROSBoardSocketHandler.highcmd_msg['bodyHeight'] + print("[INFO] WebSocketTransport -> highcmd_msg = ",ROSBoardSocketHandler.highcmd_msg) + highcmd.mode = ROSBoardSocketHandler.highcmd_msg["mode"] + highcmd.bodyHeight = ROSBoardSocketHandler.highcmd_msg["bodyHeight"] + ROSBoardSocketHandler.highcmd_msg = {} # clear highcmd_msg self.highcmd_pub.publish(highcmd) def pingpong_loop(self): From 6207b278b9494ac2752b6bb8a3446b9d08b68806 Mon Sep 17 00:00:00 2001 From: Minh Nguyen Date: Mon, 5 Jun 2023 23:10:38 +0300 Subject: [PATCH 16/16] fixed nipplejs lib missing error and cmd_vel topic for b1 --- rosboard/html/index.html | 2 +- rosboard/html/js/nipplejs.js | 1 + rosboard/html/js/viewers/JoystickController.js | 1 + rosboard/rosboard.py | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 rosboard/html/js/nipplejs.js diff --git a/rosboard/html/index.html b/rosboard/html/index.html index d37c0fbb..92c16c28 100644 --- a/rosboard/html/index.html +++ b/rosboard/html/index.html @@ -9,7 +9,7 @@ - + diff --git a/rosboard/html/js/nipplejs.js b/rosboard/html/js/nipplejs.js new file mode 100644 index 00000000..5e40ed7b --- /dev/null +++ b/rosboard/html/js/nipplejs.js @@ -0,0 +1 @@ +!function(t,i){"object"==typeof exports&&"object"==typeof module?module.exports=i():"function"==typeof define&&define.amd?define("nipplejs",[],i):"object"==typeof exports?exports.nipplejs=i():t.nipplejs=i()}(window,function(){return function(t){var i={};function e(o){if(i[o])return i[o].exports;var n=i[o]={i:o,l:!1,exports:{}};return t[o].call(n.exports,n,n.exports,e),n.l=!0,n.exports}return e.m=t,e.c=i,e.d=function(t,i,o){e.o(t,i)||Object.defineProperty(t,i,{enumerable:!0,get:o})},e.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},e.t=function(t,i){if(1&i&&(t=e(t)),8&i)return t;if(4&i&&"object"==typeof t&&t&&t.__esModule)return t;var o=Object.create(null);if(e.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:t}),2&i&&"string"!=typeof t)for(var n in t)e.d(o,n,function(i){return t[i]}.bind(null,n));return o},e.n=function(t){var i=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(i,"a",i),i},e.o=function(t,i){return Object.prototype.hasOwnProperty.call(t,i)},e.p="",e(e.s=0)}([function(t,i,e){"use strict";e.r(i);var o,n=function(t,i){var e=i.x-t.x,o=i.y-t.y;return Math.sqrt(e*e+o*o)},s=function(t){return t*(Math.PI/180)},r=function(t){return t*(180/Math.PI)},d=function(t,i,e){for(var o,n=i.split(/[ ,]+/g),s=0;s=0&&this._handlers_[t].splice(this._handlers_[t].indexOf(i),1),this},O.prototype.trigger=function(t,i){var e,o=this,n=t.split(/[ ,]+/g);o._handlers_=o._handlers_||{};for(var s=0;ss&&n<3*s&&!t.lockX?i="up":n>-s&&n<=s&&!t.lockY?i="left":n>3*-s&&n<=-s&&!t.lockX?i="down":t.lockY||(i="right"),t.lockY||(e=n>-r&&n0?"up":"down"),t.force>this.options.threshold){var d,a={};for(d in this.direction)this.direction.hasOwnProperty(d)&&(a[d]=this.direction[d]);var p={};for(d in this.direction={x:e,y:o,angle:i},t.direction=this.direction,a)a[d]===this.direction[d]&&(p[d]=!0);if(p.x&&p.y&&p.angle)return t;p.x&&p.y||this.trigger("plain",t),p.x||this.trigger("plain:"+e,t),p.y||this.trigger("plain:"+o,t),p.angle||this.trigger("dir dir:"+i,t)}else this.resetDirection();return t};var T=w;function k(t,i){return this.nipples=[],this.idles=[],this.actives=[],this.ids=[],this.pressureIntervals={},this.manager=t,this.id=k.id,k.id+=1,this.defaults={zone:document.body,multitouch:!1,maxNumberOfNipples:10,mode:"dynamic",position:{top:0,left:0},catchDistance:200,size:100,threshold:.1,color:"white",fadeTime:250,dataOnly:!1,restJoystick:!0,restOpacity:.5,lockX:!1,lockY:!1,shape:"circle",dynamicPage:!1},this.config(i),"static"!==this.options.mode&&"semi"!==this.options.mode||(this.options.multitouch=!1),this.options.multitouch||(this.options.maxNumberOfNipples=1),this.updateBox(),this.prepareNipples(),this.bindings(),this.begin(),this.nipples}k.prototype=new _,k.constructor=k,k.id=0,k.prototype.prepareNipples=function(){var t=this.nipples;t.on=this.on.bind(this),t.off=this.off.bind(this),t.options=this.options,t.destroy=this.destroy.bind(this),t.ids=this.ids,t.id=this.id,t.processOnMove=this.processOnMove.bind(this),t.processOnEnd=this.processOnEnd.bind(this),t.get=function(i){if(void 0===i)return t[0];for(var e=0,o=t.length;e