fischertechnik TXT4.0 and web sockets (1)

Two project about the web socket communication from a browser with the fischertechnik TXT4.0

Project 1

Basic code to receive data over a web socket on the TXT.

First I tested the Pyhton code in RoboPro Coding in the text mode.

import asyncio, itertools
import time
import subprocess
from lib.controller import *
from websockets import serve
#simple websocket server

async def echo(websocket):
    print('message')
    async for message in websocket:
        await websocket.send(message)


print('start')

async def handler(websocket, path):
    # Get received data from websocket
    print("wait for recv")
    while True:
      try:
          print("before")
          data = await websocket.recv()
          print("after")
      except websockets.exceptions.ConnectionClosed:
            print("connection closed")
            break
      except websockets.exceptions.ConnectionClosedError:
            print("connection closed error")
            break
      except websockets.exceptions.ConnectionClosedOK:
            print("connection closed Ok")
            break
    # await handler( websocket, path)
    # Send response back to client to acknowledge receiving message
      else:
            print('server: recv' + data)
            await websocket.send("== " + data)
            #print("send ready")
            await handler( websocket, path)


print('test1')
# Create websocket server
start_server = serve(handler, "0.0.0.0" ,  8085)
print('test2')
# Start and run websocket server forever
asyncio.get_event_loop().run_until_complete(start_server)
print('test3')
asyncio.get_event_loop().run_forever()
print('test4')


while True:
    pass
print('test5')

The web page that runs on a client and contacts the TXT4.0 websocket.

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
  <title>Test</title>
  <script type='text/javascript' language="javascript">
  <!--
  var connection;
  var keepAlive = false;
  var printError = function(error, explicit) {
    console.log(`[${explicit ? 'EXPLICIT' : 'INEXPLICIT'}] ${error.name}: ${error.message}`);
     }
  const jj= '{"actuators":[{"M":{"i":1,"p":[0,0,214,214]}},{"M":{"i":2,"p":[0,0,214,214]}},{"M":{"i":3,"p":[0,0,214,214]}}]}';
  const j={"actuators":[{"M":{"i":1,"p":[0,0,214,214]}},{"S":[0, 6,512]},{"firstName":"Peter","lastName":"Jones"}]};
  //const j=   {"actuators":[{"M":{"i":1,"p":[0,0,214,214]}},{"M":{"i":2,"p":[0,0,214,214]}},{"M":{"i":3,"p":[0,0,214,214]}}]};

  function webSockKeepAlive() {
    if (keepAlive) {
      connection.send('ping'); // Send the message 'ping' to the server
      setTimeout("webSockKeepAlive()", 10000);
    }
  }
 function sendButton1(par1,par2){
     //parameters not is use for the moment
     myJSON=JSON.stringify(j);// to a JSON object
	 console.log('sendButton: ' + myJSON);
     connection.send(myJSON);
  }
function sendButton2(par1,par2){
     //parameters not is use for the moment
     myJSON=JSON.stringify(j);// to a JSON object
	 console.log('sendButton: ' + myJSON);
     connection.send(myJSON);
  }  
function sendButton3(par1,par2){
     //parameters not is use for the moment
     myJSON=JSON.stringify(j);// to a JSON object
	 console.log('sendButton: ' + myJSON);
     connection.send(myJSON);
  }    
  
  function load() {
    connection = new  WebSocket("ws://192.168.20.175:8085/");
    connection.onopen = function () {
        var send = "init " + Math.round(Math.random()*4294967294+1);
        console.log('Client: ' + send);
        connection.send(send);
        keepAlive = true;
  //      webSockKeepAlive();
      };
    connection.onclose = () => {
            console.log('Web Socket Connection Closed');
		};	
    connection.onerror = function (error) {
        keepAlive = false;
        connection.close();
        console.log('WebSocket error: ' + error);
        alert("WebSocket error: "+ error);
      };
    connection.onmessage = function (e) {
        console.log('Message: ' + e.data);
		var msgStr = document.getElementById('msg');
		try{
		var objJ=JSON.parse(e.data);
		var tt=objJ.actuators[1].S;//tt=objJ.actuators[1].S[1];
		msgStr.innerHTML ="<strong>Data received: </strong><br/>"+  e.data+" <br/><br/><b>JSON object:</b><br> "+JSON.stringify(objJ) +" <br><br><b>JSON object selection:</b><br>"+JSON.stringify(tt);
 		
		}catch(ex){
		   if (ex instanceof SyntaxError) {
             printError(ex, true);
		     msgStr.innerHTML = "Exception: Parse syntax error, data received=<br/>"+ e.data;
 		   } else {
             printError(ex, false);
		     msgStr.innerHTML = "Exception: No parse error, data received=<br/>"+ e.data;
   	       }
 	    }
      };
  }
  //-->
  </script> 

