2

I am using Websocket, for a communication from my mobile phone to a NodeMCU, and transmitting 4 channel, data, through 4 different Websocket Ports. the NodeMCU control code is here:

// ============ By Pritam Pagla ============ //
// The Only Websocket Program is attached within master branch
// NodeMCU SIDE
#include <SoftwareSerial.h>
#include <ArduinoJson.h> // Preferably use ArduinoJson 5.x, as StaticJsonBuffer object is not defined in further versions
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include<WebSocketsServer.h>
#include "index_NodeMCU_Test1.h"; // Complete Webpage
SoftwareSerial s(D6,D5); // Rx as D6 and Tx and D5
int16_t throttle_Val = 0;
int16_t yaw_Val = 0;
int16_t pitch_Val = 0; 
int16_t roll_Val = 0;
uint8_t num; 
WStype_t type; 
uint8_t *payload; 
size_t length;
//WiFi Connection configuration
const char *ssid = "PriCopter"; // Can choose these anything one wants
const char *password = "01234567";
WebSocketsServer Twebsocket = WebSocketsServer(9001); 
WebSocketsServer Ywebsocket = WebSocketsServer(9002);
WebSocketsServer Rwebsocket = WebSocketsServer(9003);
WebSocketsServer Pwebsocket = WebSocketsServer(9004);
ESP8266WebServer server(80);
//================================================
void handleRoot() {
 String s = MAIN_page; //Read HTML contents
 server.send(200, "text/html", s); //Send web page
}
//================================================
// Setup
//================================================
 
void setup() {
s.begin(115200);
Serial.begin(115200);
 delay(100);
 
 //Setup the NodeMCU as an AP
 WiFi.softAP(ssid, password, 1, false, 1); //Use NodeMCU as an access point
 //If connection successful show IP address in serial monitor
 IPAddress IP = WiFi.softAPIP();
 Serial.print("AP IP address: ");
 Serial.println(IP);
 //Initialize Webserver
 server.on("/",handleRoot);
 
 server.begin(); 
 Twebsocket.begin();
 Ywebsocket.begin();
 Rwebsocket.begin();
 Pwebsocket.begin();
}
 
void loop() {
 StaticJsonBuffer<100> jsonBuffer;
 JsonObject& root = jsonBuffer.createObject();
 server.handleClient();
 Twebsocket.loop();
 Ywebsocket.loop();
 Rwebsocket.loop();
 Pwebsocket.loop();
 
 Twebsocket.onEvent(ThrottleEvent);
 Ywebsocket.onEvent(YawEvent);
 Rwebsocket.onEvent(RollEvent);
 Pwebsocket.onEvent(PitchEvent);
 
 Serial.print("Throttle: "); Serial.print(throttle_Val);
 Serial.print(" Yaw: "); Serial.print(yaw_Val);
 Serial.print(" Pitch: "); Serial.print(pitch_Val);
 Serial.print(" Roll: "); Serial.print(roll_Val);
 
 root["throttle"] = throttle_Val;
 root["yaw"] = yaw_Val;
 root["roll"] = pitch_Val;
 root["pitch"] = roll_Val;
 
 root.printTo(s);
 Serial.println(" (Sent)");
}
void YawEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length){
 if (type == WStype_TEXT){
 yaw_Val = (int16_t)strtol((const char*) &payload[0], NULL, 10);
 }
}
void ThrottleEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length){
 if (type == WStype_TEXT){
 throttle_Val = (int16_t)strtol((const char*) &payload[0], NULL, 10);
 }
}
void RollEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length){
 if (type == WStype_TEXT){
 roll_Val = (int16_t)strtol((const char*) &payload[0], NULL, 10);
 }
}
void PitchEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length){
 if (type == WStype_TEXT){
 pitch_Val = (int16_t)strtol((const char*) &payload[0], NULL, 10);
 }
}

and requiring Webpage details is as:

const char MAIN_page[] PROGMEM = R"=====(<!DOCTYPE html>
<html lang = 'en'>
 <head>
 <meta charset="UTF-8"/>
 <meta name="viewport"
 content="width=950, height=420 initial-scale=1.0"/>
 <meta http-equiv="X-UA-Compatible"
 content="ie=edge"/>
 <title>Drone Remote</title>
 <style>
 body{
 background-color: #333;
 color: 'black';
 display: flex;
 flex-direction: row;
 align-items: center;
 font-family: Arial, Helvetica, sans-serif;
 min-height: 100vh;
 margin:0;
 }
 #canvasL{
 background: #f0f0f0;
 border-radius: 5x
 }
 #canvasM{
 background: #f0f0f0;
 border-radius: 5px
 }
 #canvasR{
 background: #f0f0f0;
 border-radius: 5px
 }
 
 #source{
 display: none;
 }
 </style>
 </head>
 <body>
 <canvas id="canvasL" width="450" height="420"></canvas>
 <canvas id="canvasM" width="50" height="420"></canvas>
 <canvas id="canvasR" width="450" height="420"></canvas>
 <img src="data:image/png;base64,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" alt="" id="source"/>
 <script>
 const canvasL = document.getElementById('canvasL'); // Object for left canvas
