From f6658f537281694da5b9ac79b3c4415e8dc38069 Mon Sep 17 00:00:00 2001 From: Raymond Oung <44389573+ray-hrst@users.noreply.github.com> Date: Wed, 18 Mar 2020 12:50:31 +0900 Subject: [PATCH 01/15] Renames location to waypoint for status messages --- .gitignore | 1 + .../app/src/main/java/com/hapirobo/connect/MainActivity.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6e6131c..c34ddf3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +*.apk webapp/node_modules/ *venv/ .DS_Store diff --git a/android/app/src/main/java/com/hapirobo/connect/MainActivity.java b/android/app/src/main/java/com/hapirobo/connect/MainActivity.java index 340b18f..8bdc43a 100644 --- a/android/app/src/main/java/com/hapirobo/connect/MainActivity.java +++ b/android/app/src/main/java/com/hapirobo/connect/MainActivity.java @@ -192,7 +192,7 @@ public void onGoToLocationStatusChanged(@NotNull String location, @NotNull Strin try { MqttMessage message = new MqttMessage(payload.toString().getBytes(StandardCharsets.UTF_8)); - mMqttClient.publish("temi/" + sSerialNumber + "/status/locations/goto", message); + mMqttClient.publish("temi/" + sSerialNumber + "/status/waypoint/goto", message); } catch (MqttException e) { e.printStackTrace(); } From 0db01ed472021100f3d2c1fa66f065ea243b940b Mon Sep 17 00:00:00 2001 From: Raymond Oung <44389573+ray-hrst@users.noreply.github.com> Date: Wed, 18 Mar 2020 14:21:11 +0900 Subject: [PATCH 02/15] Updates README instructions --- README.md | 72 +++++++++++++++++++++++++++++++++++++++++------ android/README.md | 11 ++++++++ webapp/README.md | 6 ++-- 3 files changed, 78 insertions(+), 11 deletions(-) create mode 100644 android/README.md diff --git a/README.md b/README.md index d2476db..e2213b3 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,79 @@ # jitsi-temi -A free and open-source alternative to temi's default video-conferencing application. +A free and open-source alternative to [temi](https://www.robotemi.com/)'s default video-conferencing application. +This is still a prototype. This project uses MQTT for message transport between the client's web-brower user-interface and robot. It uses [Jitsi-Meet](https://jitsi.org/), a free open-source WebRTC framework, for video conferencing. -## TL;DR -Install APK onto temi. +The project consists of 3 components: +1. **Web Application**. A web-browser application used to tele-operate the robot. +2. **Android App.** An application that bridges robot commands/events and MQTT messages, which is to be installed on the robot. +3. **MQTT broker.** [Eclipe Mosquitto](https://mosquitto.org/) open-source MQTT broker. + + +## Folder Structure +``` +jitsi-temi Root directory +├─ android Android application to be installed on temi +├─ config MQTT broker configuration files +├─ mosquitto MQTT broker +├─ test Various test scripts +└─ webapp NodeJS/Express web server and client application +``` + + +## Repository Structure +There are 2 main branches: `master` and `devel`. Releases are generated from the `master` branch. All development takes place on the `devel` branch. + + +## Getting Started + +### Prerequisites +* [temi robot](https://www.robotemi.com/) +* [Android Studio](https://developer.android.com/studio/) +* Computer/Server with the following installed: + * [Docker](https://docs.docker.com/install/) + * [Docker Compose](https://docs.docker.com/compose/install/) +* Client computer with [Chrome](https://www.google.com/chrome/) web-browser + +### Build & Deploy Web Application Services +On your host machine, clone the repository and update submodules: ``` -adb connect -adb install connect.apk +git clone https://github.com/ray-hrst/jitsi-temi.git +git submodule update --init --recursive ``` -Start the MQTT broker and web-server: +Currently, the hostname is hard-coded into the webapp. Look for a call to the function `connectMQTT()` in `main.js` (somewhere near the end). Replace the value with your machine's hostname/IP-address: ``` +hostname -I +``` + +From the `root` directory, build and deploy the MQTT broker and web application services: +``` +cd jitsi-temi/ docker-compose build docker-compose up ``` -Start the `Connect` app on temi. Type in the IP-address of the MQTT broker and press `Connect`. +### Install Android App +Install the Android APK onto your robot, see [instructions](https://github.com/robotemi/sdk/wiki/Installing-and-Uninstalling-temi-Applications) for details. The application is called `Connect`. The Android app is available under [releases](https://github.com/ray-hrst/jitsi-temi/releases). + +To build from source, clone the repository: +``` +git clone https://github.com/ray-hrst/jitsi-temi.git +``` + +From the `android` directory, build the Android app using Android Studio. Alternatively, you can try to build the app from the command line: +``` +cd android/ +./gradlew build +``` +You will find the Android APK at `app/build/outputs/apk/debug/app-debug.apk`. + +### Usage +Start the `Connect` app on temi. Type in the `hostname` of your machine into the provided text-field, and press `Connect`. -In your web-browser, type the IP-address of the web-server. +On a computer connected to the same network as your host machine, open a web-browser and enter your host machine's hostname into the address bar. -If everything is working correctly, you should see your temi's serial number appear. Click it to start tele-operations. +If everything is working correctly, you should see your temi's serial number appear in the web application. Click it to start tele-operating the robot. ## Attributions diff --git a/android/README.md b/android/README.md new file mode 100644 index 0000000..d508263 --- /dev/null +++ b/android/README.md @@ -0,0 +1,11 @@ +# Connect Android Application + +This is an Android application built using [Eclipse Paho JavaScript Client](https://www.eclipse.org/paho/clients/js/) for communicating with the MQTT broker, [Jitsi-Meet SDK for Android](https://github.com/jitsi/jitsi-meet/tree/master/android) for video-conferencing, and the [temi SDK](https://github.com/robotemi/sdk) for robot control and event handling. + +Important Files: +* android/build.gradle +* android/app/build.gradle +* android/app/src/main/AndroidManifest.xml +* android/app/src/main/res/values/strings.xml +* android/app/src/main/res/layout/activity_main.xml +* android/app/src/main/java/com/hapirobo/connect/MainActivity.java diff --git a/webapp/README.md b/webapp/README.md index 44d27cb..8683755 100644 --- a/webapp/README.md +++ b/webapp/README.md @@ -1,6 +1,8 @@ -# Connect Webapp +# Connect Web Application -This is a webapp built on Node.js/Express. +This is a web application built on Node.js/Express. It uses the [Materialize](https://materializecss.com/) front-end framework for styling, [Eclipse Paho JavaScript Client](https://www.eclipse.org/paho/clients/js/) for communicating with the MQTT broker over WebSockets, and the [Jitsi-Meet API](https://github.com/jitsi/jitsi-meet/blob/master/doc/api.md) for video-conferencing. + +The instructions provided below are for building and deploying the web application standalone. For running the complete project, it is recommended to use Docker Compose from the root directory. ## Prerequisites From 4941be0d97af84611c565dc373f8b625961b747a Mon Sep 17 00:00:00 2001 From: Raymond Oung <44389573+ray-hrst@users.noreply.github.com> Date: Wed, 18 Mar 2020 14:25:26 +0900 Subject: [PATCH 03/15] Fixes spacing --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e2213b3..5af0ce6 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,12 @@ The project consists of 3 components: ## Folder Structure ``` -jitsi-temi Root directory -├─ android Android application to be installed on temi -├─ config MQTT broker configuration files -├─ mosquitto MQTT broker -├─ test Various test scripts -└─ webapp NodeJS/Express web server and client application +jitsi-temi Root directory +├─ android Android application to be installed on temi +├─ config MQTT broker configuration files +├─ mosquitto MQTT broker +├─ test Various test scripts +└─ webapp NodeJS/Express web server and client application ``` @@ -30,8 +30,8 @@ There are 2 main branches: `master` and `devel`. Releases are generated from the * [temi robot](https://www.robotemi.com/) * [Android Studio](https://developer.android.com/studio/) * Computer/Server with the following installed: - * [Docker](https://docs.docker.com/install/) - * [Docker Compose](https://docs.docker.com/compose/install/) + * [Docker](https://docs.docker.com/install/) + * [Docker Compose](https://docs.docker.com/compose/install/) * Client computer with [Chrome](https://www.google.com/chrome/) web-browser ### Build & Deploy Web Application Services From 06fe0260836644198d26b1ab335a9460c6636cbb Mon Sep 17 00:00:00 2001 From: Raymond Oung <44389573+ray-hrst@users.noreply.github.com> Date: Thu, 19 Mar 2020 14:32:04 +0900 Subject: [PATCH 04/15] Improves MQTT client reconnection behaviour; Remove unnecessary files --- android/app/build.gradle | 4 ++ .../com/hapirobo/connect/MainActivity.java | 64 ++++++++++++++++--- config/Dockerfile.template | 8 --- test/test_listener.py | 7 +- 4 files changed, 63 insertions(+), 20 deletions(-) delete mode 100644 config/Dockerfile.template diff --git a/android/app/build.gradle b/android/app/build.gradle index a47ee7b..56a77b3 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -46,12 +46,16 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:1.1.3' // Jitsi Meet + // https://github.com/jitsi/jitsi-meet implementation ('org.jitsi.react:jitsi-meet-sdk:2.5.1') { transitive = true } // paho-mqtt + // https://github.com/eclipse/paho.mqtt.java implementation('org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.2') + // https://github.com/eclipse/paho.mqtt.android implementation('org.eclipse.paho:org.eclipse.paho.android.service:1.1.1') // temi + // https://github.com/robotemi/sdk implementation 'com.robotemi:sdk:0.10.55' } diff --git a/android/app/src/main/java/com/hapirobo/connect/MainActivity.java b/android/app/src/main/java/com/hapirobo/connect/MainActivity.java index 8bdc43a..158930f 100644 --- a/android/app/src/main/java/com/hapirobo/connect/MainActivity.java +++ b/android/app/src/main/java/com/hapirobo/connect/MainActivity.java @@ -1,10 +1,13 @@ package com.hapirobo.connect; import androidx.appcompat.app.AppCompatActivity; + +import android.content.Context; import android.os.Bundle; import android.os.Handler; import android.util.Log; import android.view.View; +import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.Toast; @@ -51,6 +54,7 @@ public class MainActivity extends AppCompatActivity implements // periodically publishes robot status to the MQTT broker. @Override public void run() { + Log.v(TAG, "Publish status"); sHandler.postDelayed(this, 3000); try { @@ -111,6 +115,24 @@ protected void onStop() { sRobot.removeOnGoToLocationStatusChangedListener(this); } + /** + * Disconnects MQTT client from broker. + */ + @Override + protected void onDestroy() { + super.onDestroy(); + if (mMqttClient != null && mMqttClient.isConnected()) { + try { + Log.i(TAG, "[MQTT] Disconnecting MQTT client from broker"); + mMqttClient.unsubscribe("temi/" + sSerialNumber + "/command/#"); + mMqttClient.disconnect(); + Log.i(TAG, "[MQTT] Done"); + } catch (MqttException e) { + e.printStackTrace(); + } + } + } + /** * Configures robot after it is ready. * @param isReady True if robot initialized correctly; False otherwise @@ -146,8 +168,8 @@ public void onBatteryStatusChanged(@Nullable BatteryData batteryData) { } try { - MqttMessage message = new MqttMessage(payload.toString().getBytes(StandardCharsets.UTF_8)); - if (mMqttClient != null) { + if (mMqttClient != null && mMqttClient.isConnected()) { + MqttMessage message = new MqttMessage(payload.toString().getBytes(StandardCharsets.UTF_8)); mMqttClient.publish("temi/" + sSerialNumber + "/status/utils/battery", message); } } catch (MqttException e) { @@ -191,25 +213,35 @@ public void onGoToLocationStatusChanged(@NotNull String location, @NotNull Strin } try { - MqttMessage message = new MqttMessage(payload.toString().getBytes(StandardCharsets.UTF_8)); - mMqttClient.publish("temi/" + sSerialNumber + "/status/waypoint/goto", message); + if (mMqttClient != null && mMqttClient.isConnected()) { + MqttMessage message = new MqttMessage(payload.toString().getBytes(StandardCharsets.UTF_8)); + mMqttClient.publish("temi/" + sSerialNumber + "/status/waypoint/goto", message); + } } catch (MqttException e) { e.printStackTrace(); } } /** - * Connects to MQTT broker and launches Jitsi-meet conference room. + * Connects to MQTT broker. * @param v View context */ public void onButtonClick(View v) { EditText hostNameView = findViewById(R.id.edit_text_host_name); - String hostURI = "tcp://" + hostNameView.getText().toString().trim() + ":1883"; + String hostUri = "tcp://" + hostNameView.getText().toString().trim() + ":1883"; + + // hide keyboard + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(hostNameView.getWindowToken(), 0); - // TODO if already connected, disconnect from broker and reconnect // initialize MQTT - initMqtt(hostURI, "temi-" + sSerialNumber); + if (mMqttClient != null && mMqttClient.isConnected() && hostUri.equals(mMqttClient.getServerURI())) { + Toast.makeText(MainActivity.this, "Already Connected", Toast.LENGTH_SHORT).show(); + Log.i(TAG, "Already connected to MQTT broker"); + } else { + initMqtt(hostUri, "temi-" + sSerialNumber); + } } /** @@ -245,13 +277,15 @@ public void robotPublishStatus() throws JSONException { * @param clientId Identifier used to uniquely identify this client */ private void initMqtt(String hostUri, String clientId) { + Log.i(TAG, "Connecting to MQTT broker"); + mMqttClient = new MqttAndroidClient(getApplicationContext(), hostUri, clientId); mMqttClient.setCallback(new MqttCallback() { @Override public void connectionLost(Throwable cause) { + // this method is called when connection to server is lost Toast.makeText(MainActivity.this, "Connection Lost", Toast.LENGTH_SHORT).show(); Log.i(TAG, "Connection Lost"); - // this method is called when connection to server is lost } @Override @@ -269,11 +303,23 @@ public void messageArrived(String topic, MqttMessage message) throws JSONExcepti } }); + // options that control how the client connects to a server + // https://www.eclipse.org/paho/files/javadoc/org/eclipse/paho/client/mqttv3/MqttConnectOptions.html MqttConnectOptions mqttConnectOptions = new MqttConnectOptions(); + + // have the client automatically attempt to reconnect to the server if the connection is lost mqttConnectOptions.setAutomaticReconnect(true); + + // client and server should forget the state across reconnects. mqttConnectOptions.setCleanSession(true); + + // the maximum time interval the client will wait for the network connection to the MQTT server to be established [seconds] mqttConnectOptions.setConnectionTimeout(10); + // set the "Last Will and Testament" (LWT) for the connection + JSONObject payload = new JSONObject(); + mqttConnectOptions.setWill("temi/" + sSerialNumber + "/lwt", payload.toString().getBytes(StandardCharsets.UTF_8), 1, false); + try { mMqttClient.connect(mqttConnectOptions, null, new IMqttActionListener() { @Override diff --git a/config/Dockerfile.template b/config/Dockerfile.template deleted file mode 100644 index cb978c6..0000000 --- a/config/Dockerfile.template +++ /dev/null @@ -1,8 +0,0 @@ -FROM balenalib/%%BALENA_MACHINE_NAME%%-alpine -MAINTAINER R. Oung (r.oung@hapi-robo.com) - -# create app directory -WORKDIR /usr/src/config - -# copy configuration files -COPY mosquitto.conf . diff --git a/test/test_listener.py b/test/test_listener.py index 2c6c514..fa29194 100755 --- a/test/test_listener.py +++ b/test/test_listener.py @@ -16,8 +16,9 @@ CLIENT_ID = 'test-listener' # MQTT broker -MQTT_BROKER_HOST = 'localhost' -# MQTT_BROKER_HOST = '192.168.0.118' +# MQTT_BROKER_HOST = 'localhost' +MQTT_BROKER_HOST = '192.168.0.118' +# MQTT_BROKER_HOST = '192.168.0.177' MQTT_BROKER_PORT = 1883 # connection parameters @@ -25,7 +26,7 @@ MQTT_BROKER_RETRY_TIMEOUT = 10 # [seconds] # topics -TOPIC_ALL = 'temi/+/command/#' +TOPIC_ALL = 'temi/#' def on_connect(client, userdata, flags, rc): """Connect to MQTT broker and subscribe to topics From 3d97186c92d358d5d21bbc31729e72b758f8f27d Mon Sep 17 00:00:00 2001 From: Raymond Oung <44389573+ray-hrst@users.noreply.github.com> Date: Thu, 19 Mar 2020 15:54:35 +0900 Subject: [PATCH 05/15] Adds user detection and interaction listeners --- .../com/hapirobo/connect/MainActivity.java | 52 ++++++++++++++++++- test/test_listener.py | 6 +-- webapp/public/js/main.js | 3 -- 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/android/app/src/main/java/com/hapirobo/connect/MainActivity.java b/android/app/src/main/java/com/hapirobo/connect/MainActivity.java index 158930f..7ba978d 100644 --- a/android/app/src/main/java/com/hapirobo/connect/MainActivity.java +++ b/android/app/src/main/java/com/hapirobo/connect/MainActivity.java @@ -14,8 +14,10 @@ import com.robotemi.sdk.BatteryData; import com.robotemi.sdk.Robot; import com.robotemi.sdk.listeners.OnBatteryStatusChangedListener; +import com.robotemi.sdk.listeners.OnDetectionStateChangedListener; import com.robotemi.sdk.listeners.OnGoToLocationStatusChangedListener; import com.robotemi.sdk.listeners.OnRobotReadyListener; +import com.robotemi.sdk.listeners.OnUserInteractionChangedListener; import org.eclipse.paho.android.service.MqttAndroidClient; import org.eclipse.paho.client.mqttv3.IMqttActionListener; @@ -43,7 +45,9 @@ public class MainActivity extends AppCompatActivity implements OnRobotReadyListener, OnBatteryStatusChangedListener, - OnGoToLocationStatusChangedListener { + OnGoToLocationStatusChangedListener, + OnDetectionStateChangedListener, + OnUserInteractionChangedListener { private static final String TAG = "DEBUG"; private static Handler sHandler = new Handler(); @@ -102,6 +106,8 @@ protected void onStart() { sRobot.addOnRobotReadyListener(this); sRobot.addOnBatteryStatusChangedListener(this); sRobot.addOnGoToLocationStatusChangedListener(this); + sRobot.addOnDetectionStateChangedListener(this); + sRobot.addOnUserInteractionChangedListener(this); } /** @@ -113,6 +119,8 @@ protected void onStop() { sRobot.removeOnRobotReadyListener(this); sRobot.removeOnBatteryStatusChangedListener(this); sRobot.removeOnGoToLocationStatusChangedListener(this); + sRobot.removeDetectionStateChangedListener(this); + sRobot.removeOnUserInteractionChangedListener(this); } /** @@ -215,7 +223,47 @@ public void onGoToLocationStatusChanged(@NotNull String location, @NotNull Strin try { if (mMqttClient != null && mMqttClient.isConnected()) { MqttMessage message = new MqttMessage(payload.toString().getBytes(StandardCharsets.UTF_8)); - mMqttClient.publish("temi/" + sSerialNumber + "/status/waypoint/goto", message); + mMqttClient.publish("temi/" + sSerialNumber + "/event/waypoint/goto", message); + } + } catch (MqttException e) { + e.printStackTrace(); + } + } + + @Override + public void onDetectionStateChanged(int state) { + JSONObject payload = new JSONObject(); + + try { + payload.put("state", state); + } catch (JSONException e) { + e.printStackTrace(); + } + + try { + if (mMqttClient != null && mMqttClient.isConnected()) { + MqttMessage message = new MqttMessage(payload.toString().getBytes(StandardCharsets.UTF_8)); + mMqttClient.publish("temi/" + sSerialNumber + "/event/user/detection", message); + } + } catch (MqttException e) { + e.printStackTrace(); + } + } + + @Override + public void onUserInteraction(boolean isInteracting) { + JSONObject payload = new JSONObject(); + + try { + payload.put("is_interacting", isInteracting); + } catch (JSONException e) { + e.printStackTrace(); + } + + try { + if (mMqttClient != null && mMqttClient.isConnected()) { + MqttMessage message = new MqttMessage(payload.toString().getBytes(StandardCharsets.UTF_8)); + mMqttClient.publish("temi/" + sSerialNumber + "/event/user/interaction", message); } } catch (MqttException e) { e.printStackTrace(); diff --git a/test/test_listener.py b/test/test_listener.py index fa29194..6dbf3a6 100755 --- a/test/test_listener.py +++ b/test/test_listener.py @@ -17,8 +17,8 @@ # MQTT broker # MQTT_BROKER_HOST = 'localhost' -MQTT_BROKER_HOST = '192.168.0.118' -# MQTT_BROKER_HOST = '192.168.0.177' +# MQTT_BROKER_HOST = '192.168.0.118' +MQTT_BROKER_HOST = '192.168.0.177' MQTT_BROKER_PORT = 1883 # connection parameters @@ -26,7 +26,7 @@ MQTT_BROKER_RETRY_TIMEOUT = 10 # [seconds] # topics -TOPIC_ALL = 'temi/#' +TOPIC_ALL = 'temi/+/event/#' def on_connect(client, userdata, flags, rc): """Connect to MQTT broker and subscribe to topics diff --git a/webapp/public/js/main.js b/webapp/public/js/main.js index 1804320..d3b303c 100644 --- a/webapp/public/js/main.js +++ b/webapp/public/js/main.js @@ -255,9 +255,6 @@ function onMessageArrived(message) { updateRobotCollection(); break; } - case 'locations': { - break; - } case 'utils': { break; } From 44b5b0d90f8549064c0b653bda8c63d9f93809dc Mon Sep 17 00:00:00 2001 From: Raymond Oung <44389573+ray-hrst@users.noreply.github.com> Date: Thu, 19 Mar 2020 18:10:21 +0900 Subject: [PATCH 06/15] iFrames testing --- webapp/public/js/main.js | 16 ++++--- webapp/public/js/modules/videoConference.js | 4 +- webapp/views/test.html | 46 +++++++++++++++++++++ 3 files changed, 60 insertions(+), 6 deletions(-) create mode 100644 webapp/views/test.html diff --git a/webapp/public/js/main.js b/webapp/public/js/main.js index d3b303c..911c1ab 100644 --- a/webapp/public/js/main.js +++ b/webapp/public/js/main.js @@ -206,22 +206,25 @@ function mouseEvent(e) { const posX = e.offsetX; const posY = e.offsetY; - } function updateRobotList(id, payload) { - const found = robotList.find((e) => e.id === id); + const index = robotList.findIndex((e) => e.id === id); - if (found === undefined) { + if (index === -1) { console.log('Append'); robotList.push(new Robot(id, client)); + + const data = JSON.parse(payload); + robotList[robotList.length - 1].waypointList = data.waypoint_list; + robotList[robotList.length - 1].batteryPercentage = data.battery_percentage; } else { console.log('Update'); - const index = robotList.findIndex((e) => e.id === id); + const data = JSON.parse(payload); - robotList[index].batteryPercentage = data.battery_percentage; robotList[index].waypointList.length = 0; // clear array robotList[index].waypointList = data.waypoint_list; + robotList[index].batteryPercentage = data.battery_percentage; } // console.log(`Number of Robots: ${robotList.length}`); } @@ -325,3 +328,6 @@ document.querySelector('#robot-collection').addEventListener('click', selectRobo document.querySelector('#waypoint-nav').addEventListener('click', selectWaypoint); document.querySelector('#video-btn').addEventListener('click', startVidCon); // document.querySelector('#video-conference').addEventListener('mousemove', mouseEvent); + +// console.log(`Width x Height: ${screen.width} x ${screen.height}`) +// console.log(`Width x Height: ${innerWidth} x ${innerHeight}`) \ No newline at end of file diff --git a/webapp/public/js/modules/videoConference.js b/webapp/public/js/modules/videoConference.js index 37f84d4..0f786ef 100644 --- a/webapp/public/js/modules/videoConference.js +++ b/webapp/public/js/modules/videoConference.js @@ -11,7 +11,9 @@ class VideoConference { const options = { roomName: `temi-${id}`, // width: window.innerWidth, - height: window.innerHeight, + // height: window.innerHeight, + width:800, + height: 400, parentNode: document.getElementById('video-conference'), configOverwrite: {}, interfaceConfigOverwrite: { diff --git a/webapp/views/test.html b/webapp/views/test.html new file mode 100644 index 0000000..f572057 --- /dev/null +++ b/webapp/views/test.html @@ -0,0 +1,46 @@ + + + + + Test + + + + + + + +
+ +
+ + + + + + + \ No newline at end of file From 6dcc9067bb4793b54cf6e50a37eedb0799f1c2e3 Mon Sep 17 00:00:00 2001 From: Raymond Oung <44389573+ray-hrst@users.noreply.github.com> Date: Mon, 23 Mar 2020 17:26:34 +0900 Subject: [PATCH 07/15] Refactors code; Improves user interface responsiveness --- docker-compose.yml | 12 +- webapp/.eslintrc.yml | 4 +- webapp/app.js | 37 ++-- webapp/public/js/main.js | 225 +++++++++++++------- webapp/public/js/modules/robot.js | 13 ++ webapp/public/js/modules/videoConference.js | 41 ++-- webapp/views/index.pug | 50 ++--- webapp/views/test.html | 72 +++++-- 8 files changed, 294 insertions(+), 160 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 273a000..1941246 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,9 +20,9 @@ services: volumes: - config-volume:/mosquitto/config/ # SOURCE:TARGET - webapp: - build: ./webapp - depends_on: - - mqtt_broker - ports: - - "80:8080" # HOST:CONTAINER + # webapp: + # build: ./webapp + # depends_on: + # - mqtt_broker + # ports: + # - "80:8080" # HOST:CONTAINER diff --git a/webapp/.eslintrc.yml b/webapp/.eslintrc.yml index da91785..706a363 100644 --- a/webapp/.eslintrc.yml +++ b/webapp/.eslintrc.yml @@ -10,6 +10,4 @@ globals: parserOptions: ecmaVersion: 2018 sourceType: module -rules: { - no-console: 0 -} +rules: { no-console: 0 } diff --git a/webapp/app.js b/webapp/app.js index 1a21296..b203e01 100644 --- a/webapp/app.js +++ b/webapp/app.js @@ -9,34 +9,38 @@ */ // required libraries -const express = require('express'); -const path = require('path'); +const express = require("express"); +const path = require("path"); // instantiate webapp const app = express(); const port = 8080; - /* * Setup webapp */ // setup template engine -app.set('views', path.join(__dirname, 'views')); -app.set('view engine', 'pug'); +app.set("views", path.join(__dirname, "views")); +app.set("view engine", "pug"); // serve static files -app.use(express.static(path.join(__dirname, 'public'))); +app.use(express.static(path.join(__dirname, "public"))); + +// mobile route +app.get("/mobile", function(req, res) { + res.render("mobile", {}); +}); -// setup routes -app.get('/', function(req, res) { - res.render('index', { - title: ' Connect' +// desktop route +app.get("/", function(req, res) { + res.render("index", { + title: " Connect" }); }); // catch 404 and forward to the error handler app.use((req, res, next) => { - var err = new Error('Not Found'); + var err = new Error("Not Found"); err.status = 404; next(err); }); @@ -45,21 +49,20 @@ app.use((req, res, next) => { app.use((err, req, res, next) => { // set locals, only providing error in development res.locals.message = err.message; - res.locals.error = req.app.get('env') == 'development' ? err : {}; + res.locals.error = req.app.get("env") == "development" ? err : {}; // render the error page res.status(err.status || 500); - res.render('error'); + res.render("error"); }); - /* * Start listening on the port */ -app.listen(port, (err) => { +app.listen(port, err => { if (err) { - return console.log(`Something bad happened ${err}`) + return console.log(`Something bad happened ${err}`); } - console.log(`Server is listening on localhost:${port}`) + console.log(`Server is listening on localhost:${port}`); }); diff --git a/webapp/public/js/main.js b/webapp/public/js/main.js index 911c1ab..b2cf1c9 100644 --- a/webapp/public/js/main.js +++ b/webapp/public/js/main.js @@ -2,19 +2,19 @@ import { Robot } from './modules/robot.js'; import { VideoConference } from './modules/videoConference.js'; // global variables -let selectedRobot; const robotList = []; -const vidCon = new VideoConference(); +const videoCall = new VideoConference(); +let selectedRobot; let client; // DOCUMENT EVENT HANDLERS function updateRobotCollection() { - const robotNav = document.querySelector('#robot-collection'); - robotNav.className = 'collection'; + const robotCollection = document.querySelector('#robot-collection'); + robotCollection.className = 'collection'; // clear list - robotNav.textContent = ''; + robotCollection.textContent = ''; // populate list robotList.forEach((robot) => { @@ -24,12 +24,36 @@ function updateRobotCollection() { const text = document.createTextNode(robot.id); a.append(text); - robotNav.insertBefore(a, robotNav.firstChild); + robotCollection.insertBefore(a, robotCollection.firstChild); }); } -function updateWaypointNav() { - const waypointNav = document.querySelector('#waypoint-nav'); +// mobile-only +function updateWaypointCollection() { + const waypointCollection = document.querySelector('#waypoint-collection'); + waypointCollection.className = 'collection'; + + // clear list + waypointCollection.textContent = ''; + + if (selectedRobot === undefined) { + console.warn('Robot not selected'); + } else { + // populate list + selectedRobot.waypointList.forEach((waypoint) => { + const a = document.createElement('a'); + a.id = waypoint; + a.className = 'collection-item black white-text waves-effect center-align'; + const text = document.createTextNode(waypoint); + a.appendChild(text); + waypointCollection.insertBefore(a, waypointCollection.firstChild); + }); + } +} + +// desktop-only +function updateWaypointModal() { + const waypointNav = document.querySelector('#waypoint-modal'); // clear list waypointNav.textContent = ''; @@ -44,9 +68,7 @@ function updateWaypointNav() { a.id = waypoint; a.className = 'collection-item center-align waves-effect'; const text = document.createTextNode(waypoint); - a.appendChild(text); - waypointNav.insertBefore(a, waypointNav.firstChild); }); } @@ -55,28 +77,61 @@ function updateWaypointNav() { function showWaypointNav() { const elems = document.querySelectorAll('.modal'); M.Modal.init(elems, { - onOpenStart: updateWaypointNav + onOpenStart: updateWaypointModal, }); +} + +function showRobotMenu() { + console.log('Show Robot Menu'); + + // show robot menu + document.querySelector('#robot-menu').style.height = '80vh'; + document.querySelector('#robot-menu').style.display = 'block'; + + // hide other menus + document.querySelector('#robot-ctrl-panel').style.display = 'none'; +} + +function showWaypointMenu() { + console.log('Show Waypoint Menu'); + + // show waypoint menu + document.querySelector('#waypoint-menu').style.display = 'block'; + updateWaypointCollection(); + // hide other menus + document.querySelector('#robot-menu').style.display = 'none'; +} + +function showCtrlPanel() { + console.log('Show Control Panel'); + + // show control panel + document.querySelector('#robot-ctrl-panel').style.display = 'block'; + document.querySelector('#video-btn').className = 'btn-flat white-text'; + + // hide other menus + document.querySelector('#robot-menu').style.display = 'none'; } // https://keycode.info/ function keyboardEvent(e) { if (selectedRobot === undefined) { - switch(e.keyCode) { + const id = document.querySelector('#temi-id').value; + const selection = robotList.find((r) => r.id === id); + + switch (e.keyCode) { case 13: // Enter console.log('[Keycode] Enter'); - const id = document.querySelector('#temi_id').value; console.log(`Robot-ID: ${id}`); // check that the selection is valid - const selection = robotList.find((r) => r.id === id); if (selection === undefined) { M.toast({ - html: 'Invalid ID', - displayLength: 2000, - classes: 'rounded', - }); + html: 'Invalid ID', + displayLength: 2000, + classes: 'rounded', + }); } break; @@ -160,35 +215,41 @@ function updateBatteryState(value) { } } -function startVidCon() { - document.querySelector('#video-btn').style.display = 'none'; +function startVideoCall() { + document.querySelector('#video-btn').className = 'btn-flat disabled'; // start new video conference - console.log("Starting Video Conference..."); + console.log('Starting Video Conference...'); selectedRobot.cmdCall(); // start the call on the robot's side - vidCon.open(selectedRobot.id); + videoCall.open(selectedRobot.id); + // TODO: This needs to be cleaned up + videoCall.handle.on('readyToClose', () => { + console.log('Closing Video Conference...'); + videoCall.handle.dispose(); + // videoCall.handle.close(); + // TODO: end call on the robot's side; how to know if other users aren't using the robot? + showRobotMenu(); + }); } function selectRobot(e) { console.log(`Selected Robot: ${e.target.id}`); - + // check that the selection is valid const selection = robotList.find((r) => r.id === e.target.id); if (selection === undefined) { M.toast({ - html: 'Invalid ID', - displayLength: 2000, - classes: 'rounded', - }); + html: 'Invalid ID', + displayLength: 2000, + classes: 'rounded', + }); } else { // assign selected robot selectedRobot = selection; - // hide robot-menu - document.querySelector('#robot-menu').style.display = 'none'; - - // show robot control panel - document.querySelector('#robot-ctrl-panel').style.display = 'block'; + // show waypoint menu + // showWaypointMenu(); // mobile + showCtrlPanel(); // desktop // update battery state updateBatteryState(selection.batteryPercentage); @@ -200,14 +261,6 @@ function selectWaypoint(e) { console.log(`Selected Destination: ${selectedRobot.destination}`); } -// https://stackoverflow.com/questions/17106665/the-mouseevent-offsetx-i-am-getting-is-much-larger-than-actual-canvas-size -function mouseEvent(e) { - console.log(`x: ${e.offsetX} | y: ${e.offsetY}`); - - const posX = e.offsetX; - const posY = e.offsetY; -} - function updateRobotList(id, payload) { const index = robotList.findIndex((e) => e.id === id); @@ -220,7 +273,7 @@ function updateRobotList(id, payload) { robotList[robotList.length - 1].batteryPercentage = data.battery_percentage; } else { console.log('Update'); - + const data = JSON.parse(payload); robotList[index].waypointList.length = 0; // clear array robotList[index].waypointList = data.waypoint_list; @@ -233,9 +286,9 @@ function updateRobotList(id, payload) { * General message callback */ function onMessageArrived(message) { - // console.log('[RECIEVE]'); - // console.log(`Topic: ${message.destinationName}`); - // console.log(`Payload: ${message.payloadString}`); + console.log('[RECIEVE]'); + console.log(`Topic: ${message.destinationName}`); + console.log(`Payload: ${message.payloadString}`); // parse message const topicTree = message.destinationName.split('/'); @@ -243,33 +296,54 @@ function onMessageArrived(message) { const type = topicTree[2]; // [status, command] const category = topicTree[3]; - // console.log(`Robot-ID: ${robotID}`); - // console.log(`Type: ${type}`); - // console.log(`Category: ${category}`); + console.log(`Robot-ID: ${robotID}`); + console.log(`Type: ${type}`); + console.log(`Category: ${category}`); if (robotID === undefined) { console.warn('Message from undefined robot received'); } else { - if (type === 'status') { - // parse payload - switch (category) { - case 'info': { - updateRobotList(robotID, message.payloadString); - updateRobotCollection(); - break; - } - case 'utils': { - break; - } - default: { - console.warn(`Undefined category: ${category}`); - break; + switch (type) { + case 'status': + // parse payload + switch (category) { + case 'info': { + updateRobotList(robotID, message.payloadString); + updateRobotCollection(); + // updateWaypointCollection(); // mobile-only + break; + } + case 'utils': { + break; + } + default: { + console.warn(`Undefined category: ${category}`); + break; + } } - } + break; + + case 'command': + break; + + default: + break; } } } +// called when the client loses its connection +function onConnectionLost(responseObject) { + if (responseObject.errorCode !== 0) { + M.toast({ + html: 'Connection Lost', + displayLength: 2000, + classes: 'rounded', + }); + console.log(`onConnectionLost: ${responseObject.errorMessage}`); + } +} + /* * Connect to MQTT broker * ref: https://www.eclipse.org/paho/files/jsdoc/Paho.MQTT.Client.html @@ -284,27 +358,24 @@ function connectMQTT(host, port) { // sniff and display messages on MQTT bus client.onMessageArrived = onMessageArrived; + client.onConnectionLost = onConnectionLost; const options = { - // connection attempt timeout in seconds timeout: 3, - - // on successful connection + reconnect: false, onSuccess: () => { - console.log('Success'); + console.log('Successfully connected to MQTT broker'); M.toast({ - html: 'Successfully connected to MQTT broker', + html: 'Successfully Connected', displayLength: 2000, classes: 'rounded', }); client.subscribe('temi/+/status/#'); }, - - // on failed connection onFailure: (message) => { console.error(`Fail: ${message.errorMessage}`); M.toast({ - html: 'Failed to connect to MQTT broker', + html: 'Failed to Connect', displayLength: 3000, classes: 'rounded', }); @@ -315,19 +386,17 @@ function connectMQTT(host, port) { client.connect(options); } +document.body.style.backgroundColor = 'black'; + // @TODO Make this configurable // window.onload = connectMQTT('localhost', 9001); window.onload = connectMQTT('192.168.0.177', 9001); - -document.body.style.backgroundColor = 'black'; +window.onload = showRobotMenu(); document.addEventListener('DOMContentLoaded', showWaypointNav); document.addEventListener('keydown', keyboardEvent); document.querySelector('#robot-collection').addEventListener('click', selectRobot); -document.querySelector('#waypoint-nav').addEventListener('click', selectWaypoint); -document.querySelector('#video-btn').addEventListener('click', startVidCon); -// document.querySelector('#video-conference').addEventListener('mousemove', mouseEvent); - -// console.log(`Width x Height: ${screen.width} x ${screen.height}`) -// console.log(`Width x Height: ${innerWidth} x ${innerHeight}`) \ No newline at end of file +document.querySelector('#video-btn').addEventListener('click', startVideoCall); +document.querySelector('#waypoint-modal').addEventListener('click', selectWaypoint); // desktop-on +document.querySelector('#waypoint-collection').addEventListener('click', selectWaypoint); // mobile-only diff --git a/webapp/public/js/modules/robot.js b/webapp/public/js/modules/robot.js index 6862cca..02a243c 100644 --- a/webapp/public/js/modules/robot.js +++ b/webapp/public/js/modules/robot.js @@ -164,6 +164,19 @@ class Robot { this._client.send(message); } + cmdHangup() { + console.log('[CMD] Hangup'); + + // write payload in JSON format + const payload = JSON.stringify({}); + + // publish message + const message = new Paho.Message(payload); + message.destinationName = `temi/${this._id}/command/hangup`; + message.qos = 1; + this._client.send(message); + } + // https://developer.android.com/reference/android/media/AudioManager#setStreamVolume(int,%20int,%20int) cmdVolume(volume) { console.log('[CMD] Volume'); diff --git a/webapp/public/js/modules/videoConference.js b/webapp/public/js/modules/videoConference.js index 0f786ef..823e553 100644 --- a/webapp/public/js/modules/videoConference.js +++ b/webapp/public/js/modules/videoConference.js @@ -8,21 +8,40 @@ class VideoConference { if (this._handle === undefined) { this._id = id; const domain = 'meet.jit.si'; + + // https://github.com/jitsi/jitsi-meet/blob/master/interface_config.js + // https://github.com/jitsi/jitsi-meet/blob/master/config.js const options = { roomName: `temi-${id}`, - // width: window.innerWidth, - // height: window.innerHeight, - width:800, - height: 400, - parentNode: document.getElementById('video-conference'), - configOverwrite: {}, - interfaceConfigOverwrite: { - filmStripOnly: false, + width: 800, + height: 600, + parentNode: document.getElementById('video-container'), + configOverwrite: { + enableNoAudioDetection: false }, - }; + interfaceConfigOverwrite: { + DEFAULT_BACKGROUND: '#000000', + INITIAL_TOOLBAR_TIMEOUT: 1000, + TOOLBAR_TIMEOUT: 1000, + TOOLBAR_ALWAYS_VISIBLE: false, + SHOW_JITSI_WATERMARK: false, + SHOW_WATERMARK_FOR_GUESTS: false, + TOOLBAR_BUTTONS: ['microphone', 'camera', 'closedcaptions', 'desktop', 'fullscreen', 'fodeviceselection', 'hangup', 'info', 'recording', 'sharedvideo', 'settings', 'filmstrip', 'invite', 'tileview', 'download'], + SETTINGS_SECTIONS: [ 'devices' ], + CLOSE_PAGE_GUEST_HINT: false, + SHOW_PROMOTIONAL_CLOSE_PAGE: false, + RANDOM_AVATAR_URL_PREFIX: false, + RANDOM_AVATAR_URL_SUFFIX: false, + ENABLE_FEEDBACK_ANIMATION: false, + DISABLE_FOCUS_INDICATOR: false, + DISABLE_DOMINANT_SPEAKER_INDICATOR: false, + MOBILE_APP_PROMO: false, + SHOW_CHROME_EXTENSION_BANNER: false + } + } console.log(`Open Video: ${id}`); this._handle = new JitsiMeetExternalAPI(domain, options); - + return this; } } @@ -39,8 +58,6 @@ class VideoConference { get handle() { return this._handle; } - - } export { VideoConference }; diff --git a/webapp/views/index.pug b/webapp/views/index.pug index d8a0dea..ee41a79 100644 --- a/webapp/views/index.pug +++ b/webapp/views/index.pug @@ -2,42 +2,36 @@ extends layout block content - div(style='height:100vh') - - // robot control panel - nav.transparent.z-depth-0 - .nav-wrapper - .row - .col.s12 - ul#robot-ctrl-panel.right(style='display:none;') - li - a - i#video-btn.fas.fa-video - li - a - i#battery-state - li - a.modal-trigger.show-on-large(data-target='waypoint-menu') - i.material-icons location_on + div(style='height:100vh;') // robot selection menu - #robot-menu.row.valign-wrapper(style='height:80vh') - .input-field.col.s3.offset-s5 + div#robot-menu.row.valign-wrapper + div.input-field.col.l2.offset-l5.m4.offset-m4.s6.offset-s3 input#temi_id.validate.white-text(type='text') - label(for='temi_id') temi ID - #robot-collection.collection - - // video conference div - #video-conference + label(for='temi-id') temi ID + div#robot-collection + + // control panel + div#robot-ctrl-panel.container.center-align + a#video-btn.btn-flat.white-text + i.fas.fa-video + a.btn-flat.white-text.modal-trigger(data-target='waypoint-menu') + i.material-icons location_on + a.btn-flat.white-text + i#battery-state + + // video-call div + div.row.valign-wrapper + div#video-container.col.l6.offset-l3.s12.center-align // footer - .white-text(style='position:relative') + div.white-text(style='position:relative') p(style='position:fixed; bottom:0; width:100%; text-align:center') © 2020 hapi-robo st, Inc. — All Rights Reserved. + // locations modal - #waypoint-menu.modal.bottom-sheet + div#waypoint-menu.modal(style='width:30%;padding:0') .modal-content - h4 Select Location - ul#waypoint-nav.collection(style='background-color: rgba(232,232,232,0.5);') + ul#waypoint-modal.collection diff --git a/webapp/views/test.html b/webapp/views/test.html index f572057..d56ea32 100644 --- a/webapp/views/test.html +++ b/webapp/views/test.html @@ -7,39 +7,79 @@ + + + -
- +
+ + +
+
+ + +
+
- + + + + + + From fba7aed6960a6b001157df2671c44d736b918dae Mon Sep 17 00:00:00 2001 From: Raymond Oung <44389573+ray-hrst@users.noreply.github.com> Date: Mon, 23 Mar 2020 17:26:49 +0900 Subject: [PATCH 08/15] Adds mobile view --- webapp/views/mobile.pug | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 webapp/views/mobile.pug diff --git a/webapp/views/mobile.pug b/webapp/views/mobile.pug new file mode 100644 index 0000000..bdc5d89 --- /dev/null +++ b/webapp/views/mobile.pug @@ -0,0 +1,23 @@ +extends layout + +block content + + // robot selection menu + div#robot-menu(style='display:none') + div.row.valign-wrapper(style='height:80vh') + div.input-field.col.l2.offset-l5.m4.offset-m4.s6.offset-s3 + input#temi_id.validate.white-text(type='text') + label(for='temi-id') temi ID + div#robot-collection + + // waypoint selection menu + div#waypoint-menu(style='display:none') + div.row.valign-wrapper(style='height:80vh') + div.input-field.col.l2.offset-l5.m4.offset-m4.s6.offset-s3 + div.center-align.white-text + //- i#battery-state + div#waypoint-collection + + // footer + div.white-text(style='position:relative') + p(style='position:fixed; bottom:0; width:100%; text-align:center') © 2020 hapi-robo st, Inc. — All Rights Reserved. From 9504bb5ad6a28529661befa5097205e3b5dfc72d Mon Sep 17 00:00:00 2001 From: Raymond Oung <44389573+ray-hrst@users.noreply.github.com> Date: Mon, 23 Mar 2020 17:32:13 +0900 Subject: [PATCH 09/15] Uncomments webapp --- docker-compose.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 1941246..273a000 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,9 +20,9 @@ services: volumes: - config-volume:/mosquitto/config/ # SOURCE:TARGET - # webapp: - # build: ./webapp - # depends_on: - # - mqtt_broker - # ports: - # - "80:8080" # HOST:CONTAINER + webapp: + build: ./webapp + depends_on: + - mqtt_broker + ports: + - "80:8080" # HOST:CONTAINER From 029f9cbb00262fdfe39e0e2af252c0ceaf90b8d2 Mon Sep 17 00:00:00 2001 From: Raymond Oung <44389573+ray-hrst@users.noreply.github.com> Date: Tue, 24 Mar 2020 11:31:45 +0900 Subject: [PATCH 10/15] Adds HTTPS code --- .gitignore | 2 ++ webapp/app.js | 30 +++++++++++++++++++-- webapp/public/js/main.js | 12 ++++----- webapp/public/js/modules/videoConference.js | 5 +--- 4 files changed, 37 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index c34ddf3..983688e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +*.cert +*.key *.apk webapp/node_modules/ *venv/ diff --git a/webapp/app.js b/webapp/app.js index b203e01..7b48aaa 100644 --- a/webapp/app.js +++ b/webapp/app.js @@ -11,6 +11,8 @@ // required libraries const express = require("express"); const path = require("path"); +const https = require("https"); +const fs = require("fs"); // instantiate webapp const app = express(); @@ -26,6 +28,9 @@ app.set("view engine", "pug"); // serve static files app.use(express.static(path.join(__dirname, "public"))); +/* + * Setup routes + */ // mobile route app.get("/mobile", function(req, res) { res.render("mobile", {}); @@ -38,6 +43,9 @@ app.get("/", function(req, res) { }); }); +/* + * Catch errors + */ // catch 404 and forward to the error handler app.use((req, res, next) => { var err = new Error("Not Found"); @@ -59,10 +67,28 @@ app.use((err, req, res, next) => { /* * Start listening on the port */ +// HTTP app.listen(port, err => { if (err) { return console.log(`Something bad happened ${err}`); + } else { + console.log(`Server is listening on localhost:${port}`); } - - console.log(`Server is listening on localhost:${port}`); }); + +// HTTPS +// https +// .createServer( +// { +// key: fs.readFileSync("./security/server.key"), +// cert: fs.readFileSync("./security/server.cert") +// }, +// app +// ) +// .listen(port, err => { +// if (err) { +// return console.log(`Something bad happened ${err}`); +// } else { +// console.log(`Server is listening on localhost:${port}`); +// } +// }); diff --git a/webapp/public/js/main.js b/webapp/public/js/main.js index b2cf1c9..9f90551 100644 --- a/webapp/public/js/main.js +++ b/webapp/public/js/main.js @@ -286,9 +286,9 @@ function updateRobotList(id, payload) { * General message callback */ function onMessageArrived(message) { - console.log('[RECIEVE]'); - console.log(`Topic: ${message.destinationName}`); - console.log(`Payload: ${message.payloadString}`); + // console.log('[RECIEVE]'); + // console.log(`Topic: ${message.destinationName}`); + // console.log(`Payload: ${message.payloadString}`); // parse message const topicTree = message.destinationName.split('/'); @@ -296,9 +296,9 @@ function onMessageArrived(message) { const type = topicTree[2]; // [status, command] const category = topicTree[3]; - console.log(`Robot-ID: ${robotID}`); - console.log(`Type: ${type}`); - console.log(`Category: ${category}`); + // console.log(`Robot-ID: ${robotID}`); + // console.log(`Type: ${type}`); + // console.log(`Category: ${category}`); if (robotID === undefined) { console.warn('Message from undefined robot received'); diff --git a/webapp/public/js/modules/videoConference.js b/webapp/public/js/modules/videoConference.js index 823e553..6c01df9 100644 --- a/webapp/public/js/modules/videoConference.js +++ b/webapp/public/js/modules/videoConference.js @@ -15,10 +15,7 @@ class VideoConference { roomName: `temi-${id}`, width: 800, height: 600, - parentNode: document.getElementById('video-container'), - configOverwrite: { - enableNoAudioDetection: false - }, + parentNode: document.querySelector('#video-container'), interfaceConfigOverwrite: { DEFAULT_BACKGROUND: '#000000', INITIAL_TOOLBAR_TIMEOUT: 1000, From 407a175905bafd76ef533adfc468d66e871d66bd Mon Sep 17 00:00:00 2001 From: Raymond Oung <44389573+ray-hrst@users.noreply.github.com> Date: Tue, 24 Mar 2020 15:19:22 +0900 Subject: [PATCH 11/15] Adds TTS; Adds hangup --- android/app/build.gradle | 3 +- .../com/hapirobo/connect/MainActivity.java | 96 +++++++++++++++++-- webapp/public/js/main.js | 18 +++- webapp/public/js/modules/robot.js | 13 +++ webapp/public/js/modules/videoConference.js | 1 + webapp/views/index.pug | 10 +- 6 files changed, 129 insertions(+), 12 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 56a77b3..990565a 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -47,7 +47,8 @@ dependencies { // Jitsi Meet // https://github.com/jitsi/jitsi-meet - implementation ('org.jitsi.react:jitsi-meet-sdk:2.5.1') { transitive = true } + // https://github.com/jitsi/jitsi-maven-repository/tree/master/releases/org/jitsi/react/jitsi-meet-sdk + implementation ('org.jitsi.react:jitsi-meet-sdk:2.6.1') { transitive = true } // paho-mqtt // https://github.com/eclipse/paho.mqtt.java diff --git a/android/app/src/main/java/com/hapirobo/connect/MainActivity.java b/android/app/src/main/java/com/hapirobo/connect/MainActivity.java index 7ba978d..3d96290 100644 --- a/android/app/src/main/java/com/hapirobo/connect/MainActivity.java +++ b/android/app/src/main/java/com/hapirobo/connect/MainActivity.java @@ -3,6 +3,7 @@ import androidx.appcompat.app.AppCompatActivity; import android.content.Context; +import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.util.Log; @@ -11,8 +12,10 @@ import android.widget.EditText; import android.widget.Toast; +import com.facebook.react.modules.core.PermissionListener; import com.robotemi.sdk.BatteryData; import com.robotemi.sdk.Robot; +import com.robotemi.sdk.TtsRequest; import com.robotemi.sdk.listeners.OnBatteryStatusChangedListener; import com.robotemi.sdk.listeners.OnDetectionStateChangedListener; import com.robotemi.sdk.listeners.OnGoToLocationStatusChangedListener; @@ -30,8 +33,10 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jitsi.meet.sdk.JitsiMeet; -import org.jitsi.meet.sdk.JitsiMeetActivity; +import org.jitsi.meet.sdk.JitsiMeetActivityDelegate; +import org.jitsi.meet.sdk.JitsiMeetActivityInterface; import org.jitsi.meet.sdk.JitsiMeetConferenceOptions; +import org.jitsi.meet.sdk.JitsiMeetView; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -43,6 +48,7 @@ import java.util.Objects; public class MainActivity extends AppCompatActivity implements + JitsiMeetActivityInterface, OnRobotReadyListener, OnBatteryStatusChangedListener, OnGoToLocationStatusChangedListener, @@ -51,6 +57,7 @@ public class MainActivity extends AppCompatActivity implements private static final String TAG = "DEBUG"; private static Handler sHandler = new Handler(); + private static JitsiMeetView sView; private static Robot sRobot; private static String sSerialNumber; private MqttAndroidClient mMqttClient; @@ -121,6 +128,8 @@ protected void onStop() { sRobot.removeOnGoToLocationStatusChangedListener(this); sRobot.removeDetectionStateChangedListener(this); sRobot.removeOnUserInteractionChangedListener(this); + + JitsiMeetActivityDelegate.onHostPause(this); } /** @@ -129,6 +138,8 @@ protected void onStop() { @Override protected void onDestroy() { super.onDestroy(); + + // disconnect MQTT client from broker if (mMqttClient != null && mMqttClient.isConnected()) { try { Log.i(TAG, "[MQTT] Disconnecting MQTT client from broker"); @@ -139,6 +150,11 @@ protected void onDestroy() { e.printStackTrace(); } } + + // remote Jitsi view + sView.dispose(); + sView = null; + JitsiMeetActivityDelegate.onHostDestroy(this); } /** @@ -428,6 +444,14 @@ private void parseMessage(String topic, JSONObject payload) throws JSONException startCall(); break; + case "hangup": + hangupCall(); + break; + + case "tts": + sRobot.speak(TtsRequest.create(payload.getString("utterance"), true)); + break; + default: Log.i(TAG, "Invalid topic: " + topic); break; @@ -512,18 +536,78 @@ private void parseMove(String command, JSONObject payload) throws JSONException } } + /** + * Start video-call + */ private void startCall() { Log.v(TAG, "[CMD][CALL]"); - // Build options object for joining the conference. The SDK will merge the default - // one we set earlier and this one when joining. + // build options object for joining the conference + // the SDK will merge the default one we set earlier and this one when joining JitsiMeetConferenceOptions options = new JitsiMeetConferenceOptions.Builder() .setRoom("temi-" + sSerialNumber) .build(); - // Launch the new activity with the given options. The launch() method takes care - // of creating the required Intent and passing the options. - JitsiMeetActivity.launch(this, options); + // Launch the new view with the given options + sView = new JitsiMeetView(this); + sView.join(options); + setContentView(sView); + } + + /** + * Hangup video call + */ + private void hangupCall() { + Log.v(TAG, "[CMD][HANGUP]"); + + sView.leave(); + sView.dispose(); + sView = null; + JitsiMeetActivityDelegate.onHostDestroy(this); + + // set main menu + setContentView(R.layout.activity_main); + } + + @Override + protected void onActivityResult( + int requestCode, + int resultCode, + Intent data) { + super.onActivityResult(requestCode, resultCode, data); + JitsiMeetActivityDelegate.onActivityResult( + this, requestCode, resultCode, data); + } + + @Override + public void onBackPressed() { + JitsiMeetActivityDelegate.onBackPressed(); + } + + @Override + public void onNewIntent(Intent intent) { + super.onNewIntent(intent); + JitsiMeetActivityDelegate.onNewIntent(intent); + } + + @Override + public void onRequestPermissionsResult( + final int requestCode, + final String[] permissions, + final int[] grantResults) { + JitsiMeetActivityDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + + @Override + protected void onResume() { + super.onResume(); + + JitsiMeetActivityDelegate.onHostResume(this); + } + + @Override + public void requestPermissions(String[] strings, int i, PermissionListener permissionListener) { + } } diff --git a/webapp/public/js/main.js b/webapp/public/js/main.js index 9f90551..a672191 100644 --- a/webapp/public/js/main.js +++ b/webapp/public/js/main.js @@ -90,6 +90,7 @@ function showRobotMenu() { // hide other menus document.querySelector('#robot-ctrl-panel').style.display = 'none'; + document.querySelector('#tts-input').style.display = 'none'; } function showWaypointMenu() { @@ -101,6 +102,7 @@ function showWaypointMenu() { // hide other menus document.querySelector('#robot-menu').style.display = 'none'; + document.querySelector('#tts-input').style.display = 'none'; } function showCtrlPanel() { @@ -109,6 +111,7 @@ function showCtrlPanel() { // show control panel document.querySelector('#robot-ctrl-panel').style.display = 'block'; document.querySelector('#video-btn').className = 'btn-flat white-text'; + document.querySelector('#tts-input').style.display = 'block'; // hide other menus document.querySelector('#robot-menu').style.display = 'none'; @@ -133,7 +136,6 @@ function keyboardEvent(e) { classes: 'rounded', }); } - break; default: @@ -176,6 +178,13 @@ function keyboardEvent(e) { selectedRobot.cmdTiltDown(); break; + case 13: // Enter + console.log('[Keycode] Enter'); + const utterance = document.querySelector('#tts-input').value; + selectedRobot.cmdTts(utterance); + document.querySelector('#tts-input').value = ''; + break; + default: break; } @@ -225,9 +234,9 @@ function startVideoCall() { // TODO: This needs to be cleaned up videoCall.handle.on('readyToClose', () => { console.log('Closing Video Conference...'); - videoCall.handle.dispose(); - // videoCall.handle.close(); - // TODO: end call on the robot's side; how to know if other users aren't using the robot? + videoCall.close(); + // TODO: how to know if other users aren't using the robot? + selectedRobot.cmdHangup(); showRobotMenu(); }); } @@ -397,6 +406,7 @@ document.addEventListener('DOMContentLoaded', showWaypointNav); document.addEventListener('keydown', keyboardEvent); document.querySelector('#robot-collection').addEventListener('click', selectRobot); +document.querySelector('#home-btn').addEventListener('click', showRobotMenu); document.querySelector('#video-btn').addEventListener('click', startVideoCall); document.querySelector('#waypoint-modal').addEventListener('click', selectWaypoint); // desktop-on document.querySelector('#waypoint-collection').addEventListener('click', selectWaypoint); // mobile-only diff --git a/webapp/public/js/modules/robot.js b/webapp/public/js/modules/robot.js index 02a243c..0d330b9 100644 --- a/webapp/public/js/modules/robot.js +++ b/webapp/public/js/modules/robot.js @@ -177,6 +177,19 @@ class Robot { this._client.send(message); } + cmdTts(utterance) { + console.log('[CMD] TTS'); + + // write payload in JSON format + const payload = JSON.stringify({ utterance: utterance }); + + // publish message + const message = new Paho.Message(payload); + message.destinationName = `temi/${this._id}/command/tts`; + message.qos = 1; + this._client.send(message); + } + // https://developer.android.com/reference/android/media/AudioManager#setStreamVolume(int,%20int,%20int) cmdVolume(volume) { console.log('[CMD] Volume'); diff --git a/webapp/public/js/modules/videoConference.js b/webapp/public/js/modules/videoConference.js index 6c01df9..cd86fc9 100644 --- a/webapp/public/js/modules/videoConference.js +++ b/webapp/public/js/modules/videoConference.js @@ -47,6 +47,7 @@ class VideoConference { if (this._handle !== undefined) { console.log(`Close Video: ${this._id}`); this._handle.executeCommand('hangup'); + this._handle.dispose(); this._id = undefined; this._handle = undefined; } diff --git a/webapp/views/index.pug b/webapp/views/index.pug index ee41a79..e25948c 100644 --- a/webapp/views/index.pug +++ b/webapp/views/index.pug @@ -7,12 +7,14 @@ block content // robot selection menu div#robot-menu.row.valign-wrapper div.input-field.col.l2.offset-l5.m4.offset-m4.s6.offset-s3 - input#temi_id.validate.white-text(type='text') + input#temi-id.validate.white-text(type='text') label(for='temi-id') temi ID div#robot-collection // control panel div#robot-ctrl-panel.container.center-align + a#home-btn.btn-flat.white-text + i.fas.fa-home a#video-btn.btn-flat.white-text i.fas.fa-video a.btn-flat.white-text.modal-trigger(data-target='waypoint-menu') @@ -24,6 +26,12 @@ block content div.row.valign-wrapper div#video-container.col.l6.offset-l3.s12.center-align + // text input + div.row + div.input-field.col.l2.offset-l5.m4.offset-m4.s6.offset-s3 + input#tts-input.validate.white-text(type='text') + label(for='tts-input') Type to speak + // footer div.white-text(style='position:relative') p(style='position:fixed; bottom:0; width:100%; text-align:center') © 2020 hapi-robo st, Inc. — All Rights Reserved. From 19e3ea4b32e3b8ab74d9abe104a8c0dc55c1a25b Mon Sep 17 00:00:00 2001 From: Raymond Oung <44389573+ray-hrst@users.noreply.github.com> Date: Tue, 24 Mar 2020 15:28:32 +0900 Subject: [PATCH 12/15] Fixes text-input ID mixup --- webapp/public/js/main.js | 10 +++++----- webapp/views/index.pug | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/webapp/public/js/main.js b/webapp/public/js/main.js index a672191..1a41088 100644 --- a/webapp/public/js/main.js +++ b/webapp/public/js/main.js @@ -90,7 +90,7 @@ function showRobotMenu() { // hide other menus document.querySelector('#robot-ctrl-panel').style.display = 'none'; - document.querySelector('#tts-input').style.display = 'none'; + document.querySelector('#text-input').style.display = 'none'; } function showWaypointMenu() { @@ -102,7 +102,7 @@ function showWaypointMenu() { // hide other menus document.querySelector('#robot-menu').style.display = 'none'; - document.querySelector('#tts-input').style.display = 'none'; + document.querySelector('#text-input').style.display = 'none'; } function showCtrlPanel() { @@ -111,7 +111,7 @@ function showCtrlPanel() { // show control panel document.querySelector('#robot-ctrl-panel').style.display = 'block'; document.querySelector('#video-btn').className = 'btn-flat white-text'; - document.querySelector('#tts-input').style.display = 'block'; + document.querySelector('#text-input').style.display = 'block'; // hide other menus document.querySelector('#robot-menu').style.display = 'none'; @@ -180,9 +180,9 @@ function keyboardEvent(e) { case 13: // Enter console.log('[Keycode] Enter'); - const utterance = document.querySelector('#tts-input').value; + const utterance = document.querySelector('#utterance').value; selectedRobot.cmdTts(utterance); - document.querySelector('#tts-input').value = ''; + document.querySelector('#utterance').value = ''; break; default: diff --git a/webapp/views/index.pug b/webapp/views/index.pug index e25948c..aa6b531 100644 --- a/webapp/views/index.pug +++ b/webapp/views/index.pug @@ -27,10 +27,10 @@ block content div#video-container.col.l6.offset-l3.s12.center-align // text input - div.row + div#text-input.row div.input-field.col.l2.offset-l5.m4.offset-m4.s6.offset-s3 - input#tts-input.validate.white-text(type='text') - label(for='tts-input') Type to speak + input#utterance.validate.white-text(type='text') + label(for='utterance') Type to speak // footer div.white-text(style='position:relative') From 93ad989be418e389c42c910c5ba2a7eadc8370b0 Mon Sep 17 00:00:00 2001 From: Raymond Oung <44389573+ray-hrst@users.noreply.github.com> Date: Thu, 26 Mar 2020 12:14:23 +0900 Subject: [PATCH 13/15] Reverts to using Jitsi Activity --- android/app/build.gradle | 2 +- .../com/hapirobo/connect/MainActivity.java | 96 ++++++++++--------- 2 files changed, 50 insertions(+), 48 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 990565a..a21e262 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -48,7 +48,7 @@ dependencies { // Jitsi Meet // https://github.com/jitsi/jitsi-meet // https://github.com/jitsi/jitsi-maven-repository/tree/master/releases/org/jitsi/react/jitsi-meet-sdk - implementation ('org.jitsi.react:jitsi-meet-sdk:2.6.1') { transitive = true } + implementation ('org.jitsi.react:jitsi-meet-sdk:2.7.0') { transitive = true } // paho-mqtt // https://github.com/eclipse/paho.mqtt.java diff --git a/android/app/src/main/java/com/hapirobo/connect/MainActivity.java b/android/app/src/main/java/com/hapirobo/connect/MainActivity.java index 3d96290..990e565 100644 --- a/android/app/src/main/java/com/hapirobo/connect/MainActivity.java +++ b/android/app/src/main/java/com/hapirobo/connect/MainActivity.java @@ -33,6 +33,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jitsi.meet.sdk.JitsiMeet; +import org.jitsi.meet.sdk.JitsiMeetActivity; import org.jitsi.meet.sdk.JitsiMeetActivityDelegate; import org.jitsi.meet.sdk.JitsiMeetActivityInterface; import org.jitsi.meet.sdk.JitsiMeetConferenceOptions; @@ -48,7 +49,7 @@ import java.util.Objects; public class MainActivity extends AppCompatActivity implements - JitsiMeetActivityInterface, +// JitsiMeetActivityInterface, OnRobotReadyListener, OnBatteryStatusChangedListener, OnGoToLocationStatusChangedListener, @@ -152,9 +153,9 @@ protected void onDestroy() { } // remote Jitsi view - sView.dispose(); - sView = null; - JitsiMeetActivityDelegate.onHostDestroy(this); +// sView.dispose(); +// sView = null; +// JitsiMeetActivityDelegate.onHostDestroy(this); } /** @@ -550,9 +551,10 @@ private void startCall() { .build(); // Launch the new view with the given options - sView = new JitsiMeetView(this); - sView.join(options); - setContentView(sView); +// sView = new JitsiMeetView(this); +// sView.join(options); +// setContentView(sView); + JitsiMeetActivity.launch(this, options); } /** @@ -570,44 +572,44 @@ private void hangupCall() { setContentView(R.layout.activity_main); } - @Override - protected void onActivityResult( - int requestCode, - int resultCode, - Intent data) { - super.onActivityResult(requestCode, resultCode, data); - JitsiMeetActivityDelegate.onActivityResult( - this, requestCode, resultCode, data); - } - - @Override - public void onBackPressed() { - JitsiMeetActivityDelegate.onBackPressed(); - } - - @Override - public void onNewIntent(Intent intent) { - super.onNewIntent(intent); - JitsiMeetActivityDelegate.onNewIntent(intent); - } - - @Override - public void onRequestPermissionsResult( - final int requestCode, - final String[] permissions, - final int[] grantResults) { - JitsiMeetActivityDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults); - } - - @Override - protected void onResume() { - super.onResume(); - - JitsiMeetActivityDelegate.onHostResume(this); - } - - @Override - public void requestPermissions(String[] strings, int i, PermissionListener permissionListener) { - - } +// @Override +// protected void onActivityResult( +// int requestCode, +// int resultCode, +// Intent data) { +// super.onActivityResult(requestCode, resultCode, data); +// JitsiMeetActivityDelegate.onActivityResult( +// this, requestCode, resultCode, data); +// } + +// @Override +// public void onBackPressed() { +// JitsiMeetActivityDelegate.onBackPressed(); +// } + +// @Override +// public void onNewIntent(Intent intent) { +// super.onNewIntent(intent); +// JitsiMeetActivityDelegate.onNewIntent(intent); +// } + +// @Override +// public void onRequestPermissionsResult( +// final int requestCode, +// final String[] permissions, +// final int[] grantResults) { +// JitsiMeetActivityDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults); +// } + +// @Override +// protected void onResume() { +// super.onResume(); +// +// JitsiMeetActivityDelegate.onHostResume(this); +// } + +// @Override +// public void requestPermissions(String[] strings, int i, PermissionListener permissionListener) { +// +// } } From c87789a4aabbc3e9b794f35566aab061a60773c7 Mon Sep 17 00:00:00 2001 From: Raymond Oung Date: Thu, 26 Mar 2020 13:50:56 +0900 Subject: [PATCH 14/15] Removes keyboard asdw keys --- test/test_listener.py | 4 +-- webapp/public/favicon.ico | Bin 1150 -> 0 bytes webapp/public/js/main.js | 8 +++--- webapp/views/test.html | 53 +++++++++++++++++++++++++++++++++++ webapp/views/test_jitsi.html | 28 ++++++++++++++++++ 5 files changed, 87 insertions(+), 6 deletions(-) delete mode 100644 webapp/public/favicon.ico create mode 100644 webapp/views/test.html create mode 100644 webapp/views/test_jitsi.html diff --git a/test/test_listener.py b/test/test_listener.py index 2c6c514..9900b99 100755 --- a/test/test_listener.py +++ b/test/test_listener.py @@ -16,8 +16,8 @@ CLIENT_ID = 'test-listener' # MQTT broker -MQTT_BROKER_HOST = 'localhost' -# MQTT_BROKER_HOST = '192.168.0.118' +# MQTT_BROKER_HOST = 'localhost' +MQTT_BROKER_HOST = '192.168.0.177' MQTT_BROKER_PORT = 1883 # connection parameters diff --git a/webapp/public/favicon.ico b/webapp/public/favicon.ico deleted file mode 100644 index 1024fcd79f5029fecdf94abd88d399eee19a15d4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1150 zcmbW#TPtQ!7{Kx66v=T&a(tBwC6r{6Xi6k;!PHzyLLpa}C`!zXbD0^1kWiFd`3P#h zl*{@5**khm;d%S*wf1`V+WUFdS|OzHD<}wD6JaVngpv?K9T;NdX`Chm{`;5r!znItglp_$4@>Zyab(fiLNWR=ifuJGR8548BAgmZdn6d%N3YQ0_%}#TK&Q-zG48b`3Kv3 z5nA{64z5ufQeob%zx8+GH_X9W&Bt|=eaC(x8_qSX*ELSS@Cf>@IEv`VXomZr9Oi6Y z?y=u#j9UA#4*I)jM{=0U7g+ZqzT+H+QQtAwtQBvG_)IVGa9rnG=Ecu=hJL_&J)iFl dYjz);@0x!kJnN+&nfThARZjIiC;5R>sQ-tTJt+VH diff --git a/webapp/public/js/main.js b/webapp/public/js/main.js index f6eb0a4..c3f084f 100644 --- a/webapp/public/js/main.js +++ b/webapp/public/js/main.js @@ -82,25 +82,25 @@ function keyboardEvent(e) { } else { switch (e.keyCode) { case 37: // ArrowLeft - case 65: // a + // case 65: // a console.log('[Keycode] ArrowLeft / a'); selectedRobot.cmdTurnLeft(); break; case 39: // ArrowRight - case 68: // d + // case 68: // d console.log('[Keycode] ArrowRight / d'); selectedRobot.cmdTurnRight(); break; case 38: // ArrowUp - case 87: // w + // case 87: // w console.log('[Keycode] ArrowUp / w'); selectedRobot.cmdMoveFwd(); break; case 40: // ArrowDown - case 83: // s + // case 83: // s console.log('[Keycode] ArrowDown / s'); selectedRobot.cmdMoveBwd(); break; diff --git a/webapp/views/test.html b/webapp/views/test.html new file mode 100644 index 0000000..1c47401 --- /dev/null +++ b/webapp/views/test.html @@ -0,0 +1,53 @@ + + + + + Test + + + + + + + + + + + +
+ + + + +
+

Footer Text

+
+ + + + + + + \ No newline at end of file diff --git a/webapp/views/test_jitsi.html b/webapp/views/test_jitsi.html new file mode 100644 index 0000000..b2cf9cd --- /dev/null +++ b/webapp/views/test_jitsi.html @@ -0,0 +1,28 @@ + + + + + + Test Jitsi + + + + + +
+ + + + + + + \ No newline at end of file From 9df6ea67581e7e18f1a5994eb90604bcf3f6a26f Mon Sep 17 00:00:00 2001 From: Raymond Oung Date: Thu, 26 Mar 2020 14:06:51 +0900 Subject: [PATCH 15/15] Remaps tilt keys --- webapp/public/js/main.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/public/js/main.js b/webapp/public/js/main.js index 7547464..febc67d 100644 --- a/webapp/public/js/main.js +++ b/webapp/public/js/main.js @@ -168,12 +168,12 @@ function keyboardEvent(e) { selectedRobot.cmdMoveBwd(); break; - case 85: // u + case 187: // = console.log('[Keycode] u'); selectedRobot.cmdTiltUp(); break; - case 74: // j + case 189: // - console.log('[Keycode] j'); selectedRobot.cmdTiltDown(); break;