</head>
/<!-- <body onload="load()"> -->
<body>
 <input type="button" onclick="load();" value="Open websocket server"></button>
 <input type="button" onclick="connection.close();" value="Close websocket server"></button>
  <input type="button" onclick="sendButton1('msg A','10');" value="msg 1"></button>
  <input type="button" onclick="sendButton2('msg B','10');" value="msg 2"></button>
  <input type="button" onclick="sendButton3('msg C','10');" value="msg 3"></button>
  <input type="button" onclick="connection.send('msg D');" value="msg 4"></button>
  <br/>
  <p id="msg"></p>
</body>
</html>

The integration into the RoboPro Blockly structure

After testing the Python code, the code has been integrated into the Blockly structure. The callback has been placed in a sub page.
This project is available on the RoboPro Coding fischertechnik GitLab page under: “CvanLeeuwenFt/my_606_WebSocketReceiver_Bl”

The RoboPro Coding callbacks page

The main problems are:
How to to bring the data to Blockly-level?
Blockly has no user defined events and event handlers to deal with these events.

Project 2

Virtual Joystick and standard game control HTML page

A couple of years ago I already introduced a remote controller from a WIndows machine by HTML/Websocket communication with a TXT-controller.
This with the help of embedded CivetWeb server and RoboPro SLI extension elements.
I adapted this web pages to the TXT4.0 with the Python web socket.

I made use of two projects which were available on the internet:

The structure of the maps and files

The map structure for the files:
ws_ft\JoystickTXT40_2022_01_16.html
Jscript\txtWebContent\txt40_poc01.js
Jscript/txtWebContent/txt40_joystick_view.js
Jscript/txtWebContent/txt40_gamepadtest.js
css\txt40_select.css
css\txt40_txt.css

The GUI

JoystickTXT40_2022_01_16.html

<!DOCTYPE html>
<!--
(c) 2019-2022 ing. C van Leeuwen Btw. Enschede
[2022-01-16] initial changes, js file with txt40_
-->
<html>

<head>
<meta charset="UTF-8">
<title>Embedded websocket example</title>
<link href="./../css/txt40_txt.css" rel="stylesheet" type="text/css">
<link href="./../css/txt40_select.css" rel="stylesheet" type="text/css">
<script src="./../Jscript/txtWebContent/txt40_poc01.js" type="text/javascript"></script>
<script src="./../Jscript/jquery/jquery-3.5.1.min.js" type="text/javascript"></script>
<script src="./../Jscript/underscore-1.8.3/underscore-min.js" type="text/javascript"></script>
<script src="./../Jscript/backbone-1.4.0/backbone-min.js" type="text/javascript"></script>
<script src="./../Jscript/txtWebContent/txt40_joystick_view.js" type="text/javascript"></script>
<script src="./../Jscript/txtWebContent/txt40_gamepadtest.js" type="text/javascript"></script>
</head>

<body onload="load()">

<header>
	<h1>Websocket and fischertechnik TXT4.0</h1>
	<p>Project ia een vervolg op deze toepassing bij de fischertechnik TXT controller.</p>
</header>
<nav id="nav">
	<a href="#Xbox360" target="_blank">Game console</a> |
	<a href="#install " target="_blank">Install event handlers Game console</a> 
	|
	<a href="https://developer.mozilla.org/en-US/docs/Web/API/Gamepad_API/Using_the_Gamepad_API" target="_blank">
	Gamepad API (W3C Sep 14, 2021)</a> |
	<a href="https://html5gamepad.com/" target="_blank">Gamepad tester</a>
</nav>
<h1>FtWebsocket test remote (2022-01-18)</h1>
<script >
$('.example-default-value').each(function() {
    var default_value = this.value;
    $(this).focus(function() {
        if(this.value == default_value) {
            this.value = '';
        }
    });
    $(this).blur(function() {
        if(this.value == '') {
            this.value = default_value;
        }
    });
});
function f_selected_ip() {
    $('#pbutton1t').text('Connect to a fischertechnik TXT4.0 with the websocket server on: ');
    $('#pbutton1t').append($('#select_ip').val());
    $('#btn01').prop('disabled', false);  //$('#btn02').prop('disabled', false);
    $('#btn03').prop('disabled', false);  $('#btn04').prop('disabled', false);
}
function started_websocket(){
}
</script>
<div class="ip-select" style="width: 200px;" >
	<select id=select_ip name='select_ip' onchange="f_selected_ip()">
	<option id="sel" value="0">Select IP:</option>
	<option id="api_usb" value="1">192.168.7.2</option>
	<option id="api_ip_ac" value="2">192.168.8.2</option>
	<option id="api_bt" value="3">192.168.9.2</option>
	<option id="api_aa" value="4">192.168.20.176</option>
	<option id="api_ab" value="5">192.168.10.182</option>
	</select> </div>
