From 4d01c9553b38179d2a8b85ff743254220f8686ea Mon Sep 17 00:00:00 2001 From: Ahmed Mohamed Date: Wed, 29 Nov 2023 12:48:12 +0100 Subject: [PATCH 001/217] feat<>: leftovers from mobile feature regarding styles and variables names --- .vscode/settings.json | 8 - requirements.txt | 24 ++ rosnode/Dockerfile | 3 + teleop/htm/jmuxer/z_index_video_mux.js | 1 + teleop/htm/static/CSS/testFeature.css | 0 .../mobileController_a_app.js | 1 - .../mobileController_b_shape_dot.js | 4 +- .../mobileController_d_pixi.js | 4 +- teleop/htm/static/JS/testFeature.js | 0 .../htm/templates/mobile_controller_ui.html | 21 +- teleop/htm/templates/testFeature.html | 34 +-- teleop/htm/templates/user_menu.html | 2 +- teleop/logbox/app.py | 207 ++++++++++------- teleop/logbox/core.py | 95 +++++--- teleop/logbox/store.py | 173 +++++++++------ teleop/logbox/web.py | 97 ++++---- teleop/teleop/app.py | 24 +- teleop/teleop/server.py | 1 + vehicles/rover/app.py | 210 +++++++++++------- vehicles/rover/core.py | 1 + 20 files changed, 556 insertions(+), 354 deletions(-) delete mode 100644 .vscode/settings.json create mode 100644 requirements.txt create mode 100644 teleop/htm/static/CSS/testFeature.css create mode 100644 teleop/htm/static/JS/testFeature.js diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index f64a586f..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "cSpell.words": [ - "teleop" - ], - "python.analysis.typeCheckingMode": "off", - "liveServer.settings.port": 5501, - "editor.acceptSuggestionOnEnter": "on" -} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..b2d47c07 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,24 @@ +cachetools==5.3.0 +can==0.0.0 +carla==0.9.14 +ConfigParser==5.3.0 +Equation==1.2.01 +geographiclib==2.0 +gpiozero==1.6.2 +numpy==1.24.2 +onnxruntime==1.14.1 +opencv_python==4.7.0.72 +pandas==1.5.3 +pymodbus==3.2.2 +pymongo==4.3.3 +pyserial==3.5 +pyueye==4.96.952 +pyusb==1.2.1 +pyvesc==1.0.5 +pyzmq==25.0.2 +requests==2.28.2 +scikit_learn==1.2.2 +scipy==1.10.1 +simple_pid==1.0.1 +six==1.16.0 +tornado==6.2 diff --git a/rosnode/Dockerfile b/rosnode/Dockerfile index 92abc892..d56ac77f 100644 --- a/rosnode/Dockerfile +++ b/rosnode/Dockerfile @@ -1,5 +1,8 @@ FROM ros:foxy +RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 4B63CF8FDE49746E98FA01DDAD19BAB3CBF125EA +# Using new GPG key + RUN apt-get update && apt-get install -y --no-install-recommends \ python3-zmq \ nano \ diff --git a/teleop/htm/jmuxer/z_index_video_mux.js b/teleop/htm/jmuxer/z_index_video_mux.js index b4057390..9b8276ca 100644 --- a/teleop/htm/jmuxer/z_index_video_mux.js +++ b/teleop/htm/jmuxer/z_index_video_mux.js @@ -32,6 +32,7 @@ if (page_utils.get_stream_type() == 'h264') { const ws_protocol = (document.location.protocol === "https:") ? "wss://" : "ws://"; const uri = ws_protocol + document.location.hostname + ':' + port; this.socket = new WebSocket(uri); + this.socket.binaryType = 'arraybuffer'; this.socket.attempt_reconnect = true; this.socket.addEventListener('message', function(event) { diff --git a/teleop/htm/static/CSS/testFeature.css b/teleop/htm/static/CSS/testFeature.css new file mode 100644 index 00000000..e69de29b diff --git a/teleop/htm/static/JS/mobileController/mobileController_a_app.js b/teleop/htm/static/JS/mobileController/mobileController_a_app.js index ecaab100..3001b72d 100644 --- a/teleop/htm/static/JS/mobileController/mobileController_a_app.js +++ b/teleop/htm/static/JS/mobileController/mobileController_a_app.js @@ -57,7 +57,6 @@ function sendJSONCommand() { } else { console.error('WebSocket is not open. Unable to send data.'); } - setTimeout(sendJSONCommand, 1); // or adjust the delay as needed } diff --git a/teleop/htm/static/JS/mobileController/mobileController_b_shape_dot.js b/teleop/htm/static/JS/mobileController/mobileController_b_shape_dot.js index 5fc05e50..4f9e5e78 100644 --- a/teleop/htm/static/JS/mobileController/mobileController_b_shape_dot.js +++ b/teleop/htm/static/JS/mobileController/mobileController_b_shape_dot.js @@ -6,7 +6,7 @@ class Dot { drawDot(x, y) { this.graphics.clear(); - this.graphics.beginFill(0xffffff); // color for the dot + this.graphics.beginFill(0xff8fff); // color for the dot this.graphics.drawCircle(x, y, 18); // The radius is 18 (was requested to have diameter of 10mm === 36px) this.graphics.endFill(); } @@ -20,4 +20,4 @@ class Dot { } } -export { Dot }; \ No newline at end of file +export {Dot}; \ No newline at end of file diff --git a/teleop/htm/static/JS/mobileController/mobileController_d_pixi.js b/teleop/htm/static/JS/mobileController/mobileController_d_pixi.js index 56b978f3..0163d193 100644 --- a/teleop/htm/static/JS/mobileController/mobileController_d_pixi.js +++ b/teleop/htm/static/JS/mobileController/mobileController_d_pixi.js @@ -11,9 +11,11 @@ const app = new PIXI.Application({ // Add a 10px margin to the top and bottom of the canvas app.view.style.marginTop = '10px'; app.view.style.marginBottom = '10px'; +app.view.style.position = 'relative'; +app.view.style.zIndex = '3'; document.body.appendChild(app.view); -//Then add them to the canvas 9it is called stage in PiXi) +//Then add them to the canvas (it is called stage in PiXi) app.stage.addChild(topTriangle.graphics); app.stage.addChild(bottomTriangle.graphics); diff --git a/teleop/htm/static/JS/testFeature.js b/teleop/htm/static/JS/testFeature.js new file mode 100644 index 00000000..e69de29b diff --git a/teleop/htm/templates/mobile_controller_ui.html b/teleop/htm/templates/mobile_controller_ui.html index 3b7e10b3..6390c74c 100644 --- a/teleop/htm/templates/mobile_controller_ui.html +++ b/teleop/htm/templates/mobile_controller_ui.html @@ -4,27 +4,14 @@ + - Mobile controller + Phone Controller - - +
+
diff --git a/teleop/htm/templates/testFeature.html b/teleop/htm/templates/testFeature.html index d155a82b..b3061fab 100644 --- a/teleop/htm/templates/testFeature.html +++ b/teleop/htm/templates/testFeature.html @@ -4,26 +4,26 @@ Video Stream diff --git a/teleop/htm/templates/user_menu.html b/teleop/htm/templates/user_menu.html index dbd48531..724ab8f7 100644 --- a/teleop/htm/templates/user_menu.html +++ b/teleop/htm/templates/user_menu.html @@ -69,7 +69,7 @@

Settings

Controls

Events

-

Control by phone

+

Phone Controller

