├── .github └── workflows │ └── auto_deploy_docs.yaml ├── .gitignore ├── CMakeLists.txt ├── LICENSE.md ├── README.md ├── TUTORIAL_LICENSE.md ├── WEBRTC_PROJECT_LICENSE.md ├── app.js ├── bash_scripts ├── make_summary_videos.sh ├── start_desktop_dev_env.sh ├── start_server_production_env.sh ├── start_web_server_and_robot_browser.sh ├── stop_desktop_dev_env.sh ├── stop_server_production_env.sh ├── web_interface_installation.sh └── web_server_installation.sh ├── bin └── www ├── certificates ├── homemade_certificate.crt └── homemade_privkey.pem ├── controllers └── AuthController.js ├── coturn.service ├── images ├── HelloRobotLogoBar.png ├── banner.png ├── mongodb_development_credentials.png ├── operator_browser_1.png ├── operator_browser_2.png ├── operator_browser_3.png ├── operator_browser_4.png ├── operator_browser_5.png ├── operator_browser_6.png ├── operator_browser_7.png ├── operator_browser_8.png ├── robot_browser_1.png ├── robot_browser_2.png ├── robot_browser_3.png └── robot_browser_4.png ├── launch └── web_interface.launch ├── mkdocs.yml ├── models └── User.js ├── mongodb └── test-users-db-20171021 │ └── node-auth │ ├── users.bson │ └── users.metadata.json ├── operator ├── down_arrow_medium.png ├── down_arrow_small.png ├── gripper_close_medium.png ├── gripper_close_small.png ├── gripper_open_medium.png ├── gripper_open_small.png ├── left_arrow_medium.png ├── left_arrow_small.png ├── left_turn_medium.png ├── left_turn_small.png ├── operator.css ├── operator.html ├── operator.js ├── operator_acquire_av.js ├── operator_recorder.js ├── operator_ui_regions.js ├── right_arrow_medium.png ├── right_arrow_small.png ├── right_turn_medium.png ├── right_turn_small.png ├── up_arrow_medium.png └── up_arrow_small.png ├── package.json ├── package.xml ├── public ├── favicon.ico └── stylesheets │ └── style.css ├── robot ├── robot.css ├── robot.html ├── robot.js ├── robot_acquire_av.js └── ros_connect.js ├── routes └── index.js ├── shared ├── commands.js ├── send_recv_av.js ├── sensors.js └── video_dimensions.js ├── signaling_sockets.js ├── start_robot_browser.js ├── ui_elements ├── cursors │ ├── generate_cursors.sh │ ├── gripper_close.svg │ ├── gripper_open.svg │ ├── right_arrow.svg │ └── right_turn.svg └── operator_ui_test.html └── views ├── error.pug ├── index.pug ├── layout.pug ├── login.pug └── register.pug /.github/workflows/auto_deploy_docs.yaml: -------------------------------------------------------------------------------- 1 | name: Auto Deploy Docs 2 | run-name: Edit by ${{ github.actor }} triggered docs deployment 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | paths: 8 | - '**.md' 9 | jobs: 10 | Dispatch-Deploy-Workflow: 11 | runs-on: ubuntu-latest 12 | steps: 13 | 14 | - name: Print out debug info 15 | run: echo "Repo ${{ github.repository }} | Branch ${{ github.ref }} | Runner ${{ runner.os }} | Event ${{ github.event_name }}" 16 | 17 | - name: Dispatch deploy workflow 18 | uses: actions/github-script@v6 19 | with: 20 | github-token: ${{ secrets.GHA_CROSSREPO_WORKFLOW_TOKEN }} 21 | script: | 22 | await github.rest.actions.createWorkflowDispatch({ 23 | owner: 'hello-robot', 24 | repo: 'hello-robot.github.io', 25 | workflow_id: 'auto_deploy.yaml', 26 | ref: '0.3', 27 | }) 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Emacs related 2 | *~ 3 | \#*\# 4 | .\#* 5 | 6 | # Python related 7 | *.pyc 8 | *.so 9 | 10 | node_modules 11 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8.3) 2 | project(stretch_web_interface) 3 | 4 | ## Compile as C++11, supported in ROS Kinetic and newer 5 | # add_compile_options(-std=c++11) 6 | 7 | ## Find catkin macros and libraries 8 | ## if COMPONENTS list like find_package(catkin REQUIRED COMPONENTS xyz) 9 | ## is used, also find other catkin packages 10 | find_package(catkin REQUIRED COMPONENTS 11 | ) 12 | 13 | ## System dependencies are found with CMake's conventions 14 | # find_package(Boost REQUIRED COMPONENTS system) 15 | 16 | 17 | ## Uncomment this if the package has a setup.py. This macro ensures 18 | ## modules and global scripts declared therein get installed 19 | ## See http://ros.org/doc/api/catkin/html/user_guide/setup_dot_py.html 20 | # catkin_python_setup() 21 | 22 | ################################################ 23 | ## Declare ROS messages, services and actions ## 24 | ################################################ 25 | 26 | ## To declare and build messages, services or actions from within this 27 | ## package, follow these steps: 28 | ## * Let MSG_DEP_SET be the set of packages whose message types you use in 29 | ## your messages/services/actions (e.g. std_msgs, actionlib_msgs, ...). 30 | ## * In the file package.xml: 31 | ## * add a build_depend tag for "message_generation" 32 | ## * add a build_depend and a exec_depend tag for each package in MSG_DEP_SET 33 | ## * If MSG_DEP_SET isn't empty the following dependency has been pulled in 34 | ## but can be declared for certainty nonetheless: 35 | ## * add a exec_depend tag for "message_runtime" 36 | ## * In this file (CMakeLists.txt): 37 | ## * add "message_generation" and every package in MSG_DEP_SET to 38 | ## find_package(catkin REQUIRED COMPONENTS ...) 39 | ## * add "message_runtime" and every package in MSG_DEP_SET to 40 | ## catkin_package(CATKIN_DEPENDS ...) 41 | ## * uncomment the add_*_files sections below as needed 42 | ## and list every .msg/.srv/.action file to be processed 43 | ## * uncomment the generate_messages entry below 44 | ## * add every package in MSG_DEP_SET to generate_messages(DEPENDENCIES ...) 45 | 46 | ## Generate messages in the 'msg' folder 47 | # add_message_files( 48 | # FILES 49 | # Message1.msg 50 | # Message2.msg 51 | # ) 52 | 53 | ## Generate services in the 'srv' folder 54 | # add_service_files( 55 | # FILES 56 | # Service1.srv 57 | # Service2.srv 58 | # ) 59 | 60 | ## Generate actions in the 'action' folder 61 | # add_action_files( 62 | # FILES 63 | # Action1.action 64 | # Action2.action 65 | # ) 66 | 67 | ## Generate added messages and services with any dependencies listed here 68 | # generate_messages( 69 | # DEPENDENCIES 70 | # actionlib_msgs# geometry_msgs# nav_msgs# std_msgs 71 | # ) 72 | 73 | ################################################ 74 | ## Declare ROS dynamic reconfigure parameters ## 75 | ################################################ 76 | 77 | ## To declare and build dynamic reconfigure parameters within this 78 | ## package, follow these steps: 79 | ## * In the file package.xml: 80 | ## * add a build_depend and a exec_depend tag for "dynamic_reconfigure" 81 | ## * In this file (CMakeLists.txt): 82 | ## * add "dynamic_reconfigure" to 83 | ## find_package(catkin REQUIRED COMPONENTS ...) 84 | ## * uncomment the "generate_dynamic_reconfigure_options" section below 85 | ## and list every .cfg file to be processed 86 | 87 | ## Generate dynamic reconfigure parameters in the 'cfg' folder 88 | # generate_dynamic_reconfigure_options( 89 | # cfg/DynReconf1.cfg 90 | # cfg/DynReconf2.cfg 91 | # ) 92 | 93 | ################################### 94 | ## catkin specific configuration ## 95 | ################################### 96 | ## The catkin_package macro generates cmake config files for your package 97 | ## Declare things to be passed to dependent projects 98 | ## INCLUDE_DIRS: uncomment this if your package contains header files 99 | ## LIBRARIES: libraries you create in this project that dependent projects also need 100 | ## CATKIN_DEPENDS: catkin_packages dependent projects also need 101 | ## DEPENDS: system dependencies of this project that dependent projects also need 102 | catkin_package( 103 | # INCLUDE_DIRS include 104 | # LIBRARIES stretch_core 105 | # CATKIN_DEPENDS actionlib actionlib_msgs geometry_msgs nav_msgs rospy std_msgs tf tf2 106 | # DEPENDS system_lib 107 | ) 108 | 109 | ########### 110 | ## Build ## 111 | ########### 112 | 113 | ## Specify additional locations of header files 114 | ## Your package locations should be listed before other locations 115 | include_directories( 116 | # include 117 | ${catkin_INCLUDE_DIRS} 118 | ) 119 | 120 | ## Declare a C++ library 121 | # add_library(${PROJECT_NAME} 122 | # src/${PROJECT_NAME}/stretch_core.cpp 123 | # ) 124 | 125 | ## Add cmake target dependencies of the library 126 | ## as an example, code may need to be generated before libraries 127 | ## either from message generation or dynamic reconfigure 128 | # add_dependencies(${PROJECT_NAME} ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS}) 129 | 130 | ## Declare a C++ executable 131 | ## With catkin_make all packages are built within a single CMake context 132 | ## The recommended prefix ensures that target names across packages don't collide 133 | # add_executable(${PROJECT_NAME}_node src/stretch_core_node.cpp) 134 | 135 | ## Rename C++ executable without prefix 136 | ## The above recommended prefix causes long target names, the following renames the 137 | ## target back to the shorter version for ease of user use 138 | ## e.g. "rosrun someones_pkg node" instead of "rosrun someones_pkg someones_pkg_node" 139 | # set_target_properties(${PROJECT_NAME}_node PROPERTIES OUTPUT_NAME node PREFIX "") 140 | 141 | ## Add cmake target dependencies of the executable 142 | ## same as for the library above 143 | # add_dependencies(${PROJECT_NAME}_node ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS}) 144 | 145 | ## Specify libraries to link a library or executable target against 146 | # target_link_libraries(${PROJECT_NAME}_node 147 | # ${catkin_LIBRARIES} 148 | # ) 149 | 150 | ############# 151 | ## Install ## 152 | ############# 153 | 154 | # all install targets should use catkin DESTINATION variables 155 | # See http://ros.org/doc/api/catkin/html/adv_user_guide/variables.html 156 | 157 | ## Mark executable scripts (Python etc.) for installation 158 | ## in contrast to setup.py, you can choose the destination 159 | #install(PROGRAMS 160 | # DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} 161 | # ) 162 | 163 | ## Mark executables and/or libraries for installation 164 | # install(TARGETS ${PROJECT_NAME} ${PROJECT_NAME}_node 165 | # ARCHIVE DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} 166 | # LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} 167 | # RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} 168 | # ) 169 | 170 | ## Mark cpp header files for installation 171 | # install(DIRECTORY include/${PROJECT_NAME}/ 172 | # DESTINATION ${CATKIN_PACKAGE_INCLUDE_DESTINATION} 173 | # FILES_MATCHING PATTERN "*.h" 174 | # PATTERN ".svn" EXCLUDE 175 | # ) 176 | 177 | ## Mark other files for installation (e.g. launch and bag files, etc.) 178 | # install(FILES 179 | # # myfile1 180 | # # myfile2 181 | # DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION} 182 | # ) 183 | 184 | ############# 185 | ## Testing ## 186 | ############# 187 | 188 | ## Add gtest based cpp test target and link libraries 189 | # catkin_add_gtest(${PROJECT_NAME}-test test/test_stretch_core.cpp) 190 | # if(TARGET ${PROJECT_NAME}-test) 191 | # target_link_libraries(${PROJECT_NAME}-test ${PROJECT_NAME}) 192 | # endif() 193 | 194 | ## Add folders to be run by python nosetests 195 | # catkin_add_nosetests(test) 196 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The following license applies to the contents of this directory created by Hello Robot Inc. (the "Contents"), but does not cover materials from other sources. This software is intended for use with the Stretch RE1 mobile manipulator, which is a robot produced and sold by Hello Robot Inc. 2 | 3 | Copyright 2020 Hello Robot Inc. 4 | 5 | The Contents are licensed under the Apache License, Version 2.0 (the "License"). You may not use the Contents except in compliance with the License. You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, the Contents are distributed under the License are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 10 | 11 | For further information about the Contents including inquiries about dual licensing, please contact Hello Robot Inc. 12 | -------------------------------------------------------------------------------- /TUTORIAL_LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | Some of the code within this repository is derived from tutorial code found via 3 | the following links. This code relates to using mongoose, passport and express. 4 | The original code was released under the MIT License described below. 5 | 6 | https://github.com/didinj/node-express-passport-mongoose-auth 7 | https://www.djamware.com/post/58bd823080aca7585c808ebf/nodejs-expressjs-mongoosejs-and-passportjs-authentication 8 | 9 | ================ 10 | 11 | MIT License 12 | 13 | Copyright (c) 2017 Didin Jamaludin 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | SOFTWARE. 32 | -------------------------------------------------------------------------------- /WEBRTC_PROJECT_LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | The following license covers the original code from which some of the web 3 | interface code was derived (e.g., operator_acquire_av.js, robot_acquire_av.js). 4 | The original code was released in the following respository, which contains 5 | WebRTC example code. 6 | 7 | https://github.com/webrtc/samples 8 | 9 | ====================================== 10 | 11 | Copyright (c) 2014, The WebRTC project authors. All rights reserved. 12 | 13 | Redistribution and use in source and binary forms, with or without 14 | modification, are permitted provided that the following conditions are 15 | met: 16 | 17 | * Redistributions of source code must retain the above copyright 18 | notice, this list of conditions and the following disclaimer. 19 | 20 | * Redistributions in binary form must reproduce the above copyright 21 | notice, this list of conditions and the following disclaimer in 22 | the documentation and/or other materials provided with the 23 | distribution. 24 | 25 | * Neither the name of Google nor the names of its contributors may 26 | be used to endorse or promote products derived from this software 27 | without specific prior written permission. 28 | 29 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 30 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 31 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 32 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 33 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 34 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 35 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 36 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 37 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 38 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 39 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 40 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var favicon = require('serve-favicon'); 4 | var logger = require('morgan'); 5 | var bodyParser = require('body-parser'); 6 | var mongoose = require('mongoose'); 7 | var passport = require('passport'); 8 | var LocalStrategy = require('passport-local').Strategy; 9 | var helmet = require('helmet') 10 | 11 | var redis = require('redis'); 12 | var session = require('express-session'); 13 | // https://github.com/tj/connect-redis 14 | var RedisStore = require('connect-redis')(session); 15 | 16 | ///////////////// 17 | // required for socket.io to use passport's authentication 18 | // see the following website for information about this 19 | // https://www.codementor.io/tips/0217388244/sharing-passport-js-sessions-with-both-express-and-socket-io 20 | 21 | // don't confuse this with passport.socket.io, which is wrong! 22 | // https://www.npmjs.com/package/passport.socketio 23 | // https://github.com/jfromaniello/passport.socketio 24 | var passportSocketIo = require('passport.socketio'); 25 | var cookieParser = require('cookie-parser'); 26 | 27 | // https://www.npmjs.com/package/connect-redis 28 | // start redis and create the redis store 29 | // https://github.com/tj/connect-redis/blob/master/migration-to-v4.md 30 | // No password, so removed this line for now. 31 | // password: 'my secret', 32 | let redisClient = redis.createClient({ 33 | host: 'localhost', 34 | port: 6379, 35 | db: 1, 36 | }); 37 | redisClient.unref(); 38 | redisClient.on('error', console.log); 39 | 40 | let sessionStore = new RedisStore({ client: redisClient }); 41 | 42 | ///////////////// 43 | 44 | mongoose.Promise = global.Promise; 45 | console.log('start mongoose'); 46 | 47 | mongoose.connect('mongodb://localhost/node-auth', {useNewUrlParser: true, useUnifiedTopology: true}) 48 | .then(() => console.log('connection successful')) 49 | .catch((err) => console.error(err)); 50 | 51 | var index = require('./routes/index'); 52 | 53 | var app = express(); 54 | 55 | // "Helmet helps you secure your Express apps by setting various HTTP headers." 56 | console.log('use helmet'); 57 | app.use(helmet()); 58 | 59 | var use_content_security_policy = true 60 | 61 | if (use_content_security_policy) { 62 | console.log('using a content security policy'); 63 | app.use(helmet.contentSecurityPolicy({ 64 | directives:{ 65 | defaultSrc:["'self'"], 66 | scriptSrc:["'self'", "'unsafe-inline'", 'static.robotwebtools.org', 'robotwebtools.org', 'webrtc.github.io'], 67 | connectSrc:["'self'", 'ws://localhost:9090'], 68 | imgSrc: ["'self'", 'data:'], 69 | styleSrc:["'self'"], 70 | fontSrc:["'self'"]}})); 71 | } else { 72 | // Disable the content security policy. This is helpful during 73 | // development, but risky when deployed. 74 | console.log('WARNING: Not using a content security policy. This risky when deployed!'); 75 | app.use( 76 | helmet({ 77 | contentSecurityPolicy: false, 78 | }) 79 | ); 80 | } 81 | 82 | ///////////////////////// 83 | // 84 | // Only allow use of HTTPS and redirect HTTP requests to HTTPS 85 | // 86 | // code derived from 87 | // https://stackoverflow.com/questions/24015292/express-4-x-redirect-http-to-https 88 | 89 | console.log('require https'); 90 | app.all('*', ensureSecure); // at top of routing calls 91 | 92 | function ensureSecure(req, res, next){ 93 | if(req.secure){ 94 | // OK, continue 95 | return next(); 96 | }; 97 | // handle port numbers if you need non defaults 98 | res.redirect('https://' + req.hostname + req.url); 99 | }; 100 | ///////////////////////// 101 | 102 | 103 | // view engine setup 104 | console.log('set up the view engine'); 105 | app.set('views', path.join(__dirname, 'views')); 106 | app.set('view engine', 'pug'); 107 | 108 | app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); 109 | app.use(logger('dev')); 110 | app.use(bodyParser.json()); 111 | app.use(bodyParser.urlencoded({ extended: false })); 112 | 113 | var secret_string = 'you should change this secret string'; 114 | 115 | app.use(session({ 116 | secret: secret_string, 117 | store: sessionStore, 118 | resave: false, 119 | saveUninitialized: false, 120 | cookie: { 121 | secure: true, 122 | sameSite: true 123 | } 124 | })); 125 | 126 | //var RedisStore = require('connect-redis')(session); 127 | 128 | // set up passport for user authorization (logging in, etc.) 129 | console.log('set up passport'); 130 | app.use(passport.initialize()); 131 | app.use(passport.session()); 132 | 133 | 134 | // make files in the public directory available to everyone 135 | console.log('make public directory contents available to everyone'); 136 | app.use(express.static(path.join(__dirname, 'public'))); 137 | 138 | app.use('/', index); 139 | 140 | // passport configuration 141 | console.log('configure passport'); 142 | var User = require('./models/User'); 143 | passport.use(new LocalStrategy(User.authenticate())); 144 | passport.serializeUser(User.serializeUser()); 145 | passport.deserializeUser(User.deserializeUser()); 146 | 147 | console.log('set up error handling'); 148 | // catch 404 and forward to error handler 149 | app.use(function(req, res, next) { 150 | var err = new Error('Not Found'); 151 | err.status = 404; 152 | next(err); 153 | }); 154 | 155 | // error handler 156 | app.use(function(err, req, res, next) { 157 | // set locals, only providing error in development 158 | res.locals.message = err.message; 159 | res.locals.error = req.app.get('env') === 'development' ? err : {}; 160 | 161 | // render the error page 162 | res.status(err.status || 500); 163 | res.render('error'); 164 | }); 165 | 166 | ////////////////////////////////////////////////////////// 167 | 168 | // check if user is an approved and logged-in robot 169 | function isRobot(data) { 170 | return (data.user && 171 | data.user.logged_in && 172 | data.user.approved && 173 | (data.user.role === 'robot')); 174 | }; 175 | 176 | // check if user is an approved and logged-in operator 177 | function isOperator(data) { 178 | return (data.user && 179 | data.user.logged_in && 180 | data.user.approved && 181 | (data.user.role === 'operator')); 182 | }; 183 | 184 | 185 | // based on passport.socketio documentation 186 | // https://github.com/jfromaniello/passport.socketio 187 | 188 | function onAuthorizeSuccess(data, accept){ 189 | console.log(''); 190 | console.log('successful connection to socket.io'); 191 | console.log('data.user ='); 192 | console.log(data.user); 193 | 194 | if(isRobot(data) || isOperator(data)) { 195 | console.log('connection authorized!'); 196 | accept(); 197 | } else { 198 | console.log('connection attempt from unauthorized source!'); 199 | // reject connection (for whatever reason) 200 | accept(new Error('not authorized')); 201 | } 202 | } 203 | 204 | function onAuthorizeFail(data, message, error, accept){ 205 | console.log(''); 206 | console.log('#######################################################'); 207 | console.log('failed connection to socket.io:', message); 208 | console.log('data.headers ='); 209 | console.log(data.headers); 210 | 211 | // console.log('message ='); 212 | // console.log(message); 213 | // console.log('error ='); 214 | // console.log(error); 215 | // console.log('accept ='); 216 | // console.log(accept); 217 | 218 | console.log('#######################################################'); 219 | console.log(''); 220 | 221 | // error indicates whether the fail is due to an error or just an unauthorized client 222 | if(error) throw new Error(message); 223 | // send the (not-fatal) error-message to the client and deny the connection 224 | //return accept(new Error(message)); 225 | return accept(new Error("You are not authorized to connect.")); 226 | } 227 | 228 | 229 | ioauth = passportSocketIo.authorize({ 230 | key: 'connect.sid', 231 | secret: secret_string, 232 | store: sessionStore, 233 | cookieParser: cookieParser, 234 | success: onAuthorizeSuccess, 235 | fail: onAuthorizeFail 236 | }); 237 | 238 | 239 | module.exports = { 240 | app: app, 241 | ioauth: ioauth 242 | }; 243 | -------------------------------------------------------------------------------- /bash_scripts/make_summary_videos.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | for f in $@ 4 | do 5 | echo "****************************************" 6 | echo "attempting to generate 16x summary video of $f" 7 | #echo "attempting to generate 16x and 8x summary videos of $f" 8 | echo "input file = $f" 9 | echo "output file = ${f%.webm}_x16.webm" 10 | # echo "output file 2 = ${f%.webm}_x8.webm" 11 | echo "" 12 | echo "ffmpeg -i $f -an -filter:v setpts=0.0625*PTS ${f%.webm}_x16.webm" 13 | ffmpeg -i "$f" -an -filter:v "setpts=0.0625*PTS" "${f%.webm}_x16.webm" 14 | #echo "" 15 | #echo "ffmpeg -i $f -an -filter:v setpts=0.125*PTS ${f%.webm}_x8.webm" 16 | #ffmpeg -i "$f" -an -filter:v "setpts=0.125*PTS" "${f%.webm}_x8.webm" 17 | echo "" 18 | done 19 | 20 | echo "****************************************" 21 | echo "" 22 | echo "DONE!" 23 | echo "" 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /bash_scripts/start_desktop_dev_env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "****************************************" 4 | echo "attempt to bring up desktop development environment" 5 | 6 | echo "" 7 | echo "first making sure that the system is fully shutdown prior to bringing it up" 8 | echo "./stop_desktop_dev_env.sh" 9 | ./stop_desktop_dev_env.sh 10 | 11 | echo "" 12 | echo "set environment variable for development environment" 13 | echo "export HELLO_ROBOT_ENV=\"development\"" 14 | export HELLO_ROBOT_ENV="development" 15 | 16 | echo "" 17 | echo "attempting to start MongoDB..." 18 | echo "sudo systemctl start mongod.service" 19 | sudo systemctl start mongod.service 20 | 21 | echo "" 22 | echo "attempting to start Redis..." 23 | echo "sudo systemctl start redis.service" 24 | sudo systemctl start redis.service 25 | 26 | echo "" 27 | echo "attempting to start the web server..." 28 | echo "cd ../" 29 | cd ../ 30 | echo "sudo --preserve-env node ./bin/www &" 31 | sudo --preserve-env node ./bin/www & 32 | 33 | echo "" 34 | echo "finished attempt at bringing up the desktop development environment" 35 | echo "****************************************" 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /bash_scripts/start_server_production_env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "****************************************" 4 | echo "attempt to bring up server production environment" 5 | 6 | echo "" 7 | echo "first making sure that the system is fully shutdown prior to bringing it up" 8 | echo "./stop_server_production__env.sh" 9 | ./stop_server_production__env.sh 10 | 11 | echo "" 12 | echo "set environment variable for development environment" 13 | echo "export HELLO_ROBOT_ENV=\"production\"" 14 | export HELLO_ROBOT_ENV="production" 15 | 16 | echo "" 17 | echo "attempting to start MongoDB..." 18 | echo "sudo systemctl start mongod.service" 19 | sudo systemctl start mongod.service 20 | 21 | echo "" 22 | echo "attempting to start Redis..." 23 | echo "sudo systemctl start redis.service" 24 | sudo systemctl start redis.service 25 | 26 | echo "" 27 | echo "attempting to start the web server..." 28 | echo "cd /home/ubuntu/repos/stretch_web_interface/" 29 | cd /home/ubuntu/repos/stretch_web_interface/ 30 | echo "sudo --preserve-env node ./bin/www &" 31 | sudo --preserve-env node ./bin/www & 32 | 33 | echo "" 34 | echo "finished attempt to bring up server production environment" 35 | echo "****************************************" 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /bash_scripts/start_web_server_and_robot_browser.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "****************************************" 4 | echo "attempt to bring up desktop development environment" 5 | 6 | echo "" 7 | echo "first making sure that the system is fully shutdown prior to bringing it up" 8 | echo "./stop_desktop_dev_env.sh" 9 | ./stop_desktop_dev_env.sh 10 | 11 | echo "" 12 | echo "set environment variable for development environment" 13 | echo "export HELLO_ROBOT_ENV=\"development\"" 14 | export HELLO_ROBOT_ENV="development" 15 | 16 | echo "" 17 | echo "attempting to start MongoDB..." 18 | echo "sudo systemctl start mongod.service" 19 | sudo systemctl start mongod.service 20 | 21 | echo "" 22 | echo "attempting to start Redis..." 23 | echo "sudo systemctl start redis.service" 24 | sudo systemctl start redis.service 25 | 26 | echo "" 27 | echo "attempting to start the web server..." 28 | echo "cd ../" 29 | cd ../ 30 | echo "sudo --preserve-env node ./bin/www &" 31 | sudo --preserve-env node ./bin/www & 32 | echo "" 33 | 34 | echo "" 35 | echo "attempt to launch the browser for the robot and log in" 36 | echo "roscd stretch_web_interface/" 37 | roscd stretch_web_interface/ 38 | echo "node ./robot_teleop_start.js" 39 | node ./start_robot_browser.js 40 | echo "" 41 | 42 | echo "" 43 | echo "finished attempt at bringing up the desktop development environment" 44 | echo "****************************************" 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /bash_scripts/stop_desktop_dev_env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "****************************************" 4 | echo "attempt to shutdown desktop development environment" 5 | 6 | echo "" 7 | echo "remove environment variable for development environment" 8 | echo "unset HELLO_ROBOT_ENV" 9 | sudo unset HELLO_ROBOT_ENV 10 | 11 | echo "" 12 | echo "attempting to stop MongoDB..." 13 | echo "sudo systemctl stop mongod.service" 14 | sudo systemctl stop mongod.service 15 | 16 | echo "" 17 | echo "attempting to stop Redis..." 18 | echo "sudo systemctl stop redis.service" 19 | sudo systemctl stop redis.service 20 | 21 | echo "" 22 | echo "attempting to stop the web server..." 23 | echo "pkill -f \"node ./bin/www\"" 24 | sudo pkill -f "node ./bin/www" 25 | 26 | echo "" 27 | echo "finished attempt at shutting down the desktop development environment" 28 | echo "****************************************" 29 | 30 | 31 | -------------------------------------------------------------------------------- /bash_scripts/stop_server_production_env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "****************************************" 4 | echo "attempt to stop server production environment" 5 | 6 | echo "" 7 | echo "remove environment variable for server production environment" 8 | echo "unset HELLO_ROBOT_ENV" 9 | sudo unset HELLO_ROBOT_ENV 10 | 11 | echo "" 12 | echo "attempting to stop MongoDB..." 13 | echo "sudo systemctl stop mongod.service" 14 | sudo systemctl stop mongod.service 15 | 16 | echo "" 17 | echo "attempting to stop Redis..." 18 | echo "sudo systemctl stop redis.service" 19 | sudo systemctl stop redis.service 20 | 21 | echo "" 22 | echo "attempting to stop the web server..." 23 | echo "pkill -f \"node ./bin/www\"" 24 | sudo pkill -f "node ./bin/www" 25 | 26 | echo "" 27 | echo "finished attempt to stop the server production environment" 28 | echo "****************************************" 29 | 30 | 31 | -------------------------------------------------------------------------------- /bash_scripts/web_interface_installation.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "" 4 | echo "Starting web interface installation script." 5 | 6 | # APT UPDATE 7 | echo "" 8 | echo "Updating with apt." 9 | sudo apt-get --yes update 10 | echo "Done." 11 | 12 | # ROSBRIDGE 13 | echo "" 14 | echo "Installing rosbridge" 15 | sudo apt-get --yes install ros-melodic-rosbridge-server 16 | echo "Done." 17 | 18 | # NODE 14 19 | echo "" 20 | echo "Installing Node.js 14" 21 | echo "Downloading from the Internet via curl." 22 | curl -fsSL https://deb.nodesource.com/setup_14.x | sudo -E bash - 23 | echo "Installing nodejs with apt-get" 24 | sudo apt-get install -y nodejs 25 | echo "Done." 26 | 27 | # PACKAGES VIA NPM 28 | echo "" 29 | echo "Installing web-interface Node packages using npm." 30 | cd ~/catkin_ws/src/stretch_web_interface/ 31 | echo "Update to latest version of npm." 32 | sudo npm install -g npm 33 | echo "Install packages with npm." 34 | npm install 35 | echo "Done." 36 | 37 | # MONGODB 38 | echo "" 39 | echo "Installing MongoDB, which is used to store credentials for robot and operator logins." 40 | sudo apt-get --yes install mongodb 41 | echo "Done." 42 | 43 | # CHECK MONGODB 44 | echo "" 45 | echo "Look at the following output to make sure the mongodb service is working." 46 | systemctl status mongodb 47 | 48 | # INITIAL MONGODB DATABASE 49 | echo "" 50 | echo "Load developer credential database for robots and operators into MongoDB." 51 | echo "WARNING: THESE CREDENTIALS ARE WIDELY KNOWN AND SHOULD NOT BE USED OUTSIDE OF SECURE INTERNAL TESTING." 52 | cd ~/catkin_ws/src/stretch_web_interface/ 53 | mongorestore -d node-auth ./mongodb/test-users-db-20171021/node-auth/ 54 | echo "Done." 55 | 56 | # MONGODB-COMPASS 57 | echo "" 58 | echo "Installing MongoDB-Compass, which is a GUI to view the contents of a Mongo Database." 59 | wget https://downloads.mongodb.com/compass/mongodb-compass_1.12.1_amd64.deb 60 | sudo dpkg -i mongodb-compass_1.12.1_amd64.deb 61 | sudo apt --fix-broken install 62 | sudo apt -y install libgconf2-4 63 | rm mongodb-compass_1.12.1_amd64.deb 64 | 65 | # REDIS 66 | echo "" 67 | echo "Install redis for the web server." 68 | sudo apt-get --yes install redis 69 | echo "Done." 70 | 71 | # REMOVE TORNADO VIA PIP 72 | echo "" 73 | echo "Remove tornado using pip to avoid rosbridge websocket immediate disconnection issue." 74 | pip uninstall -y tornado 75 | echo "Done." 76 | 77 | echo "" 78 | echo "The web interface installation script has finished." 79 | echo "" 80 | -------------------------------------------------------------------------------- /bash_scripts/web_server_installation.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "" 4 | echo "Starting web server installation script." 5 | 6 | # APT UPDATE 7 | echo "" 8 | echo "Updating with apt." 9 | sudo apt-get --yes update 10 | echo "Done." 11 | 12 | # NODE 14 13 | echo "" 14 | echo "Installing Node.js 14" 15 | echo "Downloading from the Internet via curl." 16 | curl -fsSL https://deb.nodesource.com/setup_14.x | sudo -E bash - 17 | echo "Installing nodejs with apt-get" 18 | sudo apt-get install -y nodejs 19 | echo "Done." 20 | 21 | # PACKAGES VIA NPM 22 | echo "" 23 | echo "Installing web-interface Node packages using npm." 24 | cd ~/repos/stretch_web_interface/ 25 | echo "Update to latest version of npm." 26 | sudo npm install -g npm 27 | echo "Install packages with npm." 28 | sudo npm install 29 | echo "Done." 30 | 31 | # MONGODB 32 | echo "" 33 | echo "Installing MongoDB, which is used to store credentials for robot and operator logins." 34 | sudo apt-get --yes install mongodb 35 | echo "Done." 36 | 37 | # CHECK MONGODB 38 | echo "" 39 | echo "Look at the following output to make sure the mongodb service is working." 40 | systemctl status mongodb 41 | 42 | # REDIS 43 | echo "" 44 | echo "Install redis for the web server." 45 | sudo apt-get --yes install redis 46 | echo "Done." 47 | 48 | # COTURN 49 | echo "" 50 | echo "Install coturn for the web server." 51 | sudo apt-get --yes install coturn 52 | echo "Setup coturn.service." 53 | sudo cp ~/repos/stretch_web_interface/coturn.service /etc/systemd/system/ 54 | echo "Done." 55 | 56 | 57 | echo "" 58 | echo "The web server installation script has finished." 59 | echo "" 60 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Module Dependencies 4 | var {app, ioauth} = require('../app'); 5 | var debug = require('debug')('hello-robot-server:server'); 6 | 7 | var fs = require('fs'); 8 | 9 | if (process.env.HELLO_ROBOT_ENV && (process.env.HELLO_ROBOT_ENV === "development")) { 10 | var options = { 11 | key: fs.readFileSync('./certificates/homemade_privkey.pem'), 12 | cert: fs.readFileSync('./certificates/homemade_certificate.crt') 13 | }; 14 | } 15 | else { 16 | var options = { 17 | key: fs.readFileSync('/etc/letsencrypt/live/pilot.hello-robot.io/privkey.pem'), 18 | cert: fs.readFileSync('/etc/letsencrypt/live/pilot.hello-robot.io/cert.pem'), 19 | ca: fs.readFileSync('/etc/letsencrypt/live/pilot.hello-robot.io/chain.pem') 20 | }; 21 | } 22 | 23 | var server = require('http').Server(app); 24 | var secure_server = require('https').Server(options, app); 25 | 26 | // set up signaling server that connects robots and operators 27 | var io = require('socket.io')(secure_server); 28 | io.use(ioauth); 29 | require('../signaling_sockets.js')(io); 30 | 31 | console.log('set port number to 443') 32 | var port = 443 33 | app.set('port', port); 34 | 35 | // Listen to the port 36 | console.log('listen to port 80'); 37 | server.listen(80); 38 | secure_server.listen(port); 39 | secure_server.on('error', onError); 40 | secure_server.on('listening', onListening); 41 | 42 | // Listen for error events on an HTTP server 43 | function onError(error) { 44 | if (error.syscall !== 'listen') { 45 | throw error; 46 | } 47 | 48 | var bind = typeof port === 'string' 49 | ? 'Pipe ' + port 50 | : 'Port ' + port; 51 | 52 | // informative error messages 53 | switch (error.code) { 54 | case 'EACCES': 55 | console.error(bind + ' requires elevated privileges'); 56 | process.exit(1); 57 | break; 58 | case 'EADDRINUSE': 59 | console.error(bind + ' is already in use'); 60 | process.exit(1); 61 | break; 62 | default: 63 | throw error; 64 | } 65 | } 66 | 67 | // Listen for listening events on an HTTP server 68 | function onListening() { 69 | var addr = secure_server.address(); 70 | var bind = typeof addr === 'string' 71 | ? 'pipe ' + addr 72 | : 'port ' + addr.port; 73 | debug('Listening on ' + bind); 74 | } 75 | -------------------------------------------------------------------------------- /certificates/homemade_certificate.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICgzCCAewCCQDDhWAhEgJgdjANBgkqhkiG9w0BAQsFADCBhTELMAkGA1UEBhMC 3 | VVMxEDAOBgNVBAgMB0dlb3JnaWExEDAOBgNVBAcMB0F0bGFudGExGTAXBgNVBAoM 4 | EEhlbGxvIFJvYm90IEluYy4xFTATBgNVBAMMDENoYXJsaWUgS2VtcDEgMB4GCSqG 5 | SIb3DQEJARYRY2tAaGVsbG8tcm9ib3QuaW8wHhcNMTcwOTEzMjIxMTA3WhcNMTgw 6 | OTEzMjIxMTA3WjCBhTELMAkGA1UEBhMCVVMxEDAOBgNVBAgMB0dlb3JnaWExEDAO 7 | BgNVBAcMB0F0bGFudGExGTAXBgNVBAoMEEhlbGxvIFJvYm90IEluYy4xFTATBgNV 8 | BAMMDENoYXJsaWUgS2VtcDEgMB4GCSqGSIb3DQEJARYRY2tAaGVsbG8tcm9ib3Qu 9 | aW8wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMigicMqDsTv9Nl4+iEBN8z1 10 | l3qCD3r9eW7WNmH+BWleW7tTemiGVXgRAC8cK0WUCJ/vLm828VPGfmkbSmbgF62c 11 | Kja7kFvMXhh9/OYgIBjYHbR/9p6WwEuVkZcETJ75n+DcI1IkQKd83vQBp7RqOnxi 12 | xKP5W4l1hYiegxXwgFDNAgMBAAEwDQYJKoZIhvcNAQELBQADgYEASFUALNL3ZhDe 13 | szCfs9MuLVDytm7kcfGI1i7mWn9lvCkPGEUeDE87EeESMnGEv+aVh/OgJeVDoWq9 14 | vhM+MnmZexvTeuQ51Mje6ibv/IIxX5JTBDJdo2qBi4sBCz9tcYnSVg2Oj1cx2e/t 15 | epcNR+ppYH+tL6/K/8NiZZIPL9nmn+0= 16 | -----END CERTIFICATE----- 17 | -------------------------------------------------------------------------------- /certificates/homemade_privkey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXQIBAAKBgQDIoInDKg7E7/TZePohATfM9Zd6gg96/Xlu1jZh/gVpXlu7U3po 3 | hlV4EQAvHCtFlAif7y5vNvFTxn5pG0pm4BetnCo2u5BbzF4YffzmICAY2B20f/ae 4 | lsBLlZGXBEye+Z/g3CNSJECnfN70Aae0ajp8YsSj+VuJdYWInoMV8IBQzQIDAQAB 5 | AoGAFMVSHet/xfHV1qIIu1wF6+lNOni2o5QUe14gGTsUUllbg+RbmvC1bo3MCBSR 6 | gk2WKwC3PPpiN7soITebF1WB/d9op0tknR9m8735xZ55L+5/2WZrcu4NPyL2l9gj 7 | NUmEvhvHIpdwTXu6qEW6guyc+f/P6CEnaO4LLwiB71z0ssECQQDo0jnewBk+IGId 8 | L9VsNvAzRD3RSvOHw30v1SjGUfO5FeFLYhLa0Hq3I/Rq8aGEpRt7QnFRCxpZnSj8 9 | oj1y1MZRAkEA3JnM8X5AqyO/n/EwPG0fwFz1GznvmHhT/hA8ow5/wmfUJ+5SKwgU 10 | eQ6leWcnuB9MH3dVcUh4QqcFlPAhnca3vQJBAI5kVMRpVIbso1Uadjsy9oFEUVJ5 11 | tqvn4d6pTcDNSnR+b0X9e26cZxEvSkNF+PT5Te962XcphTodpn2sdEyQ2aECQQCy 12 | Ab05RQpDzsXq9wFYUSnk3F3ASYDHxJjqEwoK/UEkiwnL6ugM5yk2AhaOnymSzlZr 13 | sayVi8fW6NV9OEO3/8j1AkBpK/3bQtnl5ImSr7aCWtqB4da9cQC3RcC/HHA2Qeoa 14 | VBC8bLILx24Ix9wK4X5/ap50mnwgEcCd9Cvx7yX5CUFf 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /controllers/AuthController.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var mongoose = require("mongoose"); 3 | var passport = require("passport"); 4 | var User = require("../models/User"); 5 | 6 | var userController = {}; 7 | 8 | var robot_root = path.join(__dirname, '../robot'); 9 | var operator_root = path.join(__dirname, '../operator'); 10 | var shared_root = path.join(__dirname, '../shared'); 11 | 12 | // check if user is an approved and logged-in robot 13 | function isRobot(req) { 14 | return (req.user && 15 | req.user.approved && 16 | (req.user.role === 'robot')); 17 | }; 18 | 19 | // check if user is an approved and logged-in operator 20 | function isOperator(req) { 21 | return (req.user && 22 | req.user.approved && 23 | (req.user.role === 'operator')); 24 | }; 25 | 26 | // Only give approved logged-in robots access to files in the robot directory 27 | userController.robot = function(req, res) { 28 | var file = req.params.file; 29 | if (isRobot(req)) { 30 | res.sendFile(robot_root + "/" + file); 31 | } else { 32 | res.status(403).send("Not authorized to get " + file); 33 | } 34 | }; 35 | 36 | // Only give approved logged-in operators access to files in the operator directory 37 | userController.operator = function(req, res) { 38 | var file = req.params.file; 39 | if (isOperator(req)) { 40 | res.sendFile(operator_root + "/" + file); 41 | } else { 42 | res.status(403).send("Not authorized to get " + file); 43 | } 44 | }; 45 | 46 | // Only give approved logged-in operators and robots access to files in the shared directory 47 | userController.shared = function(req, res) { 48 | var file = req.params.file; 49 | if (isOperator(req) || isRobot(req)) { 50 | res.sendFile(shared_root + "/" + file); 51 | } else { 52 | res.status(403).send("Not authorized to get " + file); 53 | } 54 | }; 55 | 56 | 57 | // Restrict access to root page 58 | userController.home = function(req, res) { 59 | res.render('index', { user : req.user }); 60 | }; 61 | 62 | // Go to registration page 63 | userController.register = function(req, res) { 64 | res.render('register'); 65 | }; 66 | 67 | // Post registration 68 | userController.doRegister = function(req, res) { 69 | User.register( 70 | new User({ username : req.body.username, 71 | role: 'operator', 72 | approved: false 73 | }), 74 | req.body.password, 75 | function(err, user) 76 | { 77 | if (err) { 78 | return res.render('register', { user : user }); 79 | } 80 | 81 | passport.authenticate('local')(req, res, function () { 82 | res.redirect('/'); 83 | }); 84 | } 85 | ); 86 | }; 87 | 88 | // Go to login page 89 | userController.login = function(req, res) { 90 | res.render('login'); 91 | }; 92 | 93 | // Post login 94 | userController.doLogin = function(req, res) { 95 | passport.authenticate('local')(req, res, function () { 96 | if (isOperator(req)) { 97 | res.redirect('/operator/operator.html'); 98 | } else if (isRobot(req)) { 99 | res.redirect('/robot/robot.html'); 100 | } else { 101 | res.redirect('/'); 102 | } 103 | }); 104 | }; 105 | 106 | // logout 107 | userController.logout = function(req, res) { 108 | req.logout(); 109 | res.redirect('/'); 110 | }; 111 | 112 | module.exports = userController; 113 | -------------------------------------------------------------------------------- /coturn.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Coturn STUN and TURN server 3 | After=syslog.target network.target 4 | 5 | [Service] 6 | Type=forking 7 | PIDFile=/var/run/turnserver.pid 8 | ExecStart=/usr/bin/turnserver -c /etc/turnserver.conf -o -v 9 | Restart=on-failure 10 | IgnoreSIGPIPE=yes 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | 15 | 16 | -------------------------------------------------------------------------------- /images/HelloRobotLogoBar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hello-robot/stretch_web_interface/fb9c24cb42f577b9711bffe25e003cc1225c2fe5/images/HelloRobotLogoBar.png -------------------------------------------------------------------------------- /images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hello-robot/stretch_web_interface/fb9c24cb42f577b9711bffe25e003cc1225c2fe5/images/banner.png -------------------------------------------------------------------------------- /images/mongodb_development_credentials.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hello-robot/stretch_web_interface/fb9c24cb42f577b9711bffe25e003cc1225c2fe5/images/mongodb_development_credentials.png -------------------------------------------------------------------------------- /images/operator_browser_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hello-robot/stretch_web_interface/fb9c24cb42f577b9711bffe25e003cc1225c2fe5/images/operator_browser_1.png -------------------------------------------------------------------------------- /images/operator_browser_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hello-robot/stretch_web_interface/fb9c24cb42f577b9711bffe25e003cc1225c2fe5/images/operator_browser_2.png -------------------------------------------------------------------------------- /images/operator_browser_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hello-robot/stretch_web_interface/fb9c24cb42f577b9711bffe25e003cc1225c2fe5/images/operator_browser_3.png -------------------------------------------------------------------------------- /images/operator_browser_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hello-robot/stretch_web_interface/fb9c24cb42f577b9711bffe25e003cc1225c2fe5/images/operator_browser_4.png -------------------------------------------------------------------------------- /images/operator_browser_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hello-robot/stretch_web_interface/fb9c24cb42f577b9711bffe25e003cc1225c2fe5/images/operator_browser_5.png -------------------------------------------------------------------------------- /images/operator_browser_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hello-robot/stretch_web_interface/fb9c24cb42f577b9711bffe25e003cc1225c2fe5/images/operator_browser_6.png -------------------------------------------------------------------------------- /images/operator_browser_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hello-robot/stretch_web_interface/fb9c24cb42f577b9711bffe25e003cc1225c2fe5/images/operator_browser_7.png -------------------------------------------------------------------------------- /images/operator_browser_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hello-robot/stretch_web_interface/fb9c24cb42f577b9711bffe25e003cc1225c2fe5/images/operator_browser_8.png -------------------------------------------------------------------------------- /images/robot_browser_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hello-robot/stretch_web_interface/fb9c24cb42f577b9711bffe25e003cc1225c2fe5/images/robot_browser_1.png -------------------------------------------------------------------------------- /images/robot_browser_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hello-robot/stretch_web_interface/fb9c24cb42f577b9711bffe25e003cc1225c2fe5/images/robot_browser_2.png -------------------------------------------------------------------------------- /images/robot_browser_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hello-robot/stretch_web_interface/fb9c24cb42f577b9711bffe25e003cc1225c2fe5/images/robot_browser_3.png -------------------------------------------------------------------------------- /images/robot_browser_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hello-robot/stretch_web_interface/fb9c24cb42f577b9711bffe25e003cc1225c2fe5/images/robot_browser_4.png -------------------------------------------------------------------------------- /launch/web_interface.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Stretch Web Interface 2 | site_url: https://docs.hello-robot.com/stretch_web_interface 3 | site_description: "Hello Robot Stretch Web Interface Documentation" 4 | copyright: 'Copyright © 2022 Hello Robot Inc' 5 | site_author: Hello Robot Inc 6 | use_directory_urls: True 7 | docs_dir: . 8 | site_dir: ../site 9 | 10 | theme: 11 | name: material 12 | #font: Arial 13 | palette: 14 | - scheme: default 15 | primary: hello-robot-light 16 | toggle: 17 | icon: material/lightbulb-outline 18 | name: Switch to dark mode 19 | - scheme: slate 20 | primary: hello-robot-dark 21 | toggle: 22 | icon: material/lightbulb 23 | name: Switch to light mode 24 | logo: images/hello_robot_logo_light.png 25 | favicon: images/hello_robot_favicon.png 26 | features: 27 | - navigation.instant 28 | 29 | extra_css: 30 | - ./docs/extra.css 31 | 32 | markdown_extensions: 33 | - pymdownx.highlight 34 | - pymdownx.superfences 35 | - pymdownx.inlinehilite 36 | - pymdownx.keys 37 | - admonition 38 | 39 | plugins: 40 | - same-dir 41 | # - simple: 42 | # merge_docs_dir: true 43 | # include_extensions: [".css", ".png"] 44 | # include_folders: ['../hello_helpers'] 45 | - mike: 46 | # these fields are all optional; the defaults are as below... 47 | version_selector: true # set to false to leave out the version selector 48 | css_dir: css # the directory to put the version selector's CSS 49 | javascript_dir: js # the directory to put the version selector's JS 50 | canonical_version: null # the version for ; `null` 51 | # uses the version specified via `mike deploy` 52 | - search 53 | - tags 54 | - mkdocstrings: 55 | default_handler: python 56 | handlers: 57 | python: 58 | selection: 59 | docstring_style: numpy 60 | rendering: 61 | show_root_heading: true 62 | show_source: false 63 | members_order: source 64 | heading_level: 3 65 | show_if_no_docstring: true 66 | 67 | extra: 68 | version: 69 | provider: mike 70 | default: latest 71 | social: 72 | - icon: material/home 73 | link: https://hello-robot.com 74 | - icon: material/twitter 75 | link: https://twitter.com/HelloRobotInc 76 | - icon: material/github 77 | link: https://github.com/hello-robot 78 | - icon: material/linkedin 79 | link: https://linkedin.com/company/hello-robot-inc 80 | 81 | nav: 82 | - Overview: README.md 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /models/User.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Schema = mongoose.Schema; 3 | 4 | // "Passport-Local Mongoose is a Mongoose plugin that simplifies building username and password login with Passport" 5 | // - https://github.com/saintedlama/passport-local-mongoose 6 | var passportLocalMongoose = require('passport-local-mongoose'); 7 | 8 | // This defines the MongoDB entry ("document") associated with user accounts 9 | var UserSchema = new Schema({ 10 | username: String, // user name such as "r1" or "o1" 11 | password: String, // password, hopefully strong... 12 | role: String, // currently "robot" or "operator" 13 | approved: Boolean, // whether or not the user account has been approved to act as an operator or robot. currently admin edits mongodb entry by hand 14 | date: { type: Date, default: Date.now } // when the user account was initially created 15 | }); 16 | 17 | UserSchema.plugin(passportLocalMongoose); 18 | 19 | module.exports = mongoose.model('User', UserSchema); 20 | -------------------------------------------------------------------------------- /mongodb/test-users-db-20171021/node-auth/users.bson: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hello-robot/stretch_web_interface/fb9c24cb42f577b9711bffe25e003cc1225c2fe5/mongodb/test-users-db-20171021/node-auth/users.bson -------------------------------------------------------------------------------- /mongodb/test-users-db-20171021/node-auth/users.metadata.json: -------------------------------------------------------------------------------- 1 | {"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"node-auth.users"}]} -------------------------------------------------------------------------------- /operator/down_arrow_medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hello-robot/stretch_web_interface/fb9c24cb42f577b9711bffe25e003cc1225c2fe5/operator/down_arrow_medium.png -------------------------------------------------------------------------------- /operator/down_arrow_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hello-robot/stretch_web_interface/fb9c24cb42f577b9711bffe25e003cc1225c2fe5/operator/down_arrow_small.png -------------------------------------------------------------------------------- /operator/gripper_close_medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hello-robot/stretch_web_interface/fb9c24cb42f577b9711bffe25e003cc1225c2fe5/operator/gripper_close_medium.png -------------------------------------------------------------------------------- /operator/gripper_close_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hello-robot/stretch_web_interface/fb9c24cb42f577b9711bffe25e003cc1225c2fe5/operator/gripper_close_small.png -------------------------------------------------------------------------------- /operator/gripper_open_medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hello-robot/stretch_web_interface/fb9c24cb42f577b9711bffe25e003cc1225c2fe5/operator/gripper_open_medium.png -------------------------------------------------------------------------------- /operator/gripper_open_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hello-robot/stretch_web_interface/fb9c24cb42f577b9711bffe25e003cc1225c2fe5/operator/gripper_open_small.png -------------------------------------------------------------------------------- /operator/left_arrow_medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hello-robot/stretch_web_interface/fb9c24cb42f577b9711bffe25e003cc1225c2fe5/operator/left_arrow_medium.png -------------------------------------------------------------------------------- /operator/left_arrow_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hello-robot/stretch_web_interface/fb9c24cb42f577b9711bffe25e003cc1225c2fe5/operator/left_arrow_small.png -------------------------------------------------------------------------------- /operator/left_turn_medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hello-robot/stretch_web_interface/fb9c24cb42f577b9711bffe25e003cc1225c2fe5/operator/left_turn_medium.png -------------------------------------------------------------------------------- /operator/left_turn_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hello-robot/stretch_web_interface/fb9c24cb42f577b9711bffe25e003cc1225c2fe5/operator/left_turn_small.png -------------------------------------------------------------------------------- /operator/operator.css: -------------------------------------------------------------------------------- 1 | 2 | :root { 3 | --mode-button-width: 55px; 4 | /* --mode-button-width: 15%; */ 5 | } 6 | 7 | /* #video_div { */ 8 | /* height:90%; */ 9 | /* } */ 10 | 11 | #video_div { 12 | /* height:90%; 13 | top: 70px; 14 | left: 0px; */ 15 | /* "touch-action: manipulation" disables double-tap zoom. The default 16 | behavior creates problems when trying to click rapidly and with 17 | low-latency responsiveness on a mobile device. Panning and pinch 18 | zoom gestures are still allowed. This made a big difference with 19 | tests from a Pixel 2XL phone (Android).*/ 20 | touch-action: manipulation; 21 | } 22 | 23 | #remoteVideo { 24 | position:absolute; 25 | top: 144px; 26 | left: 10px; 27 | /*height:80%; */ 28 | height: 620px; 29 | z-index:1; 30 | } 31 | 32 | #video_ui_overlay { 33 | position:absolute; 34 | top: 144px; 35 | left: 10px; 36 | /*top: calc(0.23 * var(--video-height));*/ /*144px;*/ 37 | /*left: calc(0.16 * var(--video-height));*/ /*10px;*/ 38 | /*height:80%; */ 39 | height: 620px; 40 | z-index:2; 41 | } 42 | 43 | 44 | #nav_do_nothing_region { 45 | cursor: not-allowed; 46 | } 47 | 48 | #nav_forward_region { 49 | cursor: url('up_arrow_medium.png'), auto; 50 | } 51 | 52 | #nav_backward_region { 53 | cursor: url('down_arrow_medium.png'), auto; 54 | } 55 | 56 | #nav_turn_left_region { 57 | cursor: url('left_turn_medium.png'), auto; 58 | } 59 | 60 | #nav_turn_right_region { 61 | cursor: url('right_turn_medium.png'), auto; 62 | } 63 | 64 | 65 | #low_arm_down_region { 66 | cursor: url('down_arrow_medium.png'), auto; 67 | } 68 | 69 | #low_arm_up_region { 70 | cursor: url('up_arrow_medium.png'), auto; 71 | } 72 | 73 | #low_arm_extend_region { 74 | cursor: url('up_arrow_medium.png'), auto; 75 | } 76 | 77 | #low_arm_retract_region { 78 | cursor: url('down_arrow_medium.png'), auto; 79 | } 80 | 81 | #low_arm_base_forward_region { 82 | cursor: url('left_arrow_medium.png'), auto; 83 | } 84 | 85 | #low_arm_base_backward_region { 86 | cursor: url('right_arrow_medium.png'), auto; 87 | } 88 | 89 | 90 | #high_arm_up_region { 91 | cursor: url('up_arrow_medium.png'), auto; 92 | } 93 | 94 | #high_arm_down_region { 95 | cursor: url('down_arrow_medium.png'), auto; 96 | } 97 | 98 | #high_arm_extend_region { 99 | cursor: url('up_arrow_medium.png'), auto; 100 | } 101 | 102 | #high_arm_retract_region { 103 | cursor: url('down_arrow_medium.png'), auto; 104 | } 105 | 106 | #high_arm_base_forward_region { 107 | cursor: url('left_arrow_medium.png'), auto; 108 | } 109 | 110 | #high_arm_base_backward_region { 111 | cursor: url('right_arrow_medium.png'), auto; 112 | } 113 | 114 | 115 | #hand_open_region { 116 | cursor: url('gripper_open_medium.png'), auto; 117 | } 118 | 119 | #hand_close_region { 120 | cursor: url('gripper_close_medium.png'), auto; 121 | } 122 | 123 | #hand_in_region { 124 | cursor: url('left_turn_medium.png'), auto; 125 | } 126 | 127 | #hand_out_region { 128 | cursor: url('right_turn_medium.png'), auto; 129 | } 130 | 131 | 132 | 133 | #look_up_region { 134 | cursor: url('up_arrow_medium.png'), auto; 135 | } 136 | 137 | #look_down_region { 138 | cursor: url('down_arrow_medium.png'), auto; 139 | } 140 | 141 | #look_left_region { 142 | cursor: url('left_arrow_medium.png'), auto; 143 | } 144 | 145 | #look_right_region { 146 | cursor: url('right_arrow_medium.png'), auto; 147 | } 148 | 149 | 150 | 151 | /***************************************************/ 152 | /* Initial code prior to editing */ 153 | /* http://www.cssflow.com/snippets/toggle-switches */ 154 | /* Toggle Switch */ 155 | /* May 30, 2012 */ 156 | /* MIT License */ 157 | /***************************************************/ 158 | 159 | /* Initial code prior to editing */ 160 | /* 161 | * Copyright (c) 2012-2013 Thibaut Courouble 162 | * http://www.cssflow.com 163 | * 164 | * Licensed under the MIT License: 165 | * http://www.opensource.org/licenses/mit-license.php 166 | */ 167 | 168 | .switch { 169 | display: inline-block; 170 | position: relative; 171 | /*margin: 20px auto;*/ 172 | height: 52px; /*26px;*/ 173 | /* width: calc(var(--mode-button-width) * 6); */ 174 | width: calc(var(--mode-button-width) * 5); 175 | background: rgba(0, 0, 0, 0.25); 176 | border-radius: 3px; 177 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.3), 0 1px rgba(255, 255, 255, 0.1); 178 | } 179 | 180 | .switch-label { 181 | position: relative; 182 | z-index: 2; 183 | float: left; 184 | width: var(--mode-button-width); /*116px;*/ 185 | line-height: 52px; /*26px;*/ 186 | font-size: 11px; 187 | color: rgba(255, 255, 255, 0.35); 188 | text-align: center; 189 | text-shadow: 0 1px 1px rgba(0, 0, 0, 0.45); 190 | cursor: pointer; 191 | 192 | font-size: 15px; 193 | color: white; 194 | font-family: Trebuchet, Arial, sans-serif; 195 | /*font-weight: bold;*/ 196 | } 197 | .switch-label:active { 198 | font-weight: bold; 199 | } 200 | 201 | .switch-label-off { 202 | padding-left: 2px; 203 | } 204 | 205 | .switch-label-on { 206 | padding-right: 2px; 207 | } 208 | 209 | /* 210 | * Note: using adjacent or general sibling selectors combined with 211 | * pseudo classes doesn't work in Safari 5.0 and Chrome 12. 212 | * See this article for more info and a potential fix: 213 | * http://css-tricks.com/webkit-sibling-bug/ 214 | */ 215 | .switch-input { 216 | display: none; 217 | } 218 | .switch-input:checked + .switch-label { 219 | font-weight: bold; 220 | color: rgba(255, 255, 255, 1.0); 221 | text-shadow: 0 1px rgba(255, 255, 255, 0.25); 222 | /* transition: 0.15s ease-out; */ 223 | transition: 0.1s ease-out; 224 | } 225 | 226 | .switch-input:checked + .switch-label-0 ~ .switch-selection { 227 | left: calc(var(--mode-button-width) * 0); 228 | } 229 | 230 | .switch-input:checked + .switch-label-1 ~ .switch-selection { 231 | left: calc(var(--mode-button-width) * 1); 232 | } 233 | 234 | .switch-input:checked + .switch-label-2 ~ .switch-selection { 235 | left: calc(var(--mode-button-width) * 2); 236 | } 237 | 238 | .switch-input:checked + .switch-label-3 ~ .switch-selection { 239 | left: calc(var(--mode-button-width) * 3); 240 | } 241 | 242 | .switch-input:checked + .switch-label-4 ~ .switch-selection { 243 | left: calc(var(--mode-button-width) * 4); 244 | } 245 | 246 | /* .switch-input:checked + .switch-label-5 ~ .switch-selection { */ 247 | /* left: calc(var(--mode-button-width) * 5); */ 248 | /* } */ 249 | 250 | 251 | .switch-selection { 252 | display: block; 253 | position: relative; 254 | z-index: 1; 255 | top: 2px; 256 | left: 2px; 257 | width: var(--mode-button-width); 258 | height: 50px; /*22px;*/ 259 | background: #0000ff; /*#3aa2d0;*/ 260 | border-radius: 3px; 261 | background-image: linear-gradient(to bottom, #4fc9ee, #0000ff); /*#3aa2d0);*/ 262 | box-shadow: inset 0 1px rgba(255, 255, 255, 0.5), 0 0 2px rgba(0, 0, 0, 0.2); 263 | /*transition: left 0.15s ease-out;*/ 264 | transition: left 0.1s ease-out; 265 | } 266 | 267 | 268 | 269 | /***************************************************/ 270 | 271 | 272 | /* Initial code prior to editing */ 273 | /****************************************************************************/ 274 | /* generated "Mic On" / "Mic Off" switch with the following website */ 275 | /* https://proto.io/freebies/onoff/ */ 276 | /* more reference websites */ 277 | /* https://foundation.zurb.com/sites/docs/switch.html */ 278 | /* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox */ 279 | /****************************************************************************/ 280 | 281 | 282 | .onoffswitch { 283 | display: inline-block; 284 | left: 10px; 285 | position: relative; 286 | /*width: 97px;*/ 287 | /*width: 90px;*/ 288 | width: 50px; 289 | } 290 | 291 | .onoffswitch-checkbox { 292 | display: none; 293 | } 294 | 295 | .onoffswitch-label { 296 | display: block; 297 | overflow: hidden; 298 | cursor: pointer; 299 | border: 2px solid #999999; 300 | border-radius: 20px; 301 | } 302 | 303 | .onoffswitch-inner { 304 | display: block; 305 | width: 200%; 306 | margin-left: -100%; 307 | /*transition: margin 0.3s ease-in 0s;*/ 308 | } 309 | 310 | .onoffswitch-inner:before, .onoffswitch-inner:after { 311 | display: block; 312 | float: left; 313 | width: 50%; 314 | height: 46px; /*22px;*/ 315 | padding: 0; 316 | line-height: 46px; /*22px;*/ 317 | /*font-size: 15px;*/ 318 | font-size: 15px; 319 | color: white; 320 | font-family: Trebuchet, Arial, sans-serif; 321 | font-weight: bold; 322 | box-sizing: border-box; 323 | } 324 | 325 | .onoffswitch-inner:before { 326 | /* content: "Mic On";*/ 327 | content: "Mic"; 328 | padding-left: 10px; 329 | background-color: #F22121; color: #FFFFFF; 330 | } 331 | 332 | .onoffswitch-inner:after { 333 | /* content: "Mic Off";*/ 334 | content: "Mic"; 335 | padding-left: 10px; 336 | background-color: #EEEEEE; color: #999999; 337 | /*text-align: right;*/ 338 | } 339 | .onoffswitch-switch { 340 | /* 341 | display: block; 342 | width: 18px; 343 | margin: 8.5px; 344 | background: #FFFFFF; 345 | position: absolute; 346 | top: 0; 347 | bottom: 0; 348 | */ 349 | /*right: 58px;*/ 350 | /*right: 42px;*/ 351 | /*right: 42px;*/ 352 | /*border: 2px solid #999999; border-radius: 20px;*/ 353 | /*transition: all 0.3s ease-in 0s; */ 354 | } 355 | 356 | .onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-inner { 357 | margin-left: 0; 358 | } 359 | 360 | .onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-switch { 361 | right: 0px; 362 | } 363 | 364 | /********************************************************************/ 365 | -------------------------------------------------------------------------------- /operator/operator.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 |
17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 |
25 |
26 |
27 | 28 | 29 |
30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
47 | 48 |
49 | 50 | 54 |
55 | 56 |
57 | 58 | 59 | 60 |
61 | 62 | 63 | 64 | 65 | 66 | 67 | 72 | do nothing 73 | 74 | 75 | 79 | 80 | 86 | move forward 87 | 88 | 89 | 95 | move backward 96 | 97 | 98 | 104 | turn left 105 | 106 | 107 | 113 | turn right 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 127 | arm down 128 | 129 | 130 | 136 | arm up 137 | 138 | 139 | 145 | retract arm 146 | 147 | 148 | 154 | extend arm 155 | 156 | 157 | 163 | move forward 164 | 165 | 166 | 172 | move backward 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 186 | arm down 187 | 188 | 189 | 195 | arm up 196 | 197 | 198 | 204 | retract arm 205 | 206 | 207 | 213 | extend arm 214 | 215 | 216 | 222 | move forward 223 | 224 | 225 | 231 | move backward 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 245 | open gripper 246 | 247 | 248 | 254 | close gripper 255 | 256 | 257 | 263 | gripper in 264 | 265 | 266 | 272 | gripper out 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 286 | look up 287 | 288 | 289 | 295 | look_down 296 | 297 | 298 | 304 | look left 305 | 306 | 307 | 313 | look right 314 | 315 | 316 | 317 | 318 | 319 | 320 |
321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 332 | 333 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 351 | 352 | 353 | -------------------------------------------------------------------------------- /operator/operator.js: -------------------------------------------------------------------------------- 1 | var peer_name = "OPERATOR"; 2 | 3 | -------------------------------------------------------------------------------- /operator/operator_acquire_av.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * derived from initial code from the following website with the copyright notice below 4 | * https://github.com/webrtc/samples/blob/gh-pages/src/content/devices/input-output/js/main.js 5 | * 6 | * " 7 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. 8 | * 9 | * Use of this source code is governed by a BSD-style license 10 | * that can be found in the LICENSE file in the root of the source 11 | * tree. 12 | * " 13 | * 14 | * The full license for the original code from which the following code is derived can 15 | * be found in the file named WebRTC_Project_LICENSE.md in the same directory as this file. 16 | * 17 | */ 18 | 19 | 20 | 'use strict'; 21 | 22 | var videoElement = document.querySelector('video'); 23 | var audioInputSelect = document.querySelector('select#audioSource'); 24 | var audioOutputSelect = document.querySelector('select#audioOutput'); 25 | var audioMuteSwitch = document.getElementById('myonoffswitch'); 26 | var selectors = [audioInputSelect, audioOutputSelect]; 27 | 28 | function updateAudioToMatchMuteSwitch() { 29 | if(localStream) { 30 | var audio_track = localStream.getAudioTracks()[0]; 31 | if(audioMuteSwitch.checked) { 32 | // unmute the microphone's audio track 33 | console.log('unmute microphone'); 34 | audio_track.enabled = true; 35 | } else { 36 | // mute the microphone's audio track 37 | console.log('mute microphone'); 38 | audio_track.enabled = false; 39 | } 40 | } 41 | } 42 | 43 | audioMuteSwitch.onchange = updateAudioToMatchMuteSwitch 44 | 45 | function gotDevices(deviceInfos) { 46 | // Handles being called several times to update labels. Preserve values. 47 | var values = selectors.map(function(select) { 48 | return select.value; 49 | }); 50 | selectors.forEach(function(select) { 51 | while (select.firstChild) { 52 | select.removeChild(select.firstChild); 53 | } 54 | }); 55 | for (var i = 0; i !== deviceInfos.length; ++i) { 56 | var deviceInfo = deviceInfos[i]; 57 | var option = document.createElement('option'); 58 | option.value = deviceInfo.deviceId; 59 | if (deviceInfo.kind === 'audioinput') { 60 | option.text = deviceInfo.label || 61 | 'microphone ' + (audioInputSelect.length + 1); 62 | audioInputSelect.appendChild(option); 63 | } else if (deviceInfo.kind === 'audiooutput') { 64 | option.text = deviceInfo.label || 'speaker ' + 65 | (audioOutputSelect.length + 1); 66 | audioOutputSelect.appendChild(option); 67 | } else { 68 | // unused device, since the operator console only uses audio input at this time. 69 | //console.log('The operator console only uses audio input at this time.'); 70 | } 71 | } 72 | selectors.forEach(function(select, selectorIndex) { 73 | if (Array.prototype.slice.call(select.childNodes).some(function(n) { 74 | return n.value === values[selectorIndex]; 75 | })) { 76 | select.value = values[selectorIndex]; 77 | } 78 | }); 79 | } 80 | 81 | navigator.mediaDevices.enumerateDevices().then(gotDevices).catch(handleError); 82 | 83 | // Attach audio output device to video element using device/sink ID. 84 | function attachSinkId(element, sinkId) { 85 | if (typeof element.sinkId !== 'undefined') { 86 | element.setSinkId(sinkId) 87 | .then(function() { 88 | console.log('Success, audio output device attached: ' + sinkId); 89 | }) 90 | .catch(function(error) { 91 | var errorMessage = error; 92 | if (error.name === 'SecurityError') { 93 | errorMessage = 'You need to use HTTPS for selecting audio output ' + 94 | 'device: ' + error; 95 | } 96 | console.error(errorMessage); 97 | // Jump back to first output device in the list as it's the default. 98 | audioOutputSelect.selectedIndex = 0; 99 | }); 100 | } else { 101 | console.warn('Browser does not support output device selection.'); 102 | } 103 | } 104 | 105 | function changeAudioDestination() { 106 | var audioDestination = audioOutputSelect.value; 107 | attachSinkId(videoElement, audioDestination); 108 | } 109 | 110 | function gotStream(stream) { 111 | localStream = stream; 112 | updateAudioToMatchMuteSwitch(); 113 | // Refresh button list in case labels have become available 114 | return navigator.mediaDevices.enumerateDevices(); 115 | } 116 | 117 | function start() { 118 | var audioSource = audioInputSelect.value; 119 | var constraints = { 120 | audio: {deviceId: audioSource ? {exact: audioSource} : undefined}, 121 | video: false 122 | }; 123 | navigator.mediaDevices.getUserMedia(constraints). 124 | then(gotStream).then(gotDevices).catch(handleError); 125 | } 126 | 127 | audioInputSelect.onchange = start; 128 | audioOutputSelect.onchange = changeAudioDestination; 129 | 130 | function handleError(error) { 131 | console.log('navigator.getUserMedia error: ', error); 132 | } 133 | -------------------------------------------------------------------------------- /operator/operator_recorder.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | //////////////////////////////////////////////////////////////// 5 | //////////////////////////////////////////////////////////////// 6 | // BEGIN 7 | // initial recording code from mediarecorder open source code licensed with Apache 2.0 8 | // https://github.com/samdutton/simpl/tree/gh-pages/mediarecorder 9 | 10 | var mediaRecorder; 11 | var recordStream; 12 | var recordedBlobs; 13 | var recordCommandLog; 14 | var recordSensorLog; 15 | var recordFileName; 16 | 17 | var mimeType = 'video/webm; codecs=vp9'; 18 | //var mimeType = 'video/webm; codecs=vp8'; 19 | //var mimeType = 'video/webm; codecs=h264'; //results in sped up video 20 | //var mimeType = 'video/webm'; 21 | 22 | var recordButton = document.querySelector('button#record'); 23 | var downloadButton = document.querySelector('button#download'); 24 | 25 | 26 | recordButton.onclick = toggleRecording; 27 | downloadButton.onclick = download; 28 | 29 | /////////////////// 30 | // If the audio track from the robot is unavailable, the typical 31 | // approach fails, resulting in a video that shows just a couple of 32 | // frames. If the audio track is unavailable, this hack works, 33 | // successfully recording video without sound. For now, I'm going to 34 | // be optimistic and keep it off to simplify things. In the future 35 | // dynamically enabling this in the event of audio problems might be 36 | // worthwhile. 37 | // 38 | 39 | var use_canvas_drawing_hack = false; 40 | 41 | // attempt to fix things by rendering and capturing a new video stream 42 | // waste of comptuation and nasty hack, but maybe it will help? (This worked!) 43 | // This only records the visuals not the audio 44 | 45 | var saveTextElement = document.createElement('a'); 46 | var recordOn = false; 47 | var rDim; 48 | var recordFps; 49 | var recordCanvas; 50 | var recordContext; 51 | 52 | if (use_canvas_drawing_hack) { 53 | // rDim = {w: 1920, h:1080} 54 | rDim = {w: videoDimensions.w, h: videoDimensions.h} 55 | recordFps = 30; 56 | recordCanvas = document.createElement('canvas'); 57 | recordCanvas.width = rDim.w; 58 | recordCanvas.height = rDim.h; 59 | recordContext = recordCanvas.getContext('2d'); 60 | recordContext.fillStyle="black"; 61 | recordContext.fillRect(0, 0, rDim.w, rDim.h); 62 | recordStream = recordCanvas.captureStream(recordFps); 63 | } 64 | 65 | function drawVideoToRecord() { 66 | recordContext.drawImage(remoteVideo, 67 | 0, 0, rDim.w, rDim.h, 68 | 0, 0, rDim.w, rDim.h); 69 | if (recordOn) { 70 | requestAnimationFrame(drawVideoToRecord); 71 | } 72 | } 73 | /////////////////// 74 | 75 | function addToCommandLog( entry ) { 76 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now 77 | var timestamp = Date.now(); 78 | recordCommandLog.push( { timestamp: timestamp, entry: entry } ); 79 | } 80 | 81 | function addToSensorLog( entry ) { 82 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now 83 | var timestamp = Date.now(); 84 | recordSensorLog.push( { timestamp: timestamp, entry: entry } ); 85 | } 86 | 87 | function addToLogs(logs, entry) { 88 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now 89 | var timestamp = Date.now(); 90 | for (const l of logs) { 91 | l.push( { timestamp: timestamp, entry: entry } ); 92 | } 93 | } 94 | 95 | function addDatesToLogs(logs) { 96 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now 97 | var d = new Date(); 98 | var timestamp = d.getTime(); 99 | var humanReadableTime = d.toString(); 100 | var jsonTime = d.toJSON(); 101 | for (const l of logs) { 102 | l.push( { timestamp: timestamp, entry: humanReadableTime } ); 103 | l.push( { timestamp: timestamp, entry: jsonTime } ); 104 | } 105 | } 106 | 107 | 108 | 109 | // saveText from 110 | // https://reformatcode.com/code/angularjs/write-an-object-to-an-json-file-using-angular 111 | function saveText(text, filename){ 112 | saveTextElement.setAttribute('href', 'data:text/plain;charset=utf-u,'+encodeURIComponent(text)); 113 | saveTextElement.setAttribute('download', filename); 114 | saveTextElement.click() 115 | } 116 | 117 | function saveLogs() { 118 | console.log('Attempting to save JSON files of the recorded logs.'); 119 | console.log('recordCommandLog = '); 120 | console.log(recordCommandLog); 121 | console.log('recordSensorLog = '); 122 | console.log(recordSensorLog); 123 | 124 | saveText( JSON.stringify(recordCommandLog), 125 | recordFileName + '_command_log' + '.json'); 126 | 127 | saveText( JSON.stringify(recordSensorLog), 128 | recordFileName + '_sensor_log' + '.json'); 129 | } 130 | 131 | function handleDataAvailable(event) { 132 | if (event.data && event.data.size > 0) { 133 | recordedBlobs.push(event.data); 134 | } 135 | } 136 | 137 | function handleStop(event) { 138 | console.log('Recorder stopped: ', event); 139 | } 140 | 141 | function toggleRecording() { 142 | if (recordButton.textContent === 'Start Recording') { 143 | startRecording(); 144 | } else { 145 | stopRecording(); 146 | recordButton.textContent = 'Start Recording'; 147 | downloadButton.disabled = false; 148 | } 149 | } 150 | 151 | function checkMimeTypes() { 152 | /////////////// 153 | // initial code from 154 | // https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/isTypeSupported 155 | var types = ['video/webm', 156 | 'audio/webm', 157 | 'video/webm; codecs=vp9', 158 | 'video/webm; codecs=vp8', 159 | 'video/mp4', 160 | 'video/webm; codecs=daala', 161 | 'video/webm; codecs=h264', 162 | 'audio/webm; codecs=opus', 163 | 'video/mpeg']; 164 | 165 | console.log('***********************************************'); 166 | console.log('Checking available MIME types for MediaRecorder'); 167 | for (var i in types) { 168 | console.log( 'Is ' + types[i] + ' supported? ' + (MediaRecorder.isTypeSupported(types[i]) ? 'Maybe' : 'No')); 169 | } 170 | console.log('***********************************************'); 171 | 172 | /////////////// 173 | } 174 | 175 | function startRecording() { 176 | if(remoteStream) { 177 | 178 | recordOn = true; 179 | 180 | if(use_canvas_drawing_hack) { 181 | // use hack of drawing video in a new canvas and capturing it 182 | drawVideoToRecord(); 183 | // attempt to record audio, too... (this worked!) 184 | var recordAudio = remoteStream.getAudioTracks()[0]; 185 | if (recordAudio) { 186 | recordStream.addTrack(recordAudio); 187 | } 188 | } else { 189 | // keep it simple 190 | // maybe this will work someday? 191 | recordStream = remoteStream; 192 | // 193 | } 194 | 195 | checkMimeTypes(); 196 | 197 | var options = { 198 | mimeType : mimeType 199 | } 200 | 201 | // see if making a copy of the remote stream helps (didn't work) 202 | // see if making a copy and deleting the audio tracks helps (didn't work)0 203 | // recordStream = new MediaStream(remoteStream); 204 | // for (let a of recordStream.getAudioTracks()) { 205 | // recordStream.removeTrack(a); 206 | // } 207 | // same type of attempt with different details (didn't work) 208 | //recordStream = new MediaStream(remoteStream.getVideoTracks()); 209 | 210 | // see if getting the stream from the html video display helps (didn't work) 211 | // recordStream = remoteVideo.srcObject; 212 | 213 | // try capturing a new stream from the html display (didn't work) 214 | // recordStream = remoteVideo.captureStream(); 215 | 216 | 217 | // var options = { 218 | // audioBitsPerSecond : 128000, 219 | // videoBitsPerSecond : 2500000, 220 | // mimeType : 'video/mp4' 221 | // } 222 | 223 | // var options = { 224 | // audioBitsPerSecond : 128000, 225 | // videoBitsPerSecond : 2500000, 226 | // ignoreMutedMedia: true, 227 | // mimeType : mimeType 228 | // } 229 | 230 | //for (let a of displayStream.getAudioTracks()) { 231 | // displayStream.removeTrack(a); 232 | //} 233 | 234 | 235 | // var options = { 236 | // bitsPerSecond: 100000, 237 | // mimeType : mimeType 238 | // } 239 | 240 | //var options = {mimeType: 'video/webm', bitsPerSecond: bitsPerSecond}; 241 | 242 | recordCommandLog = []; 243 | recordSensorLog = []; 244 | recordedBlobs = []; 245 | try { 246 | mediaRecorder = new MediaRecorder(recordStream, options); 247 | 248 | } catch (e0) { 249 | console.log('Unable to create MediaRecorder with options Object: ', e0); 250 | } 251 | 252 | console.log('Created MediaRecorder', mediaRecorder); 253 | console.log('with options', options); 254 | recordButton.textContent = 'Stop Recording'; 255 | downloadButton.disabled = true; 256 | mediaRecorder.onstop = handleStop; 257 | mediaRecorder.ondataavailable = handleDataAvailable; 258 | //mediaRecorder.start(10); // collect as 10ms chunks of data 259 | var d = new Date(); 260 | recordFileName = 'operator_recording_' + d.toISOString(); 261 | addDatesToLogs([recordCommandLog, recordSensorLog]); 262 | if (requestedRobot) { 263 | addToLogs([recordCommandLog, recordSensorLog], 264 | {requestedRobot: requestedRobot}); 265 | } 266 | addToLogs([recordCommandLog, recordSensorLog], 267 | 'Just before the start of recording'); 268 | mediaRecorder.start(1000); // collect as 1s chunks of data 269 | addToLogs([recordCommandLog, recordSensorLog], 270 | 'Just after the start of recording'); 271 | //mediaRecorder.start(); // collect as one big blob 272 | console.log('MediaRecorder started', mediaRecorder); 273 | } else { 274 | console.log('remoteStream is not yet defined, so recording can not begin.'); 275 | } 276 | } 277 | 278 | function stopRecording() { 279 | addToLogs([recordCommandLog, recordSensorLog], 280 | 'Just before the end of recording.'); 281 | mediaRecorder.stop(); 282 | addToLogs([recordCommandLog, recordSensorLog], 283 | 'Just after the end of recording'); 284 | addDatesToLogs([recordCommandLog, recordSensorLog]); 285 | recordOn = false; 286 | console.log('Recorded Blobs: ', recordedBlobs); 287 | } 288 | 289 | function download() { 290 | 291 | saveLogs(); 292 | 293 | if (recordedBlobs.length > 1) { 294 | console.log('More than one blob was captured, so combining them into a single blob prior to saving a file.'); 295 | var blob = new Blob(recordedBlobs, {type: mimeType}); 296 | } else { 297 | console.log('Only a single blob was captured, so using it directly.'); 298 | var blob = recordedBlobs[0]; 299 | } 300 | 301 | console.log('Blob.size = ' + blob.size); 302 | console.log('Blob.type = ' + blob.type); 303 | 304 | var url = window.URL.createObjectURL(blob); 305 | var a = document.createElement('a'); 306 | a.style.display = 'none'; 307 | a.href = url; 308 | //a.download = 'test.webm'; 309 | a.download = recordFileName + '.webm'; 310 | document.body.appendChild(a); 311 | a.click(); 312 | 313 | setTimeout(function() { 314 | document.body.removeChild(a); 315 | window.URL.revokeObjectURL(url); 316 | //saveLog(); // attempt to save the JSON log after the video saving has been cleaned up 317 | }, 100); 318 | } 319 | 320 | // initial recording code from mediarecorder open source code 321 | // END 322 | //////////////////////////////////////////////////////////////// 323 | //////////////////////////////////////////////////////////////// 324 | 325 | -------------------------------------------------------------------------------- /operator/operator_ui_regions.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | function svgPolyString(points) { 5 | var str = 'M '; 6 | for (let p of points) { 7 | str = str + p.x + ',' + p.y + ' '; 8 | } 9 | str = str + 'Z'; 10 | return str; 11 | } 12 | 13 | function makeRectangle(ulX, ulY, width, height) { 14 | return {ul: {x:ulX, y:ulY}, 15 | ur: {x:ulX + width, y:ulY}, 16 | ll: {x:ulX, y:ulY + height}, 17 | lr: {x:ulX + width, y:ulY + height} 18 | }; 19 | } 20 | 21 | function makeSquare(ulX, ulY, width) { 22 | return makeRectangle(ulX, ulY, width, width); 23 | } 24 | 25 | function rectToPoly(rect) { 26 | return [rect.ul, rect.ur, rect.lr, rect.ll]; 27 | } 28 | 29 | function hideSvg(elementId) { 30 | document.getElementById(elementId).style.display = 'none'; 31 | } 32 | 33 | function showSvg(elementId) { 34 | document.getElementById(elementId).style.display = 'block'; 35 | } 36 | 37 | 38 | function turnModeUiOn(modeKey) { 39 | var buttonName = modeKey + '_mode_button' 40 | console.log('setting to checked: buttonName = ' + buttonName) 41 | // This might not be working as expected. I may need to set all 42 | // others to false, or find out how to appropriately utilize a 43 | // switch like this. 44 | document.getElementById(buttonName).checked = true 45 | arrangeOverlays(modeKey) 46 | for (var key in modeRegions) { 47 | if (key !== modeKey) { 48 | modeRegions[key].map(hideSvg) 49 | } 50 | } 51 | modeRegions[modeKey].map(showSvg) 52 | } 53 | 54 | 55 | var navModeRegionIds 56 | var lowArmModeRegionIds 57 | var highArmModeRegionIds 58 | var handModeRegionIds 59 | var lookModeRegionIds 60 | var modeRegions 61 | 62 | function createUiRegions(debug) { 63 | 64 | var strokeOpacity; 65 | if(debug) { 66 | strokeOpacity = 0.1; //1.0; 67 | } else { 68 | strokeOpacity = 0.0; 69 | } 70 | 71 | function setRegionPoly(elementId, poly, color) { 72 | var region = document.getElementById(elementId); 73 | region.setAttribute('stroke', color); 74 | region.setAttribute('stroke-opacity', String(strokeOpacity)); 75 | region.setAttribute('stroke-linejoin', "round"); 76 | region.setAttribute('stroke-width', "2"); 77 | 78 | region.setAttribute('d', svgPolyString(poly)); 79 | } 80 | 81 | ////////////////////////////// 82 | // set size of video region 83 | 84 | // D435i has a -90 rotation 85 | // var w = videoDimensions.w; 86 | // var h = videoDimensions.h; 87 | var w = videoDimensions.h; 88 | var h = videoDimensions.w; 89 | 90 | var video_region = document.getElementById('video_ui_overlay'); 91 | video_region.setAttribute('viewBox', '0 0 ' + w + ' ' + h); 92 | ////////////////////////////// 93 | 94 | ////////////////////////////// 95 | 96 | var sqrW, bgSqr, mdSqr, smSqr, regionPoly; 97 | var mdBar, smBar, mHoriz, lHoriz, rHoriz, mVert; 98 | var color; 99 | 100 | ///////////////////////// 101 | color = 'white' 102 | 103 | // big rectangle at the borders of the video 104 | var bgRect = makeRectangle(0, 0, w, h); 105 | // small rectangle around the mobile base 106 | //var smRect = makeSquare(w*(7.0/16.0), h*(7.0/16.0), w/8.0, h/8.0); 107 | var smRect = makeSquare((w/2.0)-(w/20.0), (h*(3.0/4.0))-(h/20.0), w/10.0, h/10.0); 108 | 109 | regionPoly = rectToPoly(smRect); 110 | setRegionPoly('nav_do_nothing_region', regionPoly, color); 111 | 112 | regionPoly = [bgRect.ul, bgRect.ur, smRect.ur, smRect.ul]; 113 | setRegionPoly('nav_forward_region', regionPoly, color); 114 | 115 | regionPoly = [bgRect.ll, bgRect.lr, smRect.lr, smRect.ll]; 116 | setRegionPoly('nav_backward_region', regionPoly, color); 117 | 118 | regionPoly = [bgRect.ul, smRect.ul, smRect.ll, bgRect.ll]; 119 | setRegionPoly('nav_turn_left_region', regionPoly, color); 120 | 121 | //var region = document.getElementById('right_arrow') 122 | //region.setAttribute('stroke-opacity', "0.1"); 123 | //region.setAttribute('fill-opacity', "0.1"); 124 | //region.setAttribute('viewBox', "-20.0 -20.0, 200.0, 200.0") 125 | 126 | //region.setAttribute('transform', "scale(0.1)"); 127 | //region.setAttribute('transform', "scale(0.1)"); 128 | //region.move(100,100) 129 | 130 | regionPoly = [bgRect.ur, smRect.ur, smRect.lr, bgRect.lr]; 131 | setRegionPoly('nav_turn_right_region', regionPoly, color); 132 | 133 | navModeRegionIds = ['nav_do_nothing_region', 'nav_forward_region', 'nav_backward_region', 'nav_turn_left_region', 'nav_turn_right_region'] 134 | 135 | 136 | /////////////////////// 137 | color = 'white' 138 | 139 | // big rectangle at the borders of the video 140 | bgRect = makeRectangle(0, 0, w, h); 141 | // small rectangle at the top of the middle of the video 142 | var tpRect = makeRectangle(w*(3.0/10.0), h/4.0, w*(4.0/10.0), h/4.0); 143 | // small rectangle at the bottom of the middle of the video 144 | var btRect = makeRectangle(w*(3.0/10.0), h/2.0, w*(4.0/10.0), h/4.0); 145 | 146 | regionPoly = rectToPoly(tpRect); 147 | setRegionPoly('low_arm_up_region', regionPoly, color); 148 | 149 | regionPoly = rectToPoly(btRect); 150 | setRegionPoly('low_arm_down_region', regionPoly, color); 151 | 152 | regionPoly = [bgRect.ul, bgRect.ur, tpRect.ur, tpRect.ul]; 153 | setRegionPoly('low_arm_extend_region', regionPoly, color); 154 | 155 | regionPoly = [bgRect.ll, bgRect.lr, btRect.lr, btRect.ll]; 156 | setRegionPoly('low_arm_retract_region', regionPoly, color); 157 | 158 | regionPoly = [bgRect.ul, tpRect.ul, btRect.ll, bgRect.ll]; 159 | setRegionPoly('low_arm_base_forward_region', regionPoly, color); 160 | 161 | regionPoly = [bgRect.ur, tpRect.ur, btRect.lr, bgRect.lr]; 162 | setRegionPoly('low_arm_base_backward_region', regionPoly, color); 163 | 164 | lowArmModeRegionIds = ['low_arm_down_region', 'low_arm_up_region', 'low_arm_extend_region', 'low_arm_retract_region','low_arm_base_forward_region','low_arm_base_backward_region'] 165 | 166 | 167 | /////////////////////// 168 | color = 'white' 169 | 170 | // big rectangle at the borders of the video 171 | bgRect = makeRectangle(0, 0, w, h); 172 | // small rectangle at the top of the middle of the video 173 | tpRect = makeRectangle(w*(3.0/10.0), h/4.0, w*(4.0/10.0), h/4.0); 174 | // small rectangle at the bottom of the middle of the video 175 | btRect = makeRectangle(w*(3.0/10.0), h/2.0, w*(4.0/10.0), h/4.0); 176 | 177 | regionPoly = rectToPoly(tpRect); 178 | setRegionPoly('high_arm_up_region', regionPoly, color); 179 | 180 | regionPoly = rectToPoly(btRect); 181 | setRegionPoly('high_arm_down_region', regionPoly, color); 182 | 183 | regionPoly = [bgRect.ul, bgRect.ur, tpRect.ur, tpRect.ul]; 184 | setRegionPoly('high_arm_extend_region', regionPoly, color); 185 | 186 | regionPoly = [bgRect.ll, bgRect.lr, btRect.lr, btRect.ll]; 187 | setRegionPoly('high_arm_retract_region', regionPoly, color); 188 | 189 | regionPoly = [bgRect.ul, tpRect.ul, btRect.ll, bgRect.ll]; 190 | setRegionPoly('high_arm_base_forward_region', regionPoly, color); 191 | 192 | regionPoly = [bgRect.ur, tpRect.ur, btRect.lr, bgRect.lr]; 193 | setRegionPoly('high_arm_base_backward_region', regionPoly, color); 194 | 195 | highArmModeRegionIds = ['high_arm_down_region', 'high_arm_up_region', 'high_arm_extend_region', 'high_arm_retract_region','high_arm_base_forward_region','high_arm_base_backward_region'] 196 | 197 | 198 | /////////////////////// 199 | color = 'white' 200 | 201 | bgRect = makeRectangle(0, 0, w, h); 202 | tpRect = makeRectangle(0, 0, w, h/4.0); 203 | btRect = makeRectangle(0, 3.0*(h/4.0), w, h/4.0); 204 | smRect = makeRectangle(w/3.0, 2.0*(h/5.0), w/3.0, h/5.0); 205 | 206 | regionPoly = rectToPoly(smRect); 207 | setRegionPoly('hand_close_region', regionPoly, color); 208 | 209 | regionPoly = rectToPoly(tpRect); 210 | setRegionPoly('hand_out_region', regionPoly, color); 211 | 212 | regionPoly = rectToPoly(btRect); 213 | setRegionPoly('hand_in_region', regionPoly, color); 214 | 215 | regionPoly = [tpRect.ll, tpRect.lr, btRect.ur, btRect.ul, tpRect.ll, 216 | smRect.ul, smRect.ll, smRect.lr, smRect.ur, smRect.ul]; 217 | setRegionPoly('hand_open_region', regionPoly, color); 218 | 219 | handModeRegionIds = ['hand_close_region', 'hand_out_region', 'hand_in_region', 'hand_open_region'] 220 | 221 | 222 | /////////////////////// 223 | color = 'white' 224 | 225 | tpRect = makeRectangle(0, 0, w, h/4.0); 226 | btRect = makeRectangle(0, 3.0*(h/4.0), w, h/4.0); 227 | var ltRect = makeRectangle(0, h/4.0, w/2.0, h/2.0); 228 | var rtRect = makeRectangle(w/2.0, h/4.0, w/2.0, h/2.0); 229 | 230 | 231 | regionPoly = rectToPoly(tpRect); 232 | setRegionPoly('look_up_region', regionPoly, color); 233 | 234 | regionPoly = rectToPoly(btRect); 235 | setRegionPoly('look_down_region', regionPoly, color); 236 | 237 | regionPoly = rectToPoly(ltRect); 238 | setRegionPoly('look_left_region', regionPoly, color); 239 | 240 | regionPoly = rectToPoly(rtRect); 241 | setRegionPoly('look_right_region', regionPoly, color); 242 | 243 | lookModeRegionIds = ['look_up_region', 'look_down_region', 'look_left_region', 'look_right_region'] 244 | 245 | 246 | modeRegions = { 'nav' : navModeRegionIds, 247 | 'low_arm' : lowArmModeRegionIds, 248 | 'high_arm' : highArmModeRegionIds, 249 | 'hand' : handModeRegionIds, 250 | 'look' : lookModeRegionIds} 251 | } 252 | 253 | 254 | 255 | function arrangeOverlays(key) { 256 | /////////////////////// 257 | var nx, ny, nw, nh; 258 | 259 | // Handle D435i 90 degree rotation 260 | //var w = videoDimensions.w; 261 | //var h = videoDimensions.h; 262 | var w = videoDimensions.h 263 | var h = videoDimensions.w 264 | 265 | nx = 0 266 | ny = 0 267 | nw = w 268 | nh = h 269 | var bigViewBox = String(nx) + ' ' + String(ny) + ' ' + String(nw) + ' ' + String(nh); 270 | 271 | var overlayName = key + '_ui_overlay' 272 | var overlay = document.getElementById(overlayName); 273 | overlay.setAttribute('viewBox', bigViewBox); 274 | 275 | } 276 | 277 | 278 | 279 | 280 | function VelocityUi(elementId, commands, cursor) { 281 | this.elementId = elementId; 282 | this.commands = commands; 283 | this.cursor = cursor; 284 | this.region = document.getElementById(this.elementId); 285 | this.clicked = false; 286 | 287 | this.scaledXY = function(e) { 288 | var rect = this.region.getBoundingClientRect(); 289 | var midX = rect.x + (rect.width/2.0); 290 | var midY = rect.y + (rect.height/2.0); 291 | var cx = e.clientX; 292 | var cy = e.clientY; 293 | var ex = e.clientX - midX; 294 | var ey = midY - e.clientY; 295 | var sx = (e.clientX - midX)/rect.width; 296 | var sy = (midY - e.clientY)/rect.height; 297 | return ([sx, sy, cx, cy]); 298 | }; 299 | 300 | this.stop = function () { 301 | if (this.clicked === true) { 302 | this.clicked = false; 303 | this.commands.stop(); 304 | console.log('stop!'); 305 | this.cursor.obj.makeInactive(); 306 | } 307 | } 308 | 309 | this.callOnXY = function(e) { 310 | var active = false; 311 | if ((this.clicked === true) & (e.buttons === 1)) { 312 | active = true; 313 | } 314 | var [sx, sy, cx, cy] = this.scaledXY(e); 315 | var [velocity, scaledVelocity] = this.commands.scaledCoordToVelocity(sx, sy); 316 | if (active) { 317 | this.commands.move(velocity); 318 | this.cursor.obj.makeActive(); 319 | console.log('velocity', velocity); 320 | } else { 321 | this.cursor.obj.makeInactive(); 322 | } 323 | var scale = this.cursor.scale(scaledVelocity); 324 | var arg = this.cursor.arg(scaledVelocity); 325 | this.cursor.obj.draw(cx, cy, scale, arg); 326 | } 327 | 328 | this.onMouseMove = function (e) { 329 | this.cursor.obj.show(); 330 | this.callOnXY(e); 331 | }; 332 | 333 | this.onMouseDown = function (e) { 334 | console.log('start...'); 335 | this.clicked = true; 336 | this.callOnXY(e); 337 | }; 338 | 339 | this.onLeave = function (e) { 340 | this.stop(); 341 | this.cursor.obj.hide(); 342 | } 343 | 344 | this.region.onmousemove = this.onMouseMove.bind(this); 345 | this.region.onmousedown = this.onMouseDown.bind(this); 346 | this.region.onmouseleave = this.onLeave.bind(this); 347 | this.region.onmouseup = this.stop.bind(this); 348 | 349 | } 350 | 351 | function createVelocityControl() { 352 | 353 | function Cursor(cursorElementId, overlayElementId, initCursor, drawCursor) { 354 | 355 | this.show = function () { 356 | //this.cursor.setAttribute('visibility', 'visible'); 357 | this.element.setAttribute('display', 'inline'); 358 | } 359 | 360 | this.hide = function () { 361 | //this.cursor.setAttribute('visibility', 'hidden'); 362 | this.element.setAttribute('display', 'none'); 363 | } 364 | 365 | this.makeActive = function () { 366 | this.element.setAttribute('fill', 'white'); 367 | this.element.setAttribute('stroke', 'black'); 368 | } 369 | 370 | this.makeInactive = function () { 371 | this.element.setAttribute('fill', 'lightgrey'); 372 | this.element.setAttribute('stroke', 'grey'); 373 | } 374 | 375 | this.draw = function (cx, cy, scale, arg) { 376 | var pt = this.overlayElement.createSVGPoint(); 377 | pt.x = cx; 378 | pt.y = cy; 379 | var svgCoord = pt.matrixTransform(this.overlayElement.getScreenCTM().inverse()); 380 | 381 | drawCursor(this.element, svgCoord, scale, arg); 382 | } 383 | 384 | this.element = document.getElementById(cursorElementId); 385 | 386 | this.overlayElement = document.getElementById(overlayElementId); 387 | 388 | initCursor(this); 389 | } 390 | 391 | function initRotationCursor(obj) { 392 | var region; 393 | region = obj.element; 394 | 395 | var arrowWidth = 0.2; 396 | var arrowRadius = 1.0; 397 | var arrowHeadWidth = 0.25 398 | var arrowHeadLength = 0.45; 399 | 400 | // unit circle upper right hand 90 deg arc 401 | // d="M 1,0 a 1,1 0 0 1 1,1" 402 | 403 | var d = ''; 404 | var r1 = arrowRadius + (arrowWidth/2.0); 405 | var x1 = r1; 406 | var y1 = 0; 407 | var x2 = 0; 408 | var y2 = y1 + r1; 409 | var r2 = arrowRadius - (arrowWidth/2.0); 410 | var x3 = 0; 411 | var y3 = y2 - arrowWidth; 412 | var x4 = r2; 413 | var y4 = 0; 414 | var ymid = y2 - (arrowWidth/2.0); 415 | 416 | // outer arc 417 | d = d + 'M ' + x1 + ',' + y1; 418 | d = d + ' A ' + r1 + ',' + r1 + ' 0 0 1 ' + x2 + ',' + y2; 419 | 420 | // arrow head 421 | d = d + ' L ' + x2 + ',' + y2; 422 | d = d + ' ' + x2 + ',' + (ymid + arrowHeadWidth); 423 | d = d + ' ' + (x2 - arrowHeadLength) + ',' + ymid; 424 | d = d + ' ' + x2 + ',' + (ymid - arrowHeadWidth); 425 | d = d + ' ' + x3 + ',' + y3; 426 | 427 | // inner arc 428 | d = d + ' A ' + r2 + ',' + r2 + ' 0 0 0 ' + x4 + ',' + y4; 429 | 430 | // flat end of arc 431 | d = d + ' Z'; 432 | 433 | region.setAttribute('d', d); 434 | } 435 | 436 | function drawRotationCursor(cursorElement, svgCoord, scale, flip) { 437 | //console.log(this); 438 | var region; 439 | region = cursorElement; 440 | var sign; 441 | if(flip) { 442 | sign = -1; 443 | } else { 444 | sign = 1; 445 | } 446 | region.setAttribute('transform', 447 | 'translate(' + svgCoord.x + ', ' + svgCoord.y + ') scale(' + sign * scale + ',' + scale + ') rotate(-58)'); 448 | } 449 | 450 | var rotationCursor = new Cursor('rotate_arrow', 'video_ui_overlay', 451 | initRotationCursor, drawRotationCursor); 452 | 453 | 454 | function initArrowCursor(obj) {} 455 | 456 | function drawArrowCursor(cursorElement, svgCoord, scale, angleDeg) { 457 | var region = cursorElement; 458 | region.setAttribute('transform', 459 | 'translate(' + svgCoord.x + ', ' + svgCoord.y + ') scale(' + scale + ') rotate(' + angleDeg + ')'); 460 | } 461 | 462 | var arrowCursor = new Cursor('down_arrow', 'video_ui_overlay', 463 | initArrowCursor, drawArrowCursor); 464 | 465 | 466 | function initGripperCursor(obj) {} 467 | 468 | function drawGripperCursor(cursorElement, svgCoord, scale, aperture) { 469 | var region = cursorElement; 470 | region.setAttribute('transform', 471 | 'translate(' + svgCoord.x + ', ' + svgCoord.y + ') scale(' + scale + ')'); 472 | 473 | region = document.getElementById('gripper_left_half'); 474 | // aperture should be between 0 and 1 475 | region.setAttribute('d', 'M ' + -aperture + ',-0.5 a 0.1,0.1 0 0 0 0,1 Z'); 476 | region = document.getElementById('gripper_right_half'); 477 | // aperture should be between 0 and 1 478 | region.setAttribute('d', 'M ' + aperture + ',-0.5 a 0.1,0.1 0 0 1 0,1 Z'); 479 | } 480 | 481 | var gripperCursor = new Cursor('gripper', 'video_ui_overlay', 482 | initGripperCursor, drawGripperCursor); 483 | 484 | var maxDegPerSec = 60.0; 485 | 486 | function mouseToDegPerSec(d, flip, negative=false) { 487 | var degPerSec; 488 | //console.log(y,flip); 489 | if(flip) { 490 | degPerSec = -(maxDegPerSec * (d - 0.5)); 491 | } else { 492 | degPerSec = maxDegPerSec * (d + 0.5); 493 | } 494 | 495 | if(degPerSec < 0.0) { 496 | degPerSec = 0.0; 497 | } 498 | if(degPerSec > maxDegPerSec) { 499 | degPerSec = maxDegPerSec; 500 | } 501 | 502 | if(flip) { 503 | degPerSec = -degPerSec; 504 | } 505 | 506 | if(negative) { 507 | degPerSec = -degPerSec; 508 | } 509 | 510 | var scaledDegPerSec = degPerSec/maxDegPerSec; 511 | 512 | return [degPerSec, scaledDegPerSec]; 513 | 514 | } 515 | 516 | function mouseToApertureWidth(sx) { 517 | var scaledVelocity = Math.abs(sx); 518 | var velocity; 519 | // aperture range: -6.0 to 14.0 520 | velocity = (scaledVelocity * 40.0) - 6.0; 521 | return [velocity, scaledVelocity]; 522 | } 523 | 524 | function doNothing () {} 525 | 526 | /////////// 527 | 528 | var gripperCloseCommands = { 529 | scaledCoordToVelocity: function(sx, sy) { return(mouseToApertureWidth(sx)); }, 530 | move: gripperSetGoal, 531 | stop: doNothing 532 | } 533 | 534 | var gripperCloseCursor = { 535 | obj: gripperCursor, 536 | scale: function(scaledVelocity) { return(40.0); }, 537 | arg: function(scaledVelocity) { return(scaledVelocity * 2.0); } 538 | } 539 | 540 | new VelocityUi('gripper_close_region', gripperCloseCommands, gripperCloseCursor); 541 | 542 | /////////// 543 | 544 | function arrowScale(scaledVelocity) { 545 | //console.log('scaledVelocity', scaledVelocity); 546 | var arrowMult = 20.0; 547 | var arrowAdd = 10.0; 548 | return ((Math.abs(scaledVelocity) * arrowMult) + arrowAdd); 549 | } 550 | 551 | function rotationScale(scaledVelocity) { 552 | var rotationMult = 50.0; 553 | var rotationAdd = 20.0; 554 | return ((Math.abs(scaledVelocity) * rotationMult) + rotationAdd); 555 | } 556 | 557 | /////////// 558 | 559 | var bendUpCommands = { 560 | scaledCoordToVelocity: function(sx, sy) { return(mouseToDegPerSec(sy, false)); }, 561 | move: wristVelocityBend, 562 | stop: wristMotionStop, 563 | } 564 | 565 | var bendUpCursor = { 566 | obj: arrowCursor, 567 | scale: function(scaledVelocity) { return(arrowScale(scaledVelocity)); }, 568 | arg: function(scaledVelocity) { return(180.0); } 569 | } 570 | 571 | new VelocityUi('wrist_bend_up_region', bendUpCommands, bendUpCursor); 572 | 573 | /////////// 574 | 575 | var bendDownCommands = { 576 | scaledCoordToVelocity: function(sx, sy) { return(mouseToDegPerSec(sy, true)); }, 577 | move: wristVelocityBend, 578 | stop: wristMotionStop, 579 | } 580 | 581 | var bendDownCursor = { 582 | obj: arrowCursor, 583 | scale: function(scaledVelocity) { return(arrowScale(scaledVelocity)); }, 584 | arg: function(scaledVelocity) { return(0.0); } 585 | } 586 | 587 | new VelocityUi('wrist_bend_down_region', bendDownCommands, bendDownCursor); 588 | 589 | /////////// 590 | 591 | var rollLeftCommands = { 592 | scaledCoordToVelocity: function(sx, sy) { return(mouseToDegPerSec(sx, true)); }, 593 | move: doNothing, 594 | stop: doNothing 595 | } 596 | 597 | var rollLeftCursor = { 598 | obj: rotationCursor, 599 | scale: function(scaledVelocity) { return(rotationScale(scaledVelocity)); }, 600 | arg: function(scaledVelocity) { return(true); } 601 | } 602 | 603 | new VelocityUi('wrist_roll_left_region', rollLeftCommands, rollLeftCursor); 604 | 605 | /////////// 606 | 607 | var rollRightCommands = { 608 | scaledCoordToVelocity: function(sx, sy) { return(mouseToDegPerSec(sx, false)); }, 609 | move: doNothing, 610 | stop: doNothing 611 | } 612 | 613 | var rollRightCursor = { 614 | obj: rotationCursor, 615 | scale: function(scaledVelocity) { return(rotationScale(scaledVelocity)); }, 616 | arg: function(scaledVelocity) { return(false); } 617 | } 618 | 619 | new VelocityUi('wrist_roll_right_region', rollRightCommands, rollRightCursor); 620 | 621 | /////////// 622 | 623 | var retractCommands = { 624 | scaledCoordToVelocity: function(sx, sy) { return(mouseToDegPerSec(sx, false, true)); }, 625 | move: doNothing, 626 | stop: doNothing 627 | } 628 | 629 | var retractCursor = { 630 | obj: arrowCursor, 631 | scale: function(scaledVelocity) { return(arrowScale(scaledVelocity)); }, 632 | arg: function(scaledVelocity) { return(-90.0); } 633 | } 634 | 635 | new VelocityUi('arm_retract_region', retractCommands, retractCursor); 636 | 637 | /////////// 638 | 639 | var extendCommands = { 640 | scaledCoordToVelocity: function(sx, sy) { return(mouseToDegPerSec(sx, true, true)); }, 641 | move: doNothing, 642 | stop: doNothing 643 | } 644 | 645 | var extendCursor = { 646 | obj: arrowCursor, 647 | scale: function(scaledVelocity) { return(arrowScale(scaledVelocity)); }, 648 | arg: function(scaledVelocity) { return(90.0); } 649 | } 650 | 651 | new VelocityUi('arm_extend_region', extendCommands, extendCursor); 652 | 653 | /////////// 654 | 655 | var raiseCommands = { 656 | scaledCoordToVelocity: function(sx, sy) { return(mouseToDegPerSec(sy, false)); }, 657 | move: doNothing, 658 | stop: doNothing, 659 | } 660 | 661 | var raiseCursor = { 662 | obj: arrowCursor, 663 | scale: function(scaledVelocity) { return(arrowScale(scaledVelocity)); }, 664 | arg: function(scaledVelocity) { return(180.0); } 665 | } 666 | 667 | new VelocityUi('lift_up_region', raiseCommands, raiseCursor); 668 | 669 | /////////// 670 | 671 | var lowerCommands = { 672 | scaledCoordToVelocity: function(sx, sy) { return(mouseToDegPerSec(sy, true)); }, 673 | move: doNothing, 674 | stop: doNothing, 675 | } 676 | 677 | var lowerCursor = { 678 | obj: arrowCursor, 679 | scale: function(scaledVelocity) { return(arrowScale(scaledVelocity)); }, 680 | arg: function(scaledVelocity) { return(0.0); } 681 | } 682 | 683 | new VelocityUi('lift_down_region', lowerCommands, lowerCursor); 684 | 685 | /////////// 686 | 687 | var forwardCommands = { 688 | scaledCoordToVelocity: function(sx, sy) { return(mouseToDegPerSec(sy, false)); }, 689 | move: doNothing, 690 | stop: doNothing, 691 | } 692 | 693 | var forwardCursor = { 694 | obj: arrowCursor, 695 | scale: function(scaledVelocity) { return(arrowScale(scaledVelocity)); }, 696 | arg: function(scaledVelocity) { return(180.0); } 697 | } 698 | 699 | new VelocityUi('robot_forward_region', forwardCommands, forwardCursor); 700 | 701 | /////////// 702 | 703 | var backwardCommands = { 704 | scaledCoordToVelocity: function(sx, sy) { return(mouseToDegPerSec(sy, true)); }, 705 | move: doNothing, 706 | stop: doNothing, 707 | } 708 | 709 | var backwardCursor = { 710 | obj: arrowCursor, 711 | scale: function(scaledVelocity) { return(arrowScale(scaledVelocity)); }, 712 | arg: function(scaledVelocity) { return(0.0); } 713 | } 714 | 715 | new VelocityUi('robot_backward_region', backwardCommands, backwardCursor); 716 | 717 | /////////// 718 | 719 | var turnLeftCommands = { 720 | scaledCoordToVelocity: function(sx, sy) { return(mouseToDegPerSec(sx, true)); }, 721 | move: doNothing, 722 | stop: doNothing 723 | } 724 | 725 | var turnLeftCursor = { 726 | obj: rotationCursor, 727 | scale: function(scaledVelocity) { return(rotationScale(scaledVelocity)); }, 728 | arg: function(scaledVelocity) { return(true); } 729 | } 730 | 731 | new VelocityUi('robot_turn_left_region', turnLeftCommands, turnLeftCursor); 732 | 733 | /////////// 734 | 735 | var turnRightCommands = { 736 | scaledCoordToVelocity: function(sx, sy) { return(mouseToDegPerSec(sx, false)); }, 737 | move: doNothing, 738 | stop: doNothing 739 | } 740 | 741 | var turnRightCursor = { 742 | obj: rotationCursor, 743 | scale: function(scaledVelocity) { return(rotationScale(scaledVelocity)); }, 744 | arg: function(scaledVelocity) { return(false); } 745 | } 746 | 747 | new VelocityUi('robot_turn_right_region', turnRightCommands, turnRightCursor); 748 | 749 | /////////// 750 | 751 | 752 | // turn off the right click menu 753 | document.oncontextmenu = function() { 754 | return false; 755 | } 756 | document.ondrag = function() { 757 | return false; 758 | } 759 | document.ondragstart = function() { 760 | return false; 761 | } 762 | } 763 | 764 | 765 | createUiRegions(true); // debug = true or false 766 | 767 | 768 | -------------------------------------------------------------------------------- /operator/right_arrow_medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hello-robot/stretch_web_interface/fb9c24cb42f577b9711bffe25e003cc1225c2fe5/operator/right_arrow_medium.png -------------------------------------------------------------------------------- /operator/right_arrow_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hello-robot/stretch_web_interface/fb9c24cb42f577b9711bffe25e003cc1225c2fe5/operator/right_arrow_small.png -------------------------------------------------------------------------------- /operator/right_turn_medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hello-robot/stretch_web_interface/fb9c24cb42f577b9711bffe25e003cc1225c2fe5/operator/right_turn_medium.png -------------------------------------------------------------------------------- /operator/right_turn_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hello-robot/stretch_web_interface/fb9c24cb42f577b9711bffe25e003cc1225c2fe5/operator/right_turn_small.png -------------------------------------------------------------------------------- /operator/up_arrow_medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hello-robot/stretch_web_interface/fb9c24cb42f577b9711bffe25e003cc1225c2fe5/operator/up_arrow_medium.png -------------------------------------------------------------------------------- /operator/up_arrow_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hello-robot/stretch_web_interface/fb9c24cb42f577b9711bffe25e003cc1225c2fe5/operator/up_arrow_small.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-robot-server", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www" 7 | }, 8 | "dependencies": { 9 | "body-parser": "^1.19.0", 10 | "connect-redis": "^6.0.0", 11 | "cookie-parser": "^1.4.5", 12 | "debug": "^4.3.1", 13 | "express": "^4.17.1", 14 | "express-session": "^1.17.2", 15 | "helmet": "^4.6.0", 16 | "mongoose": "^5.12.14", 17 | "morgan": "^1.10.0", 18 | "passport": "^0.4.1", 19 | "passport-local": "^1.0.0", 20 | "passport-local-mongoose": "^6.1.0", 21 | "passport.socketio": "^3.7.0", 22 | "pug": "^3.0.2", 23 | "puppeteer": "^10.0.0", 24 | "serve-favicon": "^2.5.0", 25 | "socket.io": "^4.1.2", 26 | "redis": "^3.1.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | stretch_web_interface 4 | 0.0.1 5 | The stretch_web_interface package 6 | 7 | 8 | 9 | 10 | Hello Robot Inc. 11 | 12 | 13 | 14 | 15 | 16 | Apache License 2.0 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | catkin 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hello-robot/stretch_web_interface/fb9c24cb42f577b9711bffe25e003cc1225c2fe5/public/favicon.ico -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #eee; 3 | } 4 | 5 | .form-signin { 6 | max-width: 330px; 7 | padding: 15px; 8 | margin: 0 auto; 9 | } 10 | .form-signin .form-signin-heading, 11 | .form-signin .checkbox { 12 | margin-bottom: 10px; 13 | } 14 | .form-signin .checkbox { 15 | font-weight: normal; 16 | } 17 | .form-signin .form-control { 18 | position: relative; 19 | height: auto; 20 | -webkit-box-sizing: border-box; 21 | -moz-box-sizing: border-box; 22 | box-sizing: border-box; 23 | padding: 10px; 24 | font-size: 16px; 25 | } 26 | .form-signin .form-control:focus { 27 | z-index: 2; 28 | } 29 | .form-signin input[type="email"] { 30 | margin-bottom: -1px; 31 | border-bottom-right-radius: 0; 32 | border-bottom-left-radius: 0; 33 | } 34 | .form-signin input[type="password"] { 35 | margin-bottom: 10px; 36 | border-top-left-radius: 0; 37 | border-top-right-radius: 0; 38 | } 39 | -------------------------------------------------------------------------------- /robot/robot.css: -------------------------------------------------------------------------------- 1 | 2 | #video-display-div { 3 | height:90%; 4 | } 5 | 6 | #video-display { 7 | position:absolute; 8 | height:90%; 9 | z-index:1; 10 | } 11 | -------------------------------------------------------------------------------- /robot/robot.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /robot/robot.js: -------------------------------------------------------------------------------- 1 | var peer_name = "ROBOT"; 2 | var recordOn = false; 3 | -------------------------------------------------------------------------------- /robot/robot_acquire_av.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * derived from initial code from the following website with the copyright notice below 4 | * https://github.com/webrtc/samples/blob/gh-pages/src/content/devices/input-output/js/main.js 5 | * 6 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. 7 | * 8 | * Use of this source code is governed by a BSD-style license 9 | * that can be found in the LICENSE file in the root of the source 10 | * tree. 11 | * 12 | * 13 | */ 14 | 15 | 'use strict'; 16 | 17 | var audioStream; 18 | 19 | var audioInId; 20 | var audioOutId; 21 | 22 | var editedFps = 15; 23 | var videoEditingCanvas = document.createElement('canvas'); 24 | var videoDisplayElement = document.querySelector('video'); 25 | 26 | var camDim = {w:videoDimensions.w, h:videoDimensions.h}; 27 | console.log('camDim', camDim); 28 | 29 | // Make room for -90 deg rotation due to D435i orientation. 30 | var editedDim = {w:camDim.h, h:camDim.w} 31 | 32 | var handRoll = 0.0; 33 | var degToRad = (2.0* Math.PI)/360.0; 34 | 35 | videoEditingCanvas.width = editedDim.w; 36 | videoEditingCanvas.height = editedDim.h; 37 | var videoEditingContext = videoEditingCanvas.getContext('2d'); 38 | videoEditingContext.fillStyle="black"; 39 | videoEditingContext.fillRect(0, 0, editedDim.w, editedDim.h); 40 | var editedVideoStream = videoEditingCanvas.captureStream(editedFps); 41 | 42 | function drawVideo() { 43 | var d; 44 | 45 | if (rosImageReceived === true) { 46 | //var d435iRotation = -90.0 * degToRad; 47 | var d435iRotation = 90.0 * degToRad; 48 | 49 | videoEditingContext.fillStyle="black"; 50 | videoEditingContext.fillRect(0, 0, editedDim.w, editedDim.h); 51 | videoEditingContext.translate(editedDim.w/2, editedDim.h/2); 52 | videoEditingContext.rotate(d435iRotation); 53 | videoEditingContext.drawImage(img, -camDim.w/2, -camDim.h/2, camDim.w, camDim.h) 54 | videoEditingContext.rotate(-d435iRotation); 55 | videoEditingContext.translate(-editedDim.w/2, -editedDim.h/2); 56 | } 57 | 58 | requestAnimationFrame(drawVideo); 59 | } 60 | 61 | function findDevices(deviceInfos) { 62 | // Handles being called several times to update labels. Preserve values. 63 | 64 | var i = 0; 65 | for (let d of deviceInfos) { 66 | console.log(''); 67 | console.log('device number ' + i); 68 | i++; 69 | console.log('kind: ' + d.kind); 70 | console.log('label: ' + d.label); 71 | console.log('ID: ' + d.deviceId); 72 | 73 | // javascript switch uses === comparison 74 | switch (d.kind) { 75 | case 'audioinput': 76 | //if(d.label === 'USB Audio Device Analog Mono') { 77 | audioInId = d.deviceId; 78 | console.log('using this device for robot audio input'); 79 | //} 80 | break; 81 | case 'audiooutput': 82 | // if(d.label === 'HDA NVidia Digital Stereo (HDMI 2)') { 83 | //if(d.label === 'USB Audio Device Analog Stereo') { 84 | audioOutId = d.deviceId; 85 | console.log('using this device for robot audio output'); 86 | //} 87 | break; 88 | default: 89 | console.log('* unrecognized kind of device * ', d); 90 | } 91 | } 92 | 93 | start(); 94 | } 95 | 96 | 97 | navigator.mediaDevices.enumerateDevices().then(findDevices).catch(handleError); 98 | 99 | // Attach audio output device to video element using device/sink ID. 100 | function attachSinkId(element, sinkId) { 101 | if (typeof element.sinkId !== 'undefined') { 102 | element.setSinkId(sinkId) 103 | .then(function() { 104 | console.log('Success, audio output device attached: ' + sinkId); 105 | }) 106 | .catch(function(error) { 107 | var errorMessage = error; 108 | if (error.name === 'SecurityError') { 109 | errorMessage = 'You need to use HTTPS for selecting audio output ' + 110 | 'device: ' + error; 111 | } 112 | console.error(errorMessage); 113 | // Jump back to first output device in the list as it's the default. 114 | audioOutputSelect.selectedIndex = 0; 115 | }); 116 | } else { 117 | console.warn('Browser does not support output device selection.'); 118 | } 119 | } 120 | 121 | function changeAudioDestination() { 122 | var audioDestination = audioOutId; 123 | attachSinkId(videoDisplayElement, audioDestination); 124 | } 125 | 126 | function gotAudioStream(stream) { 127 | console.log('setting up audioStream for the microphone'); 128 | audioStream = stream; 129 | 130 | // remove audio tracks from localStream 131 | for (let a of localStream.getAudioTracks()) { 132 | localStream.removeTrack(a); 133 | } 134 | var localAudio = stream.getAudioTracks()[0]; // get audio track from robot microphone 135 | localStream.addTrack(localAudio); // add audio track to localStream for transmission to operator 136 | } 137 | 138 | 139 | function start() { 140 | 141 | if(audioOutId) { 142 | changeAudioDestination(); 143 | } else { 144 | console.log('no audio output found or selected'); 145 | console.log('attempting to use the default audio output'); 146 | } 147 | 148 | displayStream = new MediaStream(editedVideoStream); // make a copy of the stream for local display 149 | // remove audio tracks from displayStream 150 | for (let a of displayStream.getAudioTracks()) { 151 | displayStream.removeTrack(a); 152 | } 153 | videoDisplayElement.srcObject = displayStream; // display the stream 154 | 155 | localStream = new MediaStream(editedVideoStream); 156 | 157 | var constraints; 158 | 159 | console.log('trying to obtain videos with'); 160 | console.log('width = ' + camDim.w); 161 | console.log('height = ' + camDim.h); 162 | 163 | if(audioInId) { 164 | constraints = { 165 | audio: {deviceId: {exact: audioInId}}, 166 | video: false 167 | }; 168 | console.log('attempting to acquire audio input stream'); 169 | navigator.mediaDevices.getUserMedia(constraints). 170 | then(gotAudioStream).catch(handleError); 171 | } else { 172 | console.log('the robot audio input was not found!'); 173 | } 174 | 175 | drawVideo(); 176 | } 177 | 178 | 179 | function handleError(error) { 180 | console.log('navigator.getUserMedia error: ', error); 181 | } 182 | -------------------------------------------------------------------------------- /robot/ros_connect.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var messages_received_body = []; 4 | var commands_sent_body = []; 5 | var messages_received_wrist = []; 6 | var commands_sent_wrist = []; 7 | var rosImageReceived = false 8 | var img = document.createElement("IMG") 9 | img.style.visibility = 'hidden' 10 | var rosJointStateReceived = false 11 | var jointState = null 12 | 13 | var session_body = {ws:null, ready:false, port_details:{}, port_name:"", version:"", commands:[], hostname:"", serial_ports:[]}; 14 | 15 | var session_wrist = {ws:null, ready:false, port_details:{}, port_name:"", version:"", commands:[], hostname:"", serial_ports:[]}; 16 | 17 | 18 | // connect to rosbridge websocket 19 | var ros = new ROSLIB.Ros({ 20 | url : 'ws://localhost:9090' 21 | }); 22 | 23 | ros.on('connection', function() { 24 | console.log('Connected to websocket.'); 25 | }); 26 | 27 | ros.on('error', function(error) { 28 | console.log('Error connecting to websocket: ', error); 29 | }); 30 | 31 | ros.on('close', function() { 32 | console.log('Connection to websocket has been closed.'); 33 | }); 34 | 35 | var imageTopic = new ROSLIB.Topic({ 36 | ros : ros, 37 | name : '/camera/color/image_raw/compressed', 38 | messageType : 'sensor_msgs/CompressedImage' 39 | }); 40 | 41 | imageTopic.subscribe(function(message) { 42 | //console.log('Received compressed image on ' + imageTopic.name); 43 | //console.log('message.header =', message.header) 44 | //console.log('message.format =', message.format) 45 | 46 | img.src = 'data:image/jpg;base64,' + message.data 47 | 48 | if (rosImageReceived === false) { 49 | console.log('Received first compressed image from ROS topic ' + imageTopic.name); 50 | rosImageReceived = true 51 | } 52 | //console.log('img.width =', img.width) 53 | //console.log('img.height =', img.height) 54 | //console.log('img.naturalWidth =', img.naturalWidth) 55 | //console.log('img.naturalHeight =', img.naturalHeight) 56 | //console.log('attempted to draw image to the canvas') 57 | //imageTopic.unsubscribe() 58 | }); 59 | 60 | 61 | function getJointEffort(jointStateMessage, jointName) { 62 | var jointIndex = jointStateMessage.name.indexOf(jointName) 63 | return jointStateMessage.effort[jointIndex] 64 | } 65 | 66 | function getJointValue(jointStateMessage, jointName) { 67 | var jointIndex = jointStateMessage.name.indexOf(jointName) 68 | return jointStateMessage.position[jointIndex] 69 | } 70 | 71 | var jointStateTopic = new ROSLIB.Topic({ 72 | ros : ros, 73 | name : '/stretch/joint_states/', 74 | messageType : 'sensor_msgs/JointState' 75 | }); 76 | 77 | jointStateTopic.subscribe(function(message) { 78 | 79 | jointState = message 80 | 81 | if (rosJointStateReceived === false) { 82 | console.log('Received first joint state from ROS topic ' + jointStateTopic.name); 83 | rosJointStateReceived = true 84 | } 85 | 86 | // send wrist joint effort 87 | var JointEffort = getJointEffort(jointState, 'joint_wrist_yaw') 88 | var message = {'type': 'sensor', 'subtype':'wrist', 'name':'yaw_torque', 'value': JointEffort} 89 | sendData(message) 90 | 91 | // send gripper effort 92 | JointEffort = getJointEffort(jointState, 'joint_gripper_finger_left') 93 | var message = {'type': 'sensor', 'subtype':'gripper', 'name':'gripper_torque', 'value': JointEffort} 94 | sendData(message) 95 | 96 | // send lift effort 97 | JointEffort = getJointEffort(jointState, 'joint_lift') 98 | var message = {'type': 'sensor', 'subtype':'lift', 'name':'lift_effort', 'value': JointEffort} 99 | sendData(message) 100 | 101 | // send telescoping arm effort 102 | JointEffort = getJointEffort(jointState, 'joint_arm_l0') 103 | var message = {'type': 'sensor', 'subtype':'arm', 'name':'arm_effort', 'value': JointEffort} 104 | sendData(message) 105 | 106 | 107 | // Header header 108 | // string[] name 109 | // float64[] position 110 | // float64[] velocity 111 | // float64[] effort 112 | //imageTopic.unsubscribe() 113 | }); 114 | 115 | 116 | 117 | var trajectoryClient = new ROSLIB.ActionClient({ 118 | ros : ros, 119 | serverName : '/stretch_controller/follow_joint_trajectory', 120 | actionName : 'control_msgs/FollowJointTrajectoryAction' 121 | }); 122 | 123 | 124 | function generatePoseGoal(pose){ 125 | 126 | var outStr = '{' 127 | for (var key in pose) { 128 | outStr = outStr + String(key) + ':' + String(pose[key]) + ', ' 129 | } 130 | outStr = outStr + '}' 131 | console.log('generatePoseGoal( ' + outStr + ' )') 132 | 133 | var jointNames = [] 134 | var jointPositions = [] 135 | for (var key in pose) { 136 | jointNames.push(key) 137 | jointPositions.push(pose[key]) 138 | } 139 | var newGoal = new ROSLIB.Goal({ 140 | actionClient : trajectoryClient, 141 | goalMessage : { 142 | trajectory : { 143 | joint_names : jointNames, 144 | points : [ 145 | { 146 | positions : jointPositions 147 | } 148 | ] 149 | } 150 | } 151 | }) 152 | 153 | console.log('newGoal created =' + newGoal) 154 | 155 | // newGoal.on('feedback', function(feedback) { 156 | // console.log('Feedback: ' + feedback.sequence); 157 | // }); 158 | 159 | // newGoal.on('result', function(result) { 160 | // console.log('Final Result: ' + result.sequence); 161 | // }); 162 | 163 | return newGoal 164 | } 165 | 166 | //////////////////////////////////////////////////////////////////////////////////// 167 | 168 | function loggedWebSocketSendWrist(cmd) { 169 | session_wrist.ws.send(cmd); 170 | commands_sent_wrist.push(cmd); 171 | } 172 | 173 | 174 | function sendCommandWrist(cmd) { 175 | if(session_wrist.ready) { 176 | 177 | command = JSON.stringify(cmd); 178 | loggedWebSocketSendWrist(command); 179 | } 180 | } 181 | 182 | function loggedWebSocketSendBody(cmd) { 183 | session_body.ws.send(cmd); 184 | commands_sent_body.push(cmd); 185 | } 186 | 187 | 188 | function sendCommandBody(cmd) { 189 | if(session_body.ready) { 190 | 191 | command = JSON.stringify(cmd); 192 | loggedWebSocketSendBody(command); 193 | } 194 | } 195 | 196 | //////////////////////////////////////////////////////////////////////////////////// 197 | 198 | //Called from mode switch 199 | 200 | function robotModeOn(modeKey) { 201 | console.log('robotModeOn called with modeKey = ' + modeKey) 202 | 203 | if (modeKey === 'nav') { 204 | var headNavPoseGoal = generatePoseGoal({'joint_head_pan': 0.0, 'joint_head_tilt': -1.0}) 205 | headNavPoseGoal.send() 206 | console.log('sending navigation pose to head') 207 | } 208 | 209 | if (modeKey === 'low_arm') { 210 | var headManPoseGoal = generatePoseGoal({'joint_head_pan': -1.57, 'joint_head_tilt': -0.9}) 211 | headManPoseGoal.send() 212 | console.log('sending manipulation pose to head') 213 | } 214 | 215 | if (modeKey === 'high_arm') { 216 | var headManPoseGoal = generatePoseGoal({'joint_head_pan': -1.57, 'joint_head_tilt': -0.45}) 217 | headManPoseGoal.send() 218 | console.log('sending manipulation pose to head') 219 | } 220 | } 221 | 222 | //////////////////////////////////////////////////////////////////////////////////// 223 | 224 | //Called from button click 225 | function baseTranslate(dist, vel) { 226 | // distance in centimeters 227 | // velocity in centimeters / second 228 | console.log('sending baseTranslate command') 229 | 230 | if (dist > 0.0){ 231 | var baseForwardPoseGoal = generatePoseGoal({'translate_mobile_base': -0.02}) 232 | baseForwardPoseGoal.send() 233 | } else if (dist < 0.0) { 234 | var baseBackwardPoseGoal = generatePoseGoal({'translate_mobile_base': 0.02}) 235 | baseBackwardPoseGoal.send() 236 | } 237 | //sendCommandBody({type: "base",action:"translate", dist:dist, vel:vel}); 238 | } 239 | 240 | function baseTurn(ang_deg, vel) { 241 | // angle in degrees 242 | // velocity in centimeter / second (linear wheel velocity - same as BaseTranslate) 243 | console.log('sending baseTurn command') 244 | 245 | if (ang_deg > 0.0){ 246 | var baseTurnLeftPoseGoal = generatePoseGoal({'rotate_mobile_base': -0.1}) 247 | baseTurnLeftPoseGoal.send() 248 | } else if (ang_deg < 0.0) { 249 | var baseTurnRightPoseGoal = generatePoseGoal({'rotate_mobile_base': 0.1}) 250 | baseTurnRightPoseGoal.send() 251 | } 252 | //sendCommandBody({type: "base",action:"turn", ang:ang_deg, vel:vel}); 253 | } 254 | 255 | 256 | function sendIncrementalMove(jointName, jointValueInc) { 257 | console.log('sendIncrementalMove start: jointName =' + jointName) 258 | if (jointState !== null) { 259 | var newJointValue = getJointValue(jointState, jointName) 260 | newJointValue = newJointValue + jointValueInc 261 | console.log('poseGoal call: jointName =' + jointName) 262 | var pose = {[jointName]: newJointValue} 263 | var poseGoal = generatePoseGoal(pose) 264 | poseGoal.send() 265 | return true 266 | } 267 | return false 268 | } 269 | 270 | 271 | 272 | function armMove(dist, timeout) { 273 | console.log('attempting to sendarmMove command') 274 | var jointValueInc = 0.0 275 | if (dist > 0.0) { 276 | jointValueInc = 0.02 277 | } else if (dist < 0.0) { 278 | jointValueInc = -0.02 279 | } 280 | sendIncrementalMove('wrist_extension', jointValueInc) 281 | //sendCommandBody({type: "arm", action:"move", dist:dist, timeout:timeout}); 282 | } 283 | 284 | function liftMove(dist, timeout) { 285 | console.log('attempting to sendliftMove command') 286 | var jointValueInc = 0.0 287 | if (dist > 0.0) { 288 | jointValueInc = 0.02 289 | } else if (dist < 0.0) { 290 | jointValueInc = -0.02 291 | } 292 | sendIncrementalMove('joint_lift', jointValueInc) 293 | //sendCommandBody({type: "lift", action:"move", dist:dist, timeout:timeout}); 294 | } 295 | 296 | function gripperDeltaAperture(deltaWidthCm) { 297 | // attempt to change the gripper aperture 298 | console.log('attempting to sendgripper delta command'); 299 | var jointValueInc = 0.0 300 | if (deltaWidthCm > 0.0) { 301 | jointValueInc = 0.05 302 | } else if (deltaWidthCm < 0.0) { 303 | jointValueInc = -0.05 304 | } 305 | sendIncrementalMove('joint_gripper_finger_left', jointValueInc) 306 | //sendCommandWrist({type:'gripper', action:'delta', delta_aperture_cm:deltaWidthCm}); 307 | } 308 | 309 | function wristMove(angRad) { 310 | console.log('attempting to send wristMove command') 311 | var jointValueInc = 0.0 312 | if (angRad > 0.0) { 313 | jointValueInc = 0.1 314 | } else if (angRad < 0.0) { 315 | jointValueInc = -0.1 316 | } 317 | sendIncrementalMove('joint_wrist_yaw', jointValueInc) 318 | } 319 | 320 | function headTilt(angRad) { 321 | console.log('attempting to send headTilt command') 322 | sendIncrementalMove('joint_head_tilt', angRad) 323 | } 324 | 325 | function headPan(angRad) { 326 | console.log('attempting to send headPan command') 327 | sendIncrementalMove('joint_head_pan', angRad) 328 | } 329 | 330 | 331 | //////////////////////////////////////////////////////////////////////////////////// 332 | 333 | function armHome() { 334 | console.log('sending armHome command') 335 | sendCommandBody({type: "arm", action:"home"}); 336 | } 337 | 338 | function liftHome() { 339 | console.log('sending liftHome command') 340 | sendCommandBody({type: "lift", action:"home"}); 341 | } 342 | 343 | function wristStopMotion() { 344 | console.log('sending wrist stop motion command'); 345 | sendCommandWrist({type:'wrist', action:'stop_motion'}); 346 | } 347 | 348 | function wristBendVelocity(deg_per_sec) { 349 | console.log('sending wrist bend velocity of ' + deg_per_sec + ' command'); 350 | sendCommandWrist({type:'wrist', action:'bend_velocity', angle:deg_per_sec}); 351 | } 352 | 353 | function wristAutoBend(angleDeg) { 354 | // attempt to bend the wrist by deltaAngle degrees 355 | //console.log('*** no wrist bend control exists yet ***'); 356 | console.log('sending auto wrist bend to ' + angleDeg + ' command'); 357 | sendCommandWrist({type:'wrist', action:'auto_bend', angle:angleDeg}); 358 | } 359 | 360 | function initFixedWrist() { 361 | // try to emulate a fixed wrist with gripper flat and bent down 45 degrees from horizontal 362 | console.log('sending init_fixed_wrist command'); 363 | sendCommandWrist({type:'wrist', action:'init_fixed_wrist'}); 364 | } 365 | 366 | function wristBend(deltaAngle) { 367 | // attempt to bend the wrist by deltaAngle degrees 368 | //console.log('*** no wrist bend control exists yet ***'); 369 | console.log('sending wrist bend command'); 370 | sendCommandWrist({type:'wrist', action:'bend', angle:deltaAngle}); 371 | } 372 | 373 | function wristRoll(deltaAngle) { 374 | // attempt to roll the wrist by deltaAngle degrees 375 | //console.log('*** no wrist roll control exists yet ***'); 376 | console.log('sending wrist roll command'); 377 | sendCommandWrist({type:'wrist', action:'roll', angle:deltaAngle}); 378 | } 379 | 380 | function gripperGoalAperture(goalWidthCm) { 381 | // attempt to change the gripper aperture 382 | console.log('sending gripper command'); 383 | sendCommandWrist({type:'gripper', action:'width', goal_aperture_cm:goalWidthCm}); 384 | } 385 | 386 | function gripperGoalAperture(goalWidthCm) { 387 | // attempt to change the gripper aperture 388 | console.log('sending gripper command'); 389 | sendCommandWrist({type:'gripper', action:'width', goal_aperture_cm:goalWidthCm}); 390 | } 391 | 392 | function gripperFullyClose() { 393 | console.log('sending fully close gripper command'); 394 | sendCommandWrist({type:'gripper', action:'fully_close'}); 395 | } 396 | 397 | function gripperHalfOpen() { 398 | console.log('sending half open gripper command'); 399 | sendCommandWrist({type:'gripper', action:'half_open'}); 400 | } 401 | 402 | function gripperFullyOpen() { 403 | console.log('sending fully open gripper command'); 404 | sendCommandWrist({type:'gripper', action:'fully_open'}); 405 | } 406 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var auth = require("../controllers/AuthController.js"); 4 | 5 | // restrict index for logged in user only 6 | router.get('/', auth.home); 7 | 8 | // route to register page 9 | router.get('/register', auth.register); 10 | 11 | // route for register action 12 | router.post('/register', auth.doRegister); 13 | 14 | // route to login page 15 | router.get('/login', auth.login); 16 | 17 | // route for login action 18 | router.post('/login', auth.doLogin); 19 | 20 | // route for logout action 21 | router.get('/logout', auth.logout); 22 | 23 | // route for robot directory 24 | router.get('/robot/:file', auth.robot); 25 | 26 | // route for operator directory 27 | router.get('/operator/:file', auth.operator); 28 | 29 | // route for shared directory 30 | router.get('/shared/:file', auth.shared); 31 | 32 | module.exports = router; 33 | -------------------------------------------------------------------------------- /shared/commands.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | function lookLeft() { 4 | var cmd = {type:"command", 5 | subtype:"head", 6 | name:"left", 7 | modifier:"medium"}; 8 | sendData(cmd); 9 | } 10 | 11 | function lookRight() { 12 | var cmd = {type:"command", 13 | subtype:"head", 14 | name:"right", 15 | modifier:"medium"}; 16 | sendData(cmd); 17 | } 18 | 19 | function lookUp() { 20 | var cmd = {type:"command", 21 | subtype:"head", 22 | name:"up", 23 | modifier:"medium"}; 24 | sendData(cmd); 25 | } 26 | 27 | function lookDown() { 28 | var cmd = {type:"command", 29 | subtype:"head", 30 | name:"down", 31 | modifier:"medium"}; 32 | sendData(cmd); 33 | } 34 | 35 | function moveForwardMedium() { 36 | var cmd = {type:"command", 37 | subtype:"drive", 38 | name:"forward", 39 | modifier:"medium"}; 40 | sendData(cmd); 41 | } 42 | 43 | function moveForwardSmall() { 44 | var cmd = {type:"command", 45 | subtype:"drive", 46 | name:"forward", 47 | modifier:"small"}; 48 | sendData(cmd); 49 | } 50 | 51 | function moveBackwardMedium() { 52 | var cmd = {type:"command", 53 | subtype:"drive", 54 | name:"backward", 55 | modifier:"medium"}; 56 | sendData(cmd); 57 | } 58 | 59 | function moveBackwardSmall() { 60 | var cmd = {type:"command", 61 | subtype:"drive", 62 | name:"backward", 63 | modifier:"small"}; 64 | sendData(cmd); 65 | } 66 | 67 | function turnLeftMedium() { 68 | var cmd = {type:"command", 69 | subtype:"drive", 70 | name:"turn_left", 71 | modifier:"medium"}; 72 | sendData(cmd); 73 | } 74 | 75 | function turnLeftSmall() { 76 | var cmd = {type:"command", 77 | subtype:"drive", 78 | name:"turn_left", 79 | modifier:"small"}; 80 | sendData(cmd); 81 | } 82 | 83 | function turnRightMedium() { 84 | var cmd = {type:"command", 85 | subtype:"drive", 86 | name:"turn_right", 87 | modifier:"medium"}; 88 | sendData(cmd); 89 | } 90 | 91 | function turnRightSmall() { 92 | var cmd = {type:"command", 93 | subtype:"drive", 94 | name:"turn_right", 95 | modifier:"small"}; 96 | sendData(cmd); 97 | } 98 | 99 | function liftUpMedium() { 100 | var cmd = {type:"command", 101 | subtype:"lift", 102 | name:"up", 103 | modifier:"medium"}; 104 | sendData(cmd); 105 | } 106 | 107 | function liftUpSmall() { 108 | var cmd = {type:"command", 109 | subtype:"lift", 110 | name:"up", 111 | modifier:"small"}; 112 | sendData(cmd); 113 | } 114 | 115 | function liftDownMedium() { 116 | var cmd = {type:"command", 117 | subtype:"lift", 118 | name:"down", 119 | modifier:"medium"}; 120 | sendData(cmd); 121 | } 122 | 123 | function liftDownSmall() { 124 | var cmd = {type:"command", 125 | subtype:"lift", 126 | name:"down", 127 | modifier:"small"}; 128 | sendData(cmd); 129 | } 130 | 131 | function armRetractMedium() { 132 | var cmd = {type:"command", 133 | subtype:"arm", 134 | name:"retract", 135 | modifier:"medium"}; 136 | sendData(cmd); 137 | } 138 | 139 | function armRetractSmall() { 140 | var cmd = {type:"command", 141 | subtype:"arm", 142 | name:"retract", 143 | modifier:"small"}; 144 | sendData(cmd); 145 | } 146 | 147 | function armExtendMedium() { 148 | var cmd = {type:"command", 149 | subtype:"arm", 150 | name:"extend", 151 | modifier:"medium"}; 152 | sendData(cmd); 153 | } 154 | 155 | function armExtendSmall() { 156 | var cmd = {type:"command", 157 | subtype:"arm", 158 | name:"extend", 159 | modifier:"small"}; 160 | sendData(cmd); 161 | } 162 | 163 | 164 | function wristVelocityBend(degPerSec) { 165 | var cmd = {type:"command", 166 | subtype:"wrist", 167 | name:"bend_velocity", 168 | modifier:degPerSec}; 169 | sendData(cmd); 170 | } 171 | 172 | function gripperSetGoal(goalWidthCm) { 173 | var cmd = {type:"command", 174 | subtype:"gripper", 175 | name:"set_goal", 176 | modifier:goalWidthCm}; 177 | sendData(cmd); 178 | } 179 | 180 | function gripperClose() { 181 | var cmd = {type:"command", 182 | subtype:"gripper", 183 | name:"close", 184 | modifier:"medium"}; 185 | sendData(cmd); 186 | } 187 | 188 | function gripperOpen() { 189 | var cmd = {type:"command", 190 | subtype:"gripper", 191 | name:"open", 192 | modifier:"medium"}; 193 | sendData(cmd); 194 | } 195 | 196 | function gripperCloseFull() { 197 | var cmd = {type:"command", 198 | subtype:"gripper", 199 | name:"fully_close", 200 | modifier:"medium"}; 201 | sendData(cmd); 202 | } 203 | 204 | function gripperOpenHalf() { 205 | var cmd = {type:"command", 206 | subtype:"gripper", 207 | name:"half_open", 208 | modifier:"medium"}; 209 | sendData(cmd); 210 | } 211 | 212 | function gripperOpenFull() { 213 | var cmd = {type:"command", 214 | subtype:"gripper", 215 | name:"fully_open", 216 | modifier:"medium"}; 217 | sendData(cmd); 218 | } 219 | 220 | function wristMotionStop() { 221 | var cmd = {type:"command", 222 | subtype:"wrist", 223 | name:"stop_all_motion", 224 | modifier:"medium"}; 225 | sendData(cmd); 226 | } 227 | 228 | function wristVelocityBend(deg_per_sec) { 229 | var cmd = {type:"command", 230 | subtype:"wrist", 231 | name:"bend_velocity", 232 | modifier:deg_per_sec}; 233 | sendData(cmd); 234 | } 235 | 236 | function wristBendDown() { 237 | var cmd = {type:"command", 238 | subtype:"wrist", 239 | name:"bend_down", 240 | modifier:"medium"}; 241 | sendData(cmd); 242 | } 243 | 244 | function wristBendUp() { 245 | var cmd = {type:"command", 246 | subtype:"wrist", 247 | name:"bend_up", 248 | modifier:"medium"}; 249 | sendData(cmd); 250 | } 251 | 252 | 253 | function wristIn() { 254 | var cmd = {type:"command", 255 | subtype:"wrist", 256 | name:"in", 257 | modifier:"medium"}; 258 | sendData(cmd); 259 | } 260 | 261 | function wristOut() { 262 | var cmd = {type:"command", 263 | subtype:"wrist", 264 | name:"out", 265 | modifier:"medium"}; 266 | sendData(cmd); 267 | } 268 | 269 | function wristBendAuto(ang_deg) { 270 | var cmd = {type:"command", 271 | subtype:"wrist", 272 | name:"auto_bend", 273 | modifier:ang_deg}; 274 | sendData(cmd); 275 | } 276 | 277 | function wristRollRight() { 278 | var cmd = {type:"command", 279 | subtype:"wrist", 280 | name:"roll_right", 281 | modifier:"medium"}; 282 | sendData(cmd); 283 | } 284 | 285 | function wristRollLeft() { 286 | var cmd = {type:"command", 287 | subtype:"wrist", 288 | name:"roll_left", 289 | modifier:"medium"}; 290 | sendData(cmd); 291 | } 292 | 293 | 294 | //var cameraToVideoMapping = {nav: 'big', arm: 'smallTop', hand: 'smallBot'}; 295 | var interfaceMode = 'nav'; 296 | var interfaceModifier = 'no_wrist'; 297 | 298 | 299 | function turnModeOn(modeKey) { 300 | console.log('turnModeOn: modeKey = ' + modeKey) 301 | var cmd; 302 | if(noWristOn === false) { 303 | cmd = {type:"command", 304 | subtype:"mode", 305 | name : modeKey, 306 | modifier:"none"}; 307 | interfaceModifier = 'none'; 308 | } else { 309 | cmd = {type:"command", 310 | subtype:"mode", 311 | name : modeKey, 312 | modifier:"no_wrist"}; 313 | interfaceModifier = 'no_wrist'; 314 | } 315 | interfaceMode = modeKey 316 | sendData(cmd) 317 | turnModeUiOn(modeKey) 318 | } 319 | 320 | modeKeys = ['nav', 'low_arm', 'high_arm', 'hand', 'look'] 321 | 322 | function createModeCommands() { 323 | modeCommands = {} 324 | for (var index in modeKeys) { 325 | var key = modeKeys[index] 326 | // function inside a function used so that commandKey will not 327 | // change when key changes. For example, without this, the 328 | // interface mode and robotModeOn commands use key = 'look' 329 | // (last mode) whenever a function is executed. 330 | modeCommands[key] = function(commandKey) { 331 | return function(modifier) { 332 | if(modifier === 'no_wrist') { 333 | interfaceModifier = 'no_wrist'; 334 | } else { 335 | if(modifier !== 'none') { 336 | console.log('ERROR: modeCommands modifier unrecognized = ', modifier); 337 | } 338 | interfaceModifier = 'none'; 339 | } 340 | console.log('mode: command received with interfaceModifier = ' + interfaceModifier + ' ...executing'); 341 | interfaceMode = commandKey 342 | robotModeOn(commandKey) 343 | } 344 | } (key) 345 | } 346 | return modeCommands 347 | } 348 | 349 | var modeCommands = createModeCommands() 350 | 351 | 352 | function executeCommandBySize(size, command, smallCommandArgs, mediumCommandArgs) { 353 | switch(size) { 354 | case "small": 355 | command(...smallCommandArgs); 356 | break; 357 | case "medium": 358 | command(...mediumCommandArgs); 359 | break; 360 | default: 361 | console.log('executeCommandBySize: size unrecognized, so doing nothing'); 362 | console.log('executeCommandBySize: size = ' + size); 363 | } 364 | } 365 | 366 | 367 | var headCommands = { 368 | "up": function(size) { 369 | console.log('head: up command received...executing'); 370 | headTilt(0.1) 371 | }, 372 | "down": function(size) { 373 | console.log('head: down command received...executing'); 374 | headTilt(-0.1) 375 | }, 376 | "left": function(size) { 377 | console.log('head: left command received...executing'); 378 | headPan(0.1) 379 | }, 380 | "right": function(size) { 381 | console.log('head: right command received...executing'); 382 | headPan(-0.1) 383 | } 384 | } 385 | 386 | var driveCommands = { 387 | "forward": function(size) { 388 | console.log('drive: forward command received...executing'); 389 | 390 | // executeCommandBySize(size, baseTranslate, 391 | // [-1.0, 10.0], // -1cm at 10 cm/s 392 | // [-10.0, 40.0]); // -10cm at 40 cm/s 393 | 394 | executeCommandBySize(size, baseTranslate, 395 | [-10.0, 200.0], //dist (mm), speed (mm/s) 396 | [-100.0, 200.0]); //dist (mm), speed (mm/s) 397 | 398 | }, 399 | "backward": function(size) { 400 | console.log('drive: backward command received...executing'); 401 | 402 | // executeCommandBySize(size, baseTranslate, 403 | // [1.0, 10.0], // 1cm at 10 cm/s 404 | // [10.0, 40.0]); // 10cm at 40 cm/s 405 | 406 | executeCommandBySize(size, baseTranslate, 407 | [10.0, 200.0], //dist (mm), speed (mm/s) 408 | [100.0, 200.0]); //dist (mm), speed (mm/s) 409 | }, 410 | "turn_right": function(size) { 411 | console.log('drive: turn_right command received...executing'); 412 | 413 | // executeCommandBySize(size, baseTurn, 414 | // [1.0, 10.0], // 1deg at 10 cm/s wheel velocity 415 | // [10.0, 20.0]); // 10deg at 20 cm/s wheel velocity 416 | 417 | executeCommandBySize(size, baseTurn, 418 | [1.0, 300.0], // angle (deg), angular speed (deg/s) 419 | [10.0, 300.0]); // angle (deg), angular speed (deg/s) 420 | 421 | }, 422 | "turn_left": function(size) { 423 | console.log('drive: turn_left command received...executing'); 424 | // executeCommandBySize(size, baseTurn, 425 | // [-1.0, 10.0], // -1deg at 10 cm/s wheel velocity 426 | // [-10.0, 20.0]); // -10deg at 20 cm/s wheel velocity 427 | 428 | executeCommandBySize(size, baseTurn, 429 | [-1.0, 300.0], // angle (deg), angular speed (deg/s) 430 | [-10.0, 300.0]); // angle (deg), angular speed (deg/s) 431 | } 432 | } 433 | 434 | var liftCommands = { 435 | "up": function(size) { 436 | console.log('lift: up command received...executing'); 437 | 438 | // executeCommandBySize(size, lift, 439 | // [1.0, 10.0], // 1cm at 10 cm/s 440 | // [5.0, 20.0]); // 5cm at 30 cm/s 441 | 442 | executeCommandBySize(size, liftMove, 443 | [10.0, -1], // dist (mm), timeout (s) 444 | [100.0, -1]); // dist (mm), timeout (s) 445 | 446 | 447 | }, 448 | "down": function(size) { 449 | console.log('lift: down command received...executing'); 450 | 451 | // executeCommandBySize(size, lift, 452 | // [-1.0, 10.0], // -1cm at 10 cm/s 453 | // [-5.0, 20.0]); // -5cm at 30 cm/s 454 | 455 | executeCommandBySize(size, liftMove, 456 | [-10.0, -1], // dist (mm), timeout (s) 457 | [-100.0, -1]); // dist (mm), timeout (s) 458 | 459 | } 460 | } 461 | 462 | var armCommands = { 463 | "extend": function(size) { 464 | console.log('arm: extend command received...executing'); 465 | // executeCommandBySize(size, arm, 466 | // [1.0, 10.0], // 1cm at 10 cm/s 467 | // [5.0, 20.0]); // 5cm at 20 cm/s 468 | 469 | executeCommandBySize(size, armMove, 470 | [10.0, -1], // dist (mm), timeout (s) 471 | [100.0, -1]); // dist (mm), timeout (s) 472 | }, 473 | "retract": function(size) { 474 | console.log('arm: retract command received...executing'); 475 | // executeCommandBySize(size, arm, 476 | // [-1.0, 10.0], // -1cm at 10 cm/s 477 | // [-5.0, 20.0]); // -5cm at 20 cm/s 478 | 479 | executeCommandBySize(size, armMove, 480 | [-10.0, -1], // dist (mm), timeout (s) 481 | [-100.0, -1]); // dist (mm), timeout (s) 482 | 483 | } 484 | } 485 | 486 | 487 | var wristCommands = { 488 | "in": function(nothing) { 489 | console.log('wrist: wrist_in command received...executing'); 490 | wristMove(0.1) 491 | }, 492 | "out": function(nothing) { 493 | console.log('wrist: wrist_out command received...executing'); 494 | wristMove(-0.1) 495 | }, 496 | "stop_all_motion": function(nothing) { 497 | console.log('wrist: stop all motion command received...executing'); 498 | wristStopMotion(); 499 | }, 500 | "bend_velocity": function(deg_per_sec) { 501 | console.log('wrist: bend velocity of ' + deg_per_sec + ' command received...executing'); 502 | wristBendVelocity(deg_per_sec); 503 | }, 504 | "auto_bend": function(ang_deg) { 505 | console.log('wrist: auto bend to ' + ang_deg + ' command received...executing'); 506 | wristAutoBend(ang_deg); 507 | }, 508 | "init_fixed_wrist": function(size) { 509 | console.log('wrist: init_fixed_wrist command received...executing'); 510 | initFixedWrist(); 511 | }, 512 | "bend_up": function(size) { 513 | console.log('wrist: bend_up command received...executing'); 514 | wristBend(5.0); // attempt to bed the wrist upward by 5 degrees 515 | }, 516 | "bend_down": function(size) { 517 | console.log('wrist: bend_down command received...executing'); 518 | wristBend(-5.0); // attempt to bed the wrist downward by 5 degrees 519 | }, 520 | "roll_left": function(size) { 521 | console.log('wrist: roll_left command received...executing'); 522 | wristRoll(-5.0); // attempt to roll the wrist to the left (clockwise) by 5 degrees 523 | }, 524 | "roll_right": function(size) { 525 | console.log('wrist: roll_right command received...executing'); 526 | wristRoll(5.0); // attempt to roll the wrist to the right (counterclockwise) by 5 degrees 527 | } 528 | } 529 | 530 | var gripperCommands = { 531 | "set_goal": function(goalWidthCm) { 532 | console.log('gripper: set_goal command received...executing'); 533 | gripperGoalAperture(goalWidthCm); 534 | }, 535 | "open": function(size) { 536 | console.log('gripper: open command received...executing'); 537 | gripperDeltaAperture(1.0); // attempt to increase the gripper aperature width by one unit 538 | }, 539 | "close": function(size) { 540 | console.log('gripper: close command received...executing'); 541 | gripperDeltaAperture(-1.0); // attempt to decrease the gripper aperature width by one unit 542 | }, 543 | "fully_close": function(size) { 544 | console.log('gripper: fully close command received...executing'); 545 | gripperFullyClose(); 546 | }, 547 | "half_open": function(size) { 548 | console.log('gripper: half open command received...executing'); 549 | gripperHalfOpen(); 550 | }, 551 | "fully_open": function(size) { 552 | console.log('gripper: fully open command received...executing'); 553 | gripperFullyOpen(); 554 | } 555 | } 556 | 557 | var commands = { 558 | "drive": driveCommands, 559 | "lift": liftCommands, 560 | "arm": armCommands, 561 | "wrist": wristCommands, 562 | "gripper": gripperCommands, 563 | "head": headCommands, 564 | "mode": modeCommands 565 | } 566 | 567 | function executeCommand(obj) { 568 | if ("type" in obj) { 569 | if (obj.type === "command") { 570 | commands[obj.subtype][obj.name](obj.modifier); 571 | return; 572 | } 573 | } 574 | console.log('ERROR: the argument to executeCommand was not a proper command object: ' + obj); 575 | } 576 | -------------------------------------------------------------------------------- /shared/send_recv_av.js: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // 4 | // initial code retrieved from the following link on 9/13/2017 5 | // https://github.com/googlecodelabs/webrtc-web/blob/master/step-05/js/main.js 6 | // 7 | // initial code licensed with Apache License 2.0 8 | // 9 | 10 | 'use strict'; 11 | 12 | var objects_received = []; 13 | var objects_sent = []; 14 | 15 | var isChannelReady = false; 16 | var isStarted = false; 17 | var localStream; 18 | var pc; 19 | var remoteStream; 20 | var displayStream; 21 | var turnReady; 22 | 23 | var requestedRobot; 24 | 25 | var dataChannel; 26 | var dataConstraint; 27 | 28 | // Free STUN server offered by Google 29 | var pcConfig = { 30 | 'iceServers': [{ 31 | 'urls': 'stun:stun.l.google.com:19302' 32 | }] 33 | }; 34 | 35 | // Prototype STUN and TURN server used internally by Hello Robot 36 | // 37 | // var pcConfig = { 38 | // iceServers: [ 39 | // { urls: "stun:pilot.hello-robot.io:5349", 40 | // username: "r1", 41 | // credential: "kWJuyF5i2jh0"}, 42 | // { urls: "turn:pilot.hello-robot.io:5349", 43 | // username: "r1", 44 | // credential: "kWJuyF5i2jh0"} 45 | // ] 46 | // }; 47 | 48 | //////////////////////////////////////////////////////////// 49 | // safelyParseJSON code copied from 50 | // https://stackoverflow.com/questions/29797946/handling-bad-json-parse-in-node-safely 51 | // on August 18, 2017 52 | function safelyParseJSON (json) { 53 | // This function cannot be optimised, it's best to 54 | // keep it small! 55 | var parsed; 56 | 57 | try { 58 | parsed = JSON.parse(json); 59 | } catch (e) { 60 | // Oh well, but whatever... 61 | } 62 | 63 | return parsed; // Could be undefined! 64 | } 65 | //////////////////////////////////////////////////////////// 66 | 67 | 68 | ///////////////////////////////////////////// 69 | 70 | var socket = io.connect(); 71 | 72 | socket.on('created', function(room) { 73 | console.log('Created room ' + room); 74 | }); 75 | 76 | socket.on('full', function(room) { 77 | console.log('Room ' + room + ' is full'); 78 | }); 79 | 80 | socket.on('join', function (room){ 81 | console.log('Another peer made a request to join room ' + room); 82 | console.log('This peer is the ' + peer_name + '!'); 83 | isChannelReady = true; 84 | maybeStart(); 85 | }); 86 | 87 | socket.on('joined', function(room) { 88 | console.log('joined: ' + room); 89 | isChannelReady = true; 90 | }); 91 | 92 | //////////////////////////////////////////////// 93 | 94 | if (peer_name === 'OPERATOR') { 95 | var robotToControlSelect = document.querySelector('select#robotToControl'); 96 | robotToControlSelect.onchange = connectToRobot; 97 | } 98 | 99 | function availableRobots() { 100 | console.log('asking server what robots are available'); 101 | socket.emit('what robots are available'); 102 | } 103 | 104 | function connectToRobot() { 105 | var robot = robotToControlSelect.value; 106 | if(robot === 'no robot connected') { 107 | console.log('no robot selected'); 108 | console.log('attempt to hangup'); 109 | hangup(); 110 | } else { 111 | console.log('attempting to connect to robot ='); 112 | console.log(robot); 113 | requestedRobot = robot; 114 | socket.emit('join', robot); 115 | } 116 | } 117 | 118 | socket.on('available robots', function(available_robots) { 119 | console.log('received response from the server with available robots'); 120 | console.log('available_robots ='); 121 | console.log(available_robots); 122 | 123 | // remove any old options 124 | while (robotToControlSelect.firstChild) { 125 | robotToControlSelect.removeChild(robotToControlSelect.firstChild); 126 | } 127 | 128 | var option = document.createElement('option'); 129 | option.value = 'no robot connected'; 130 | option.text = 'no robot connected'; 131 | robotToControlSelect.appendChild(option); 132 | 133 | // add all new options 134 | for (let r of available_robots) { 135 | option = document.createElement('option'); 136 | option.value = r; 137 | option.text = r; 138 | robotToControlSelect.appendChild(option); 139 | } 140 | }); 141 | 142 | /////////////////////////////////////////////////// 143 | 144 | function sendWebRTCMessage(message) { 145 | console.log('Client sending WebRTC message: ', message); 146 | socket.emit('webrtc message', message); 147 | } 148 | 149 | // This client receives a message 150 | socket.on('webrtc message', function(message) { 151 | console.log('Client received message:', message); 152 | if (message === 'got user media') { 153 | maybeStart(); 154 | } else if (message.type === 'offer') { 155 | if ((peer_name === 'ROBOT') && !isStarted) { 156 | maybeStart(); 157 | } else if ((peer_name === 'OPERATOR') && !isStarted) { 158 | maybeStart(); 159 | } 160 | pc.setRemoteDescription(new RTCSessionDescription(message)); 161 | doAnswer(); 162 | } else if (message.type === 'answer' && isStarted) { 163 | pc.setRemoteDescription(new RTCSessionDescription(message)); 164 | } else if (message.type === 'candidate' && isStarted) { 165 | var candidate = new RTCIceCandidate({ 166 | sdpMLineIndex: message.label, 167 | candidate: message.candidate 168 | }); 169 | pc.addIceCandidate(candidate); 170 | } else if (message === 'bye' && isStarted) { 171 | handleRemoteHangup(); 172 | } 173 | }); 174 | 175 | //////////////////////////////////////////////////// 176 | 177 | 178 | var remoteVideo = document.querySelector('#remoteVideo'); 179 | 180 | 181 | function maybeStart() { 182 | console.log('>>>>>>> maybeStart() ', isStarted, localStream, isChannelReady); 183 | if (!isStarted && isChannelReady) { 184 | console.log('>>>>>> creating peer connection'); 185 | createPeerConnection(); 186 | if (localStream != undefined) { 187 | console.log('adding local media stream to peer connection'); 188 | pc.addStream(localStream); 189 | } 190 | console.log('This peer is the ' + peer_name + '.'); 191 | if (peer_name === 'ROBOT') { 192 | dataConstraint = null; 193 | dataChannel = pc.createDataChannel('DataChannel', dataConstraint); 194 | console.log('Creating data channel.'); 195 | dataChannel.onmessage = onReceiveMessageCallback; 196 | dataChannel.onopen = onDataChannelStateChange; 197 | dataChannel.onclose = onDataChannelStateChange; 198 | doCall(); 199 | } 200 | isStarted = true; 201 | } 202 | } 203 | 204 | window.onbeforeunload = function() { 205 | sendWebRTCMessage('bye'); 206 | }; 207 | 208 | ///////////////////////////////////////////////////////// 209 | 210 | function createPeerConnection() { 211 | try { 212 | pc = new RTCPeerConnection(pcConfig); 213 | pc.onicecandidate = handleIceCandidate; 214 | pc.ondatachannel = dataChannelCallback; 215 | pc.onopen = function() { 216 | console.log('RTC channel opened.'); 217 | }; 218 | pc.onaddstream = handleRemoteStreamAdded; 219 | pc.onremovestream = handleRemoteStreamRemoved; 220 | console.log('Created RTCPeerConnnection'); 221 | } catch (e) { 222 | console.log('Failed to create PeerConnection, exception: ' + e.message); 223 | alert('Cannot create RTCPeerConnection object.'); 224 | return; 225 | } 226 | } 227 | 228 | function handleIceCandidate(event) { 229 | console.log('icecandidate event: ', event); 230 | if (event.candidate) { 231 | sendWebRTCMessage({ 232 | type: 'candidate', 233 | label: event.candidate.sdpMLineIndex, 234 | id: event.candidate.sdpMid, 235 | candidate: event.candidate.candidate 236 | }); 237 | } else { 238 | console.log('End of candidates.'); 239 | } 240 | } 241 | 242 | function handleRemoteStreamAdded(event) { 243 | console.log('Remote stream added.'); 244 | if (peer_name === 'OPERATOR') { 245 | console.log('OPERATOR: starting to display remote stream'); 246 | remoteVideo.srcObject = event.stream; 247 | } else if (peer_name === 'ROBOT') { 248 | console.log('ROBOT: adding remote audio to display'); 249 | // remove audio tracks from displayStream 250 | for (let a of displayStream.getAudioTracks()) { 251 | displayStream.removeTrack(a); 252 | } 253 | var remoteaudio = event.stream.getAudioTracks()[0]; // get remotely captured audio track 254 | displayStream.addTrack(remoteaudio); // add remotely captured audio track to the local display 255 | videoDisplayElement.srcObject = displayStream; 256 | } 257 | 258 | remoteStream = event.stream; 259 | } 260 | 261 | function handleCreateOfferError(event) { 262 | console.log('createOffer() error: ', event); 263 | } 264 | 265 | function doCall() { 266 | console.log('Sending offer to peer'); 267 | pc.createOffer(setLocalAndSendMessage, handleCreateOfferError); 268 | } 269 | 270 | function doAnswer() { 271 | console.log('Sending answer to peer.'); 272 | pc.createAnswer().then( 273 | setLocalAndSendMessage, 274 | onCreateSessionDescriptionError 275 | ); 276 | } 277 | 278 | function setLocalAndSendMessage(sessionDescription) { 279 | pc.setLocalDescription(sessionDescription); 280 | console.log('setLocalAndSendMessage sending message', sessionDescription); 281 | sendWebRTCMessage(sessionDescription); 282 | } 283 | 284 | function onCreateSessionDescriptionError(error) { 285 | console.log('Failed to create session description: ' + error.toString()); 286 | } 287 | 288 | function handleRemoteStreamRemoved(event) { 289 | console.log('Remote stream removed. Event: ', event); 290 | } 291 | 292 | function hangup() { 293 | console.log('Hanging up.'); 294 | stop(); 295 | sendWebRTCMessage('bye'); 296 | } 297 | 298 | function handleRemoteHangup() { 299 | console.log('Session terminated.'); 300 | stop(); 301 | } 302 | 303 | function stop() { 304 | isStarted = false; 305 | // isAudioMuted = false; 306 | // isVideoMuted = false; 307 | pc.close(); 308 | pc = null; 309 | } 310 | 311 | //////////////////////////////////////////////////////////// 312 | // RTCDataChannel 313 | // on Sept. 15, 2017 copied initial code from 314 | // https://github.com/googlecodelabs/webrtc-web/blob/master/step-03/js/main.js 315 | // initial code licensed with Apache License 2.0 316 | //////////////////////////////////////////////////////////// 317 | 318 | function sendData(obj) { 319 | if (isStarted && (dataChannel.readyState === 'open')) { 320 | var data = JSON.stringify(obj); 321 | switch(obj.type) { 322 | case 'command': 323 | if (recordOn && addToCommandLog) { 324 | addToCommandLog(obj); 325 | } 326 | objects_sent.push(obj); 327 | dataChannel.send(data); 328 | console.log('Sent Data: ' + data); 329 | break; 330 | case 'sensor': 331 | // unless being recorded, don't store or write information to the console due to high 332 | // frequency and large amount of data (analogous to audio and video). 333 | dataChannel.send(data); 334 | break; 335 | default: 336 | console.log('*************************************************************'); 337 | console.log('REQUEST TO SEND UNRECOGNIZED MESSAGE TYPE, SO NOTHING SENT...'); 338 | console.log('Received Data: ' + event.data); 339 | console.log('Received Object: ' + obj); 340 | console.log('*************************************************************'); 341 | } 342 | } 343 | } 344 | 345 | function closeDataChannels() { 346 | console.log('Closing data channels.'); 347 | dataChannel.close(); 348 | console.log('Closed data channel with label: ' + dataChannel.label); 349 | console.log('Closed peer connections.'); 350 | } 351 | 352 | function dataChannelCallback(event) { 353 | console.log('Data channel callback executed.'); 354 | dataChannel = event.channel; 355 | dataChannel.onmessage = onReceiveMessageCallback; 356 | dataChannel.onopen = onDataChannelStateChange; 357 | dataChannel.onclose = onDataChannelStateChange; 358 | } 359 | 360 | function onReceiveMessageCallback(event) { 361 | var obj = safelyParseJSON(event.data); 362 | switch(obj.type) { 363 | case 'command': 364 | objects_received.push(obj); 365 | console.log('Received Data: ' + event.data); 366 | //console.log('Received Object: ' + obj); 367 | executeCommand(obj); 368 | break; 369 | case 'sensor': 370 | // unless being recorded, don't store or write information to the console due to high 371 | // frequency and large amount of data (analogous to audio and video). 372 | if (recordOn && addToSensorLog) { 373 | addToSensorLog(obj); 374 | } 375 | receiveSensorReading(obj); 376 | break; 377 | default: 378 | console.log('*******************************************************'); 379 | console.log('UNRECOGNIZED MESSAGE TYPE RECEIVED, SO DOING NOTHING...'); 380 | console.log('Received Data: ' + event.data); 381 | console.log('Received Object: ' + obj); 382 | console.log('*******************************************************'); 383 | } 384 | } 385 | 386 | function onDataChannelStateChange() { 387 | var readyState = dataChannel.readyState; 388 | console.log('Data channel state is: ' + readyState); 389 | if (readyState === 'open') { 390 | runOnOpenDataChannel(); 391 | } else { 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /shared/sensors.js: -------------------------------------------------------------------------------- 1 | 2 | var driveSensors = { 3 | } 4 | 5 | var liftSensors = { 6 | "lift_effort": function(value) { 7 | // adjust for the effort needed to hold the arm in place 8 | // against gravity 9 | var adjusted_value = value - 53.88; 10 | var armUpRegion1 = document.querySelector('#low_arm_up_region'); 11 | var armUpRegion2 = document.querySelector('#high_arm_up_region'); 12 | 13 | var armDownRegion1 = document.querySelector('#low_arm_down_region'); 14 | var armDownRegion2 = document.querySelector('#high_arm_down_region'); 15 | 16 | var redRegion1; 17 | var redRegion2; 18 | 19 | var nothingRegion1; 20 | var nothingRegion2; 21 | 22 | if (adjusted_value > 0.0) { 23 | redRegion1 = armUpRegion1; 24 | redRegion2 = armUpRegion2; 25 | nothingRegion1 = armDownRegion1; 26 | nothingRegion2 = armDownRegion2; 27 | } else { 28 | redRegion1 = armDownRegion1; 29 | redRegion2 = armDownRegion2; 30 | nothingRegion1 = armUpRegion1; 31 | nothingRegion2 = armUpRegion2; 32 | } 33 | redRegion1.setAttribute('fill', 'red'); 34 | redRegion2.setAttribute('fill', 'red'); 35 | 36 | // make the torque positive and multiply it by a factor to 37 | // make sure the video will always be visible even with 38 | var redOpacity = Math.abs(adjusted_value) * 0.005; 39 | 40 | redRegion1.setAttribute('fill-opacity', redOpacity); 41 | redRegion2.setAttribute('fill-opacity', redOpacity); 42 | 43 | nothingRegion1.setAttribute('fill-opacity', 0.0); 44 | nothingRegion2.setAttribute('fill-opacity', 0.0); 45 | } 46 | } 47 | 48 | 49 | var armSensors = { 50 | "arm_effort": function(value) { 51 | var armExtendRegion1 = document.querySelector('#low_arm_extend_region'); 52 | var armExtendRegion2 = document.querySelector('#high_arm_extend_region'); 53 | 54 | var armRetractRegion1 = document.querySelector('#low_arm_retract_region'); 55 | var armRetractRegion2 = document.querySelector('#high_arm_retract_region'); 56 | 57 | var redRegion1; 58 | var redRegion2; 59 | 60 | var nothingRegion1; 61 | var nothingRegion2; 62 | 63 | if (value > 0.0) { 64 | redRegion1 = armExtendRegion1; 65 | redRegion2 = armExtendRegion2; 66 | nothingRegion1 = armRetractRegion1; 67 | nothingRegion2 = armRetractRegion2; 68 | } else { 69 | redRegion1 = armRetractRegion1; 70 | redRegion2 = armRetractRegion2; 71 | nothingRegion1 = armExtendRegion1; 72 | nothingRegion2 = armExtendRegion2; 73 | } 74 | redRegion1.setAttribute('fill', 'red'); 75 | redRegion2.setAttribute('fill', 'red'); 76 | 77 | // make the torque positive and multiply it by a factor to 78 | // make sure the video will always be visible even with 79 | var redOpacity = Math.abs(value) * 0.005; 80 | 81 | redRegion1.setAttribute('fill-opacity', redOpacity); 82 | redRegion2.setAttribute('fill-opacity', redOpacity); 83 | 84 | nothingRegion1.setAttribute('fill-opacity', 0.0); 85 | nothingRegion2.setAttribute('fill-opacity', 0.0); 86 | } 87 | } 88 | 89 | var wristSensors = { 90 | "yaw_torque": function(value) { 91 | var yawInRegion = document.querySelector('#hand_in_region'); 92 | var yawOutRegion = document.querySelector('#hand_out_region'); 93 | var redRegion; 94 | var nothingRegion; 95 | if (value > 0.0) { 96 | redRegion = yawOutRegion; 97 | nothingRegion = yawInRegion; 98 | } else { 99 | redRegion = yawInRegion; 100 | nothingRegion = yawOutRegion; 101 | } 102 | redRegion.setAttribute('fill', 'red'); 103 | // make the torque positive and multiply it by a factor to 104 | // make sure the video will always be visible even with 105 | var redOpacity = Math.abs(value) * 0.005; 106 | redRegion.setAttribute('fill-opacity', redOpacity); 107 | nothingRegion.setAttribute('fill-opacity', 0.0); 108 | }, 109 | "bend_torque": function(value) { 110 | var bendUpRegion = document.querySelector('#wrist_bend_up_region'); 111 | var bendDownRegion = document.querySelector('#wrist_bend_down_region'); 112 | var redRegion; 113 | var nothingRegion; 114 | if (value > 0.0) { 115 | redRegion = bendUpRegion; 116 | nothingRegion = bendDownRegion; 117 | } else { 118 | redRegion = bendDownRegion; 119 | nothingRegion = bendUpRegion; 120 | } 121 | redRegion.setAttribute('fill', 'red'); 122 | // make the torque positive and multiply it by a factor to 123 | // make sure the video will always be visible even with 124 | var redOpacity = Math.abs(value) * 0.8; 125 | redRegion.setAttribute('fill-opacity', redOpacity); 126 | nothingRegion.setAttribute('fill-opacity', 0.0); 127 | }, 128 | "roll_torque": function(value) { 129 | var rollLeftRegion = document.querySelector('#wrist_roll_left_region'); 130 | var rollRightRegion = document.querySelector('#wrist_roll_right_region'); 131 | var redRegion; 132 | var nothingRegion; 133 | if (value > 0.0) { 134 | redRegion = rollLeftRegion; 135 | nothingRegion = rollRightRegion; 136 | } else { 137 | redRegion = rollRightRegion; 138 | nothingRegion = rollLeftRegion; 139 | } 140 | redRegion.setAttribute('fill', 'red'); 141 | // make the torque positive and multiply it by a factor to 142 | // make sure the video will always be visible even with 143 | var redOpacity = Math.abs(value) * 0.8; 144 | redRegion.setAttribute('fill-opacity', redOpacity); 145 | nothingRegion.setAttribute('fill-opacity', 0.0); 146 | } 147 | } 148 | 149 | var gripperSensors = { 150 | "gripper_torque": function(value) { 151 | var handCloseRegion = document.querySelector('#hand_close_region'); 152 | var handOpenRegion = document.querySelector('#hand_open_region'); 153 | var redRegion; 154 | var nothingRegion; 155 | if (value > 0.0) { 156 | redRegion = handOpenRegion; 157 | nothingRegion = handCloseRegion; 158 | } else { 159 | redRegion = handCloseRegion; 160 | nothingRegion = handOpenRegion; 161 | } 162 | redRegion.setAttribute('fill', 'red'); 163 | // make the torque positive and multiply it by a factor to 164 | // make sure the video will always be visible even with 165 | var redOpacity = Math.abs(value) * 0.005; 166 | redRegion.setAttribute('fill-opacity', redOpacity); 167 | nothingRegion.setAttribute('fill-opacity', 0.0); 168 | } 169 | } 170 | 171 | var sensors = { 172 | "drive": driveSensors, 173 | "lift": liftSensors, 174 | "arm": armSensors, 175 | "wrist": wristSensors, 176 | "gripper": gripperSensors 177 | } 178 | 179 | function receiveSensorReading(obj) { 180 | if ("type" in obj) { 181 | if (obj.type === "sensor") { 182 | sensors[obj.subtype][obj.name](obj.value); 183 | return; 184 | } 185 | } 186 | 187 | console.log('ERROR: the argument to receiveSensorReading was not a proper command object: ' + obj); 188 | } 189 | -------------------------------------------------------------------------------- /shared/video_dimensions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | function generateVideoDimensions() { 5 | 6 | //var iw = 640; 7 | //var ih = 480; 8 | 9 | // D435i 1280x720 for now. Could be 1920 x 1080 if launch file 10 | // were changed. The camera is rotated -90 on our robots. For 11 | // efficiency for now render as small images for video transport 12 | // 360x640. 13 | var iw = 640; 14 | var ih = 360; 15 | var cameraFpsIdeal = 15.0; 16 | var ix = (iw - ih)/2.0; 17 | var oneUnit = ih/2.0; 18 | var dExtra = iw - (3.0*oneUnit); 19 | var aspectRatio = (oneUnit + (dExtra/2.0))/oneUnit; 20 | 21 | var bigDim = {sx: ix - (dExtra/4.0), 22 | sy: 0, 23 | sw: ih + (dExtra/2.0), 24 | sh: ih, 25 | dx: 0, 26 | dy: 0, 27 | dw: ih + (dExtra/2.0), 28 | dh: ih}; 29 | 30 | var smallTopDim = {sx: (iw - (aspectRatio * ih))/2.0, 31 | sy: 0, 32 | sw: aspectRatio * ih, 33 | sh: ih, 34 | dx: ih + (dExtra/2.0), 35 | dy: 0, 36 | dw: (ih/2.0) + (dExtra/2.0), 37 | dh: ih/2.0}; 38 | 39 | var smallBotDim = {sx: (iw - (aspectRatio * ih))/2.0, 40 | sy: 0, 41 | sw: aspectRatio * ih, 42 | sh: ih, 43 | dx: ih + (dExtra/2.0), 44 | dy: ih/2.0, 45 | dw: (ih/2.0) + (dExtra/2.0), 46 | dh: ih/2.0}; 47 | 48 | var smallBotDimNoWrist = {sx: (iw - (aspectRatio * ih))/2.0, 49 | sy: 0, 50 | sw: aspectRatio * ih, 51 | sh: ih, 52 | dx: ih + (dExtra/2.0), 53 | dy: ih/4.0, 54 | dw: (ih/2.0) + (dExtra/2.0), 55 | dh: ih/2.0}; 56 | 57 | var smallBotDimZoom = {sx: ((1.0/5.0) * (aspectRatio * ih)) + ((iw - (aspectRatio * ih))/2.0), 58 | sy: 0, 59 | sw: (2.0/3.0) * (aspectRatio * ih), 60 | sh: (2.0/3.0) * ih, 61 | dx: ih + (dExtra/2.0), 62 | dy: ih/2.0, 63 | dw: (ih/2.0) + (dExtra/2.0), 64 | dh: ih/2.0}; 65 | 66 | var smallBotDimZoomNoWrist = {sx: ((1.0/5.0) * (aspectRatio * ih)) + ((iw - (aspectRatio * ih))/2.0), 67 | sy: 0, 68 | sw: (2.0/3.0) * (aspectRatio * ih), 69 | sh: (2.0/3.0) * ih, 70 | dx: ih + (dExtra/2.0), 71 | dy: ih/4.0, 72 | dw: (ih/2.0) + (dExtra/2.0), 73 | dh: ih/2.0}; 74 | 75 | return {w:iw, h:ih, cameraFpsIdeal:cameraFpsIdeal, big: bigDim, smallTop: smallTopDim, smallBot: smallBotDim, smallBotNoWrist: smallBotDimNoWrist, smallBotZoom: smallBotDimZoom, smallBotZoomNoWrist: smallBotDimZoomNoWrist}; 76 | } 77 | 78 | var videoDimensions = generateVideoDimensions(); 79 | -------------------------------------------------------------------------------- /signaling_sockets.js: -------------------------------------------------------------------------------- 1 | 2 | // Some of this code may have been derived from code featured in the following article: 3 | // https://www.html5rocks.com/en/tutorials/webrtc/infrastructure/ 4 | 5 | function createSignalingSocket(io) { 6 | 7 | var numClients = 0; 8 | var connected_robots = new Set(); 9 | var available_robots = new Set(); 10 | var namespace = '/'; 11 | 12 | function sendAvailableRobotsUpdate(socket) { 13 | // let operators know about available robots 14 | console.log('letting operators know about available robots'); 15 | console.log('available_robots ='); 16 | console.log(available_robots); 17 | var robots = Array.from(available_robots.values()); 18 | socket.to('operators').emit('available robots', robots); 19 | } 20 | 21 | io.on('connection', function(socket){ 22 | //////////////// 23 | // see 24 | // https://www.codementor.io/tips/0217388244/sharing-passport-js-sessions-with-both-express-and-socket-io 25 | // for more information about socket.io using passport middleware 26 | console.log('new socket.io connection'); 27 | console.log('socket.handshake = '); 28 | console.log(socket.handshake); 29 | 30 | var user = socket.request.user; 31 | var role = user.role; 32 | var robot_operator_room = 'none'; 33 | 34 | if(role === 'robot') { 35 | 36 | var robot_name = user.username; 37 | var room = robot_name; // use the robot's name as the robot's room identifier 38 | 39 | console.log('A ROBOT HAS CONNECTED'); 40 | console.log('intended room name = ' + room); 41 | 42 | // If the robot's room already exists, disconnect all 43 | // sockets in the room. For example, this will disconnect 44 | // operators that were connected to the robot in a 45 | // previous session. 46 | 47 | io.of(namespace).in(room).disconnectSockets(true) 48 | 49 | // Add this robot's socket to the following two rooms: 50 | // 1) a new room named after the robot used to pair with an operator 51 | // 2) a room named "robots" to which all robots are added 52 | socket.join([room, 'robots']); 53 | robot_operator_room = room; 54 | console.log('adding robot to the "robots" room'); 55 | console.log('creating room for the robot and having it join the room'); 56 | connected_robots.add(robot_name); 57 | available_robots.add(robot_name); 58 | // let operators know about the new robot 59 | sendAvailableRobotsUpdate(socket); 60 | // io.to(room).emit('a room for the robot has been created, and the robot is in it'); // broadcast to everyone in the room 61 | 62 | console.log('connected robots = ' + Array.from(connected_robots).join(' ')) 63 | console.log('available robots = ' + Array.from(available_robots).join(' ')) 64 | 65 | } else { 66 | if(role === 'operator') { 67 | console.log('AN OPERATOR HAS CONNECTED'); 68 | // create the robot operator pairing room and add to the robots room 69 | socket.join('operators', () => { 70 | console.log('adding operator to the "operators" room'); 71 | }); 72 | 73 | // let operator know about available robots 74 | 75 | var robots = Array.from(connected_robots.values()); 76 | console.log('available_robots ='); 77 | console.log(available_robots); 78 | var robots = Array.from(available_robots.values()); 79 | socket.emit('available robots', robots); 80 | } 81 | } 82 | 83 | // https://github.com/socketio/socket.io/blob/master/docs/API.md 84 | // "A client always connects to / (the main namespace), then 85 | // potentially connect to other namespaces (while using the 86 | // same underlying connection)." 87 | 88 | // convenience function to log server messages on the client 89 | 90 | socket.on('what robots are available', function() { 91 | if(role === 'operator') { 92 | console.log('operator has requested the available robots'); 93 | console.log('available_robots ='); 94 | console.log(available_robots); 95 | log('Received request for the available robots'); 96 | var robots = Array.from(available_robots.values()); 97 | socket.emit('available robots', robots); 98 | } else { 99 | console.log('NO REPLY SENT: non-operator requested the available robots'); 100 | } 101 | }); 102 | 103 | 104 | socket.on('webrtc message', function(message) { 105 | console.log('Client sent WebRTC message: ', message); 106 | if(robot_operator_room !== 'none') { 107 | console.log('sending WebRTC message to any other clients in the room named "' + 108 | robot_operator_room + '".'); 109 | socket.to(robot_operator_room).emit('webrtc message', message); 110 | 111 | if(message === 'bye') { 112 | if(role === 'operator') { 113 | console.log('Attempting to have the operator leave the robot room.'); 114 | console.log(''); 115 | socket.leave(robot_operator_room); 116 | available_robots.add(robot_operator_room); 117 | robot_operator_room = 'none'; 118 | sendAvailableRobotsUpdate(socket); 119 | } 120 | } 121 | } else { 122 | console.log('robot_operator_room is none, so there is nobody to send the WebRTC message to'); 123 | } 124 | }); 125 | 126 | 127 | socket.on('join', function(room) { 128 | console.log('Received request to join room ' + room); 129 | 130 | numClients = io.sockets.adapter.rooms.get(room).size 131 | console.log('Requested room ' + room + ' currently has ' + numClients + ' client(s)'); 132 | 133 | if (numClients < 1) { 134 | //socket.join(room); 135 | console.log('*********************************************'); 136 | console.log('RECEIVED REQUEST TO JOIN A NON-EXISTENT ROOM'); 137 | console.log('THIS IS UNUSUAL AND SHOULD BE AVOIDED'); 138 | console.log('Client ID ' + socket.id + ' created room ' + room); 139 | console.log('DOING NOTHING...'); 140 | console.log('Apparently, no robot exists with the requested name'); 141 | console.log('Since no room exists with the requested name'); 142 | console.log('*********************************************'); 143 | //socket.emit('created', room, socket.id); 144 | } else if (numClients < 2) { 145 | console.log('Client ID ' + socket.id + ' joined room ' + room); 146 | io.sockets.in(room).emit('join', room); 147 | socket.join(room); 148 | available_robots.delete(room); 149 | robot_operator_room = room; 150 | sendAvailableRobotsUpdate(socket); 151 | socket.emit('joined', room, socket.id); 152 | io.sockets.in(room).emit('ready'); 153 | } else { // max two clients 154 | socket.emit('full', room); 155 | } 156 | }); 157 | 158 | socket.on('disconnect', function(){ 159 | console.log('socket disconnected'); 160 | if(user.role === 'robot') { 161 | var robot_name = user.username; 162 | console.log('ROBOT "' + robot_name + '" DISCONNECTED'); 163 | console.log('attempting to delete it from the set'); 164 | robot_operator_room = 'none'; 165 | // could this result in deleting an element of the set 166 | // that should be there because of asynchronous 167 | // execution? 168 | connected_robots.delete(robot_name); 169 | available_robots.delete(robot_name); 170 | sendAvailableRobotsUpdate(socket); // might be good to include this in an object that tracks the robots 171 | } 172 | console.log('user disconnected'); 173 | }); 174 | 175 | }); 176 | 177 | // "Disconnects this client. If value of close is true, closes the underlying connection." 178 | // - https://socket.io/docs/server-api/ 179 | // console.log('disconnecting...'); 180 | // socket.disconnect(true) 181 | }; 182 | 183 | //////////////////////////////////////////////////////// 184 | 185 | module.exports = createSignalingSocket; 186 | 187 | -------------------------------------------------------------------------------- /start_robot_browser.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // used documentation and initial example code from 4 | // https://github.com/GoogleChrome/puppeteer 5 | // and 6 | // https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md 7 | 8 | const puppeteer = require('puppeteer'); 9 | const logId = 'start_robot_browser.js'; 10 | const calibrateRobot = true; 11 | const startServers = true; 12 | const fastBoot = true; 13 | 14 | (async () => { 15 | try { 16 | const type_delay = 1; 17 | 18 | const navigation_timeout_ms = 30000; //30 seconds (default is 30 seconds) 19 | const min_idle_time = 1000; 20 | var try_again = false; 21 | var num_tries = 0; 22 | var max_tries = -1; // -1 means try forever 23 | 24 | /////////////////////////////////////////////// 25 | // sleep code from 26 | // https://stackoverflow.com/questions/951021/what-is-the-javascript-version-of-sleep 27 | function sleep(ms) { 28 | return new Promise(resolve => setTimeout(resolve, ms)); 29 | } 30 | /////////////////////////////////////////////// 31 | 32 | const browser = await puppeteer.launch({ 33 | headless: false, // default is true 34 | ignoreHTTPSErrors: true, // avoid ERR_CERT_COMMON_NAME_INVALID 35 | args: ['--use-fake-ui-for-media-stream'] //gives permission to access the robot's cameras and microphones (cleaner and simpler than changing the user directory) 36 | }); 37 | const page = await browser.newPage(); 38 | const mouse = page.mouse; 39 | 40 | // The following loop makes this script more robust, such as being 41 | // able to keep trying until the server comes up. It might also be 42 | // able to handle the WiFi network not coming up prior to this 43 | // script running, but I haven't tested it. 44 | do { 45 | console.log(logId + ': trying to reach login page...'); 46 | num_tries++; 47 | await page.goto('https://localhost/login', 48 | {timeout:navigation_timeout_ms 49 | } 50 | ).then( 51 | function(response){ 52 | console.log(logId + ': ==================='); 53 | console.log(logId + ': no error caught! page reached?'); 54 | console.log(response); 55 | if(response === null) { 56 | console.log(logId + ': page.goto returned null, so try again'); 57 | try_again = true; 58 | } else { 59 | console.log(logId + ': page.goto returned something other than null, so proceed with fingers crossed...'); 60 | try_again = false; 61 | } 62 | console.log(logId + ': ==================='); 63 | 64 | }).catch( 65 | function(error) { 66 | console.log(logId + ': ==================='); 67 | console.log(logId + ': promise problem with login page goto attempt'); 68 | console.log(error); 69 | try_again = true; 70 | console.log(logId + ': so going to try again...'); 71 | console.log(logId + ': ==================='); 72 | }); 73 | if ((max_tries != -1) && (num_tries >= max_tries)) { 74 | try_again = false; 75 | } 76 | } while (try_again); 77 | 78 | console.log(logId + ': page ='); 79 | console.log(page); 80 | 81 | console.log(logId + ': type username'); 82 | await page.type('#inputUsername', 'r1'); 83 | 84 | console.log(logId + ': type password'); 85 | await page.type('#inputPassword', 'NQUeUb98'); 86 | 87 | console.log(logId + ': click submit'); 88 | await page.click('#submitButton'); 89 | 90 | console.log(logId + ': start script complete'); 91 | 92 | } catch ( e ) { 93 | console.log(logId + ': *********************************************'); 94 | console.log(logId + ': *** SCRIPT STEPS SKIPPED DUE TO AN ERROR! ***'); 95 | console.log(logId + ': *** error = ***'); 96 | console.log(logId + ': ' + e ); 97 | console.log(logId + ': *********************************************'); 98 | } 99 | })(); 100 | -------------------------------------------------------------------------------- /ui_elements/cursors/generate_cursors.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "****************************************" 3 | echo "attempting to generate straight arrows in cardinal directions and two sizes..." 4 | 5 | inkscape -z -e gripper_close_small.png -w 20 -h 20 gripper_close.svg 6 | inkscape -z -e gripper_close_medium.png -w 48 -h 48 gripper_close.svg 7 | 8 | inkscape -z -e gripper_open_small.png -w 20 -h 20 gripper_open.svg 9 | inkscape -z -e gripper_open_medium.png -w 48 -h 48 gripper_open.svg 10 | 11 | inkscape -z -e right_arrow_small.png -w 20 -h 20 right_arrow.svg 12 | inkscape -z -e right_arrow_medium.png -w 48 -h 48 right_arrow.svg 13 | 14 | convert right_arrow_small.png -rotate 90 down_arrow_small.png 15 | convert right_arrow_medium.png -rotate 90 down_arrow_medium.png 16 | 17 | convert right_arrow_small.png -rotate 180 left_arrow_small.png 18 | convert right_arrow_medium.png -rotate 180 left_arrow_medium.png 19 | 20 | convert right_arrow_small.png -rotate 270 up_arrow_small.png 21 | convert right_arrow_medium.png -rotate 270 up_arrow_medium.png 22 | 23 | inkscape -z -e right_turn_small.png -w 30 -h 30 right_turn.svg 24 | inkscape -z -e right_turn_medium.png -w 60 -h 60 right_turn.svg 25 | 26 | convert right_turn_small.png -flop left_turn_small.png 27 | convert right_turn_medium.png -flop left_turn_medium.png 28 | 29 | echo "****************************************" 30 | echo "copying the results to the operator directory" 31 | 32 | cp *.png ../../operator/ 33 | 34 | echo "****************************************" 35 | echo "attempting to delete generated images in the current directory" 36 | echo "rm *_small.png" 37 | rm *_small.png 38 | echo "rm *_medium.png" 39 | rm *_medium.png 40 | 41 | echo "****************************************" 42 | echo "done" 43 | -------------------------------------------------------------------------------- /ui_elements/cursors/gripper_close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ui_elements/cursors/gripper_open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ui_elements/cursors/right_arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ui_elements/cursors/right_turn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ui_elements/operator_ui_test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 21 | do nothing 22 | 23 | 24 | 30 | move forward medium 31 | 32 | 33 | 39 | move forward small 40 | 41 | 42 | 48 | move backward medium 49 | 50 | 51 | 57 | move backward small 58 | 59 | 60 | 66 | turn left medium 67 | 68 | 69 | 75 | turn left small 76 | 77 | 78 | 84 | turn right medium 85 | 86 | 87 | 93 | turn right small 94 | 95 | 96 | 97 | 98 | 99 | 105 | bend wrist upward 106 | 107 | 108 | 114 | bend wrist downward 115 | 116 | 117 | 123 | rotate wrist left 124 | 125 | 126 | 132 | rotate wrist right 133 | 134 | 135 | 141 | open gripper 142 | 143 | 144 | 150 | close gripper 151 | 152 | 153 | 154 | 155 | 156 | 162 | lift up medium 163 | 164 | 165 | 171 | lift up small 172 | 173 | 174 | 180 | lift down medium 181 | 182 | 183 | 189 | lift down small 190 | 191 | 192 | 198 | retract arm medium 199 | 200 | 201 | 207 | retract arm small 208 | 209 | 210 | 216 | extend arm medium 217 | 218 | 219 | 225 | extend arm small 226 | 227 | 228 | 229 | 230 |
231 | 232 | 233 | 237 | 238 | 239 | 240 | -------------------------------------------------------------------------------- /views/error.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= message 5 | h2= error.status 6 | pre #{error.stack} 7 | -------------------------------------------------------------------------------- /views/index.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1 Hello Robot Inc. 5 | p Development Code 6 | -------------------------------------------------------------------------------- /views/layout.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title= title 5 | 6 | link(rel='stylesheet', href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css', integrity='sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u', crossorigin='anonymous') 7 | link(rel='stylesheet', href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css', integrity='sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp', crossorigin='anonymous') 8 | link(rel='stylesheet', href='/stylesheets/style.css') 9 | body 10 | nav.navbar.navbar-default 11 | div.container-fluid 12 | div.navbar-header 13 | a.navbar-brand(href='#') Hello Robot Inc. 14 | ul.nav.navbar-nav.navbar-right 15 | if (!user) 16 | li 17 | a(href='/login') Login 18 | li 19 | a(href='/register') Register 20 | if (user) 21 | li 22 | a Welcome #{user.name} 23 | li 24 | a(href='/logout') Logout 25 | 26 | div.container 27 | div.content 28 | block content 29 | 30 | script(src='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js', integrity='sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa', crossorigin='anonymous') 31 | -------------------------------------------------------------------------------- /views/login.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | .container 5 | form.form-signin(role='form', action='/login', method='post') 6 | h2.form-signin-heading Please sign in 7 | input.form-control(type='text', name='username', id='inputUsername', placeholder='Username', required, autofocus) 8 | input.form-control(type='password', name='password', id='inputPassword', placeholder='Password') 9 | button.btn.btn-lg.btn-primary.btn-block(type='submit', id='submitButton') LOGIN 10 | -------------------------------------------------------------------------------- /views/register.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | .container 5 | form.form-signin(role='form', action="/register",method="post", style='max-width: 300px;') 6 | h2.form-signin-heading Sign Up here 7 | input.form-control(type='text', name="username", placeholder='Your Username') 8 | input.form-control(type='password', name="password", placeholder='Your Password') 9 | button.btn.btn-lg.btn-primary.btn-block(type='submit') Sign Up 10 | --------------------------------------------------------------------------------