<div>
	<p id="pbutton1" >
	<button id="btn01" name="btn01" onclick="startWebsocketConnection($('#select_ip').val())" type="button" disabled="disabled">Start websocket  
	</button> <p id="pbutton1t" >
  Connected to a fischertechnik TXT4.0 with the websocket server on:</p>
	</p>
</div>
<h2 id="start">Virtual joystick demo</h2>
<script id="joystick-view" type="text/html">
     <canvas id="joystickCanvas"  width="<%= squareSize %>" height="<%= squareSize %>"
                style="border:1px solid #000000; width: <%= squareSize %>px; height: <%= squareSize %>px;">
 	  </canvas>
	 </script>
<div class="grid-container">
	<div id="joystickContent">
	</div>
	<div class="square1">
		<div id="js_field">
			<p>local x,y= [ <span id="xVal"></span>, <span id="yVal"></span>]= [<span id="xyVal"></span>]<br />
			</p>
		</div>
		<div id="websock_text_field">
			No websocket conn yet</div>
		<div id="motor_field">
			No motor data yet</div>
		<div id="input_field">
			No input data yet</div>
		<div id="counter_field">
			No fast counter data yet</div>
		<div id="message_field">
			No message data yet</div>
		<div>
			<p>Xbox Hat= <span id="xBoxHat"></span><br />
			Joystick = <span id="xBoxJoy"></span><br />
			Zaxis= <span id="xBoxZax"></span><br />
			Xbox: <span id="xBoxBut"></span></p>
		</div>
	</div>
</div>
<script src="./../Jscript/txtWebContent/txt40_poc01_joystick.js" type="text/javascript"></script>
<div style="Background-color: #8bf;">
	<h2 id="Xbox360">Standard game controller</h2>
	<p>Activate the game controller listener,<br />
	Press a button on your controller to start </p>
	<script src="" type="text/javascript"></script>
	<div id="install">
		<p>
		<button id="btn03" onclick="myGamepad.instalGameEvents()" type="button" disabled>
		install Txt game controller listeners</button>
		<button id="btn04" onclick="myGamepad.removeGameEvents()" type="button" disabled>
		remove Txt game controller listeners</button>
		</p>
	</div>
</div>
<footer>
	<p>(c)2019-2022 C.van Leeuwen Enschede</p>
</footer>
</body>
</html>

txt40_poc01.js

/*(c) 2019-2022 ing. C. v. Leeuwen  Bwt. Enschede
 *txt40_poc01.js, short version without JSON for motors and inputs
 */
function isJSON(item) {
    item = typeof item !== "string" ? JSON.stringify(item) : item;
    try {
        item = JSON.parse(item);
    } catch (e) {
        return false;
    }
    if (typeof item === "object" && item !== null) {
        return true;
    }
    return false;
}

var i = 0;
function load() {
    connection = null;
};
/*
 * Return isSuccesful
 */
function startWebsocketConnection( mapStr = '/ws_txt161', ipStr = '127.0.0.1', portStr = '8080') {
    mapStr = typeof mapStr !== 'undefined' ? mapStr : '/websocket/fischertechnik';
    connection = null;
    websock_text_field = document.getElementById('websock_text_field');
    connection = new WebSocket('ws:' + '//' + ipStr + ':' + portStr + mapStr);
    connection.bufferType = "arraybuffer"; // default is "blob"
 
    connection.onmessage = function (e) {
        var charMode = typeof e.data === "string";
        if (charMode) {
            websock_text_field.innerHTML = '<p>text mode </p>';
            counter_field.innerHTML = '<p>' + e.data + '</p>';
            /* 			code removed			*/ // end isJSON
        } // end string mode
        else { // binary mode
            websock_text_field.innerHTML = '<p>binary mode </p>';
        }
    }
	
    connection.onclose = function (event) {
        if (event.wasClean) { //
            websock_text_field.innerHTML += '<p>closed normal </p>';
            alert('[close] Connection closed cleanly, code=${event.code} reason=${event.reason}');
        } else {
            // e.g. server process killed or network down
            // event.code is usually 1006 in this case
            websock_text_field.innerHTML += '<p>closed died </p>';
            alert('[close] Connection died, reason= ${event.reason}');
        }
    }
    connection.onerror = function (error) {
        alert('WebSocket error and is going to close, reason=${error}');
        websock_text_field.innerHTML += '<p>error </p>';
        connection.close();
    }
    return true;
}