const Lctx = canvasL.getContext('2d');
const canvasM = document.getElementById('canvasM'); // Object for Middle canvas
const Mctx = canvasM.getContext('2d');
const canvasR = document.getElementById('canvasR'); // Object for Right canvas
const Rctx = canvasR.getContext('2d');
const LcenterX = (canvasL.width / 2); // Marking centre for left Joystick
const LcenterY = (canvasL.height / 2);
const RcenterX = (canvasR.width / 2); // Marking centre for Right Joystick
const RcenterY = (canvasR.height / 2);
const McenterX = (canvasM.width / 2); // Marking centre for Middle Portion
const McenterY = (canvasM.height / 2);
var Tsocket = new WebSocket('ws://' + window.location.hostname + ':9001/');
var Ysocket = new WebSocket('ws://' + window.location.hostname + ':9002/');
var Psocket = new WebSocket('ws://' + window.location.hostname + ':9003/');
var Rsocket = new WebSocket('ws://' + window.location.hostname + ':9004/');
const image = document.getElementById('source');
var throttle_Val = 0;
var yaw_Val = 0;
var pitch_Val = 0;
var roll_Val = 0;
const throttle = "t";
const yaw = "y";
const pitch = "p";
const roll = "r";
const LJoyStickBall = { // defining left joystick as an object
 w: 60,
 h: 60,
 x: LcenterX,
 y: LcenterY+195,
 speed: 1,
 dx: 0,
 dy: 0
};
const RJoyStickBall = { // defining right joystick as an object
 w: 60,
 h: 60,
 x: RcenterX,
 y: RcenterY,
 speed: 1,
 dx: 0,
 dy: 0
};
function drawLeftCenterBall(){ //Drawing the Left Joystick ball
 Lctx.drawImage(image, LJoyStickBall.x-30, LJoyStickBall.y-30, LJoyStickBall.w, LJoyStickBall.h);
}
function drawRightCenterBall(){ //Drawing the Right Joystick ball
Rctx.drawImage(image, RJoyStickBall.x-30, RJoyStickBall.y-30, RJoyStickBall.w, RJoyStickBall.h);
}
function LnewPos() { // changing position for keyboard activity
 LJoyStickBall.x += LJoyStickBall.dx;
 LJoyStickBall.y += LJoyStickBall.dy;
 LdetectBoundary();
}
function RnewPos() { // changing position for keyboard activity
 RJoyStickBall.x += RJoyStickBall.dx;
 RJoyStickBall.y += RJoyStickBall.dy;
 RdetectBoundary();
}
function LdetectBoundary(){ // defining the boundary for the LeftJoystickCenterBall
 // Left side
 if (LJoyStickBall.x < LcenterX-195) {
 LJoyStickBall.x = LcenterX-195;
 }
 // Right side
 if (LJoyStickBall.x > LcenterX+195) {
 LJoyStickBall.x = LcenterX+195;
 }
 // Top side
 if (LJoyStickBall.y < LcenterY-195) {
 LJoyStickBall.y = LcenterY-195;
 }
 // Bottom side
 if (LJoyStickBall.y > LcenterY+195) {
 LJoyStickBall.y = LcenterY+195;
 }
}
function RdetectBoundary(){ // defining the boundary for the LeftJoystickCenterBall
 // Left side
 if (RJoyStickBall.x < RcenterX-195) {
 RJoyStickBall.x = RcenterX-195;
 }
 // Right side
 if (RJoyStickBall.x > RcenterX+195) {
 RJoyStickBall.x = RcenterX+195;
 }
 // Top side
 if (RJoyStickBall.y < RcenterY-195) {
 RJoyStickBall.y = RcenterY-195;
 }
 // Bottom side
 if (RJoyStickBall.y > RcenterY+195) {
 RJoyStickBall.y = RcenterY+195;
 }
}
function drawDesigns(){ // 4 Convrntric circles and 2 crossed st lines for better interface
 Lctx.beginPath();
 Lctx.lineWidth = 2.5;
 Lctx.moveTo(LcenterX, LcenterY-195); // Crossing lines on left
 Lctx.lineTo(LcenterX, LcenterY+195);
 Lctx.moveTo(LcenterX-195, LcenterY);
 Lctx.lineTo(LcenterX+195, LcenterY);
 Lctx.arc(LcenterX, LcenterY, 195, 0, Math.PI * 2); //Concentric circles on Left
 Lctx.arc(LcenterX, LcenterY, 150, 0, Math.PI * 2);
 Lctx.arc(LcenterX, LcenterY, 100, 0, Math.PI * 2);
 Lctx.arc(LcenterX, LcenterY, 50, 0, Math.PI * 2);
 Lctx.stroke();
 Rctx.beginPath();
 Rctx.lineWidth = 2.5;
 Rctx.moveTo(RcenterX, RcenterY-195); // Crossing lines on Right
 Rctx.lineTo(RcenterX, RcenterY+195);
 Rctx.moveTo(RcenterX-195, RcenterY);
 Rctx.lineTo(RcenterX+195, RcenterY);
 Rctx.arc(RcenterX, RcenterY, 195, 0, Math.PI * 2); //Concentric circles on Right
 Rctx.arc(RcenterX, RcenterY, 150, 0, Math.PI * 2);
 Rctx.arc(RcenterX, RcenterY, 100, 0, Math.PI * 2);
 Rctx.arc(RcenterX, RcenterY, 50, 0, Math.PI * 2);
 Rctx.stroke();
}
function clearScreen(){ // Clearing the LeftCanvas and RightCanvas on every loop of the Animation
 Lctx.clearRect(0, 0, canvasL.width, canvasL.height);
 Rctx.clearRect(0, 0, canvasR.width, canvasR.height);
}
function ConsoleOUT(a,b,c,d){
 throttle_Val = (405-a);
 yaw_Val = (b-225);
 pitch_Val = (210-c);
 roll_Val = (d-225);
 console.log('Throttle: '+throttle_Val+' Yaw: '+yaw_Val+' Pitch: '+pitch_Val+' Roll: '+roll_Val);
}
function LeftUpdate(){
 clearScreen(); //Left Joystick
 drawDesigns();
 drawLeftCenterBall();
 drawRightCenterBall();
 LnewPos();
 RnewPos();
 ConsoleOUT(LJoyStickBall.y,LJoyStickBall.x,RJoyStickBall.y,RJoyStickBall.x);
 requestAnimationFrame(LeftUpdate);
 
}
function RightUpdate(){ //Right Joystick
 clearScreen();
 drawDesigns();
 drawLeftCenterBall();
 drawRightCenterBall();
 LnewPos();
 RnewPos();
 ConsoleOUT(LJoyStickBall.y,LJoyStickBall.x,RJoyStickBall.y,RJoyStickBall.x);
 requestAnimationFrame(RightUpdate);
}
function LmoveUp() { // For KeyboardActivity 
 LJoyStickBall.dy = -LJoyStickBall.speed;
}
function LmoveDown() {
 LJoyStickBall.dy = LJoyStickBall.speed;
}
function LmoveRight() {
 LJoyStickBall.dx = LJoyStickBall.speed;
}
function LmoveLeft() {
 LJoyStickBall.dx = -LJoyStickBall.speed;
}
function RmoveUp() {
 RJoyStickBall.dy = -RJoyStickBall.speed;
}
function RmoveDown() {
 RJoyStickBall.dy = RJoyStickBall.speed;
}
function RmoveRight() {
 RJoyStickBall.dx = RJoyStickBall.speed;
}
function RmoveLeft() {
 RJoyStickBall.dx = -RJoyStickBall.speed;
}
canvasL.addEventListener('touchstart', function(e){ // 'touchstart' event in LeftCanvas
 e.preventDefault(); 
 canvasL.addEventListener('touchmove', function(e){
 var touchobj = e.touches[0]; // reference first touch point for this event
 LJoyStickBall.x = touchobj.pageX;
 LJoyStickBall.y = touchobj.pageY;
 sendData(throttle,405-LJoyStickBall.y);
 sendData(yaw,LJoyStickBall.x-225);
 }, false)
 }, false)
 canvasL.addEventListener('touchend', function(e){ // 'touchend' event in LeftCanvas
 e.preventDefault();
 LJoyStickBall.x = LcenterX;
 sendData(yaw,0);
 }, false)