diff --git a/teleop/logbox/app.py b/teleop/logbox/app.py index 79dfd640..ec1efaab 100644 --- a/teleop/logbox/app.py +++ b/teleop/logbox/app.py @@ -30,14 +30,14 @@ def _interrupt(): def _str(c, attr): - return 'null' if (c is None or c.get(attr) is None) else str(c.get(attr)) + return "null" if (c is None or c.get(attr) is None) else str(c.get(attr)) def _bool(c, attr): return 1 if (c is not None and bool(c.get(attr))) else 0 -def _float(c, attr, default=0.): +def _float(c, attr, default=0.0): try: return default if (c is None or c.get(attr) is None) else float(c.get(attr)) except ValueError: @@ -45,10 +45,10 @@ def _float(c, attr, default=0.): def _driver_mode_as_int(pil): - dr = None if pil is None else pil.get('driver') - if dr == 'driver_mode.teleop.direct': + dr = None if pil is None else pil.get("driver") + if dr == "driver_mode.teleop.direct": return 1 - elif dr == 'driver_mode.inference.dnn': + elif dr == "driver_mode.inference.dnn": return 2 else: return 0 @@ -56,11 +56,11 @@ def _driver_mode_as_int(pil): def _driver_type_as_int(pil, attr): x = _str(pil, attr) - if x == 'src.console': + if x == "src.console": return 1 - elif x == 'src.dnn.pre-intervention': + elif x == "src.dnn.pre-intervention": return 2 - elif x == 'src.dnn': + elif x == "src.dnn": return 4 else: return 0 @@ -68,13 +68,13 @@ def _driver_type_as_int(pil, attr): def _driver_type_as_str(x): if x == 1: - return 'src.console' + return "src.console" elif x == 2: - return 'src.dnn.pre-intervention' + return "src.dnn.pre-intervention" elif x == 4: - return 'src.dnn' + return "src.dnn" else: - return 'null' + return "null" class PackageApplication(Application): @@ -82,11 +82,18 @@ def __init__(self, mongo, user, event, hz=1e-2, sessions_dir=os.getcwd()): super(PackageApplication, self).__init__(quit_event=event, run_hz=hz) self._mongo = mongo self._user = user - self._recorder_dir = get_or_create_directory(os.path.join(sessions_dir, 'autopilot')) - self._photo_dir = get_or_create_directory(os.path.join(sessions_dir, 'photos', 'cam0')) + # Folder where training sessions are stored + # Usually it is /sessions/autopilot/ + # It is accessed through the FTP service to download + self._recorder_dir = get_or_create_directory( + os.path.join(sessions_dir, "autopilot") + ) + self._photo_dir = get_or_create_directory( + os.path.join(sessions_dir, "photos", "cam0") + ) # The vehicle config fields cannot be determined here but the fields should not be empty. - self._vehicle = 'na' - self._config = 'na' + self._vehicle = "na" + self._config = "na" def setup(self): pass @@ -95,51 +102,71 @@ def finish(self): pass def _mark(self, item): - self._mongo.update_event({'_id': item.get('_id')}, {'$set': {"lb_is_packaged": 1}}) + self._mongo.update_event( + {"_id": item.get("_id")}, {"$set": {"lb_is_packaged": 1}} + ) def _write_out_photos(self): if not self._user.is_busy(wait_sec=10): items = self._mongo.list_all_non_packaged_photo_events() if len(items) > 0 and not self._user.is_busy(wait_sec=10): - _directory = os.path.join(self._photo_dir, datetime.fromtimestamp(items[0].get('time') * 1e-6).strftime('%Y%B')) + _directory = os.path.join( + self._photo_dir, + datetime.fromtimestamp(items[0].get("time") * 1e-6).strftime( + "%Y%B" + ), + ) _directory = get_or_create_directory(_directory) - with open(os.path.join(_directory, 'photo.log'), 'a+') as f: + with open(os.path.join(_directory, "photo.log"), "a+") as f: for item in items: self._mark(item) - _timestamp = item.get('time') - _dts = datetime.fromtimestamp(_timestamp * 1e-6).strftime('%Y%b%dT%H%M%S') - latitude = str(item.get('veh_gps_latitude'))[:8].replace('.', '_') - longitude = str(item.get('veh_gps_longitude'))[:8].replace('.', '_') + _timestamp = item.get("time") + _dts = datetime.fromtimestamp(_timestamp * 1e-6).strftime( + "%Y%b%dT%H%M%S" + ) + latitude = str(item.get("veh_gps_latitude"))[:8].replace( + ".", "_" + ) + longitude = str(item.get("veh_gps_longitude"))[:8].replace( + ".", "_" + ) fname = "{}_lat{}_long{}.jpg".format(_dts, latitude, longitude) - cv2.imwrite(os.path.join(_directory, fname), cv2_image_from_bytes(item.get('img_buffer'))) - f.write("{} {} latitude {} longitude {}\r\n".format(_timestamp, fname, latitude, longitude)) + cv2.imwrite( + os.path.join(_directory, fname), + cv2_image_from_bytes(item.get("img_buffer")), + ) + f.write( + "{} {} latitude {} longitude {}\r\n".format( + _timestamp, fname, latitude, longitude + ) + ) def _event(self, row, lenience_ms=30): - _timestamp = row.get('time') - _steer_src = _driver_type_as_str(row.get('pil_steering_driver')) - _speed_src = _driver_type_as_str(row.get('pil_speed_driver')) + _timestamp = row.get("time") + _steer_src = _driver_type_as_str(row.get("pil_steering_driver")) + _speed_src = _driver_type_as_str(row.get("pil_speed_driver")) event = Event( timestamp=_timestamp, - image_shape=row.get('img_shape'), - jpeg_buffer=row.get('img_buffer'), + image_shape=row.get("img_shape"), + jpeg_buffer=row.get("img_buffer"), steer_src=_steer_src, speed_src=_speed_src, command_src=_steer_src, - steering=row.get('pil_steering'), - desired_speed=row.get('pil_desired_speed'), - actual_speed=row.get('veh_velocity'), - heading=row.get('veh_heading'), - throttle=row.get('pil_throttle'), + steering=row.get("pil_steering"), + desired_speed=row.get("pil_desired_speed"), + actual_speed=row.get("veh_velocity"), + heading=row.get("veh_heading"), + throttle=row.get("pil_throttle"), command=None, - x_coordinate=row.get('veh_gps_latitude'), - y_coordinate=row.get('veh_gps_longitude'), - inference_brake=row.get('inf_obstruction') + x_coordinate=row.get("veh_gps_latitude"), + y_coordinate=row.get("veh_gps_longitude"), + inference_brake=row.get("inf_obstruction"), ) event.vehicle = self._vehicle event.vehicle_config = self._config lenience = lenience_ms * 1e3 - pil_valid = abs(_timestamp - row.get('pil_time')) < lenience - veh_valid = abs(_timestamp - row.get('veh_time')) < lenience + pil_valid = abs(_timestamp - row.get("pil_time")) < lenience + veh_valid = abs(_timestamp - row.get("veh_time")) < lenience event.valid = pil_valid and veh_valid return event @@ -159,7 +186,11 @@ def _package_next(self): events = list(filter(lambda x: x.valid, map(self._event, reversed(items)))) if len(events) < 1: return False - logger.info("Packaging {} valid events out of {} total items.".format(len(events), len(items))) + logger.info( + "Packaging {} valid events out of {} total items.".format( + len(events), len(items) + ) + ) _archive = create_data_source(events[0].timestamp, self._recorder_dir) try: _archive.open() @@ -216,40 +247,42 @@ def __init__(self, mongo, user, state, event, config_dir=os.getcwd()): def _insert(self, trigger, content, save_image=False): _time, pil, veh, inf, image_md, image = content _img_fields = prepare_image_persist(image, persist=save_image) - _pil_steering_scale = _float(pil, 'steering_scale', default=1.) - self._mongo.insert_event({ - 'time': _time, - 'trigger': trigger, - 'pil_time': get_timestamp(pil), - 'pil_cruise_speed': _str(pil, 'cruise_speed'), - 'pil_desired_speed': _str(pil, 'desired_speed'), - 'pil_driver_mode': _driver_mode_as_int(pil), - 'pil_steering_driver': _driver_type_as_int(pil, 'steering_driver'), - 'pil_speed_driver': _driver_type_as_int(pil, 'speed_driver'), - 'pil_is_steering_intervention': _bool(pil, 'forced_steering'), - 'pil_is_throttle_intervention': _bool(pil, 'forced_throttle'), - 'pil_is_save_event': _bool(pil, 'save_event'), - 'pil_steering_scale': _pil_steering_scale, - 'pil_steering': _str(pil, 'steering'), - 'pil_throttle': _str(pil, 'throttle'), - 'veh_time': get_timestamp(veh), - 'veh_heading': _str(veh, 'heading'), - 'veh_gps_latitude': _str(veh, 'latitude_geo'), - 'veh_gps_longitude': _str(veh, 'longitude_geo'), - 'veh_velocity': _str(veh, 'velocity'), - 'veh_is_velocity_trusted': _bool(veh, 'trust_velocity'), - 'inf_time': get_timestamp(inf), - 'inf_steer_action': _str(inf, 'action'), - 'inf_obstruction': _str(inf, 'obstacle'), - 'inf_steer_confidence': _str(inf, 'steer_confidence'), - 'inf_obstruction_confidence': _str(inf, 'brake_confidence'), - 'inf_total_penalty': _str(inf, 'total_penalty'), - 'img_time': get_timestamp(image_md), - 'img_shape': _img_fields[0], - 'img_num_bytes': _img_fields[1], - 'img_buffer': _img_fields[2], - 'lb_is_packaged': 0 - }) + _pil_steering_scale = _float(pil, "steering_scale", default=1.0) + self._mongo.insert_event( + { + "time": _time, + "trigger": trigger, + "pil_time": get_timestamp(pil), + "pil_cruise_speed": _str(pil, "cruise_speed"), + "pil_desired_speed": _str(pil, "desired_speed"), + "pil_driver_mode": _driver_mode_as_int(pil), + "pil_steering_driver": _driver_type_as_int(pil, "steering_driver"), + "pil_speed_driver": _driver_type_as_int(pil, "speed_driver"), + "pil_is_steering_intervention": _bool(pil, "forced_steering"), + "pil_is_throttle_intervention": _bool(pil, "forced_throttle"), + "pil_is_save_event": _bool(pil, "save_event"), + "pil_steering_scale": _pil_steering_scale, + "pil_steering": _str(pil, "steering"), + "pil_throttle": _str(pil, "throttle"), + "veh_time": get_timestamp(veh), + "veh_heading": _str(veh, "heading"), + "veh_gps_latitude": _str(veh, "latitude_geo"), + "veh_gps_longitude": _str(veh, "longitude_geo"), + "veh_velocity": _str(veh, "velocity"), + "veh_is_velocity_trusted": _bool(veh, "trust_velocity"), + "inf_time": get_timestamp(inf), + "inf_steer_action": _str(inf, "action"), + "inf_obstruction": _str(inf, "obstacle"), + "inf_steer_confidence": _str(inf, "steer_confidence"), + "inf_obstruction_confidence": _str(inf, "brake_confidence"), + "inf_total_penalty": _str(inf, "total_penalty"), + "img_time": get_timestamp(image_md), + "img_shape": _img_fields[0], + "img_num_bytes": _img_fields[1], + "img_buffer": _img_fields[2], + "lb_is_packaged": 0, + } + ) def setup(self): self._insert(TRIGGER_SERVICE_START, content=self._state.pull()[:-1]) @@ -262,9 +295,11 @@ def step(self): _time, pil, veh, inf, image_md, image, pilot_all = self._state.pull() _contents = (_time, pil, veh, inf, image_md, image) # The pilot message save_event attribute is set to true by the autopilot driver to indicate the image needs recording. - _operator = pil is not None and pil.get('driver') is not None - _operator = _operator and (bool(pil.get('forced_steering', 0)) or bool(pil.get('forced_throttle', 0))) - _train = pil is not None and bool(pil.get('save_event', 0)) + _operator = pil is not None and pil.get("driver") is not None + _operator = _operator and ( + bool(pil.get("forced_steering", 0)) or bool(pil.get("forced_throttle", 0)) + ) + _train = pil is not None and bool(pil.get("save_event", 0)) # Keep tabs on whether there is a user in operational command. if _operator or _train: self._user.touch() @@ -275,6 +310,18 @@ def step(self): self._queue_operator.append(_time) self._insert(TRIGGER_DRIVE_OPERATOR, content=_contents, save_image=False) # Scan the pilot commands for photo requests and process them. - if any([cmd.get('button_right', 0) == 1 for cmd in ([] if pilot_all is None else pilot_all)]): + if any( + [ + cmd.get("button_right", 0) == 1 + for cmd in ([] if pilot_all is None else pilot_all) + ] + ): self._photos.append(_contents) - list(map(lambda x: self._insert(TRIGGER_PHOTO_SNAPSHOT, content=x, save_image=True), self._photos.pop_all())) + list( + map( + lambda x: self._insert( + TRIGGER_PHOTO_SNAPSHOT, content=x, save_image=True + ), + self._photos.pop_all(), + ) + ) diff --git a/teleop/logbox/core.py b/teleop/logbox/core.py index 8da655f3..15e668d7 100644 --- a/teleop/logbox/core.py +++ b/teleop/logbox/core.py @@ -9,20 +9,20 @@ from byodr.utils import timestamp -TRIGGER_SERVICE_START = 2 ** 0 -TRIGGER_SERVICE_END = 2 ** 1 -TRIGGER_PHOTO_SNAPSHOT = 2 ** 2 -TRIGGER_DRIVE_OPERATOR = 2 ** 3 -TRIGGER_DRIVE_TRAINER = 2 ** 4 +TRIGGER_SERVICE_START = 2**0 +TRIGGER_SERVICE_END = 2**1 +TRIGGER_PHOTO_SNAPSHOT = 2**2 +TRIGGER_DRIVE_OPERATOR = 2**3 +TRIGGER_DRIVE_TRAINER = 2**4 def get_timestamp(c, default=-1): - return default if c is None else c.get('time') + return default if c is None else c.get("time") def jpeg_encode(image, quality=95): - """Higher quality leads to slightly more cpu load. Default cv jpeg quality is 95. """ - return cv2.imencode('.jpg', image, [int(cv2.IMWRITE_JPEG_QUALITY), quality])[1] + """Higher quality leads to slightly more cpu load. Default cv jpeg quality is 95.""" + return cv2.imencode(".jpg", image, [int(cv2.IMWRITE_JPEG_QUALITY), quality])[1] def cv2_image_from_bytes(b): @@ -66,7 +66,7 @@ def is_busy(self, wait_sec=30): def _nearest(items, ts, default=None): r = default dt = abs(ts - get_timestamp(r)) - for item in ([] if items is None else items): + for item in [] if items is None else items: delta = abs(ts - get_timestamp(item)) if delta < dt: dt = delta @@ -77,12 +77,16 @@ def _nearest(items, ts, default=None): class SharedState(object): def __init__(self, channels, hz=20): self._hz = hz - self._patience = (1e6 / hz) + self._patience = 1e6 / hz self._camera, self._pilot, self._vehicle, self._inference = channels self._cached = (None, None, None) def _expire(self, cmd, stamp): - return None if cmd is None or abs(stamp - get_timestamp(cmd)) > self._patience else cmd + return ( + None + if cmd is None or abs(stamp - get_timestamp(cmd)) > self._patience + else cmd + ) def get_hz(self): return self._hz @@ -121,33 +125,54 @@ def close(self): def ensure_indexes(self): _coll = self._database.events - _create_index_if_not_exists(_coll, [('time', pymongo.DESCENDING)], name='idx_time_descending', unique=True) - _create_index_if_not_exists(_coll, [('time', pymongo.DESCENDING), - ('pil_is_save_event', pymongo.ASCENDING), - ('lb_is_packaged', pymongo.ASCENDING), - ('img_num_bytes', pymongo.ASCENDING)], name='idx_non_packaged_save_events') - _create_index_if_not_exists(_coll, [('trigger', pymongo.ASCENDING), - ('lb_is_packaged', pymongo.ASCENDING), - ('img_num_bytes', pymongo.ASCENDING)], name='idx_non_packaged_save_events') + _create_index_if_not_exists( + _coll, + [("time", pymongo.DESCENDING)], + name="idx_time_descending", + unique=True, + ) + _create_index_if_not_exists( + _coll, + [ + ("time", pymongo.DESCENDING), + ("pil_is_save_event", pymongo.ASCENDING), + ("lb_is_packaged", pymongo.ASCENDING), + ("img_num_bytes", pymongo.ASCENDING), + ], + name="idx_non_packaged_save_events", + ) + _create_index_if_not_exists( + _coll, + [ + ("trigger", pymongo.ASCENDING), + ("lb_is_packaged", pymongo.ASCENDING), + ("img_num_bytes", pymongo.ASCENDING), + ], + name="idx_non_packaged_save_events", + ) def load_event_image_fields(self, object_id): # Images are stored as jpeg encoded bytes. - event = self._database.events.find_one({'_id': ObjectId(object_id)}) - return None if event is None else event.get('img_shape'), event.get('img_num_bytes'), event.get('img_buffer') + event = self._database.events.find_one({"_id": ObjectId(object_id)}) + return ( + None if event is None else event.get("img_shape"), + event.get("img_num_bytes"), + event.get("img_buffer"), + ) def paginate_events(self, load_image=False, **kwargs): _filter = {} - _projection = {} if load_image else {'img_buffer': False} - start = kwargs.get('start', 0) - length = kwargs.get('length', 10) - time_order = kwargs.get('order', -1) + _projection = {} if load_image else {"img_buffer": False} + start = kwargs.get("start", 0) + length = kwargs.get("length", 10) + time_order = kwargs.get("order", -1) cursor = self._database.events.find( filter=_filter, projection=_projection, - sort=[('time', time_order)], + sort=[("time", time_order)], batch_size=length, limit=length, - skip=start + skip=start, ) return cursor.collection.count_documents(_filter), cursor @@ -164,11 +189,21 @@ def update_event(self, query, update): def list_next_batch_of_non_packaged_save_events(self, batch_size=1000): # The event must have an associated image. - _filter = {'pil_is_save_event': 1, 'lb_is_packaged': 0, 'img_num_bytes': {'$gt': 0}} - cursor = self._database.events.find(filter=_filter, sort=[('time', -1)], batch_size=batch_size, limit=batch_size) + _filter = { + "pil_is_save_event": 1, + "lb_is_packaged": 0, + "img_num_bytes": {"$gt": 0}, + } + cursor = self._database.events.find( + filter=_filter, sort=[("time", -1)], batch_size=batch_size, limit=batch_size + ) return list(cursor) def list_all_non_packaged_photo_events(self): - _filter = {'trigger': TRIGGER_PHOTO_SNAPSHOT, 'lb_is_packaged': 0, 'img_num_bytes': {'$gt': 0}} + _filter = { + "trigger": TRIGGER_PHOTO_SNAPSHOT, + "lb_is_packaged": 0, + "img_num_bytes": {"$gt": 0}, + } cursor = self._database.events.find(filter=_filter) return list(cursor) diff --git a/teleop/logbox/store.py b/teleop/logbox/store.py index 9baeaf19..45039c32 100644 --- a/teleop/logbox/store.py +++ b/teleop/logbox/store.py @@ -19,27 +19,31 @@ def create_data_source(timestamp, directory=os.getcwd()): - assert os.path.exists(directory), "The directory '{}' does not exist.".format(directory) + assert os.path.exists(directory), "The directory '{}' does not exist.".format( + directory + ) return ZipDataSource(timestamp, directory=directory) class Event(object): - def __init__(self, - timestamp, - image_shape, - jpeg_buffer, - steer_src, - speed_src, - command_src, - steering, - desired_speed, - actual_speed, - heading, - throttle, - command, - x_coordinate, - y_coordinate, - inference_brake): + def __init__( + self, + timestamp, + image_shape, + jpeg_buffer, + steer_src, + speed_src, + command_src, + steering, + desired_speed, + actual_speed, + heading, + throttle, + command, + x_coordinate, + y_coordinate, + inference_brake, + ): self.timestamp = timestamp self.image_shape = image_shape self.jpeg_buffer = jpeg_buffer @@ -90,7 +94,9 @@ class ZipDataSource(AbstractDataSource): """ def __init__(self, timestamp, directory=os.getcwd()): - assert os.path.exists(directory), "The directory '{}' does not exist.".format(directory) + assert os.path.exists(directory), "The directory '{}' does not exist.".format( + directory + ) self._date_time = datetime.fromtimestamp(timestamp * 1e-6) self._directory = directory self._lock = multiprocessing.Lock() @@ -102,12 +108,14 @@ def __init__(self, timestamp, directory=os.getcwd()): def _zip_file_at_write(self): # Create the directories now to avoid empty listings. _now = self._date_time - _directory = os.path.join(self._directory, _now.strftime('%Y'), _now.strftime('%m%B')) + _directory = os.path.join( + self._directory, _now.strftime("%Y"), _now.strftime("%m%B") + ) if not os.path.exists(_directory): _mask = os.umask(000) os.makedirs(_directory, mode=0o775) os.umask(_mask) - return os.path.join(_directory, self._session + '.zip') + return os.path.join(_directory, self._session + ".zip") def __len__(self): with self._lock: @@ -123,36 +131,57 @@ def open(self): if self._running: return self._running = True - self._session = self._date_time.strftime('%Y%b%dT%H%M_%S%s') - self._data = pd.DataFrame(columns=['time', - 'vehicle', - 'vehicle_conf', - 'image_uri', - 'steer_src', - 'speed_src', - 'steering', - 'desired_speed', - 'actual_speed', - 'heading', - 'throttle', - 'turn_src', - 'turn_val', - 'x_coord', - 'y_coord', - 'inference_brake']) + self._session = self._date_time.strftime("%Y%b%dT%H%M_%S%s") + self._data = pd.DataFrame( + columns=[ + "time", + "vehicle", + "vehicle_conf", + "image_uri", + "steer_src", + "speed_src", + "steering", + "desired_speed", + "actual_speed", + "heading", + "throttle", + "turn_src", + "turn_val", + "x_coord", + "y_coord", + "inference_brake", + ] + ) def close(self, run_gc=True): with self._lock: if self._running and len(self._data) > 0: - logger.info("Writing session '{}' with {} rows.".format(self._session, len(self._data))) - with zipfile.ZipFile(self._zip_file_at_write(), mode='a', compression=0) as archive: + logger.info( + "Writing session '{}' with {} rows.".format( + self._session, len(self._data) + ) + ) + with zipfile.ZipFile( + self._zip_file_at_write(), mode="a", compression=0 + ) as archive: buf = StringIO() self._data.to_csv(buf, index=False) - archive.writestr('{}.csv'.format(self._session), buf.getvalue()) - _image_shape_str = 'null' if self._image_shape is None else 'x'.join(map(str, self._image_shape)) - archive.writestr('meta-inf/manifest.mf', ZipDataSource.MF_TEMPLATE.format( - **dict(num_entries=len(self._data), uuid_node=hex(uuid.getnode()), image_shape=_image_shape_str) - )) + archive.writestr("{}.csv".format(self._session), buf.getvalue()) + _image_shape_str = ( + "null" + if self._image_shape is None + else "x".join(map(str, self._image_shape)) + ) + archive.writestr( + "meta-inf/manifest.mf", + ZipDataSource.MF_TEMPLATE.format( + **dict( + num_entries=len(self._data), + uuid_node=hex(uuid.getnode()), + image_shape=_image_shape_str, + ) + ), + ) self._running = False self._session = None self._data = None @@ -163,7 +192,9 @@ def create_event(self, event): with self._lock: # The store may have been closed while waiting on the lock. if self._running: - assert self._image_shape is None or self._image_shape == event.image_shape, "The image shape should be consistent." + assert ( + self._image_shape is None or self._image_shape == event.image_shape + ), "The image shape should be consistent." self._image_shape = event.image_shape timestamp = event.timestamp steering = float(event.steering) @@ -172,24 +203,38 @@ def create_event(self, event): throttle = float(event.throttle) steer_src = event.steer_src filename = "{}__st{:+2.2f}__th{:+2.2f}__dsp{:+2.1f}__he{:+2.2f}__{}.jpg".format( - str(timestamp), steering, throttle, desired_speed, heading, str(steer_src) + str(timestamp), + steering, + throttle, + desired_speed, + heading, + str(steer_src), ) - with zipfile.ZipFile(self._zip_file_at_write(), mode='a', compression=0) as archive: - archive.writestr(filename, BytesIO(np.frombuffer(memoryview(event.jpeg_buffer), dtype=np.uint8)).getvalue()) + with zipfile.ZipFile( + self._zip_file_at_write(), mode="a", compression=0 + ) as archive: + archive.writestr( + filename, + BytesIO( + np.frombuffer(memoryview(event.jpeg_buffer), dtype=np.uint8) + ).getvalue(), + ) # - self._data.loc[len(self._data)] = [timestamp, - event.vehicle, - event.vehicle_config, - filename, - event.steer_src, - event.speed_src, - event.steering, - event.desired_speed, - event.actual_speed, - event.heading, - event.throttle, - event.command_src, - event.command, - event.x_coordinate, - event.y_coordinate, - event.inference_brake] + self._data.loc[len(self._data)] = [ + timestamp, + event.vehicle, + event.vehicle_config, + filename, + event.steer_src, + event.speed_src, + event.steering, + event.desired_speed, + event.actual_speed, + event.heading, + event.throttle, + event.command_src, + event.command, + event.x_coordinate, + event.y_coordinate, + event.inference_brake, + ] diff --git a/teleop/logbox/web.py b/teleop/logbox/web.py index 6db16cbe..0348167f 100644 --- a/teleop/logbox/web.py +++ b/teleop/logbox/web.py @@ -13,11 +13,11 @@ logger = logging.getLogger(__name__) _trigger_names = { - TRIGGER_SERVICE_START: 'startup', - TRIGGER_SERVICE_END: 'shutdown', - TRIGGER_PHOTO_SNAPSHOT: 'photo', - TRIGGER_DRIVE_OPERATOR: 'teleop', - TRIGGER_DRIVE_TRAINER: 'train' + TRIGGER_SERVICE_START: "startup", + TRIGGER_SERVICE_END: "shutdown", + TRIGGER_PHOTO_SNAPSHOT: "photo", + TRIGGER_DRIVE_OPERATOR: "teleop", + TRIGGER_DRIVE_TRAINER: "train", } @@ -28,14 +28,14 @@ def _trigger_str(trigger): raise ValueError("Unexpected trigger '{}'.".format(trigger)) -def _float_scaled(x, scale=1.): +def _float_scaled(x, scale=1.0): try: return x if x is None else (float(x) / scale) except ValueError: return x -def _float_or_default(x, default=0.): +def _float_or_default(x, default=0.0): try: return default if x is None else float(x) except ValueError: @@ -47,36 +47,40 @@ def __init__(self): pass def __call__(self, *args, **kwargs): - _steering_scale = _float_or_default(kwargs.get('pil_steering_scale'), default=1.) - _pil_steering = _float_scaled(kwargs.get('pil_steering'), scale=_steering_scale) - _inf_steering = _float_scaled(kwargs.get('inf_steer_action'), scale=_steering_scale) + _steering_scale = _float_or_default( + kwargs.get("pil_steering_scale"), default=1.0 + ) + _pil_steering = _float_scaled(kwargs.get("pil_steering"), scale=_steering_scale) + _inf_steering = _float_scaled( + kwargs.get("inf_steer_action"), scale=_steering_scale + ) return [ - str(kwargs.get('_id')), - kwargs.get('time'), - 1 if kwargs.get('img_num_bytes', 0) > 0 else 0, - _trigger_str(kwargs.get('trigger')), - kwargs.get('pil_driver_mode'), - kwargs.get('pil_cruise_speed'), - kwargs.get('pil_desired_speed'), - kwargs.get('veh_velocity'), + str(kwargs.get("_id")), + kwargs.get("time"), + 1 if kwargs.get("img_num_bytes", 0) > 0 else 0, + _trigger_str(kwargs.get("trigger")), + kwargs.get("pil_driver_mode"), + kwargs.get("pil_cruise_speed"), + kwargs.get("pil_desired_speed"), + kwargs.get("veh_velocity"), _pil_steering, - kwargs.get('pil_throttle'), - kwargs.get('pil_is_steering_intervention'), - kwargs.get('pil_is_throttle_intervention'), - kwargs.get('pil_is_save_event'), - kwargs.get('veh_gps_latitude'), - kwargs.get('veh_gps_longitude'), + kwargs.get("pil_throttle"), + kwargs.get("pil_is_steering_intervention"), + kwargs.get("pil_is_throttle_intervention"), + kwargs.get("pil_is_save_event"), + kwargs.get("veh_gps_latitude"), + kwargs.get("veh_gps_longitude"), _inf_steering, - kwargs.get('inf_obstruction'), - kwargs.get('inf_steer_confidence'), - kwargs.get('inf_obstruction_confidence') + kwargs.get("inf_obstruction"), + kwargs.get("inf_steer_confidence"), + kwargs.get("inf_obstruction_confidence"), ] class DataTableRequestHandler(web.RequestHandler): # noinspection PyAttributeOutsideInit def initialize(self, **kwargs): - self._box = kwargs.get('mongo_box') + self._box = kwargs.get("mongo_box") self._view = WebEventViewer() def data_received(self, chunk): @@ -87,18 +91,20 @@ def get(self): # logger.info(self.request.arguments) try: # Parse the draw as integer for security reasons: https://datatables.net/manual/server-side. - draw = int(self.get_query_argument('draw')) - start = int(self.get_query_argument('start')) - length = int(self.get_query_argument('length')) - time_order = self.get_query_argument('order[0][dir]') - time_order = 1 if time_order == 'desc' else -1 + draw = int(self.get_query_argument("draw")) + start = int(self.get_query_argument("start")) + length = int(self.get_query_argument("length")) + time_order = self.get_query_argument("order[0][dir]") + time_order = 1 if time_order == "desc" else -1 except ValueError: draw = 0 start = 0 length = 10 time_order = -1 - c_total, cursor = self._box.paginate_events(start=start, length=length, order=time_order) + c_total, cursor = self._box.paginate_events( + start=start, length=length, order=time_order + ) data = [] for _ in range(length): try: @@ -106,17 +112,12 @@ def get(self): except StopIteration: break - blob = dict( - draw=draw, - recordsTotal=c_total, - recordsFiltered=c_total, - data=data - ) + blob = dict(draw=draw, recordsTotal=c_total, recordsFiltered=c_total, data=data) # Include the error property when errors occur. message = json.dumps(blob) response_code = 200 - self.set_header('Content-Type', 'application/json') - self.set_header('Content-Length', len(message)) + self.set_header("Content-Type", "application/json") + self.set_header("Content-Length", len(message)) self.set_status(response_code) self.write(message) @@ -124,14 +125,16 @@ def get(self): class JPEGImageRequestHandler(web.RequestHandler): # noinspection PyAttributeOutsideInit def initialize(self, **kwargs): - self._box = kwargs.get('mongo_box') + self._box = kwargs.get("mongo_box") def data_received(self, chunk): pass @tornado.gen.coroutine def get(self): - _fields = self._box.load_event_image_fields(self.get_query_argument('object_id')) + _fields = self._box.load_event_image_fields( + self.get_query_argument("object_id") + ) _written = False if _fields is not None: _exists = _fields[1] > 0 @@ -139,10 +142,10 @@ def get(self): img = cv2.resize(cv2_image_from_bytes(_fields[-1]), (200, 80)) _bytes = jpeg_encode(img).tobytes() self.set_status(200) - self.set_header('Content-Type', 'image/jpeg') - self.set_header('Content-Length', len(_bytes)) + self.set_header("Content-Type", "image/jpeg") + self.set_header("Content-Length", len(_bytes)) self.write(_bytes) _written = True if not _written: self.set_status(404) - self.write(u'') + self.write("") diff --git a/teleop/teleop/app.py b/teleop/teleop/app.py index 385d3db5..9aaad49a 100644 --- a/teleop/teleop/app.py +++ b/teleop/teleop/app.py @@ -157,8 +157,8 @@ async def get(self): self.finish() -class Index(tornado.web.RequestHandler): - """The Main landing page""" +class DirectingUser(tornado.web.RequestHandler): + """Directing the user based on their used device""" def get(self): user_agent_str = self.request.headers.get("User-Agent") @@ -171,15 +171,21 @@ def get(self): ) self.redirect("/mobile_controller_ui") else: - # else render the index page - self.render("../htm/templates/index.html") + # else redirect to normal control page + self.redirect("/normalcontrol") + + +class NormalControlUI(tornado.web.RequestHandler): + """The normal user interface""" + + def get(self): + self.render("../htm/templates/index.html") class UserMenu(tornado.web.RequestHandler): """The user menu setting page""" def get(self): - print("navigating to the user menu page") self.render("../htm/templates/user_menu.html") @@ -191,7 +197,7 @@ def get(self): class TestFeatureUI(tornado.web.RequestHandler): - """Load the user interface for mobile controller""" + """Load the user interface for testing""" def get(self): self.render("../htm/templates/testFeature.html") @@ -350,7 +356,8 @@ def teleop_publish(cmd): main_app = web.Application( [ # Landing page - (r"/", Index), + (r"/", DirectingUser), + (r"/normalcontrol", NormalControlUI), (r"/user_menu", UserMenu), # Navigate to user menu settings page ( r"/mobile_controller_ui", @@ -436,7 +443,8 @@ def teleop_publish(cmd): web.StaticFileHandler, {"path": os.path.join(os.path.sep, "app", "htm", "static")}, ), - ] + ], # Disable request logging with an empty lambda expression + log_function=lambda *args, **kwargs: None, ) http_server = HTTPServer(main_app, xheaders=True) port_number = 8080 diff --git a/teleop/teleop/server.py b/teleop/teleop/server.py index 83114ceb..4cc27ab8 100644 --- a/teleop/teleop/server.py +++ b/teleop/teleop/server.py @@ -341,6 +341,7 @@ def on_message(self, message): ).tobytes(), binary=True, ) + except Exception as e: logger.error( "Camera socket@on_message: {} {}".format(e, traceback.format_exc()) diff --git a/vehicles/rover/app.py b/vehicles/rover/app.py index ffb55d86..cb8106b5 100644 --- a/vehicles/rover/app.py +++ b/vehicles/rover/app.py @@ -9,14 +9,19 @@ from byodr.utils import Application, PeriodicCallTrace from byodr.utils import timestamp, Configurable -from byodr.utils.ipc import JSONPublisher, ImagePublisher, LocalIPCServer, json_collector, ReceiverThread +from byodr.utils.ipc import ( + JSONPublisher, + ImagePublisher, + LocalIPCServer, + json_collector, + ReceiverThread, +) from byodr.utils.location import GeoTracker from byodr.utils.option import parse_option, hash_dict -from byodr.utils.ip_getter import get_ip_number from core import GpsPollerThread, PTZCamera, ConfigurableImageGstSource logger = logging.getLogger(__name__) -log_format = '%(levelname)s: %(asctime)s %(filename)s %(funcName)s %(message)s' +log_format = "%(levelname)s: %(asctime)s %(filename)s %(funcName)s %(message)s" class RasRemoteError(IOError): @@ -37,13 +42,13 @@ def __init__(self, master_uri, speed_factor): def _on_receive(self, msg): # Some robots do not have a sensor for speed. # If velocity is not part of the ras message try to come up with a proxy for speed. - if 'velocity' in msg.keys(): - value = float(msg.get('velocity')) + if "velocity" in msg.keys(): + value = float(msg.get("velocity")) else: # The motor effort is calculated as the motor scale * the actual throttle. # In case the robot's maximum speed is hardware limited to 10km/h and the motor scale is 4 # the speed factor is set to 10 / 4 / 3.6. - value = float(msg.get('motor_effort')) * self._motor_effort_speed_factor + value = float(msg.get("motor_effort")) * self._motor_effort_speed_factor self._values.append((value, timestamp())) def get(self): @@ -56,7 +61,9 @@ def get(self): def start(self): # The receiver thread is not restartable. - self._receiver = ReceiverThread(url=('{}:5555'.format(self._ras_uri)), topic=b'ras/drive/status') + self._receiver = ReceiverThread( + url=("{}:5555".format(self._ras_uri)), topic=b"ras/drive/status" + ) self._receiver.add_listener(self._on_receive) self._receiver.start() @@ -73,25 +80,24 @@ def __init__(self): self._gps = None self._geo = GeoTracker() - # ?? Get the location of the robot inside the simulation ?? def _track(self): - latitude, longitude = (None, None) if self._gps is None else (self._gps.get_latitude(), self._gps.get_longitude()) + latitude, longitude = ( + (None, None) + if self._gps is None + else (self._gps.get_latitude(), self._gps.get_longitude()) + ) position = None if None in (latitude, longitude) else (latitude, longitude) return self._geo.track(position) - # ?? Start measuring the speed of the robot inside the simulation ?? def _start_odometer(self): _master_uri, _speed_factor = self._odometer_config self._odometer = RasSpeedOdometer(_master_uri, _speed_factor) self._odometer.start() - # ?? Stop measuring the speed of the robot inside the simulation ?? def _quit_odometer(self): if self._odometer is not None: self._odometer.quit() - # ?? Function that returns the state of the robot inside the simulation ?? - # State: Longitute, latitude, speed, direction and timestamp def state(self): with self._lock: y_vel, trust_velocity = 0, 0 @@ -102,15 +108,19 @@ def state(self): except RasRemoteError as rre: # After 5 seconds do a hard reboot of the remote connection. if rre.timeout > 5000: - logger.info("Hard odometer reboot at {} ms timeout.".format(rre.timeout)) + logger.info( + "Hard odometer reboot at {} ms timeout.".format(rre.timeout) + ) self._quit_odometer() self._start_odometer() - return dict(latitude_geo=latitude, - longitude_geo=longitude, - heading=bearing, - velocity=y_vel, - trust_velocity=trust_velocity, - time=timestamp()) + return dict( + latitude_geo=latitude, + longitude_geo=longitude, + heading=bearing, + velocity=y_vel, + trust_velocity=trust_velocity, + time=timestamp(), + ) def internal_quit(self, restarting=False): self._quit_odometer() @@ -119,12 +129,18 @@ def internal_quit(self, restarting=False): def internal_start(self, **kwargs): errors = [] - _master_uri = parse_option('ras.master.uri', str, 'tcp://192.168.'+get_ip_number()+'.32', errors, **kwargs) - _speed_factor = parse_option('ras.non.sensor.speed.factor', float, 0.50, errors, **kwargs) + _master_uri = parse_option( + "ras.master.uri", str, "tcp://192.168.1.32", errors, **kwargs + ) + _speed_factor = parse_option( + "ras.non.sensor.speed.factor", float, 0.50, errors, **kwargs + ) self._odometer_config = (_master_uri, _speed_factor) self._start_odometer() - _gps_host = parse_option('gps.provider.host', str, '192.168.'+get_ip_number()+'.1', errors, **kwargs) - _gps_port = parse_option('gps.provider.port', str, '502', errors, **kwargs) + _gps_host = parse_option( + "gps.provider.host", str, "192.168.1.1", errors, **kwargs + ) + _gps_port = parse_option("gps.provider.port", str, "502", errors, **kwargs) self._gps = GpsPollerThread(_gps_host, _gps_port) self._gps.start() return errors @@ -135,7 +151,7 @@ def __init__(self): super(RoverHandler, self).__init__() self._platform = Platform() self._process_frequency = 10 - self._patience_micro = 100. + self._patience_micro = 100.0 self._gst_calltrace = PeriodicCallTrace(seconds=10.0) self._gst_sources = [] self._ptz_cameras = [] @@ -157,18 +173,28 @@ def internal_quit(self, restarting=False): def internal_start(self, **kwargs): errors = [] - self._process_frequency = parse_option('clock.hz', int, 80, errors, **kwargs) - self._patience_micro = parse_option('patience.ms', int, 100, errors, **kwargs) * 1000. + self._process_frequency = parse_option("clock.hz", int, 80, errors, **kwargs) + self._patience_micro = ( + parse_option("patience.ms", int, 100, errors, **kwargs) * 1000.0 + ) self._platform.restart(**kwargs) errors.extend(self._platform.get_errors()) if not self._gst_sources: - front_camera = ImagePublisher(url='ipc:///byodr/camera_0.sock', topic='aav/camera/0') - rear_camera = ImagePublisher(url='ipc:///byodr/camera_1.sock', topic='aav/camera/1') - self._gst_sources.append(ConfigurableImageGstSource('front', image_publisher=front_camera)) - self._gst_sources.append(ConfigurableImageGstSource('rear', image_publisher=rear_camera)) + front_camera = ImagePublisher( + url="ipc:///byodr/camera_0.sock", topic="aav/camera/0" + ) + rear_camera = ImagePublisher( + url="ipc:///byodr/camera_1.sock", topic="aav/camera/1" + ) + self._gst_sources.append( + ConfigurableImageGstSource("front", image_publisher=front_camera) + ) + self._gst_sources.append( + ConfigurableImageGstSource("rear", image_publisher=rear_camera) + ) if not self._ptz_cameras: - self._ptz_cameras.append(PTZCamera('front')) - self._ptz_cameras.append(PTZCamera('rear')) + self._ptz_cameras.append(PTZCamera("front")) + self._ptz_cameras.append(PTZCamera("rear")) for item in self._gst_sources + self._ptz_cameras: item.restart(**kwargs) errors.extend(item.get_errors()) @@ -177,36 +203,48 @@ def internal_start(self, **kwargs): def get_video_capabilities(self): # The video dimensions are determined by the websocket services. front, rear = self._gst_sources - return { - 'front': {'ptz': front.get_ptz()}, - 'rear': {'ptz': rear.get_ptz()} - } + return {"front": {"ptz": front.get_ptz()}, "rear": {"ptz": rear.get_ptz()}} def _check_gst_sources(self): self._gst_calltrace(lambda: list(map(lambda x: x.check(), self._gst_sources))) def _cycle_ptz_cameras(self, c_pilot, c_teleop): + """Moves the camera based on the command from teleop + + Args: + c_pilot (JSON): Command from pilot + c_teleop (JSON): command to pilot + """ # The front camera ptz function is enabled for teleop direct driving only. # Set the front camera to the home position anytime the autopilot is switched on. if self._ptz_cameras and c_teleop is not None: - c_camera = c_teleop.get('camera_id', -1) - _north_pressed = bool(c_teleop.get('button_y', 0)) - _is_teleop = (c_pilot is not None and c_pilot.get('driver') == 'driver_mode.teleop.direct') + c_camera = c_teleop.get("camera_id", -1) + # Lower camera is 0 and upper is 1 + _north_pressed = bool(c_teleop.get("button_y", 0)) + _is_teleop = ( + c_pilot is not None + and c_pilot.get("driver") == "driver_mode.teleop.direct" + ) if _north_pressed: - self._ptz_cameras[0].add({'goto_home': 1}) + # North = Triangle in PS4 controller + self._ptz_cameras[0].add({"goto_home": 1}) elif c_camera in (0, 1) and (c_camera == 1 or _is_teleop): # Ignore the pan value on the front camera unless explicitly specified with a button press. - _south_pressed = bool(c_teleop.get('button_a', 0)) - _west_pressed = bool(c_teleop.get('button_x', 0)) + # Hold Square then press X to set the current position for PT to be home position + _south_pressed = bool(c_teleop.get("button_a", 0)) # X + _west_pressed = bool(c_teleop.get("button_x", 0)) # Square _read_pan = _west_pressed or c_camera > 0 - tilt_value = c_teleop.get('tilt', 0) - pan_value = c_teleop.get('pan', 0) if _read_pan else 0 - _set_home = _west_pressed and abs(tilt_value) < 1e-2 and abs(pan_value) < 1e-2 - command = {'pan': pan_value, - 'tilt': tilt_value, - 'set_home': 1 if _set_home else 0, - 'goto_home': 1 if _south_pressed else 0 - } + tilt_value = c_teleop.get("tilt", 0) + pan_value = c_teleop.get("pan", 0) if _read_pan else 0 + _set_home = ( + _west_pressed and abs(tilt_value) < 1e-2 and abs(pan_value) < 1e-2 + ) + command = { + "pan": pan_value, + "tilt": tilt_value, + "set_home": 1 if _set_home else 0, + "goto_home": 1 if _south_pressed else 0, + } self._ptz_cameras[c_camera].add(command) def step(self, c_pilot, c_teleop): @@ -229,21 +267,23 @@ def __init__(self, handler=None, config_dir=os.getcwd()): def _check_user_file(self): # One user configuration file is optional and can be used to persist settings. - _candidates = glob.glob(os.path.join(self._config_dir, '*.ini')) + _candidates = glob.glob(os.path.join(self._config_dir, "*.ini")) if len(_candidates) == 0: - shutil.copyfile('config.template', os.path.join(self._config_dir, 'config.ini')) + shutil.copyfile( + "config.template", os.path.join(self._config_dir, "config.ini") + ) logger.info("Created a new user configuration file from template.") def _config(self): parser = SafeConfigParser() - [parser.read(_f) for _f in glob.glob(os.path.join(self._config_dir, '*.ini'))] - cfg = dict(parser.items('vehicle')) if parser.has_section('vehicle') else {} - cfg.update(dict(parser.items('camera')) if parser.has_section('camera') else {}) + [parser.read(_f) for _f in glob.glob(os.path.join(self._config_dir, "*.ini"))] + cfg = dict(parser.items("vehicle")) if parser.has_section("vehicle") else {} + cfg.update(dict(parser.items("camera")) if parser.has_section("camera") else {}) self.logger.info(cfg) return cfg def _capabilities(self): - return {'vehicle': 'rover1', 'video': self._handler.get_video_capabilities()} + return {"vehicle": "rover1", "video": self._handler.get_video_capabilities()} def setup(self): if self.active(): @@ -254,7 +294,9 @@ def setup(self): self._check_user_file() _restarted = self._handler.restart(**_config) if _restarted: - self.ipc_server.register_start(self._handler.get_errors(), self._capabilities()) + self.ipc_server.register_start( + self._handler.get_errors(), self._capabilities() + ) _frequency = self._handler.get_process_frequency() self.set_hz(_frequency) self.logger.info("Processing at {} Hz.".format(_frequency)) @@ -269,44 +311,56 @@ def finish(self): # super(RoverApplication, self).run() # profiler.dump_stats('/config/rover.stats') - - # Function that is called continuously - # Receives commands from pilot and teleop def step(self): - rover, pilot, teleop, publisher = self._handler, self.pilot, self.teleop, self.state_publisher + rover, pilot, teleop, publisher = ( + self._handler, + self.pilot, + self.teleop, + self.state_publisher, + ) c_pilot = self._latest_or_none(pilot, patience=rover.get_patience_micro()) c_teleop = self._latest_or_none(teleop, patience=rover.get_patience_micro()) _state = rover.step(c_pilot, c_teleop) publisher.publish(_state) chat = self.ipc_chatter() - if chat and chat.get('command') == 'restart': + if chat and chat.get("command") == "restart": self.setup() def main(): - parser = argparse.ArgumentParser(description='Rover main.') - parser.add_argument('--name', type=str, default='none', help='Process name.') - parser.add_argument('--config', type=str, default='/config', help='Config directory path.') + parser = argparse.ArgumentParser(description="Rover main.") + parser.add_argument("--name", type=str, default="none", help="Process name.") + parser.add_argument( + "--config", type=str, default="/config", help="Config directory path." + ) args = parser.parse_args() application = RoverApplication(config_dir=args.config) quit_event = application.quit_event - # Sockets used to receive data from pilot and teleop. - pilot = json_collector(url='ipc:///byodr/pilot.sock', topic=b'aav/pilot/output', event=quit_event) - teleop = json_collector(url='ipc:///byodr/teleop.sock', topic=b'aav/teleop/input', event=quit_event) - ipc_chatter = json_collector(url='ipc:///byodr/teleop_c.sock', topic=b'aav/teleop/chatter', pop=True, event=quit_event) - - # Sockets used to send data to other services - application.state_publisher = JSONPublisher(url='ipc:///byodr/vehicle.sock', topic='aav/vehicle/state') - application.ipc_server = LocalIPCServer(url='ipc:///byodr/vehicle_c.sock', name='platform', event=quit_event) - - # Getting data from the received sockets declared above + pilot = json_collector( + url="ipc:///byodr/pilot.sock", topic=b"aav/pilot/output", event=quit_event + ) + teleop = json_collector( + url="ipc:///byodr/teleop.sock", topic=b"aav/teleop/input", event=quit_event + ) + ipc_chatter = json_collector( + url="ipc:///byodr/teleop_c.sock", + topic=b"aav/teleop/chatter", + pop=True, + event=quit_event, + ) + + application.state_publisher = JSONPublisher( + url="ipc:///byodr/vehicle.sock", topic="aav/vehicle/state" + ) + application.ipc_server = LocalIPCServer( + url="ipc:///byodr/vehicle_c.sock", name="platform", event=quit_event + ) application.pilot = lambda: pilot.get() application.teleop = lambda: teleop.get() application.ipc_chatter = lambda: ipc_chatter.get() - # Starting the socket threads threads = [pilot, teleop, ipc_chatter, application.ipc_server] if quit_event.is_set(): return 0 @@ -319,6 +373,6 @@ def main(): if __name__ == "__main__": - logging.basicConfig(format=log_format, datefmt='%Y%m%d:%H:%M:%S %p %Z') + logging.basicConfig(format=log_format, datefmt="%Y%m%d:%H:%M:%S %p %Z") logging.getLogger().setLevel(logging.INFO) main() diff --git a/vehicles/rover/core.py b/vehicles/rover/core.py index f1ce16d3..79d970d1 100644 --- a/vehicles/rover/core.py +++ b/vehicles/rover/core.py @@ -17,6 +17,7 @@ logger = logging.getLogger(__name__) + CH_NONE, CH_THROTTLE, CH_STEERING, CH_BOTH = (0, 1, 2, 3) CTL_LAST = 0 From 6e27b25f41b05c04c13e79cb629e70215db8c1ce Mon Sep 17 00:00:00 2001 From: Ahmed Mahfouz Date: Wed, 29 Nov 2023 16:19:33 +0100 Subject: [PATCH 002/217] feat: create router class with function to get SSID There are other two functions but they were created by Berna --- common/byodr/utils/ssh.py | 99 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 common/byodr/utils/ssh.py diff --git a/common/byodr/utils/ssh.py b/common/byodr/utils/ssh.py new file mode 100644 index 00000000..e44f5683 --- /dev/null +++ b/common/byodr/utils/ssh.py @@ -0,0 +1,99 @@ +import logging +import paramiko, time, re + +# Declaring the logger +logging.basicConfig( + format="%(levelname)s: %(asctime)s %(filename)s %(funcName)s %(message)s", + datefmt="%Y%m%d:%H:%M:%S %p %Z", +) +logging.getLogger().setLevel(logging.INFO) +logger = logging.getLogger(__name__) + + +class Router: + def __init__(self, ip, username="root", password="Modem001", port=22): + self.ip = ip + self.username = username + self.password = password + self.port = int(port) # Default value for SSH port + + def fetch_ssid(self, command): + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + # Connect to the SSH server + client.connect(self.ip, self.port, self.username, self.password) + stdin, stdout, stderr = client.exec_command(command) + output = stdout.read().decode("utf-8") + client.close() + return output + + def get_router_arp_table(): + try: + # Read the ARP table from /proc/net/arp + with open("/proc/net/arp", "r") as arp_file: + arp_table = arp_file.read() + + return arp_table + except Exception as e: + logger.error(f"An error occurred: {str(e)}") + return [] + + def get_filtered_router_arp_table(arp_table, last_digit_of_localIP): + try: + filtered_arp_table = [] + + # Split the ARP table into lines + arp_lines = arp_table.split("\n") + local_ip_prefix = f"192.168.{last_digit_of_localIP}." + + # Extract and add "IP address" and "Flags" to the filtered table which is what we need + for line in arp_lines: + columns = line.split() + if len(columns) >= 2: + ip = columns[0] + flags = columns[2] + if ip == f"{local_ip_prefix}1" or ip == f"{local_ip_prefix}2": + filtered_arp_table.append({"IP address": ip, "Flags": flags}) + + return filtered_arp_table + except Exception as e: + logger.error(f"An error occurred while filtering ARP table: {str(e)}") + return [] + + +class Cameras: + def __init__( + self, segment_network_prefix, username="admin", password="SteamGlamour4" + ): + self.ip_front = f"{segment_network_prefix}.64" + self.ip_back = f"{segment_network_prefix}.65" + self.username = username + self.password = password + + def get_interface_info(self): + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + client.connect(self.ip_front, username=self.username, password=self.password) + + channel = client.invoke_shell() + time.sleep(1) # Wait for the shell to initialize + channel.send("ifconfig eth0\n") + time.sleep(1) + channel.send("exit\n") + output = channel.recv( + 65535 + ).decode() # Huge amount of bytes to read, because it captures everything since the shell is open, and not the result of command only + + client.close() + + # Get Internet Address, Broadcast Address and Subnet Mask. + match = re.search(r"inet addr:(\S+) Bcast:(\S+) Mask:(\S+)", output) + if not match: + return "No matching interface information found." + + # Create JSON string directly + json_output = '{{"inet addr": "{}", "Bcast": "{}", "Mask": "{}"}}'.format( + match.group(1), match.group(2), match.group(3) + ) + + return json_output From 865a4ae2de56c6000aef978afef25c9f8012517a Mon Sep 17 00:00:00 2001 From: Ahmed Mahfouz Date: Wed, 29 Nov 2023 16:20:30 +0100 Subject: [PATCH 003/217] feat use the new fetch ssid in utils and remove old call --- teleop/teleop/app.py | 11 +++++------ teleop/teleop/getSSID.py | 13 ------------- 2 files changed, 5 insertions(+), 19 deletions(-) delete mode 100644 teleop/teleop/getSSID.py diff --git a/teleop/teleop/app.py b/teleop/teleop/app.py index 9aaad49a..a54ee4ce 100644 --- a/teleop/teleop/app.py +++ b/teleop/teleop/app.py @@ -24,13 +24,15 @@ from byodr.utils import Application, hash_dict, ApplicationExit from byodr.utils.ipc import CameraThread, JSONPublisher, JSONZmqClient, json_collector from byodr.utils.navigate import FileSystemRouteDataSource, ReloadableDataSource +from byodr.utils.ssh import Router from logbox.app import LogApplication, PackageApplication from logbox.core import MongoLogBox, SharedUser, SharedState from logbox.web import DataTableRequestHandler, JPEGImageRequestHandler from .server import * from htm.plot_training_sessions_map.draw_training_sessions import draw_training_sessions -from .getSSID import fetch_ssid + +router = Router(ip="192.168.1.1") logger = logging.getLogger(__name__) @@ -140,11 +142,7 @@ async def get(self): # name of python function to run, ip of the router, ip of SSH, username, password, command to get the SSID ssid = await loop.run_in_executor( None, - fetch_ssid, - router_IP, - 22, - "root", - "Modem001", + router.fetch_ssid, "uci get wireless.@wifi-iface[0].ssid", ) @@ -152,6 +150,7 @@ async def get(self): self.write(ssid) except Exception as e: logger.error(f"Error fetching SSID of current robot: {e}") + logger.error(traceback.format_exc()) # This will pri self.set_status(500) self.write("Error fetching SSID of current robot.") self.finish() diff --git a/teleop/teleop/getSSID.py b/teleop/teleop/getSSID.py deleted file mode 100644 index 4e0d0764..00000000 --- a/teleop/teleop/getSSID.py +++ /dev/null @@ -1,13 +0,0 @@ -import paramiko - - -def fetch_ssid(host, port, username, password, command): - # Create an SSH client instance - client = paramiko.SSHClient() - client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - # Connect to the SSH server - client.connect(host, port, username, password) - stdin, stdout, stderr = client.exec_command(command) - output = stdout.read().decode("utf-8") - client.close() - return output From 7daa37350164898bf561a3eacec200ebebeb2c40 Mon Sep 17 00:00:00 2001 From: Ahmed Mahfouz Date: Wed, 29 Nov 2023 16:19:33 +0100 Subject: [PATCH 004/217] feat: create camera and router class Router class to get the SSID and other two functions but they were created by Berna. Camera class has one function to get the ip of camera --- common/byodr/utils/ssh.py | 99 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 common/byodr/utils/ssh.py diff --git a/common/byodr/utils/ssh.py b/common/byodr/utils/ssh.py new file mode 100644 index 00000000..e44f5683 --- /dev/null +++ b/common/byodr/utils/ssh.py @@ -0,0 +1,99 @@ +import logging +import paramiko, time, re + +# Declaring the logger +logging.basicConfig( + format="%(levelname)s: %(asctime)s %(filename)s %(funcName)s %(message)s", + datefmt="%Y%m%d:%H:%M:%S %p %Z", +) +logging.getLogger().setLevel(logging.INFO) +logger = logging.getLogger(__name__) + + +class Router: + def __init__(self, ip, username="root", password="Modem001", port=22): + self.ip = ip + self.username = username + self.password = password + self.port = int(port) # Default value for SSH port + + def fetch_ssid(self, command): + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + # Connect to the SSH server + client.connect(self.ip, self.port, self.username, self.password) + stdin, stdout, stderr = client.exec_command(command) + output = stdout.read().decode("utf-8") + client.close() + return output + + def get_router_arp_table(): + try: + # Read the ARP table from /proc/net/arp + with open("/proc/net/arp", "r") as arp_file: + arp_table = arp_file.read() + + return arp_table + except Exception as e: + logger.error(f"An error occurred: {str(e)}") + return [] + + def get_filtered_router_arp_table(arp_table, last_digit_of_localIP): + try: + filtered_arp_table = [] + + # Split the ARP table into lines + arp_lines = arp_table.split("\n") + local_ip_prefix = f"192.168.{last_digit_of_localIP}." + + # Extract and add "IP address" and "Flags" to the filtered table which is what we need + for line in arp_lines: + columns = line.split() + if len(columns) >= 2: + ip = columns[0] + flags = columns[2] + if ip == f"{local_ip_prefix}1" or ip == f"{local_ip_prefix}2": + filtered_arp_table.append({"IP address": ip, "Flags": flags}) + + return filtered_arp_table + except Exception as e: + logger.error(f"An error occurred while filtering ARP table: {str(e)}") + return [] + + +class Cameras: + def __init__( + self, segment_network_prefix, username="admin", password="SteamGlamour4" + ): + self.ip_front = f"{segment_network_prefix}.64" + self.ip_back = f"{segment_network_prefix}.65" + self.username = username + self.password = password + + def get_interface_info(self): + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + client.connect(self.ip_front, username=self.username, password=self.password) + + channel = client.invoke_shell() + time.sleep(1) # Wait for the shell to initialize + channel.send("ifconfig eth0\n") + time.sleep(1) + channel.send("exit\n") + output = channel.recv( + 65535 + ).decode() # Huge amount of bytes to read, because it captures everything since the shell is open, and not the result of command only + + client.close() + + # Get Internet Address, Broadcast Address and Subnet Mask. + match = re.search(r"inet addr:(\S+) Bcast:(\S+) Mask:(\S+)", output) + if not match: + return "No matching interface information found." + + # Create JSON string directly + json_output = '{{"inet addr": "{}", "Bcast": "{}", "Mask": "{}"}}'.format( + match.group(1), match.group(2), match.group(3) + ) + + return json_output From 90194cfe3738bb63e6f36ecaa1801cac54e0fafe Mon Sep 17 00:00:00 2001 From: Ahmed Mahfouz Date: Wed, 29 Nov 2023 16:20:30 +0100 Subject: [PATCH 005/217] feat use the new fetch ssid in utils and remove old call --- teleop/teleop/app.py | 11 +++++------ teleop/teleop/getSSID.py | 13 ------------- 2 files changed, 5 insertions(+), 19 deletions(-) delete mode 100644 teleop/teleop/getSSID.py diff --git a/teleop/teleop/app.py b/teleop/teleop/app.py index 9aaad49a..a54ee4ce 100644 --- a/teleop/teleop/app.py +++ b/teleop/teleop/app.py @@ -24,13 +24,15 @@ from byodr.utils import Application, hash_dict, ApplicationExit from byodr.utils.ipc import CameraThread, JSONPublisher, JSONZmqClient, json_collector from byodr.utils.navigate import FileSystemRouteDataSource, ReloadableDataSource +from byodr.utils.ssh import Router from logbox.app import LogApplication, PackageApplication from logbox.core import MongoLogBox, SharedUser, SharedState from logbox.web import DataTableRequestHandler, JPEGImageRequestHandler from .server import * from htm.plot_training_sessions_map.draw_training_sessions import draw_training_sessions -from .getSSID import fetch_ssid + +router = Router(ip="192.168.1.1") logger = logging.getLogger(__name__) @@ -140,11 +142,7 @@ async def get(self): # name of python function to run, ip of the router, ip of SSH, username, password, command to get the SSID ssid = await loop.run_in_executor( None, - fetch_ssid, - router_IP, - 22, - "root", - "Modem001", + router.fetch_ssid, "uci get wireless.@wifi-iface[0].ssid", ) @@ -152,6 +150,7 @@ async def get(self): self.write(ssid) except Exception as e: logger.error(f"Error fetching SSID of current robot: {e}") + logger.error(traceback.format_exc()) # This will pri self.set_status(500) self.write("Error fetching SSID of current robot.") self.finish() diff --git a/teleop/teleop/getSSID.py b/teleop/teleop/getSSID.py deleted file mode 100644 index 4e0d0764..00000000 --- a/teleop/teleop/getSSID.py +++ /dev/null @@ -1,13 +0,0 @@ -import paramiko - - -def fetch_ssid(host, port, username, password, command): - # Create an SSH client instance - client = paramiko.SSHClient() - client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - # Connect to the SSH server - client.connect(host, port, username, password) - stdin, stdout, stderr = client.exec_command(command) - output = stdout.read().decode("utf-8") - client.close() - return output From 46faf4f14c56f62af2cb9685b2ba4fe16466f2a7 Mon Sep 17 00:00:00 2001 From: Ahmed Mahfouz Date: Thu, 30 Nov 2023 09:25:55 +0100 Subject: [PATCH 006/217] feat: add function to get mac and ip of connected devices The function returns JSON sorted incrementally for the ips of connected devices to the segments robot --- common/byodr/utils/ssh.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/common/byodr/utils/ssh.py b/common/byodr/utils/ssh.py index e44f5683..6b4d22ea 100644 --- a/common/byodr/utils/ssh.py +++ b/common/byodr/utils/ssh.py @@ -1,5 +1,6 @@ import logging -import paramiko, time, re +import paramiko, time, re, json +from ipaddress import ip_address # Declaring the logger logging.basicConfig( @@ -27,6 +28,37 @@ def fetch_ssid(self, command): client.close() return output + def fetch_ip_and_mac(self): + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + try: + client.connect("192.168.1.1", int(22), "root", "Modem001") + stdin, stdout, stderr = client.exec_command("ip neigh") + output = stdout.read().decode("utf-8") + error = stderr.read().decode("utf-8") + if error: + print("Command error output: %s", error) + except Exception as e: + print("Error during SSH connection or command execution: %s", e) + return + finally: + client.close() + + devices = [] + for line in output.splitlines(): + # it looks for a pattern like number.number.number.number + # It looks for a pattern of six groups of two hexadecimal digits that are separated by either : or - + match = re.search( + r"(\d+\.\d+\.\d+\.\d+).+?([0-9A-Fa-f]{2}(?:[:-][0-9A-Fa-f]{2}){5})", + line, + ) + if match: + ip, mac_address = match.groups() + devices.append({"ip": ip, "mac": mac_address}) + sorted_devices = sorted(devices, key=lambda x: ip_address(x['ip'])) + + print("Devices found: ", sorted_devices) + def get_router_arp_table(): try: # Read the ARP table from /proc/net/arp From b71c0226bf02218b712c6f65f839d8fd4738aeb2 Mon Sep 17 00:00:00 2001 From: Ahmed Mahfouz Date: Thu, 30 Nov 2023 10:02:06 +0100 Subject: [PATCH 007/217] feat: Change styles hierarchy and add padding to body The body was using a class just for it, which I removed. Moved the style to separated file .css --- teleop/htm/static/CSS/user_menu.css | 46 ++++ .../mobileController_b_shape_dot.js | 4 +- teleop/htm/templates/user_menu.html | 215 ++++++++---------- 3 files changed, 137 insertions(+), 128 deletions(-) create mode 100644 teleop/htm/static/CSS/user_menu.css diff --git a/teleop/htm/static/CSS/user_menu.css b/teleop/htm/static/CSS/user_menu.css new file mode 100644 index 00000000..0591ddb6 --- /dev/null +++ b/teleop/htm/static/CSS/user_menu.css @@ -0,0 +1,46 @@ +body { + margin: 0; +} + +.application-body { + display: flex; + height: 100vh; + flex-direction: row; +} + +.application-content { + flex: 1; + padding: 20px; + font-family: 'Arial'; + background: whitesmoke; +} + +.application-nav { + /* last value is the width of the columns */ + flex: 0 0 12em; + padding: 1em 0 0 1em; + border-right: 1px solid gray; +} + +.application-nav a { + font-family: 'Arial'; + font-size: 20px; + color: darkblue; +} + +.application-nav a:link, +a:visited { + text-decoration: none; +} + +.application-nav a:hover { + text-decoration: underline; +} + +.application-nav a.active { + font-weight: bold; +} + +table#logbox td { + white-space: nowrap; +} \ No newline at end of file diff --git a/teleop/htm/static/JS/mobileController/mobileController_b_shape_dot.js b/teleop/htm/static/JS/mobileController/mobileController_b_shape_dot.js index 4f9e5e78..dd0e55cc 100644 --- a/teleop/htm/static/JS/mobileController/mobileController_b_shape_dot.js +++ b/teleop/htm/static/JS/mobileController/mobileController_b_shape_dot.js @@ -6,7 +6,7 @@ class Dot { drawDot(x, y) { this.graphics.clear(); - this.graphics.beginFill(0xff8fff); // color for the dot + this.graphics.beginFill(0xffffff); // Color for the dot this.graphics.drawCircle(x, y, 18); // The radius is 18 (was requested to have diameter of 10mm === 36px) this.graphics.endFill(); } @@ -20,4 +20,4 @@ class Dot { } } -export {Dot}; \ No newline at end of file +export { Dot }; \ No newline at end of file diff --git a/teleop/htm/templates/user_menu.html b/teleop/htm/templates/user_menu.html index 724ab8f7..3225fd73 100644 --- a/teleop/htm/templates/user_menu.html +++ b/teleop/htm/templates/user_menu.html @@ -1,139 +1,102 @@ - - - - Menu - - - - - - - - - -
-
- -
-
-
-
-
- - - - - - - - - - + // Activate the selected screen. + var screen = window.localStorage.getItem('user.menu.screen'); + if (screen == null) { + screen = 'settings'; + } + _user_menu_route_screen(screen); + }); + \ No newline at end of file From d4f28f5278880db49aa181bfab1b6ce223ba232f Mon Sep 17 00:00:00 2001 From: Ahmed Mahfouz Date: Fri, 1 Dec 2023 10:20:30 +0100 Subject: [PATCH 008/217] feat: add router function to scan for networks and filter the data The functions scan for all discoverable networks, returns a filtered JSON for the data after replacing the unknown data from IE information with one that is known based on the data in it. There is DocString for each new function. I don't think it is thread safe, will check this with local mode --- common/byodr/utils/ssh.py | 127 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 123 insertions(+), 4 deletions(-) diff --git a/common/byodr/utils/ssh.py b/common/byodr/utils/ssh.py index 6b4d22ea..fabc9c76 100644 --- a/common/byodr/utils/ssh.py +++ b/common/byodr/utils/ssh.py @@ -32,7 +32,7 @@ def fetch_ip_and_mac(self): client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) try: - client.connect("192.168.1.1", int(22), "root", "Modem001") + client.connect(self.ip, self.port, self.username, self.password) stdin, stdout, stderr = client.exec_command("ip neigh") output = stdout.read().decode("utf-8") error = stderr.read().decode("utf-8") @@ -55,11 +55,11 @@ def fetch_ip_and_mac(self): if match: ip, mac_address = match.groups() devices.append({"ip": ip, "mac": mac_address}) - sorted_devices = sorted(devices, key=lambda x: ip_address(x['ip'])) + sorted_devices = sorted(devices, key=lambda x: ip_address(x["ip"])) print("Devices found: ", sorted_devices) - def get_router_arp_table(): + def get_router_arp_table(): ###################################################### try: # Read the ARP table from /proc/net/arp with open("/proc/net/arp", "r") as arp_file: @@ -70,7 +70,9 @@ def get_router_arp_table(): logger.error(f"An error occurred: {str(e)}") return [] - def get_filtered_router_arp_table(arp_table, last_digit_of_localIP): + def get_filtered_router_arp_table( + arp_table, last_digit_of_localIP + ): ###################################################### try: filtered_arp_table = [] @@ -92,6 +94,123 @@ def get_filtered_router_arp_table(arp_table, last_digit_of_localIP): logger.error(f"An error occurred while filtering ARP table: {str(e)}") return [] + # Functions fetch_wifi_networks, parse_iwlist_output, parse_ie_data, extract_security_info all are working together + def fetch_wifi_networks(self): + """ + Connects to an SSH server and retrieves a list of available Wi-Fi networks. + Parses the output from the 'iwlist wlan0 scan' command to extract network details. + + Returns: + list of dict: A list containing information about each network, including (ESSID, MAC, channel, security, IE information). + """ + command = "iwlist wlan0 scan" + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + try: + # Connect to the SSH server + client.connect(self.ip, self.port, self.username, self.password) + stdin, stdout, stderr = client.exec_command(command) + output = stdout.read().decode("utf-8") + + return self.parse_iwlist_output(output) + finally: + client.close() + + def parse_iwlist_output(self, output): + """Parses the output from the 'iwlist wlan0 scan' command. + + Args: + output (str): The raw output string from the 'iwlist wlan0 scan' command. + + Returns: + list of dict: A list of dictionaries, each representing a network with information such as (ESSID, MAC, channel, security, and IE info). + """ + networks = [] + current_network = {} + + for line in output.splitlines(): + if "Cell" in line and "Address" in line: + if current_network: + networks.append(current_network) + current_network = {} + current_network["MAC"] = line.split()[-1] + elif "ESSID:" in line: + current_network["ESSID"] = line.split('"')[1] + elif "Channel:" in line: + current_network["Channel"] = line.split(":")[-1] + elif line.strip().startswith("IE: IEEE 802.11i/WPA2 Version 1"): + security_info = self.extract_security_info(line, output.splitlines()) + current_network["Security"] = security_info + elif line.strip().startswith("IE: Unknown"): + if "IE Information" not in current_network: + current_network["IE Information"] = {} + + ie_data = line.strip().split(": ", 2)[-1] + ie_key, ie_value = parse_ie_data(ie_data) + if ie_key: + current_network["IE Information"][ie_key] = ie_value + + # Reorder the dictionary to show ESSID first + ordered_networks = [] + for network in networks: + ordered_network = { + k: network[k] + for k in ["ESSID", "MAC", "Channel", "Security", "IE Information"] + if k in network + } + ordered_networks.append(ordered_network) + + return ordered_networks + + def parse_ie_data(self, ie_data): + """Parses and interprets a single Information Element (IE) data entry. + + Args: + ie_data (str): A string representing an IE data entry. + + Returns: + tuple: A key-value pair representing the IE type and its data. + Returns (None, None) if the IE type is unrecognized. + """ + # This function can be expanded to interpret more IE types + ie_type = ie_data[:2] + if ie_type == "00": + return "Vendor Specific IE", ie_data[2:] + elif ie_type == "01": + return "Supported Rates", ie_data[2:] + elif ie_type == "03": + return "DS Parameter Set (Channel Information)", ie_data[2:] + elif ie_type == "07": + return "Country Information", ie_data[2:] + + return None, None # Return None if IE type is not recognized + + def extract_security_info(self, start_line, all_lines): + """Returns a JSON for the security information in the security line of scanned networks + + Args: + start_line (str): The line where security information starts in the output. + all_lines (list of str): All lines of the scan output. + + Returns: + dict: A dictionary with security information such as WPA2 version, group cipher, pairwise ciphers, and authentication suites. + """ + security_info = {} + index = all_lines.index(start_line) + for line in all_lines[index:]: + if line.strip().startswith("IE: IEEE 802.11i/WPA2 Version 1"): + security_info["WPA2 Version"] = line.split(":")[-1].strip() + elif line.strip().startswith("Group Cipher"): + security_info["Group Cipher"] = line.split(":")[-1].strip() + elif line.strip().startswith("Pairwise Ciphers"): + security_info["Pairwise Ciphers"] = line.split(":")[-1].strip() + elif line.strip().startswith("Authentication Suites"): + security_info["Authentication Suites"] = line.split(":")[-1].strip() + elif line.strip().startswith("IE:"): + break # Stop at the next IE + return security_info + class Cameras: def __init__( From 58a99cc2822db9062658b96ed55d4d38cb0e6a5f Mon Sep 17 00:00:00 2001 From: Ahmed Mahfouz Date: Fri, 1 Dec 2023 10:27:46 +0100 Subject: [PATCH 009/217] doc add DocString to other functions --- common/byodr/utils/ssh.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/common/byodr/utils/ssh.py b/common/byodr/utils/ssh.py index fabc9c76..5213aaf6 100644 --- a/common/byodr/utils/ssh.py +++ b/common/byodr/utils/ssh.py @@ -29,6 +29,7 @@ def fetch_ssid(self, command): return output def fetch_ip_and_mac(self): + """Get list of all connected devices to the current segment""" client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) try: @@ -147,7 +148,7 @@ def parse_iwlist_output(self, output): current_network["IE Information"] = {} ie_data = line.strip().split(": ", 2)[-1] - ie_key, ie_value = parse_ie_data(ie_data) + ie_key, ie_value = self.parse_ie_data(ie_data) if ie_key: current_network["IE Information"][ie_key] = ie_value @@ -213,6 +214,10 @@ def extract_security_info(self, start_line, all_lines): class Cameras: + """Class to deal with the SSH for the camera + Functions: get_interface_info() + """ + def __init__( self, segment_network_prefix, username="admin", password="SteamGlamour4" ): @@ -222,12 +227,19 @@ def __init__( self.password = password def get_interface_info(self): + """Fetch current network details from the camera + + Returns: + JSON: Internet Address, Broadcast Address and Subnet Mask. + """ client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect(self.ip_front, username=self.username, password=self.password) channel = client.invoke_shell() - time.sleep(1) # Wait for the shell to initialize + time.sleep( + 1 + ) # Wait for the shell to initialize. IMPORTANT WHEN WORKING WITH THE CAMERA channel.send("ifconfig eth0\n") time.sleep(1) channel.send("exit\n") From 9b74d796ee19313f910b9b8e67533698c2c2afec Mon Sep 17 00:00:00 2001 From: Ahmed Mahfouz Date: Fri, 1 Dec 2023 10:29:48 +0100 Subject: [PATCH 010/217] deb: add 2 debugger for scanning networks function There is one pretty debugger with indentation and another one just for IP and MAC. It turns out the function was thread safe already. --- common/byodr/utils/ssh.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/common/byodr/utils/ssh.py b/common/byodr/utils/ssh.py index 5213aaf6..55baad40 100644 --- a/common/byodr/utils/ssh.py +++ b/common/byodr/utils/ssh.py @@ -114,7 +114,13 @@ def fetch_wifi_networks(self): stdin, stdout, stderr = client.exec_command(command) output = stdout.read().decode("utf-8") - return self.parse_iwlist_output(output) + scanned_networks = self.parse_iwlist_output(output) + # DEBUGGING + # print(json.dumps(scanned_networks, indent=4)) # Pretty print the JSON + + # for network in scanned_networks: + # print(network.get("ESSID"), network.get("MAC"), end="\n") + return scanned_networks finally: client.close() From ef9a58e087ef86b8773edb9149a3464227df020f Mon Sep 17 00:00:00 2001 From: Ahmed Mahfouz Date: Fri, 1 Dec 2023 10:54:05 +0100 Subject: [PATCH 011/217] feat: relocate JS files for userMenu and Index There were lots of files in the root of JS folder --- teleop/htm/static/CSS/user_menu.css | 8 + teleop/htm/static/JS/Index/index.js | 44 ++ teleop/htm/static/JS/Index/index_a_utils.js | 182 ++++++ teleop/htm/static/JS/Index/index_b_gamepad.js | 193 +++++++ teleop/htm/static/JS/Index/index_c_screen.js | 519 ++++++++++++++++++ .../htm/static/JS/Index/index_d_navigator.js | 201 +++++++ teleop/htm/static/JS/Index/index_e_teleop.js | 269 +++++++++ .../JS/Index/index_f_trainingSessions.js | 36 ++ teleop/htm/static/JS/Index/index_video_hlp.js | 103 ++++ .../htm/static/JS/Index/index_video_mjpeg.js | 435 +++++++++++++++ .../htm/static/JS/userMenu/menu_controls.js | 170 ++++++ teleop/htm/static/JS/userMenu/menu_logbox.js | 153 ++++++ .../htm/static/JS/userMenu/menu_settings.js | 155 ++++++ teleop/htm/templates/index.html | 20 +- .../htm/templates/mobile_controller_ui.html | 1 - teleop/htm/templates/user_menu.html | 13 +- teleop/teleop/app.py | 2 +- 17 files changed, 2484 insertions(+), 20 deletions(-) create mode 100644 teleop/htm/static/JS/Index/index.js create mode 100644 teleop/htm/static/JS/Index/index_a_utils.js create mode 100644 teleop/htm/static/JS/Index/index_b_gamepad.js create mode 100644 teleop/htm/static/JS/Index/index_c_screen.js create mode 100644 teleop/htm/static/JS/Index/index_d_navigator.js create mode 100644 teleop/htm/static/JS/Index/index_e_teleop.js create mode 100644 teleop/htm/static/JS/Index/index_f_trainingSessions.js create mode 100644 teleop/htm/static/JS/Index/index_video_hlp.js create mode 100644 teleop/htm/static/JS/Index/index_video_mjpeg.js create mode 100644 teleop/htm/static/JS/userMenu/menu_controls.js create mode 100644 teleop/htm/static/JS/userMenu/menu_logbox.js create mode 100644 teleop/htm/static/JS/userMenu/menu_settings.js diff --git a/teleop/htm/static/CSS/user_menu.css b/teleop/htm/static/CSS/user_menu.css index 0591ddb6..f532a9fd 100644 --- a/teleop/htm/static/CSS/user_menu.css +++ b/teleop/htm/static/CSS/user_menu.css @@ -22,6 +22,14 @@ body { border-right: 1px solid gray; } +.application-nav p:first-child { + padding-bottom: 50px +} + +.application-nav p:last-child { + padding-top: 50px +} + .application-nav a { font-family: 'Arial'; font-size: 20px; diff --git a/teleop/htm/static/JS/Index/index.js b/teleop/htm/static/JS/Index/index.js new file mode 100644 index 00000000..ce16952c --- /dev/null +++ b/teleop/htm/static/JS/Index/index.js @@ -0,0 +1,44 @@ +// Set the name of the hidden property and the change event for visibility +var hidden, visibilityChange; +if (typeof document.hidden !== "undefined") { // Opera 12.10 and Firefox 18 and later support + hidden = "hidden"; + visibilityChange = "visibilitychange"; +} else if (typeof document.msHidden !== "undefined") { + hidden = "msHidden"; + visibilityChange = "msvisibilitychange"; +} else if (typeof document.webkitHidden !== "undefined") { + hidden = "webkitHidden"; + visibilityChange = "webkitvisibilitychange"; +} + +function start_all_handlers() { + navigator_start_all(); + teleop_start_all(); + mjpeg_start_all(); + h264_start_all(); +} + +function stop_all_handlers() { + navigator_stop_all(); + teleop_stop_all(); + mjpeg_stop_all(); + h264_stop_all(); +} + +function handleVisibilityChange() { + if (document[hidden]) { + stop_all_handlers(); + } else { + start_all_handlers(); + } +} + +$(function () { + if (!dev_tools.is_develop()) { + window.history.pushState({}, 'application_index_loaded', '/'); + } + document.addEventListener(visibilityChange, handleVisibilityChange, false); + window.addEventListener('focus', start_all_handlers); + window.addEventListener('blur', stop_all_handlers); + start_all_handlers(); +}); \ No newline at end of file diff --git a/teleop/htm/static/JS/Index/index_a_utils.js b/teleop/htm/static/JS/Index/index_a_utils.js new file mode 100644 index 00000000..db97ef46 --- /dev/null +++ b/teleop/htm/static/JS/Index/index_a_utils.js @@ -0,0 +1,182 @@ +jQuery.fn.visible = function () { + return this.css('visibility', 'visible'); +}; + +jQuery.fn.invisible = function () { + return this.css('visibility', 'hidden'); +}; + +jQuery.fn.is_visible = function () { + return this.css('visibility') == 'visible'; +}; + +function extend(proto, literal) { + var result = Object.create(proto); + Object.keys(literal).forEach(function (key) { + result[key] = literal[key]; + }); + return result; +} + +// Define the sockets that will be used for communication +var socket_utils = { + _init: function () { + // noop. + }, + create_socket: function (path, binary = true, reconnect = 100, assign = function (e) { }) { + ws_protocol = (document.location.protocol === "https:") ? "wss://" : "ws://"; + ws_url = ws_protocol + document.location.hostname + ":" + document.location.port + path; + ws = new WebSocket(ws_url); + // console.log(ws) //list of websockets used + if (binary) { + ws.binaryType = 'arraybuffer'; + } + assign(ws); + var _assigned_on_close = ws.onclose; + ws.onclose = function () { + if (typeof ws.is_reconnect == "function" && ws.is_reconnect()) { + setTimeout(function () { socket_utils.create_socket(path, binary, reconnect, assign); }, reconnect); + } + if (typeof _assigned_on_close === "function") { + _assigned_on_close(); + } + }; + } +} + +var dev_tools = { + _develop: null, + _vehicle: null, + _image_cache: new Map(), + + _random_choice: function (arr) { + return arr[Math.floor(Math.random() * arr.length)]; + }, + + _parse_develop: function () { + const params = new URLSearchParams(window.location.search); + var _develop = params.get('develop'); + if (_develop != undefined) { + _develop = ['xga', 'svga', 'vga'].includes(_develop) ? _develop : 'xga'; + } + return _develop; + }, + + _init: function () { + this._develop = this._parse_develop(); + }, + + is_develop: function () { + return this._develop == undefined ? this._parse_develop() : this._develop; + }, + + get_img_dimensions: function () { + const _key = this.is_develop(); + switch (_key) { + case "xga": + return [1024, 768]; + case "svga": + return [800, 600]; + case "vga": + return [640, 480]; + default: + return [320, 240]; + } + }, + + get_img_url: function (camera_position) { + const _key = this.is_develop(); + return '/develop/' + this._vehicle + '/img_' + camera_position + '_' + _key + '.jpg'; + }, + + get_img_blob: async function (camera_position, callback) { + const _url = this.get_img_url(camera_position); + if (this._image_cache.has(_url)) { + callback(this._image_cache.get(_url)); + } else { + const response = await fetch(_url); + const blob = await response.blob(); + this._image_cache.set(_url, blob); + callback(blob); + // console.log("Image cached " + _url); + } + }, + + set_next_resolution: function () { + var _next = null; + const _key = this.is_develop(); + switch (_key) { + case "xga": + _next = 'svga'; + break; + case "svga": + _next = 'vga'; + break; + case "vga": + _next = 'qvga'; + break; + default: + _next = 'xga'; + } + this._develop = _next; + } +} + +var page_utils = { + _capabilities: null, + + _init: function () { + // noop. + }, + + request_capabilities: function (callback) { + const _instance = this; + if (_instance._capabilities == undefined) { + if (dev_tools.is_develop()) { + _instance._capabilities = { + 'platform': { + 'vehicle': dev_tools._random_choice(['rover1', 'carla1']), + 'video': { 'front': { 'ptz': 0 }, 'rear': { 'ptz': 0 } } + } + }; + callback(_instance._capabilities); + } else { + jQuery.get("/teleop/system/capabilities", function (data) { + _instance._capabilities = data; + callback(_instance._capabilities); + }); + } + } else { + callback(_instance._capabilities); + } + }, + + get_stream_type: function () { + if (dev_tools.is_develop()) { + return 'mjpeg'; + } + var stream_type = window.localStorage.getItem('video.stream.type'); + if (stream_type == null) { + return 'h264'; + } else { + return stream_type; + } + }, + + set_stream_type: function (stream_type) { + window.localStorage.setItem('video.stream.type', stream_type); + } +} + + +// --------------------------------------------------- Initialisations follow --------------------------------------------------------- // +socket_utils._init(); +dev_tools._init(); +page_utils._init(); + +document.addEventListener("DOMContentLoaded", function () { + page_utils.request_capabilities(function (_capabilities) { + dev_tools._vehicle = _capabilities.platform.vehicle; + console.log('Received platform vehicle ' + dev_tools._vehicle); + }); +}); diff --git a/teleop/htm/static/JS/Index/index_b_gamepad.js b/teleop/htm/static/JS/Index/index_b_gamepad.js new file mode 100644 index 00000000..3e26cb7f --- /dev/null +++ b/teleop/htm/static/JS/Index/index_b_gamepad.js @@ -0,0 +1,193 @@ +//serves as a base or default controller +var NoneController = { + gamepad_index: 0, + steering: 0, + throttle: 0, + pan: 0, + tilt: 0, + button_y: 0, + button_b: 0, + button_x: 0, + button_a: 0, + button_left: 0, + button_right: 0, + button_center: 0, + arrow_up: 0, + arrow_down: 0, + arrow_left: 0, + arrow_right: 0, + healthy: false, + + reset: function () { + this.healthy = false; + }, + + collapse: function (value, zone = 0) { + result = Math.abs(value) <= zone ? 0 : value > 0 ? value - zone : value + zone; + // Scale back. + return result / (1 - zone); + }, + + gamepad: function () { + return navigator.getGamepads()[this.gamepad_index]; + }, + + set_throttle: function (left_trigger, right_trigger) { + // Observed gamepads which reported half-full throttle before use of any buttons. + if (!this.healthy && left_trigger < 0.01 && right_trigger < 0.01) { + this.healthy = true; + } + if (this.healthy) { + this.throttle = right_trigger > 0.01 ? -1 * right_trigger : left_trigger; + } else { + this.throttle = 0; + } + }, + + poll: function () { + return false; + } +} + +var Xbox360StandardController = extend(NoneController, { + threshold: 0.195, + + poll: function () { + pad = this.gamepad(); + if (pad != undefined) { + this.set_throttle(pad.buttons[6].value, pad.buttons[7].value) + this.steering = this.collapse(pad.axes[2], this.threshold); + this.pan = this.collapse(pad.axes[0], this.threshold); + this.tilt = this.collapse(pad.axes[1], this.threshold); + this.button_y = pad.buttons[3].value; + this.button_b = pad.buttons[1].value; + this.button_x = pad.buttons[2].value; + this.button_a = pad.buttons[0].value; + this.button_left = pad.buttons[4].value; + this.button_right = pad.buttons[5].value; + this.button_center = pad.buttons[16].value; + this.arrow_up = pad.buttons[12].value; + this.arrow_down = pad.buttons[13].value; + this.arrow_left = pad.buttons[14].value; + this.arrow_right = pad.buttons[15].value; + } + return this.healthy; + } +}); + +var PS4StandardController = extend(NoneController, { + threshold: 0.09, + + poll: function () { + // On ubuntu 18 under chrome button 17 does not exist - use button 16. + pad = this.gamepad(); + if (pad != undefined) { + this.set_throttle(pad.buttons[6].value, pad.buttons[7].value) + this.steering = this.collapse(pad.axes[2], this.threshold); + this.pan = this.collapse(pad.axes[0], this.threshold); + this.tilt = this.collapse(pad.axes[1], this.threshold); + this.button_y = pad.buttons[3].value; + this.button_b = pad.buttons[1].value; + this.button_x = pad.buttons[2].value; + this.button_a = pad.buttons[0].value; + this.button_left = pad.buttons[4].value; + this.button_right = pad.buttons[5].value; + this.button_center = pad.buttons[16].value; + this.arrow_up = pad.buttons[12].value; + this.arrow_down = pad.buttons[13].value; + this.arrow_left = pad.buttons[14].value; + this.arrow_right = pad.buttons[15].value; + } + return this.healthy; + } +}); + +var gamepad_controller = { + controller: Object.create(NoneController), + + _create_gamepad: function (gamepad) { + // 45e-28e-Xbox 360 Wired Controller / Xbox Wireless Controller (STANDARD GAMEPAD Vendor: 045e Product: 02fd) + // Wireless Controller (STANDARD GAMEPAD Vendor: 054c Product: 09cc) + var gid = gamepad.id; + var result = null; + if (gamepad.mapping == 'standard' && gid.includes('45e')) { + result = Object.create(Xbox360StandardController); + } else if (gamepad.mapping == 'standard') { + result = Object.create(PS4StandardController); + } + if (result) { + result.gamepad_index = gamepad.index; + } + return result; + }, + // Handle gamepad connections. When a gamepad connects, it checks if the gamepad is supported, then assigns the appropriate controller type (Xbox360StandardController or PS4StandardController) to the gamepad_controller object + _connect: function (gamepad, connecting) { + if (connecting) { + controller = this._create_gamepad(gamepad); + if (controller != undefined) { + this.controller = controller; + console.log("Connected " + gamepad.id + " - mapping = '" + gamepad.mapping + "'."); + } else { + console.log("Gamepad " + gamepad.id + " - mapping = '" + gamepad.mapping + "' not supported."); + } + } else { + this.controller = Object.create(NoneController); + console.log("Disconnected " + gamepad.id + "."); + } + }, + + is_active: function () { + return this.controller.poll(); + }, + + reset: function () { + this.controller.reset(); + }, + + get_command: function () { + const ct = this.controller; + // Skip buttons when not pressed to save bandwidth. + var command = {}; + command.steering = ct.steering; + command.throttle = ct.throttle; + command.pan = ct.pan; + command.tilt = ct.tilt; + if (ct.button_center) { + command.button_center = ct.button_center; + } + if (ct.button_left) { + command.button_left = ct.button_left; + } + if (ct.button_right) { + command.button_right = ct.button_right; + } + if (ct.button_a) { + command.button_a = ct.button_a; + } + if (ct.button_b) { + command.button_b = ct.button_b; + } + if (ct.button_x) { + command.button_x = ct.button_x; + } + if (ct.button_y) { + command.button_y = ct.button_y; + } + if (ct.arrow_up) { + command.arrow_up = ct.arrow_up; + } + if (ct.arrow_down) { + command.arrow_down = ct.arrow_down; + } + if (ct.arrow_left) { + command.arrow_left = ct.arrow_left; + } + if (ct.arrow_right) { + command.arrow_right = ct.arrow_right; + } + return command; + } +} + +window.addEventListener("gamepadconnected", function (e) { gamepad_controller._connect(e.gamepad, true); }, false); +window.addEventListener("gamepaddisconnected", function (e) { gamepad_controller._connect(e.gamepad, false); }, false); diff --git a/teleop/htm/static/JS/Index/index_c_screen.js b/teleop/htm/static/JS/Index/index_c_screen.js new file mode 100644 index 00000000..c9304712 --- /dev/null +++ b/teleop/htm/static/JS/Index/index_c_screen.js @@ -0,0 +1,519 @@ +var screen_utils = { + _version: '0.55.0', + _arrow_images: {}, + _wheel_images: {}, + _navigation_icons: {}, + + _create_image: function(url) { + im = new Image(); + im.src = url; + return im; + }, + + _init: function() { + this._arrow_images.left = this._create_image('../assets/im_arrow_left.png?v=' + this._version); + this._arrow_images.right = this._create_image('../assets/im_arrow_right.png?v=' + this._version); + this._arrow_images.ahead = this._create_image('../assets/im_arrow_up.png?v=' + this._version); + this._arrow_images.none = this._create_image('../assets/im_arrow_none.png?v=' + this._version); + this._wheel_images.black = this._create_image('../assets/im_wheel_black.png?v=' + this._version); + this._wheel_images.blue = this._create_image('../assets/im_wheel_blue.png?v=' + this._version); + this._wheel_images.red = this._create_image('../assets/im_wheel_red.png?v=' + this._version); + this._navigation_icons.play = this._create_image('../assets/icon_play.png?v=' + this._version); + this._navigation_icons.pause = this._create_image('../assets/icon_pause.png?v=' + this._version); + }, + + _decorate_server_message: function(message) { + message._is_on_autopilot = message.ctl == 5; + message._has_passage = message.inf_total_penalty < 1; + if (message.geo_head == undefined) { + message.geo_head_text = 'n/a'; + } else { + message.geo_head_text = message.geo_head.toFixed(2); + } + return message; + }, + + _turn_arrow_img: function(turn) { + switch(turn) { +// case "intersection.left": +// return this._arrow_images.left; +// case "intersection.right": +// return this._arrow_images.right; +// case "intersection.ahead": +// return this._arrow_images.ahead; + default: + return this._arrow_images.none; + } + }, + + _steering_wheel_img: function(message) { + if (message._is_on_autopilot && message._has_passage) { + return this._wheel_images.blue; + } + if (message._has_passage) { + return this._wheel_images.black; + } + return this._wheel_images.red; + }, + + _navigation_icon: function(state) { + switch(state) { + case "play": + return this._navigation_icons.play; + default: + return this._navigation_icons.pause; + } + } +} + + +var path_renderer = { + _init: function() { + const _instance = this; + }, + + _render_trapezoid: function(ctx, positions, fill) { + ctx.lineWidth = 0.5; + ctx.strokeStyle = 'rgb(255, 255, 255)'; + ctx.fillStyle = 'rgba(100, 217, 255, 0.3)'; + ctx.beginPath(); + ctx.moveTo(positions[0][0], positions[0][1]); + ctx.lineTo(positions[1][0], positions[1][1]); + ctx.lineTo(positions[2][0], positions[2][1]); + ctx.lineTo(positions[3][0], positions[3][1]); + ctx.closePath(); + ctx.stroke(); + ctx.fill(); + }, + + _get_constants: function() { + switch(dev_tools._vehicle) { + case "rover1": + return [400/640, 120/480, 6/640, 8/480, 0.65, 0.65, 0.8, 0.7, 65/640, 2/480]; + default: + return [400/640, 74/480, 6/640, 8/480, 0.65, 0.65, 0.8, 0.7, 80/640, 2/480]; + } + }, + + _render_path: function(ctx, path) { + const canvas = ctx.canvas; + const _constants = this._get_constants(); + const tz_width = _constants[0] * canvas.width; + const tz_height = _constants[1] * canvas.height; + const gap = _constants[2] * canvas.width; + const cut = _constants[3] * canvas.height; + const taper = _constants[4]; + const height_shrink = _constants[5]; + const gap_shrink = _constants[6]; + const cut_shrink = _constants[7]; + const w_steering = _constants[8] * canvas.width; + const h_steering = _constants[9] * canvas.height; + + // Start from the middle of the base of the trapezoid. + var a_x = (canvas.width / 2) - (tz_width / 2); + var a_y = canvas.height - gap; + var b_x = a_x + tz_width; + var b_y = a_y; + var idx = 0; + + path.forEach(function(element) { + // Start in the lower left corner and draw counter clockwise. + var w_base = b_x - a_x; + var w_off = (w_base - (w_base * taper)) / 2; + var v_height = tz_height * (height_shrink ** idx); + steer_dx = w_steering * element; + steer_dy = h_steering * element; + var c_x = b_x - w_off + steer_dx; + var c_y = b_y - v_height + (element > 0? steer_dy: 0); + var d_x = a_x + w_off + steer_dx; + var d_y = a_y - v_height - (element < 0? steer_dy: 0); + path_renderer._render_trapezoid(ctx, [[a_x, a_y], [b_x, b_y], [c_x, c_y], [d_x, d_y]]); + // The next step starts from the top of the previous with gap. + var c_shrink = .5 * cut * (cut_shrink ** idx); + var g_shrink = gap * (gap_shrink ** idx); + a_x = d_x + c_shrink; + a_y = d_y - g_shrink; + b_x = c_x - c_shrink; + b_y = c_y - g_shrink; + idx++; + }); + } +} + +var teleop_screen = { + el_viewport_container: null, + el_drive_bar: null, + el_drive_values: null, + el_pilot_bar: null, + el_message_box: null, + overlay_center_markers: null, + overlay_left_markers: null, + overlay_right_markers: null, + command_turn: null, + command_ctl: null, + server_message_listeners: [], + in_debug: 0, + is_connection_ok: 0, + controller_status: 0, + c_msg_connection_lost: "Connection lost - please wait or refresh the page.", + c_msg_controller_err: "Controller not detected - please press a button on the device.", + c_msg_teleop_view_only: "Another user is in control - you can remain as viewer or take over.", + active_camera: 'front', // The active camera is rendered on the main display. + _debug_values_listeners: [], + camera_activation_listeners: [], + selected_camera: null, // Select a camera for ptz control. + camera_selection_listeners: [], + _camera_cycle_timer: null, + _photo_snapshot_timer: null, + _last_server_message: null, + + _init() { + this.controller_status = gamepad_controller.is_active(); + this.el_viewport_container = $("div#viewport_container"); + this.el_drive_bar = $("div#debug_drive_bar"); + this.el_drive_values = $("div#debug_drive_values"); + this.el_pilot_bar = $("div#pilot_drive_values"); + this.el_message_box_container = $("div#message_box_container"); + this.el_message_box_message = this.el_message_box_container.find("div#message_box_message"); + this.el_button_take_control = this.el_message_box_container.find("input#message_box_button_take_control"); + this.overlay_center_markers = [$("div#overlay_center_distance0"), $("div#overlay_center_distance1")]; + this.overlay_left_markers = [$("div#overlay_left_marker0"), $("div#overlay_left_marker1")]; + this.overlay_right_markers = [$("div#overlay_right_marker0"), $("div#overlay_right_marker1")]; + this.add_camera_selection_listener(function() { + teleop_screen._on_camera_selection(); + }); + this._select_camera('rear'); + this.toggle_debug_values(true); + this.el_pilot_bar.click(function() { + teleop_screen.toggle_debug_values(); + }); + }, + + _on_viewport_container_resize: function() { + // Must use the singleton reference because async functions call us. + const _instance = teleop_screen; + const vh = window.innerHeight - _instance.el_viewport_container.offset().top; + // Leave a little space at the very bottom. + _instance.el_viewport_container.height(vh - 2); + // Calculate the new marker locations. + var _markers = [0.24 * vh, 0.12 * vh, 0.18 * vh, 0.10 * vh]; + if (dev_tools._vehicle == 'rover1') { + _markers = [0.52 * vh, 0.25 * vh, 0.45 * vh, 0.31 * vh]; + } + _instance._set_distance_indicators(_markers); + }, + + _set_distance_indicators: function(values) { + this.overlay_center_markers[0].css('bottom', values[0]); + this.overlay_center_markers[1].css('bottom', values[1]); + this.overlay_left_markers[0].css('bottom', values[2]); + this.overlay_left_markers[1].css('bottom', values[3]); + this.overlay_right_markers[0].css('bottom', values[2]); + this.overlay_right_markers[1].css('bottom', values[3]); + }, + + _render_distance_indicators: function() { + // const _show = this.in_debug && this.active_camera == 'front'? true: false; + const _show = this.active_camera == 'front'; + const _hard_yellow = 'rgba(255, 255, 120, 0.99)'; + const _soft_yellow = 'rgba(255, 255, 120, 0.50)'; + if (_show) { + const _m = this._last_server_message; + const _color = _m != undefined && _m._is_on_autopilot && _m.max_speed < 1e-3? _hard_yellow: _soft_yellow; + this.overlay_center_markers[0].css('color', `${_color}`); + this.overlay_center_markers[1].css('color', `${_color}`); + this.overlay_left_markers[0].css('color', `${_color}`); + this.overlay_left_markers[1].css('color', `${_color}`); + this.overlay_right_markers[0].css('color', `${_color}`); + this.overlay_right_markers[1].css('color', `${_color}`); + this.overlay_center_markers[0].css('border-bottom', `2px solid ${_color}`); + this.overlay_center_markers[1].css('border-bottom', `3px solid ${_color}`); + this.overlay_left_markers[0].css('border-bottom', `3px solid ${_color}`); + this.overlay_left_markers[1].css('border-top', `2px solid ${_color}`); + this.overlay_right_markers[0].css('border-bottom', `3px solid ${_color}`); + this.overlay_right_markers[1].css('border-top', `2px solid ${_color}`); + } + [this.overlay_center_markers, this.overlay_left_markers, this.overlay_right_markers].flat().forEach(function(_m) { + if (_show) { + _m.show(); + } else { + _m.hide(); + } + }); + }, + + _select_next_camera: function() { + // The active camera cannot be undefined. + const _next = this.selected_camera == undefined? this.active_camera: this.selected_camera == 'front'? 'rear': 'front'; + this._select_camera(_next); + }, + + _select_camera: function(name) { + this.selected_camera = name; + this.camera_selection_listeners.forEach(function(cb) {cb();}); + }, + + _cycle_camera_selection: function(direction) { + this._select_next_camera(); + if (this.selected_camera != undefined) { + console.log("Camera " + this.selected_camera + " is selected for ptz control.") + } + this._camera_cycle_timer = null; + }, + + _schedule_camera_cycle: function(direction) { + if (this._camera_cycle_timer == undefined) { + this._camera_cycle_timer = setTimeout(function() {teleop_screen._cycle_camera_selection();}, 150); + } + }, + + _schedule_photo_snapshot_effect: function() { + if (this._photo_snapshot_timer != undefined) { + clearTimeout(this._photo_snapshot_timer); + } + this._photo_snapshot_timer = setTimeout(function() {teleop_screen.el_viewport_container.fadeOut(50).fadeIn(50);}, 130); + }, + + _server_message: function(message) { + this._last_server_message = message; + + $('span#pilot_steering').text(message.ste.toFixed(3)); + $('span#pilot_throttle').text(message.thr.toFixed(3)); + // It may be the inference service is not (yet) available. + const _debug = this.in_debug; + if (message.inf_surprise != undefined) { + $('span#inference_brake_critic').text(message.inf_brake_critic.toFixed(2)); + $('span#inference_obstacle').text(message.inf_brake.toFixed(2)); + $('span#inference_surprise').text(message.inf_surprise.toFixed(2)); + $('span#inference_critic').text(message.inf_critic.toFixed(2)); + $('span#inference_fps').text(message.inf_hz.toFixed(0)); + $('span#inference_desired_speed').text(message.des_speed.toFixed(1)); + // Calculate the color schemes. + const red_brake = Math.min(255., message.inf_brake_penalty * 2. * 255.); + const green_brake = (1. - 2. * Math.max(0., message.inf_brake_penalty - 0.5)) * 255.; + $('span#inference_brake_critic').css('color', `rgb(${red_brake}, ${green_brake}, 0)`); + $('span#inference_obstacle').css('color', `rgb(${red_brake}, ${green_brake}, 0)`); + const red_steer = Math.min(255., message.inf_steer_penalty * 2. * 255.); + const green_steer = (1. - 2. * Math.max(0., message.inf_steer_penalty - 0.5)) * 255.; + $('span#inference_surprise').css('color', `rgb(${red_steer}, ${green_steer}, 0)`); + $('span#inference_critic').css('color', `rgb(${red_steer}, ${green_steer}, 0)`); + } + // + if (this.command_turn != message.turn) { + this.command_turn = message.turn; + $('img#arrow').attr('src', screen_utils._turn_arrow_img(message.turn).src); + } + // des_speed is the desired speed + // vel_y is the actual vehicle speed + var el_alpha_speed = $('div#alpha_speed_value'); + var el_alpha_speed_label = $('div#alpha_speed_label'); + var el_beta_speed_container = $('div#beta_speed'); + var el_beta_speed = $('div#beta_speed_value'); + if (message._is_on_autopilot) { + el_alpha_speed.text(message.max_speed.toFixed(1)); + el_beta_speed.text(message.vel_y.toFixed(1)); + } else { + el_alpha_speed.text(message.vel_y.toFixed(1)); + } + // + var el_steering_wheel = $('img#steeringWheel'); + var el_autopilot_status = $('#autopilot_status'); + var str_command_ctl = message.ctl + '_' + message._has_passage; + if (this.command_ctl != str_command_ctl) { + this.command_ctl = str_command_ctl; + el_steering_wheel.attr('src', screen_utils._steering_wheel_img(message).src); + if (message._is_on_autopilot) { + el_alpha_speed_label.text('MAX'); + el_beta_speed_container.show(); + } else { + el_alpha_speed_label.text('km/h'); + el_beta_speed_container.hide(); + el_autopilot_status.text('00:00:00'); + el_autopilot_status.css('color', 'white'); + } + this._render_distance_indicators(); + } + if (message._is_on_autopilot && message.ctl_activation > 0) { + // Convert the time from milliseconds to seconds. + const _diff = 1e-3 * message.ctl_activation; + const _hours = Math.floor(_diff / 3600); + const _mins = Math.floor((_diff - _hours * 3600) / 60); + const _secs = Math.floor(_diff - _hours * 3600 - _mins * 60); + const _zf_h = ('00' + _hours).slice(-2) + const _zf_m = ('00' + _mins).slice(-2) + const _zf_s = ('00' + _secs).slice(-2) + el_autopilot_status.text(`${_zf_h}:${_zf_m}:${_zf_s}`); + el_autopilot_status.css('color', 'rgb(100, 217, 255)'); + } + var display_rotation = Math.floor(message.ste * 90.0) + el_steering_wheel.css('transform', "rotate(" + display_rotation + "deg)"); + }, + + _on_camera_selection: function() { + const _active = this.active_camera; + const _selected = this.selected_camera; + const viewport_container = this.el_viewport_container; + const overlay_container = $('div#overlay_image_container'); + const overlay_image = $('img#overlay_image'); + + viewport_container.removeClass('selected'); + overlay_image.removeClass('selected'); + if (_selected == _active) { + viewport_container.addClass('selected'); + } else if (_selected != undefined) { + overlay_image.addClass('selected'); + } + }, + + add_toggle_debug_values_listener: function(cb) { + this._debug_values_listeners.push(cb); + }, + + toggle_debug_values: function(show) { + if (show == undefined) { + show = !this.in_debug; + } + if (show) { + this.el_drive_bar.show(); + this._on_viewport_container_resize(); + this.el_pilot_bar.css({'cursor': 'zoom-out'}); + } else { + // this.el_drive_bar.hide(); + // this._on_viewport_container_resize(); + this.el_pilot_bar.css({'cursor': 'zoom-in'}); + + } + this.in_debug = show? 1: 0; + this._render_distance_indicators(); + this._debug_values_listeners.forEach(function(cb) {cb(show);}); + }, + + add_camera_activation_listener: function(cb) { + this.camera_activation_listeners.push(cb); + }, + + add_camera_selection_listener: function(cb) { + this.camera_selection_listeners.push(cb); + }, + + toggle_camera: function() { + const name = this.active_camera == 'front'? 'rear': 'front'; + this.active_camera = name; + this.selected_camera = null; + this._render_distance_indicators(); + this.camera_activation_listeners.forEach(function(cb) {cb(name);}); + this.camera_selection_listeners.forEach(function(cb) {cb();}); + }, + + controller_update: function(command) { + const message_box_container = this.el_message_box_container; + const message_box_message = this.el_message_box_message; + const button_take_control = this.el_button_take_control; + const is_connection_ok = this.is_connection_ok; + const controller_status = this.controller_status; + const c_msg_connection_lost = this.c_msg_connection_lost; + const c_msg_controller_err = this.c_msg_controller_err; + const c_msg_teleop_view_only = this.c_msg_teleop_view_only; + var show_message = false; + var show_button = false; + if (!is_connection_ok) { + message_box_message.text(c_msg_connection_lost); + message_box_message.removeClass(); + message_box_message.addClass('error_message'); + show_message = true; + } else if (controller_status == 0) { + message_box_message.text(c_msg_controller_err); + message_box_message.removeClass(); + message_box_message.addClass('warning_message'); + show_message = true; + } else if (controller_status == 2) { + message_box_message.text(c_msg_teleop_view_only); + message_box_message.removeClass(); + message_box_message.addClass('warning_message'); + show_message = true; + show_button = true; + } + if (show_message) { + if (show_button) { + button_take_control.show(); + } else { + button_take_control.hide(); + } + message_box_container.show(); + this._on_viewport_container_resize(); + } else { + message_box_container.hide(); + this._on_viewport_container_resize(); + } + // + if (command.arrow_left) { + this._schedule_camera_cycle(); + } else if (command.arrow_right) { + this._schedule_camera_cycle(); + } + // + if (command.button_right) { + this._schedule_photo_snapshot_effect(); + } + }, + + on_canvas_init: function(width, height) { + $('span#debug_screen_dimension').text(width + 'x' + height); + }, + + canvas_update: function(ctx) { + const message = this._last_server_message; + const _ap = message != undefined && message._is_on_autopilot; + if (_ap && this.active_camera == 'front' && this.in_debug) { + path_renderer._render_path(ctx, message.nav_path); + } + } +} + + +function screen_set_distance_indicators() { + switch(dev_tools._vehicle) { + case "rover1": + this.overlay_center_markers[0].css('bottom', 'calc(45vh - 0px)'); + this.overlay_center_markers[1].css('bottom', 'calc(22vh - 0px)'); + this.overlay_left_markers[0].css('bottom', 'calc(30vh - 0px)'); + this.overlay_left_markers[1].css('bottom', 'calc(40vh - 0px)'); + this.overlay_right_markers[0].css('bottom', 'calc(30vh - 0px)'); + this.overlay_right_markers[1].css('bottom', 'calc(40vh - 0px)'); + break; + default: + this.overlay_center_markers[0].css('bottom', 'calc(20vh - 0px)'); + this.overlay_center_markers[1].css('bottom', 'calc(10vh - 0px)'); + this.overlay_left_markers[0].css('bottom', 'calc(10vh - 0px)'); + this.overlay_left_markers[1].css('bottom', 'calc(15vh - 0px)'); + this.overlay_right_markers[0].css('bottom', 'calc(10vh - 0px)'); + this.overlay_right_markers[1].css('bottom', 'calc(15vh - 0px)'); + } +} + + +function screen_poll_platform() { + if (dev_tools._vehicle == undefined) { + setTimeout(function() {screen_poll_platform();}, 200); + } else { + teleop_screen._on_viewport_container_resize(); + teleop_screen._render_distance_indicators(); + } +} + +// --------------------------------------------------- Initialisations follow --------------------------------------------------------- // +screen_utils._init(); + +document.addEventListener("DOMContentLoaded", function() { + if (page_utils.get_stream_type() == 'mjpeg') { + $('a#video_stream_mjpeg').addClass('active'); + $('a#video_stream_h264').addClass('inactive'); + } else { + $('a#video_stream_mjpeg').addClass('inactive'); + $('a#video_stream_h264').addClass('active'); + } + teleop_screen._init(); + screen_poll_platform(); + teleop_screen._on_viewport_container_resize(); + window.onresize = teleop_screen._on_viewport_container_resize; +}); diff --git a/teleop/htm/static/JS/Index/index_d_navigator.js b/teleop/htm/static/JS/Index/index_d_navigator.js new file mode 100644 index 00000000..c876495a --- /dev/null +++ b/teleop/htm/static/JS/Index/index_d_navigator.js @@ -0,0 +1,201 @@ +class RealNavigatorController { + constructor() { + const location = document.location; + this.nav_path = location.protocol + "//" + location.hostname + ":" + location.port + "/ws/nav"; + this.random_id = Math.random(); // For browser control of navigation image urls. + this.el_current_image = null; + this.el_next_image = null; + this.el_image_width = null; + this.el_image_height = null; + this.el_route = null; + this.el_point = null; + this.el_route_select_prev = null; + this.el_route_select_next = null; + this.navigation_images = [null, null]; + this.routes = []; + this.selected_route = null; + this.started = false; + this.backend_active = false; + this.in_mouse_over = false; + this.in_debug = false; + } + _initialize() { + this.el_current_image = $('img#current_navigation_image'); + this.el_next_image = $('img#next_navigation_image'); + this.el_image_width = this.el_current_image.width(); + this.el_image_height = this.el_current_image.height(); + this.el_route = $('span#navigation_route_name'); + this.el_point = $('span#navigation_point_name'); + this.el_route_select_prev = $('span#navigation_route_sel_prev'); + this.el_route_select_next = $('span#navigation_route_sel_next'); + this.el_point.text(''); + } + _has_next_route() { + const route = this.selected_route; + const t_size = this.routes == undefined ? 0 : this.routes.length; + if (t_size > 0 && (route == undefined || this.routes.indexOf(route) + 1 < t_size)) { + return true; + } + return false; + } + _has_prev_route() { + const route = this.selected_route; + const t_size = this.routes == undefined ? 0 : this.routes.length; + if (t_size > 0 && (route == undefined || this.routes.indexOf(route) - 1 >= 0)) { + return true; + } + return false; + } + _set_routes(response) { + const routes = response.routes; + const selected = response.selected; + this.routes = routes; + if (routes.length == 0) { + this._select_route(null); + } else if (routes.indexOf(selected) != -1) { + this._select_route(selected); + } else { + this._select_route(routes[0]); + } + } + _select_route(name) { + this.selected_route = name; + this.el_route.text(name); + } + _select_prev_route() { + const route = this.selected_route; + const t_size = this.routes == undefined ? 0 : this.routes.length; + if (t_size > 0) { + var idx = this.routes.indexOf(route) - 1; + this._select_route(this.routes[idx < 0 ? t_size - 1 : idx]); + } + } + _select_next_route() { + const route = this.selected_route; + const t_size = this.routes == undefined ? 0 : this.routes.length; + if (t_size > 0) { + var idx = this.routes.indexOf(route) + 1; + this._select_route(this.routes[idx >= t_size ? 0 : idx]); + } + } + _render_navigation_images(current_src, next_src) { + this.el_current_image.attr('src', current_src).width(this.el_image_width).height(this.el_image_height); + this.el_next_image.attr('src', next_src).width(this.el_image_width).height(this.el_image_height); + } + _update_visibility() { + if (this.routes.length < 1) { + $('div#navigation_image_container').invisible(); + $('div#navigation_route_container').invisible(); + } else { + $('div#navigation_image_container').visible(); + $('div#navigation_route_container').visible(); + } + } + _navigation_img_url(_id) { + return this.nav_path + '?im=' + _id + '&r=' + this.selected_route + '&n=' + this.random_id; + } + _schedule_navigation_image_update() { + var current_src = screen_utils._navigation_icon(this.backend_active ? 'pause' : 'play').src + var next_src = this._navigation_img_url(this.navigation_images[1]); + if (this.backend_active && !this.in_mouse_over) { + current_src = this._navigation_img_url(this.navigation_images[0]); + } + const _instance = this; + setTimeout(function () { + _instance._render_navigation_images(current_src, next_src); + }, 0); + } + _mouse_over() { + this.in_mouse_over = true; + this._schedule_navigation_image_update(); + } + _mouse_out() { + this.in_mouse_over = false; + this._schedule_navigation_image_update(); + } + _toggle_route() { + var command = { 'action': 'toggle', 'route': this.selected_route }; + $.post('teleop/navigation/routes', JSON.stringify(command)) + .fail(function (xhr, status, error) { + console.log(error); + }) + .done(function (data) { + // console.log(data); + }); + } + _list_routes() { + const _instance = this; + $.get("/teleop/navigation/routes?action=list", function (response) { + _instance._set_routes(response); + _instance._update_visibility(); + }); + } + _server_message(message) { + $('span#navigation_geo_lat').text(message.geo_lat.toFixed(6)); + $('span#navigation_geo_long').text(message.geo_long.toFixed(6)); + $('span#navigation_heading').text(message.geo_head_text); + if (this.started) { + if (message.inf_surprise != undefined) { + const _nni_dist = Math.min(1, message.nav_distance[1]); + $('span#navigation_match_image_distance').text(_nni_dist.toFixed(2)); + $('span#navigation_current_command').text(message.nav_command.toFixed(1)); + $('span#navigation_direction').text(message.nav_direction.toFixed(2)); + // Make the next expected navigation image stand out more as it comes closer. + $('img#next_navigation_image').css('opacity', 1. - 0.85 * _nni_dist); + + } + const backend_active = message.nav_active; + const is_backend_change = backend_active != this.backend_active; + const is_image_change = message.nav_image[0] != this.navigation_images[0] || message.nav_image[1] != this.navigation_images[1]; + this.backend_active = backend_active; + this.navigation_images = message.nav_image; + if (is_image_change || is_backend_change) { + this._schedule_navigation_image_update(); + } + if (backend_active) { + this.el_point.text(message.nav_point); + } + } + } +} + +class FakeNavigatorController extends RealNavigatorController { + constructor() { + super(); + } + _toggle_route() { + } + _list_routes() { + } +} + +// In development mode there is no use of a backend. +const navigator_controller = dev_tools.is_develop() ? new FakeNavigatorController() : new RealNavigatorController(); + +function navigator_start_all() { + navigator_controller.started = true; + navigator_controller._list_routes(); +} + +function navigator_stop_all() { + navigator_controller.started = false; +} + + +// --------------------------------------------------- Initialisations follow --------------------------------------------------------- // +teleop_screen.add_toggle_debug_values_listener(function (show) { + navigator_controller.in_debug = show; + navigator_controller._update_visibility(); +}); + +document.addEventListener("DOMContentLoaded", function () { + navigator_controller._initialize(); + navigator_controller.el_current_image.mouseover(function () { navigator_controller._mouse_over() }); + navigator_controller.el_current_image.mouseout(function () { navigator_controller._mouse_out() }); + navigator_controller.el_current_image.click(function () { navigator_controller._toggle_route() }); + navigator_controller.el_route_select_prev.click(function () { navigator_controller._select_prev_route() }); + navigator_controller.el_route_select_next.click(function () { navigator_controller._select_next_route() }); + server_socket.add_server_message_listener(function (message) { + navigator_controller._server_message(message); + }); +}); diff --git a/teleop/htm/static/JS/Index/index_e_teleop.js b/teleop/htm/static/JS/Index/index_e_teleop.js new file mode 100644 index 00000000..6f5ffaa0 --- /dev/null +++ b/teleop/htm/static/JS/Index/index_e_teleop.js @@ -0,0 +1,269 @@ +class RealServerSocket { + constructor() { + this.server_message_listeners = []; + } + _notify_server_message_listeners(message) { + this.server_message_listeners.forEach(function (cb) { + cb(message); + }); + } + add_server_message_listener(cb) { + this.server_message_listeners.push(cb); + } + _capture() { + const _instance = this; + const _socket = _instance.socket; + if (_socket != undefined && _socket.readyState == 1) { + _socket.send('{}'); + } + } + _start_socket() { + const _instance = this; + const _socket = _instance.socket; + if (_socket == undefined) { + socket_utils.create_socket("/ws/log", false, 250, function (ws) { + _instance.socket = ws; + ws.attempt_reconnect = true; + ws.is_reconnect = function () { + return ws.attempt_reconnect; + } + ws.onopen = function () { + console.log("Server socket connection established."); + _instance._capture(); + }; + ws.onclose = function () { + console.log("Server socket connection closed."); + }; + ws.onmessage = function (evt) { + var message = JSON.parse(evt.data); + // console.log(message); + setTimeout(function () { _instance._capture(); }, 40); + setTimeout(function () { + _instance._notify_server_message_listeners(screen_utils._decorate_server_message(message)); + }, 0); + }; + }); + } + } + _stop_socket() { + const _instance = this; + const _socket = _instance.socket; + if (_socket != undefined) { + _socket.attempt_reconnect = false; + if (_socket.readyState < 2) { + _socket.close(); + } + _instance.socket = null; + } + } +} + + +class FakeServerSocket extends RealServerSocket { + constructor() { + super(); + this._running = false; + this._num_steps = 0; + this._navigation_path_x = [0, 0, 0, 0, 0]; + this._navigation_path_dx = [0.10, 0.10, 0.10, 0.10, 0.10]; + } + _create_message() { + // Update the navigation path. + if (this._num_steps == 100000) { + this._num_steps = 0; + this._navigation_path_x = [0, 0, 0, 0, 0]; + } else { + this._num_steps++; + const _navigation_path_dx = this._navigation_path_dx; + const _a = this._navigation_path_x.map(function (num, idx) { return num + _navigation_path_dx[idx] * Math.PI; }); + this._navigation_path_x = _a; + } + // Simulate the path. + const _navigation_path = this._navigation_path_x.map(function (e) { return Math.sin(e); }); + // Simulate the server message based on the gamepad + const gamepad_command = gamepad_controller.is_active() ? gamepad_controller.get_command() : {}; + + return { + 'ctl': gamepad_command.button_y ? 5 : 2, + 'ctl_activation': new Date().getTime() - Math.floor(Math.random() * 1e7), + 'geo_head': -85.13683911988542, + 'geo_lat': 49.00155759398021, + 'geo_long': 8.002592177745152, + 'head': -85.13683911988542, + 'inf_brake': 0.031192919239401817, + 'inf_brake_critic': 0.7258226275444031, + 'inf_critic': 0.38173234462738037, + 'inf_hz': 29.67614871413384, + 'inf_steer_penalty': Math.random(), + 'inf_brake_penalty': Math.random(), + 'inf_total_penalty': 0.031138078539692818, + 'inf_surprise': 0.24887815117835999, + 'max_speed': dev_tools._random_choice([0, 1, 2]), + 'nav_active': 0, + 'nav_command': 0, + 'nav_direction': -0.05672478172928095, + 'nav_distance': [1, 1], + 'nav_image': [-1, -1], + 'nav_path': _navigation_path, + 'nav_point': "", + 'des_speed': Math.floor(Math.random() * 10), + 'ste': gamepad_command.steering ? gamepad_command.steering : 0, + 'thr': gamepad_command.throttle ? gamepad_command.throttle : 0, + 'turn': "general.fallback", + 'vel_y': Math.floor(Math.random() * 10) + }; + } + _capture() { + const _instance = this; + if (_instance._running) { + var message = _instance._create_message(); + super._notify_server_message_listeners(screen_utils._decorate_server_message(message)); + setTimeout(function () { _instance._capture(); }, 250); + } + } + _start_socket() { + this._running = true; + this._capture(); + } + _stop_socket() { + this._running = false; + } +} + +class RealGamepadSocket { + constructor() { + // noop. + // + } + _send(command) { + if (this.socket != undefined && this.socket.readyState == 1) { + console.log("command") + this.socket.send(JSON.stringify(command)); + } + } + _request_take_over_control() { + var command = {}; + command._operator = 'force'; + this._send(command); + } + _capture(server_response) { + const gc_active = gamepad_controller.is_active(); + var gamepad_command = gc_active ? gamepad_controller.get_command() : {}; + // The selected camera for ptz control can also be undefined. + gamepad_command.camera_id = -1; + if (teleop_screen.selected_camera == 'front') { + gamepad_command.camera_id = 0; + } else if (teleop_screen.selected_camera == 'rear') { + gamepad_command.camera_id = 1; + } + this._send(gamepad_command); + if (server_response != undefined && server_response.control == 'operator') { + teleop_screen.controller_status = gc_active; + } else if (server_response != undefined) { + teleop_screen.controller_status = 2; + } + //gamepad_command is where the commands from the controller are stored.if there are buttons are pressed, they will be added to this JSON object + //steering - throttle - pan - tilt - camera_id + // console.log(gamepad_command) + teleop_screen.controller_update(gamepad_command); + } + _start_socket() { + const _instance = this; + if (_instance.socket == undefined) { + socket_utils.create_socket("/ws/ctl", false, 250, function (ws) { + _instance.socket = ws; + ws.attempt_reconnect = true; + ws.is_reconnect = function () { + return ws.attempt_reconnect; + } + ws.onopen = function () { + // console.log("Operator socket connection was established."); + teleop_screen.is_connection_ok = 1; + _instance._capture(); + }; + ws.onclose = function () { + teleop_screen.is_connection_ok = 0; + teleop_screen.controller_update({}); + //console.log("Operator socket connection was closed."); + }; + ws.onerror = function () { + teleop_screen.controller_status = gamepad_controller.is_active(); + teleop_screen.is_connection_ok = 0; + teleop_screen.controller_update({}); + }; + ws.onmessage = function (evt) { + // console.log(evt) + var message = JSON.parse(evt.data); + setTimeout(function () { _instance._capture(message); }, 0); + }; + }); + } + } + _stop_socket() { + const _instance = this; + if (_instance.socket != undefined) { + _instance.socket.attempt_reconnect = false; + if (_instance.socket.readyState < 2) { + _instance.socket.close(); + } + _instance.socket = null; + } + } +} + + +class FakeGamepadSocket extends RealGamepadSocket { + constructor() { + super(); + this._operator = dev_tools._random_choice(['operator', 'viewer']); + } + _send(command) { + } + _request_take_over_control() { + this._operator = 'operator'; + } + _poll() { + const _instance = this; + if (_instance._running) { + super._capture(JSON.parse('{"control":"' + _instance._operator + '"}')); + setTimeout(function () { _instance._poll(); }, 100); + } + } + _start_socket() { + teleop_screen.is_connection_ok = 1; + this._running = true; + this._poll(); + } + _stop_socket() { + this._running = false; + teleop_screen.is_connection_ok = 0; + teleop_screen.controller_update({}); + } +} + + +// In development mode there is no use of a backend. +const server_socket = dev_tools.is_develop() ? new FakeServerSocket() : new RealServerSocket(); +const gamepad_socket = dev_tools.is_develop() ? new FakeGamepadSocket() : new RealGamepadSocket(); + +function teleop_start_all() { + gamepad_controller.reset(); + server_socket._start_socket(); + gamepad_socket._start_socket(); +} + +function teleop_stop_all() { + server_socket._stop_socket(); + gamepad_socket._stop_socket(); + gamepad_controller.reset(); +} + + +document.addEventListener("DOMContentLoaded", function () { + server_socket.add_server_message_listener(function (message) { + teleop_screen._server_message(message); + }); + $("input#message_box_button_take_control").click(function () { + gamepad_socket._request_take_over_control(); + }); +}); diff --git a/teleop/htm/static/JS/Index/index_f_trainingSessions.js b/teleop/htm/static/JS/Index/index_f_trainingSessions.js new file mode 100644 index 00000000..7ed6289e --- /dev/null +++ b/teleop/htm/static/JS/Index/index_f_trainingSessions.js @@ -0,0 +1,36 @@ +function run_draw_map_python() { + fetch("/run_draw_map_python") + .then((response) => response.json()) + .then((map_date_href) => { + // console.log(map_date_href); + const fileList = document.getElementById("fileList"); + fileList.innerHTML = ""; + for (var map_date in map_date_href) { + const listItem = document.createElement("li"); + const link = document.createElement("a"); + link.href = map_date_href[map_date]; + link.target = "_blank"; + link.textContent = map_date; + listItem.appendChild(link); + fileList.appendChild(listItem); + } + }) + .catch((error) => { + console.error("Error:", error); + }); +} +window.onload = function () { + var a = document.getElementById("open_training_sessions_list"); + var popup = document.getElementById("popupWindow"); + a.onclick = function () { + popup.style.display = "flex"; + return false; + }; +}; + +function hidePopup() { + var popup = document.getElementById("popupWindow"); + popup.style.display = "none"; + const fileList = document.getElementById("fileList"); + fileList.innerHTML = ""; // Clear existing file list +} diff --git a/teleop/htm/static/JS/Index/index_video_hlp.js b/teleop/htm/static/JS/Index/index_video_hlp.js new file mode 100644 index 00000000..a3e4f599 --- /dev/null +++ b/teleop/htm/static/JS/Index/index_video_hlp.js @@ -0,0 +1,103 @@ +if (page_utils.get_stream_type() == 'h264') { + class CameraSocketResumer { + constructor(uri, reconnect_ms) { + this.uri = uri; + this.reconnect_ms = reconnect_ms; + this.attempt_reconnect = true; + } + onopen(player) { + player.playStream(); + } + onclose(player) { + if (this.attempt_reconnect) { + setTimeout(function() {player.connect(this.uri);}, this.reconnect_ms); + } + } + } + + var canvas_controller = { + el_parent: null, + el_canvas_id: 'viewport_canvas', + el_canvas: null, + context_2d: null, + + init: function(el_parent) { + this.el_parent = el_parent; + }, + replace: function(canvas) { + if (this.el_canvas != undefined) { + this.el_canvas.remove(); + } + this.context_2d = canvas.getContext('2d'); + this.el_canvas = canvas; + this.el_parent.appendChild(this.el_canvas); + }, + create: function() { + canvas = document.getElementById(this.el_canvas_id); + // The canvas dimensions are set by the player. + // canvas = document.createElement("canvas"); + // canvas.id = this.el_canvas_id; + // canvas.style.cssText = 'width: 100% !important; height: 100% !important;'; + return canvas; + } + } + + var camera_controller = { + wsavc: null, + socket: null, + + start: function(camera_position) { + const port = camera_position == 'front' ? 9001 : 9002; + const ws_protocol = (document.location.protocol === "https:") ? "wss://" : "ws://"; + const uri = ws_protocol + document.location.hostname + ':' + port; + this.socket = new CameraSocketResumer(uri, 100); + // The webgl context does not have a 2d rendering context. + this.wsavc = new WSAvcPlayer(canvas_controller.create(), "yuv", this.socket); + this.wsavc.on('canvasReady', function(width, height) { + canvas_controller.replace(camera_controller.wsavc.canvas); + teleop_screen.on_canvas_init(width, height); + }); + this.wsavc.on('canvasRendered', function() { + // Do not run the canvas draws in parallel. + if (canvas_controller.context_2d != undefined) { + teleop_screen.canvas_update(canvas_controller.context_2d); + } + }); + this.wsavc.connect(uri); + }, + stop: function() { + if (this.socket != undefined && this.wsavc != undefined) { + this.socket.attempt_reconnect = false; + this.wsavc.disconnect(); + this.socket = null; + delete this.wsavc.ws; + delete this.wsavc; + } + } + } + + teleop_screen.add_camera_activation_listener(function(position) { + camera_controller.stop(); + camera_controller.start(position); + }); +} + +function h264_start_all() { + if (page_utils.get_stream_type() == 'h264' && camera_controller != undefined && canvas_controller != undefined + && camera_controller.socket == undefined) { + canvas_controller.init(document.getElementById('viewport_container')); + camera_controller.start(teleop_screen.active_camera); + } +} + +function h264_stop_all() { + if (page_utils.get_stream_type() == 'h264' && camera_controller != undefined) { + camera_controller.stop(); + } +} + +document.addEventListener("DOMContentLoaded", function() { + if (!dev_tools.is_develop()) { + $("a#video_stream_h264").click(function() {page_utils.set_stream_type('h264'); location.reload();}); + } +}); diff --git a/teleop/htm/static/JS/Index/index_video_mjpeg.js b/teleop/htm/static/JS/Index/index_video_mjpeg.js new file mode 100644 index 00000000..ab07a679 --- /dev/null +++ b/teleop/htm/static/JS/Index/index_video_mjpeg.js @@ -0,0 +1,435 @@ +class MJPEGFrameController { + constructor() { + this._target_timeout = null; + // larger = more smoothing + this._time_smoothing = 0.80; + this._actual_fps = 0; + this._target_fps = 0; + this._timeout = 0; + this._duration = 1000; + this._request_start = performance.now(); + this.max_jpeg_quality = 50; + this.jpeg_quality = 20; + this.min_jpeg_quality = 25; + this.update_framerate(); + } + set_target_fps(x) { + this._actual_fps = x; + this._target_fps = x; + this._target_timeout = this._target_fps > 0? 1000. / this._target_fps: null; + } + get_actual_fps() { + return this._actual_fps; + } + update_framerate() { + if (this._target_timeout == undefined) { + this._timeout = null; + } else { + const _now = performance.now(); + const _duration = _now - this._request_start; + this._duration = this._time_smoothing * this._duration + (1 - this._time_smoothing) * _duration; + this._request_start = _now; + this._actual_fps = Math.round(1000.0 / this._duration); + var q_step = Math.min(1, Math.max(-1, this._actual_fps - this._target_fps)); + this.jpeg_quality = Math.min(this.max_jpeg_quality, Math.max(this.min_jpeg_quality, this.jpeg_quality + q_step)); + this._timeout = Math.max(0, this._target_timeout - this._duration); + } + return this._timeout + } +} + +class MJPEGControlLocalStorage { + constructor() { + this.min_jpeg_quality = 25; + this.max_jpeg_quality = 50; + this.load(); + } + set_max_quality(val) { + if (val > 0 && val <= 100) { + this.max_jpeg_quality = val; + // Modify the minimum quality in lockstep with the maximum. + var _min = val / 2.0; + if (_min < 5) { + _min = 5; + } + this.min_jpeg_quality = _min; + } + } + increase_quality() { + this.set_max_quality(this.max_jpeg_quality + 5); + } + decrease_quality() { + this.set_max_quality(this.max_jpeg_quality - 5); + } + load() { + var _quality_max = window.localStorage.getItem('mjpeg.quality.max'); + if (_quality_max != null) { + this.set_max_quality(JSON.parse(_quality_max)); + } + } + save() { + window.localStorage.setItem('mjpeg.quality.max', JSON.stringify(this.max_jpeg_quality)); + } +} + +class RealCameraController { + constructor(camera_position, frame_controller, message_callback) { + this.camera_position = camera_position; + this.message_callback = message_callback; + this.frame_controller = frame_controller; + this._socket_capture_timer = null; + this._socket_close_timer = null; + this.socket = null; + } + _clear_socket_close_timer() { + if (this._socket_close_timer != undefined) { + clearTimeout(this._socket_close_timer); + } + } + _clear_socket_capture_timer() { + if (this._socket_capture_timer != undefined) { + clearTimeout(this._socket_capture_timer); + } + } + capture() { + var _instance = this; + _instance._clear_socket_close_timer(); + _instance._clear_socket_capture_timer(); + _instance._socket_close_timer = setTimeout(function() { + if (_instance.socket != undefined) { + _instance.socket.close(4001, "Done waiting for the server to respond."); + } + }, 1000); + // E.g. '{"camera": "front", "quality": 50, "display": "vga"}' + if (_instance.socket != undefined && _instance.socket.readyState == 1) { + _instance.socket.send(JSON.stringify({ + quality: _instance.frame_controller.jpeg_quality + })); + } + } + set_rate(rate) { + var _instance = this; + switch(rate) { + case "fast": + _instance.frame_controller.set_target_fps(16); + _instance._socket_capture_timer = setTimeout(function() {_instance.capture();}, 0); + break; + case "slow": + _instance.frame_controller.set_target_fps(4); + _instance._socket_capture_timer = setTimeout(function() {_instance.capture();}, 0); + break; + default: + _instance.frame_controller.set_target_fps(0); + } + } + start_socket() { + var _instance = this; + var _cam_uri = "/ws/cam/" + _instance.camera_position; + socket_utils.create_socket(_cam_uri, true, 250, function(ws) { + _instance.socket = ws; + ws.attempt_reconnect = true; + ws.is_reconnect = function() { + return ws.attempt_reconnect; + } + ws.onopen = function() { + //console.log("MJPEG " + _instance.camera_position + " camera connection established."); + //_instance.capture(); + }; + ws.onclose = function() { + //console.log("MJPEG " + _instance.camera_position + " camera connection closed."); + }; + ws.onmessage = function(evt) { + _instance._clear_socket_close_timer(); + const _timeout = _instance.frame_controller.update_framerate(); + if (_timeout != undefined && _timeout >= 0) { + _instance._socket_capture_timer = setTimeout(function() {_instance.capture();}, _timeout); + } + setTimeout(function() { + var cmd = null; + if (typeof evt.data == "string") { + cmd = evt.data; + } else { + cmd = new Blob([new Uint8Array(evt.data)], {type: "image/jpeg"}); + } + _instance.message_callback(cmd); + }, 0); + }; + }); + } + stop_socket() { + if (this.socket != undefined) { + this.socket.attempt_reconnect = false; + if (this.socket.readyState < 2) { + try { + this.socket.close(); + } catch(err) {} + } + } + this.socket = null; + } +} + +class FakeCameraController extends RealCameraController { + constructor(camera_position, frame_controller, message_callback) { + super(camera_position, frame_controller, message_callback); + this._running = false; + } + capture() { + const _instance = this; + _instance._clear_socket_capture_timer(); + if (_instance._running) { + dev_tools.get_img_blob(_instance.camera_position, function(blob) { + _instance.message_callback(blob); + const _timeout = _instance.frame_controller.update_framerate(); + _instance._socket_capture_timer = setTimeout(function() { + _instance.capture(); + }, _timeout + Math.floor(Math.random() * 10) - Math.floor(Math.random() * 10)); + }); + } + } + start_socket() { + const _dim = dev_tools.get_img_dimensions(); + this.message_callback(JSON.stringify({action: 'init', width: _dim[0], height: _dim[1]})); + this._running = true; + this.capture(); + } + stop_socket() { + this._running = false; + } +} + + +// One for each camera. +var front_camera_frame_controller = new MJPEGFrameController(); +var rear_camera_frame_controller = new MJPEGFrameController(); + +// Accessed outside of this module. +var mjpeg_page_controller = { + store: new MJPEGControlLocalStorage(), + camera_init_listeners: [], + camera_image_listeners: [], + el_preview_image: null, + el_overlay_image: null, + expand_camera_icon: null, + overlay_image_container: null, + + init: function(cameras) { + this.cameras = cameras; + this.apply_limits(); + this.refresh_page_values(); + this.el_preview_image = $('img#mjpeg_camera_preview_image'); + this.el_overlay_image = $('img#overlay_image'); + this.expand_camera_icon = $('img#expand_camera_icon'); + this.overlay_image_container = $('div#overlay_image_container'); + this.expand_camera_icon.css({'cursor': 'zoom-in'}); + this.overlay_image_container.invisible(); + this.set_camera_framerates(teleop_screen.active_camera); + $("img#caret_down").click(function() { + mjpeg_page_controller.decrease_quality(); + mjpeg_page_controller.refresh_page_values(); + }); + $("img#caret_up").click(function() { + mjpeg_page_controller.increase_quality(); + mjpeg_page_controller.refresh_page_values(); + }); + this.el_preview_image.click(function() { + teleop_screen.toggle_camera(); + }); + // Set the image receiver handlers. + this.add_camera_listener( + function(position, _cmd) {}, + function(position, _blob) { + // Show the other camera in preview. + if (position != teleop_screen.active_camera) { + mjpeg_page_controller.el_preview_image.attr('src', _blob); + mjpeg_page_controller.el_overlay_image.attr('src', _blob); + } + } + ); + this.expand_camera_icon.click(function() { + const _instance = mjpeg_page_controller; + const _visible = _instance.overlay_image_container.is_visible(); + if (_visible) { + const _state = _instance.el_overlay_image.width() < 480? 'small': 'medium'; + switch(_state) { + case "small": + _instance.el_overlay_image.width(480); + _instance.el_overlay_image.height(320); + _instance.el_overlay_image.css({'opacity': 0.5}); + _instance.expand_camera_icon.css({'cursor': 'zoom-out'}); + break; + default: + _instance.el_overlay_image.width(320); + _instance.el_overlay_image.height(240); + _instance.el_overlay_image.css({'opacity': 1}); + _instance.overlay_image_container.invisible(); + _instance.expand_camera_icon.css({'cursor': 'zoom-in'}); + } + } else { + _instance.overlay_image_container.visible(); + _instance.expand_camera_icon.css({'cursor': 'zoom-in'}); + } + // Reset the framerate. + _instance.set_camera_framerates(teleop_screen.active_camera); + }); + }, + refresh_page_values: function() { + $('span#mjpeg_quality_val').text(this.get_max_quality()); + }, + add_camera_listener: function(cb_init, cb_image) { + this.camera_init_listeners.push(cb_init); + this.camera_image_listeners.push(cb_image); + }, + notify_camera_init_listeners: function(camera_position, _cmd) { + this.camera_init_listeners.forEach(function(cb) { + cb(camera_position, _cmd); + }); + }, + notify_camera_image_listeners: function(camera_position, _blob) { + this.camera_image_listeners.forEach(function(cb) { + cb(camera_position, _blob); + }); + }, + apply_limits: function() { + _instance = this; + this.cameras.forEach(function(cam) { + cam.frame_controller.max_jpeg_quality = _instance.get_max_quality(); + cam.frame_controller.min_jpeg_quality = _instance.get_min_quality(); + }); + }, + get_max_quality: function() { + return this.store.max_jpeg_quality; + }, + get_min_quality: function() { + return this.store.min_jpeg_quality; + }, + increase_quality: function() { + this.store.increase_quality(); + this.apply_limits(); + this.store.save(); + }, + decrease_quality: function() { + this.store.decrease_quality(); + this.apply_limits(); + this.store.save(); + }, + set_camera_framerates: function(position) { + // The camera rates depend on visibility on the main screen and on the overlay div. + const _mjpeg = page_utils.get_stream_type() == 'mjpeg'; + const overlay_visible = this.overlay_image_container.is_visible() + const _front_camera = this.cameras[0]; + const _rear_camera = this.cameras[1]; + if (_mjpeg) { + _front_camera.set_rate(position == 'front' || overlay_visible? 'fast': 'slow'); + _rear_camera.set_rate(position == 'rear' || overlay_visible? 'fast': 'slow'); + } else { + _front_camera.set_rate(position == 'front'? 'off': overlay_visible? 'fast': 'slow'); + _rear_camera.set_rate(position == 'rear'? 'off': overlay_visible? 'fast': 'slow'); + } + } +} + +// Setup both the camera socket consumers. +mjpeg_rear_camera_consumer = function(_blob) { + if(typeof _blob == "string") { + mjpeg_page_controller.notify_camera_init_listeners(mjpeg_rear_camera.camera_position, JSON.parse(_blob)); + } else { + mjpeg_page_controller.notify_camera_image_listeners(mjpeg_rear_camera.camera_position, URL.createObjectURL(_blob)); + $('span#rear_camera_framerate').text(rear_camera_frame_controller.get_actual_fps().toFixed(0)); + } +} +mjpeg_front_camera_consumer = function(_blob) { + if(typeof _blob == "string") { + mjpeg_page_controller.notify_camera_init_listeners(mjpeg_front_camera.camera_position, JSON.parse(_blob)); + } else { + mjpeg_page_controller.notify_camera_image_listeners(mjpeg_front_camera.camera_position, URL.createObjectURL(_blob)); + $('span#front_camera_framerate').text(front_camera_frame_controller.get_actual_fps().toFixed(0)); + } +} + +// In development mode there is no use of a backend. +if (dev_tools.is_develop()) { + mjpeg_rear_camera = new FakeCameraController('rear', rear_camera_frame_controller, mjpeg_rear_camera_consumer); + mjpeg_front_camera = new FakeCameraController('front', front_camera_frame_controller, mjpeg_front_camera_consumer); +} else { + mjpeg_rear_camera = new RealCameraController('rear', rear_camera_frame_controller, mjpeg_rear_camera_consumer); + mjpeg_front_camera = new RealCameraController('front', front_camera_frame_controller, mjpeg_front_camera_consumer); +} + +function mjpeg_start_all() { + if (mjpeg_rear_camera != undefined) { + mjpeg_rear_camera.start_socket(); + } + if (mjpeg_front_camera != undefined) { + mjpeg_front_camera.start_socket(); + } +} + +function mjpeg_stop_all() { + if (mjpeg_rear_camera != undefined) { + mjpeg_rear_camera.stop_socket(); + } + if (mjpeg_front_camera != undefined) { + mjpeg_front_camera.stop_socket(); + } +} + +document.addEventListener("DOMContentLoaded", function() { + mjpeg_page_controller.init([mjpeg_front_camera, mjpeg_rear_camera]); + + if (dev_tools.is_develop()) { + $("a#video_stream_mjpeg").click(function() { + mjpeg_stop_all(); + dev_tools.set_next_resolution(); + mjpeg_start_all(); + }); + } else { + $("a#video_stream_mjpeg").click(function() { + page_utils.set_stream_type('mjpeg'); + location.reload(); + }); + } + + // Set the socket desired fps when the active camera changes. + teleop_screen.add_camera_activation_listener(function(position) { + setTimeout(function() { + mjpeg_page_controller.set_camera_framerates(position); + }, 100); + }); + + // Build the main view if required. + if (page_utils.get_stream_type() == 'mjpeg') { + // const el_viewport = document.getElementById('viewport_container'); + // const el_main_camera_display = document.createElement("canvas"); + const el_main_camera_display = document.getElementById("viewport_canvas"); + // el_main_camera_display.style.cssText = 'width: 100% !important; height: 100% !important;'; + // The canvas dimension will be set when we open the websocket. + // el_main_camera_display.width = 640; + // el_main_camera_display.height = 480; + // el_viewport.appendChild(el_main_camera_display); + const display_ctx = el_main_camera_display.getContext('2d'); + // Render images for the active camera. + mjpeg_page_controller.add_camera_listener( + function(position, _cmd) { + if (teleop_screen.active_camera == position && _cmd.action == "init") { + el_main_camera_display.width = _cmd.width; + el_main_camera_display.height = _cmd.height; + teleop_screen.on_canvas_init(_cmd.width, _cmd.height); + } + }, + function(position, _blob) { + if (teleop_screen.active_camera == position) { + var img = new Image(); + img.onload = function() { + // Do not run the canvas draws in parallel. + display_ctx.drawImage(img, 0, 0); + teleop_screen.canvas_update(display_ctx); + }; + // Set the src to trigger the image load. + img.src = _blob; + } + } + ); + } +}); + diff --git a/teleop/htm/static/JS/userMenu/menu_controls.js b/teleop/htm/static/JS/userMenu/menu_controls.js new file mode 100644 index 00000000..a03cf397 --- /dev/null +++ b/teleop/htm/static/JS/userMenu/menu_controls.js @@ -0,0 +1,170 @@ +class RealControlsBackend { + constructor() { + } + _call_get_channel_config(cb) { + $.get("/teleop/pilot/controls/relay/conf", function(response) {cb(response['config']);}); + } + _call_get_channel_states(cb) { + $.get("/teleop/pilot/controls/relay/state", function(response) {cb(response['states']);}); + } + _call_save_channel_state(channel, value, cb) { + const command = {'channel': channel, 'action': ((value == true || value == 'true')? 'on': 'off')}; + $.post("/teleop/pilot/controls/relay/state", JSON.stringify(command)).done(function(response) { + cb(response['channel'], response['value']); + }); + } +} + +class FakeControlsBackend extends RealControlsBackend { + constructor() { + super(); + this._config = [false, false, true, false]; + this._states = [false, false, false, true]; + } + _call_get_channel_config(cb) { + cb(this._config); + } + _call_get_channel_states(cb) { + cb(this._states); + } + _call_save_channel_state(channel, value, cb) { + this._states[channel] = value; + cb(channel, value); + } +} + + +var menu_controls = { + _backend: null, + + _init: function(el_parent) { + // In development mode there is no use of a backend. + this._backend = dev_tools.is_develop()? new FakeControlsBackend(): new RealControlsBackend(); + // Construct the dom elements. + const div_column = $("
", {id: 'column'}); + div_column.append($("