txt40_poc01_joystick.js

/*
* (c) 2019-2022 ing. C. v. Leeuwen  Bwt. Enschede
* combine the virtual joystick with the standard game controller input.
* and the listerner registration.
* txt40_poc01_joystick.js based on : 
*/
var myGamepad = new X360Gamepad("MyOne");

$(document).ready(
		function() {
			window.addEventListener("XboxConHat",// "joystickLeftX",
			function(e) {
				$("#xBoxHat").html("" + e.detail.Hat // + ","
						+ " ");
				// alert('Event from joystickLeftX');
				if (connection !== null) {
					connection.send('{"xbox1":["hat",' + e.detail.Hat + ']}');
				}
			});
			window.addEventListener("XboxConZax",// "joystickLeftX",
			function(e) {
				$("#xBoxZax").html(
						"[" + e.detail.Zax[0] + "," + e.detail.Zax[1] + "] =");
				// alert('Event from joystickLeftX');
				if (connection !== null) {
					connection.send('{"xbox1":["zax",' + e.detail.Zax[0] + ','
							+ e.detail.Zax[1] + ']}');
				}
			});

			window.addEventListener("XboxConJoy",// "joystickLeftX",
			function(e) {
				$("#xBoxJoy").html(
						" X [" + e.detail.X[0] + "," + e.detail.X[1] + ","
								+ e.detail.X[2] + "," + e.detail.X[3] + ","
								+ e.detail.X[4] + "] Y=" + e.detail.Y);
				// alert('Event from joystickLeftX');
				if (connection !== null) {
					connection.send('{"xbox1":["joy",' + e.detail.X[0] + ','
							+ e.detail.X[1] + ',' + e.detail.X[2] + ','
							+ e.detail.X[3] + ']}');
				}
			});

			window.addEventListener("XboxConBut",// "buttonsXbox",
			function(e) {
				var s = " X [";
				for (x in e.detail.X) {
					s += e.detail.X[x] + ",";
				}
				s.slice(0, s.lenght - 2);
				s += "] Y=" + e.detail.Y;
				$("#xBoxBut").html(s);
				if (connection !== null) {
					connection.send('{"xbox1":["but",' + e.detail.X[0] + ','
							+ e.detail.X[1] + ',' + e.detail.X[2] + ','
							+ e.detail.X[3] + ',' + e.detail.X[4] + ','
							+ e.detail.X[5] + ',' + e.detail.X[6] + ','
							+ e.detail.X[7] + ',' + e.detail.X[8] + ','
							+ e.detail.X[9] + ']}');
				}

			});
			// ==========================================================================
			var joystickView = new JoystickView(400, 'joystick-view',
					'joystickCanvas', function(callbackView) {
						$("#joystickContent").append(callbackView.render().el);
						setTimeout(function() {
							callbackView.renderSprite();
						}, 0);
					});

			joystickView.bind("verticalMove", function(y) {
				y = Math.round(y * 15.0);
/*				if (connection !== null) {
					connection.send('{"jsl":["joyY",' + y + ']}');
				}
*/				
				$("#yVal").html(y);
			});

			joystickView.bind("horizontalMove", function(x) {
				x = Math.round(x * 15.0);
/*				if (connection !== null) {
					connection.send('{ "jsl":["joyX",' + x.toFixed(3) + ']}');
				}
*/			
				$("#xVal").html(x);
			});
			joystickView.bind("move", function(x,y) {
				x = Math.round(x * 15.0);y = Math.round(y * 15.0);
				if (connection !== null) {
					connection.send('{ "jsl":["joy",' + x + ',' + y+ ']}');
				}
	
				$("#xyVal").html( x+','+y );
			});
		});

txt40_poc01_view.js

/*
* (c) 2019-2022 ing. C. v. Leeuwen  Bwt. Enschede
* txt40_poc01_view.js based on the work of : 
* Jerome Etienne, Dublin, Ireland
* https://github.com/jeromeetienne/virtualjoystick.js  Jerome Etienne, Dublin, Ireland
*/

var INACTIVE = 0;
var ACTIVE = 1;
var SECONDS_INACTIVE = 20.0;
var INTERVAL_MS=100;
function loadSprite(src, callback) {
    var sprite = new Image();
    sprite.onload = callback;
    sprite.src = src;
    return sprite;
};