canvasR.addEventListener('touchstart', function(e){ // 'touchstart' event in RightCanvas
 e.preventDefault(); 
 canvasR.addEventListener('touchmove', function(e){
 var touchobj = e.touches[0]; // reference first touch point for this event
 RJoyStickBall.x = touchobj.pageX - (canvasR.width + RJoyStickBall.w);
 RJoyStickBall.y = touchobj.pageY - RJoyStickBall.h;
 sendData(pitch,210-RJoyStickBall.y);
 sendData(roll,RJoyStickBall.x-225);
 
 }, false)
 }, false)
 canvasR.addEventListener('touchend', function(e){ // 'touchend' event in RightCanvas
 e.preventDefault();
 RJoyStickBall.x = RcenterX;
 RJoyStickBall.y = RcenterY;
 sendData(pitch,0);
 sendData(roll,0);
 }, false)
 function sendData(a,b){
 if(a == throttle){
 Tsocket.send(b);
 }
 if(a == yaw){
 Ysocket.send(b);
 }
 if(a == roll){
 Rsocket.send(b);
 }
 if(a == pitch){
 Psocket.send(b);
 }
 
 // switch(a){
 // case throttle:
 // Tsocket.send(b);
 // break;
 // case yaw:
 // Ysocket.send(b);
 // break;
 // case roll:
 // Rsocket.send(b);
 // break;
 // case pitch:
 // Psocket.send(b);
 // break;
 // default:
 // console.log('Interruption Occured.')
 // }
 }
 document.addEventListener('keydown', function (e) { // When certain key is pressed down
 if (e.key == 'ArrowRight') {
 LmoveRight();
 sendData(yaw,LJoyStickBall.x-225);
 } if (e.key == 'ArrowLeft') {
 LmoveLeft();
 sendData(yaw,LJoyStickBall.x-225);
 } if (e.key == 'ArrowUp') {
 LmoveUp();
 sendData(throttle,405-LJoyStickBall.y);
 } if (e.key == 'ArrowDown') {
 LmoveDown();
 sendData(throttle,405-LJoyStickBall.y);
 }
 if(e.key == 'w'){
 RmoveUp();
 sendData(pitch,210-RJoyStickBall.y);
 }
 if(e.key == 's'){
 RmoveDown();
 sendData(pitch,210-RJoyStickBall.y);
 }
 if(e.key == 'a'){
 RmoveLeft();
 sendData(roll,RJoyStickBall.x-225);
 }
 if(e.key == 'd'){
 RmoveRight();
 sendData(roll,RJoyStickBall.x-225);
 }
 });
 document.addEventListener('keyup', function (e) { // When certain key is released 
 if (
 e.key == 'ArrowLeft' ||
 e.key == 'ArrowRight' ||
 e.key == 'ArrowUp' ||
 e.key == 'ArrowDown' ||
 e.key == 'w' ||
 e.key == 's' ||
 e.key == 'a' ||
 e.key == 'd'
 ) {
 LJoyStickBall.dx = 0;
 LJoyStickBall.dy = 0;
 RJoyStickBall.dx = 0;
 RJoyStickBall.dy = 0;
 LJoyStickBall.x = LcenterX;
 RJoyStickBall.x = RcenterX;
 RJoyStickBall.y = RcenterY;
 sendData(yaw,0);
 sendData(pitch,0);
 sendData(roll,0);
 }
 });
 LeftUpdate();
 RightUpdate();
 </script>
 </body>