").text("Primary relays")); + const div_relays = $("
", {id: 'relay_channels'}); + div_column.append(div_relays); + el_parent.append(div_column); + this._channels = []; + this._channels.push(this._create_channel(div_relays, 3)); + this._channels.push(this._create_channel(div_relays, 4)); + this._apply_backend_config(); + this._apply_backend_states(); + }, + + _apply_backend_config: function() { + const channels = menu_controls._channels; + menu_controls._backend._call_get_channel_config(function(config) { + channels.forEach(function(channel) { + if (config[channel._index]) { + channel.setPulsed(); + } + }); + }); + }, + + _apply_backend_states: function() { + const channels = menu_controls._channels; + channels.forEach(function(channel) { + channel.flag(); + channel.disable(); + }); + menu_controls._backend._call_get_channel_states(function(states) { + channels.forEach(function(channel) { + channel.enable(); + channel.setValue(states[channel._index]); + channel.un_flag(); + }); + }); + }, + + _on_channel_select: function(channel) { + // This method is called when the user sets a value and also when a value is set by code. + if (!channel.isFlagged()) { + menu_controls._backend._call_save_channel_state(channel._index + 1, channel.getValue(), function(ch, x) {}); + setTimeout(function() {menu_controls._apply_backend_states();}, 250); + } + }, + + _create_channel: function(el_parent, number){ + const _channel_container = $("

"); + const _name = 'channel' + number; + const _field = $("

", {id: _name}); + const _legend = $("", {id: _name + '_label'}).text('Channel ' + number); + _field.append(_legend); + _field.append($("", {id: _name + '_off', name: _name, type: 'radio', value: false, checked: true})); + _field.append($("