JoystickView = Backbone.View.extend({
    events: {
        "touchstart":"startControl","touchmove":"move","touchend":"endControl",
        "mousedown": "startControl","mouseup": "endControl","mousemove": "move"
    },
    initialize: function(squareSize, jsview ,jscanvas,finishedLoadCallback){
        this.squareSize = squareSize;
        // this.template = _.template($("#joystick-view").html());
       // t='"'+jsview+'"';
      // $(`#${this.name}`).hide()
        this.template = _.template($("#"+jsview ).html());
        this.state = INACTIVE;
        this.x = 0; this.xX=0;
        this.y = 0; this.yY=0;
        this.canvas = null;
        this.jscanvas = jscanvas;
        this.context = null;
        this.radius = (this.squareSize / 2) * 0.5;
        this.finishedLoadCallback = finishedLoadCallback;
        this.joyStickLoaded = false;
        this.backgroundLoaded = false;
        this.lastTouch = new Date().getTime();
        self = this;
        setTimeout(function(){
            self._retractJoystickForInactivity();
        }, 1000);
        this.sprite = loadSprite("./../img/button.png", function(){
            self.joyStickLoaded = true;     self._tryCallback();
        });
        this.background = loadSprite("./../img/canvas.png", function(){
            self.backgroundLoaded = true;   self._tryCallback();
        });
    },
    _retractJoystickForInactivity: function(){
        var framesPerSec = 15;
        var self = this;
        setTimeout(function(){
            var currentTime = new Date().getTime();
            if(currentTime - self.lastTouch >= SECONDS_INACTIVE * 1000){
                self._retractToMiddle();  self.renderSprite();
            }
            self._retractJoystickForInactivity();
        }, parseInt(1000 / framesPerSec, 10));
    },
    _tryCallback: function(){
        if(this.backgroundLoaded && this.joyStickLoaded){
            var self = this;
            this.finishedLoadCallback(self);
        }
    },
    startControl: function(evt){
        this.state = ACTIVE;
    },
    endControl: function(evt){
        this.state = INACTIVE;
        this.x = 0;
        this.y = 0;
        this.renderSprite();
        this._triggerChange();
    },
    move: function(evt){
        if(this.state == INACTIVE){
            return;
        }
		now = new Date().getTime();
       var interval =now-this.lastTouch;    
       if ((interval>INTERVAL_MS) || (interval<0))  {
		   this.lastTouch = new Date().getTime();
           var x, y;
          if(evt.originalEvent && evt.originalEvent.touches){
            evt.preventDefault();
            var left = 0;
            var fromTop = 0;
            elem = $(this.canvas)[0];
            while(elem) {
                left = left + parseInt(elem.offsetLeft);
                fromTop = fromTop + parseInt(elem.offsetTop);
                elem = elem.offsetParent;
            }
            x = evt.originalEvent.touches[0].clientX - left;
            y = evt.originalEvent.touches[0].clientY - fromTop;
        } else {
            x = evt.offsetX;  y = evt.offsetY;
        }
        this._mutateToCartesian(x, y);        this._triggerChange();
		}
    },
    
    _triggerChange: function(){
       	if( (this.x!=this.xX) ||(this.y!=this.yY)) {
        	
            var xPercent = this.x / this.radius;
            var yPercent = this.y / this.radius;
            if(Math.abs(xPercent) > 1.0){
                xPercent /= Math.abs(xPercent);
            }
            if(Math.abs(yPercent) > 1.0){
                yPercent /= Math.abs(yPercent);
            }
            this.trigger("horizontalMove", xPercent);
            this.trigger("verticalMove", yPercent);
            this.trigger("move",xPercent,yPercent);
            this.xX=this.x;this.yY=this.y;
        	}
    },
    _mutateToCartesian: function(x, y){
        x -= (this.squareSize) / 2;
        y *= -1;
        y += (this.squareSize) / 2;
        if(isNaN(y)){ y = this.squareSize / 2;     }
        this.x = x;        this.y = y;
        if(this._valuesExceedRadius(this.x, this.y)){this._traceNewValues(); }
        this.renderSprite();
    },
    _retractToMiddle: function(){
        var percentLoss = 0.1;
        var toKeep = 1.0 - percentLoss;
        var xSign = 1;        var ySign = 1;
        if(this.x != 0){
            xSign = this.x / Math.abs(this.x);
        }
        if(this.y != 0) {
            ySign = this.y / Math.abs(this.y);        }
        this.x = Math.floor(toKeep * Math.abs(this.x)) * xSign;
        this.y = Math.floor(toKeep * Math.abs(this.y)) * ySign;
    },
    _traceNewValues: function(){
    	// y was a problem 2020-01-23 v
    	if((this.x>-1.0)&&(this.x<1.0) ){
    		        this.y = Math.sign(this.y)* this.radius;return;   }
    	// if((y>-1.0)&&(y<=1.0) ){
    	// this.y = y; this.x =x / Math.abs(x)* this.radius;return; }
    	// =========================================
        var slope = this.y / this.x;
        var xIncr = 1;
        if(this.x < 0){
            xIncr = -1;
        }
        for(var x=0; x<this.squareSize / 2; x+=xIncr){
            var y = x * slope;
            if(this._valuesExceedRadius(x, y)){
                break;
            }
        }
        this.x = x;        this.y = y;
    },
    _cartesianToCanvas: function(x, y){
        var newX = x + this.squareSize / 2;
        var newY = y - (this.squareSize / 2);
        newY = newY * -1;
        return {   x: newX,   y: newY       }
    },
    _valuesExceedRadius: function(x, y){
        if(x === 0){
            return Math.abs( y) > this.radius;
        }
        return Math.pow(x, 2) + Math.pow(y, 2) > Math.pow(this.radius, 2);
    },
    renderSprite: function(){
        var originalWidth = 89;        var originalHeight = 89;
        var spriteWidth = 50;        var spriteHeight = 50;
        var pixelsLeft = 0; // ofset for sprite on img
        var pixelsTop = 0; // offset for sprite on img
        var coords = this._cartesianToCanvas(this.x, this.y);
        if(this.context == null){            return;       }
        // hack dunno why I need the 2x
        this.context.clearRect(0, 0, this.squareSize * 2, this.squareSize);

        var backImageSize = 300;
        this.context.drawImage(this.background,
            0,            0,      backImageSize,  backImageSize,
            0,            0,      this.squareSize,   this.squareSize
        )
        this.context.drawImage(this.sprite,
            pixelsLeft,            pixelsTop,
            originalWidth,            originalHeight,
            coords.x - spriteWidth / 2,    coords.y - spriteHeight / 2,
            spriteWidth,          spriteHeight
        );
    },
    render: function(){
        var renderData = {
            squareSize: this.squareSize
        };
        this.$el.html(this.template(renderData));
        // this.canvas = this.$('#joystickCanvas')[0];
        this.canvas = this.$("#"+ this.jscanvas)[0];
        this.context = this.canvas.getContext('2d');
        this.renderSprite();
        return this;
    }
});