</html>
)=====";

Now, I am new to Web based projects and clearly new to Websockets. In the beginning, i used single websocket port and transmitted the data of the four channels as JSON file, but differentiating the data in 4 different channels, increased the speed slighly, but still one problem is consistent throughout, i.e. after a few seconds, (30-35), it starts to achieve latency, and it stays untill the page is not refreshed again. Can someone please suggest any solution for this, as I am using this for controlling my drone, therefore, any unnecessary latency can be dangerous.

For further information, you can see detailed description and similar codes in this repo.

Thanks in advance.

asked Jul 14, 2020 at 10:16
4
  • Your WebSocket server listens on port 81, whereas your client connects to 9000–9003. Are you sure the HTML you posted matches the sketch just above it? Please, make sure the elements of your question are consistent with one another. Commented Jul 14, 2020 at 12:55
  • Ohh, I'm really sorry for that, actually I made several versions of this project, and previously I did with, one websocket port, and actually I mistakenly pasted it from that project. I'm just making it right. Commented Jul 14, 2020 at 13:10
  • now you can check! Commented Jul 14, 2020 at 13:15
  • Having 4 servers isn't neccessary. Does the latency happen from time to time and does recover or does the latency remain high? I currently have a similar issue with the WebsocketsClient from that repo Commented Jul 15, 2020 at 16:56

1 Answer 1

1

Not really a proper answer, but a few ideas that hopefully could help you debug the issue:

  • I doubt using multiple WebSocket servers really helps, as you end up adding more context switching, i.e. you have to loop through all of them even when they have no data to process.
  • SoftwareSerial tends to be really slow, as it blocks execution of the sketch while it is sending. You may try to disable it, at least while you troubleshoot this latency issue.
  • Calling Serial.print() on every loop iteration is also going to slow the sketch down until it is no faster than the serial port. Try to print only once in a while, or only when things actually change.
  • Both Firefox and Chrome have "developer tools" which should help you diagnose the problem. At the very least you should be able to see whether the slowdown comes from the server or from the client.
answered Jul 14, 2020 at 14:48
1
  • No, i just used SoftwareSerial for the SBUS between NodeMCU and Nano, that has nothing to do with websocket, and without that, it still gives the same problem. Commented Jul 19, 2020 at 8:05

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.