├── .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 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
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 |
--------------------------------------------------------------------------------