txt40_gamepadtest.js

/*
 * (c)2020-2022 ing. C.van Leeuwen Btw. Enschede.
 * txt40_gamepadtest.js
 * Changed into a prototype class structure and add events for the XBOX360 controller 
 * Xbox360 interface for the fischertechnik TXT controller Websocket SLI. (custom RoboPro element).
 * Needs: 
 *         underscore.js, 
 * Note:
 * https://stackoverflow.com/questions/2025789/preserving-a-reference-to-this-in-javascript-prototype-functions
 * Based on Gamepad API Test  Written in 2013 by Ted Mielczarek <ted@mielczarek.org>
 * To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide. This software is distributed without any warranty.
 * You should have received a copy of the CC0 Public Domain Dedication along with this software. If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
 */
/*
 * ToDo: only events in case of changes in the strucure: 
 * Hat= 0..8, Z-axis= {0..15,0..15], Joysticks= [leftX,leftY, rightX, rightY]
 * Button =[x,y,a,b, leftFront, rightFront,back, start]
 *  
 */
/*
 * Conversion of the HAT button array into 1..8, 0=not pressed
 */
function convertHat(hatButtonArray) {
	var status = 0;
	if (hatButtonArray[0] && hatButtonArray[3])
		status = 2;
	else if (hatButtonArray[3] && hatButtonArray[1])
		status = 4;
	else if (hatButtonArray[1] && hatButtonArray[2])
		status = 6;
	else if (hatButtonArray[2] && hatButtonArray[0])
		status = 8;
	else if (hatButtonArray[0])
		status = 1;
	else if (hatButtonArray[3])
		status = 3;
	else if (hatButtonArray[1])
		status = 5;
	else if (hatButtonArray[2])
		status = 7;
	return status;
};
function X360Gamepad(name) {
	// constructor(){};
	this.name = name;
	this.haveEvents = 'GamepadEvent' in window;
	this.haveWebkitEvents = 'WebKitGamepadEvent' in window;
	rAF = window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame
			|| window.requestAnimationFrame;
	this.controllers = {};
	this.XboxConJoyEvent = null;// new Event("XboxConJoy")
	this.XboxConHatEvent = null;// new Event("XboxConHat")
	this.XboxConButEvent = null;// new Event("XboxConBut")
	this.XboxConZaxEvent = null;// new Event("XboxConZax")
	// this.buttons15X = [];
	this.buttons16X = [];// last update buttons true/false
	this.buttons17X = [];// last update buttons 0/1
	this.buttons18X = [];// Last update Z axis
	this.buttons19X = [];
	this.axis15X = [ 0, 0, 0, 0 ];// last update axis
	this.sX = 0;// last update Hat
	this.lastEvents = 0;
}

X360Gamepad.prototype.addgamepad = function(gamepad) {
	this.controllers[gamepad.index] = gamepad;
	var d = document.createElement("div");
	d.setAttribute("id", "controller" + gamepad.index);
	var t = document.createElement("h3");
	t.appendChild(document.createTextNode("gamepad: " + gamepad.id));
	d.appendChild(t);
	var b = document.createElement("div");
	b.className = "buttons";
	for (var i = 0; i < gamepad.buttons.length; i++) {
		var e = document.createElement("span");
		e.className = "button";
		e.innerHTML = i;
		b.appendChild(e);
	}
	;
	d.appendChild(b);
	var a = document.createElement("div");
	a.className = "axes";
	for (i = 0; i < gamepad.axes.length; i++) {
		e = document.createElement("meter");
		e.className = "axis";
		e.setAttribute("min", "-1");
		e.setAttribute("max", "1");
		e.setAttribute("value", "0");
		e.innerHTML = i;
		a.appendChild(e);
	}
	d.appendChild(a);
	document.getElementById("start").style.display = "none";
	// document.body.appendChild(d);
	// document.getElementById("install").appendChild(d);
	dd = document.getElementById("install");
	dd.insertBefore(d, dd.childNodes[0]);
	var u = self.updateStatus;
	rAF(this.updateStatus.bind(this));
};

X360Gamepad.prototype.connecthandler = function(e) {
	this.addgamepad(e.gamepad);
};

X360Gamepad.prototype.disconnecthandler = function(e) {
	this.removegamepad(e.gamepad);
};

X360Gamepad.prototype.removegamepad = function(gamepad) {
	var d = document.getElementById("controller" + gamepad.index);
	document.body.removeChild(d);
	delete this.controllers[gamepad.index];
};
// callback for the requestAnimationFrame
X360Gamepad.prototype.updateStatus = function(timestamp) {
	self = this;
	self.scangamepads();
	for (j in self.controllers) {
		var controller = self.controllers[j];
		var d = document.getElementById("controller" + j);
		var buttons = d.getElementsByClassName("button");
		var buttons15 = [];// all buttons in pairs [true/false,value]+hat
		// [number]
		// var buttons16 = [];//buttons true/false
		var buttons17 = [];// buttons 0/1
		var buttons18 = [];// Z axis 0.0..1.0 => 0..15
		var buttons19 = []; // Hat
		for (var i = 0; i < controller.buttons.length; i++) {
			var b = buttons[i];
			var val = controller.buttons[i];
			var pressed = val == 1.0;
			if (typeof (val) == "object") {
				pressed = val.pressed;
				val = val.value;
			}
			var pct = Math.round(val * 100) + "%";
			b.style.backgroundSize = pct + " " + pct;
			if (pressed) {
				b.className = "button pressed";
			} else {
				b.className = "button";
			}
			buttons15.push(pressed);
			buttons15.push(val);
			if ([ 0, 1, 2, 3, 4, 5, 8, 9, 10, 11 ].includes(i)) {
				// buttons16.push(pressed);
				buttons17.push(val);
			} else if ([ 6, 7 ].includes(i)) {
				// buttons16.push(pressed);
				val = Math.round(val * 15.0)
				buttons18.push(val);
			} else if ([ 12, 13, 14, 15 ].includes(i)) {
				// buttons16.push(pressed);
				buttons19.push(pressed);
			}
		}// end for the buttons
		var axes = d.getElementsByClassName("axis");
		var axis15 = [];
		for (var i = 0; i < controller.axes.length; i++) {
			var a = axes[i];
			var x = controller.axes[i]
			a.innerHTML = i + ": " + x.toFixed(4);
			a.setAttribute("value", x);
			if((i===1)||(i===3)) axis15.push(-1*Math.round(x * 15.0))
		       else axis15.push(Math.round(x * 15.0));
		
		}
		// end for the axis
		// =========================================================================
		// extra for events
		// ============================================================================
		var s = convertHat(buttons19);
		buttons15.push(s);// general array
		// =========================================================================
		// fire events every ...msec
		// ============================================================================
		if (timestamp - this.lastEvents > 180) {
			this.lastEvents = timestamp; // only when changed
			var b = buttons15;
			// var bl = buttons15.length; //var rl = r.length;
			var zax = buttons18;// Z axis [0..15,0..15]
			var but = buttons17;// val 0/1

			var r = axis15;// preserve for the event

			if (!_.isEqual(this.axis15X, axis15)) {
				this.axis15X = Array.from(axis15);
				axis15.push("ch");

				this.XboxConJoyEvent = new CustomEvent("XboxConJoy", {
					detail : {
						X : r,
						Y : "-"
					}
				});
				dispatchEvent(this.XboxConJoyEvent);

			} else {/* Joystick no change */
				axis15.push("--");
			}

			if (!_.isEqual(this.buttons17X, buttons17)) {
				this.buttons17X = buttons17;
				this.XboxConButEvent = new CustomEvent("XboxConBut", {
					detail : {
						X : but,
						Y : "-"
					}
				});
				dispatchEvent(this.XboxConButEvent);
			} else {/* buttons 0/1 no change */
			}

			if (!_.isEqual(this.buttons18X, buttons18)) {
				this.buttons18X = buttons18;
				this.XboxConZaxEvent = new CustomEvent("XboxConZax", {
					detail : {
						Zax : zax
					}
				});
				dispatchEvent(this.XboxConZaxEvent);
			} else {/* Z axis no change */
			}
			
			if (this.sX != s) {
				this.sX = s;
				this.XboxConHatEvent = new CustomEvent("XboxConHat", {
					detail : {
						Hat : s
					}
				});
				dispatchEvent(this.XboxConHatEvent);
			} else {/* Hat no change */
			}
		}
	}// end for a controller
	rAF(this.updateStatus.bind(this));
};

X360Gamepad.prototype.scangamepads = function() {
	var gamepads = navigator.getGamepads ? navigator.getGamepads()
			: (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);
	for (var i = 0; i < gamepads.length; i++) {
		if (gamepads[i]) {
			if (!(gamepads[i].index in this.controllers)) {
				addgamepad(gamepads[i]);
			} else {
				this.controllers[gamepads[i].index] = gamepads[i];
			}
		}
	}
};

X360Gamepad.prototype.instalGameEvents = function() {
	self = this;
	if (this.haveEvents) {
		window.addEventListener("gamepadconnected", self.connecthandler
				.bind(this));
		window.addEventListener("gamepaddisconnected", self.disconnecthandler
				.bind(this));
	} else if (this.haveWebkitEvents) {
		window.addEventListener("webkitgamepadconnected", self.connecthandler
				.bind(this));
		window.addEventListener("webkitgamepaddisconnected",
				self.disconnecthandler.bind(this));
	} else {
		setInterval(this.scangamepads.bind(this), 500);
	}
};
X360Gamepad.prototype.removeGameEvents = function() {
	if (this.haveEvents) {
		window.removeEventListener("gamepadconnected", this.connecthandler
				.bind(this));
		window.removeEventListener("gamepaddisconnected",
				self.disconnecthandler.bind(this));
	} else if (this.haveWebkitEvents) {
		window.removeEventListener("webkitgamepadconnected",
				self.connecthandler.bind(this));
		window.removeEventListener("webkitgamepaddisconnected",
				self.disconnecthandler.bind(this));
	} else {
		setInterval(this.scangamepads.bind(this), 500);
	}
};

txt40_txt.css

@charset "ISO-8859-1";
table, th, td {
	border: 1px solid black;
	border-collapse: collapse;
}

thead>tr>td {
	background-color: #f5f5f5;
}

tbody>tr>td {
	background-color: #f5f5c5;
}

.buttons, .axes {
	padding: 1em;
}

.axis {
	min-width: 200px;
	margin: 1em;
}

.boxed {
	border: 1px solid green;
}

.square1 {
	Background-color: #8af;
}


.button {
	padding: 1em;
	border-radius: 20px;
	border: 1px solid black;
	background-image:
		url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAAxJREFUCNdjYPjPAAACAgEAqiqeJwAAAABJRU5ErkJggg==);
	background-size: 0% 0%;
	background-position: 50% 50%;
	background-repeat: no-repeat;
}

.pressed {
	border: 1px solid red;
}

.grid-container {
	width: auto;
	max-width: 870px;
	justify-content: center;
	 display : table;
	grid-template-columns: 425px 425px;
	background-color: #213433;
	display: grid;
	grid-gap : 10px;
	background-color : #213433;
	padding: 10px;
}

.grid-container>div {
	height: 410px;
	width: 410px;
	background-color: rgba(100, 255, 255, 0.8);
	text-align: left;
	padding: 10px;
	font-size: 14px;
	border: 1px solid #000000;
}
Print Friendly, PDF & Email