Ooshimus.com

Ooshimus.com

Controlling A Website with a TV Remote!!!

Controlling A Website with a TV Remote!!!

Almost...

Subscribe to my newsletter and never miss my upcoming articles

Listen to this article

This blog post isn’t to act as a tutorial but rather help to demonstrate the power of existing open source frameworks when combined with a little imagination. Looking back on this project I’m sure there are a million ways that this project could have been optimised but it would require complete reworking: I’ll attempt to explain a few of these as I go along. I decided to write this in hindsight since there was a lot of trial and error over the month long project.

The Idea

Essentially, to create a website that can be controlled from a mobile phone. Why? Because I’ve not seen it implemented in a website-like scenario before and the idea seemed awesome.

Project Outline

Here is the general architecture of how the website should work:

Agsb21.png

Originally I had the sites setup like this (these links still work but with outdated code: the current website is accessible through agsb21.tk if this link stops working then just write in the comments below and I’ll try to fix it!) eoycontroller.aiyushgupta.repl.co - OLD LINK aiyush-g.github.io/EOYMag - OLD LINK

After lots of different setups I found this to be best:

  1. A single github repository that would redirect to the different pages for /desktop and /mobile.
  2. Server hosted on Replit hacker account

I also defined some basic user controls that worked on desktop:

F - fullscreen
1-9 change channels
0 - go back a channel 
Up/ Down arrow - to scroll 
Scroll Up/Down – to scroll

But this is where the idea get a little epic, if you enter the code onto the ‘remote’ that is on your phone and click enter, you can then control the ‘tv’ via the remote - turn on sound for some basic SFX :)

Hopefully this outlines the project and you understand how it should work. If you have any questions about the architecture, feel free to ask down below in the comments and if you think I should open source all the source code to the project then also write down below in the comments.

Research Communication

This was the main issue that I needed to understand – how can I setup a Kahoot-esq situation where a mobile phone can control a desktop device. At first I thought to setup a REST-API but you can already imagine how bloated and unbelievably slow that would be, also there is only one way communication which would be very hard to handle never mind having more than one client connect.

So, this is where I luckily stumbled across WebSocket’s. Luckily at the time of researching this subject I made notes on the topic which I have down below, so whether you are a seasoned web sockets master or a newbie like me web sockets is doable.

I quickly discovered the contrived OSI Networking Layers (there are more below but here is what I quickly found):

Transport Layer – (TCP, UDP) – how packets are transferred over the web. The TCP ensures that the packets are received in the order that they are sent. UDP doesn’t care for order – ie video data.

Internet Layer (IPv4, IPv6) – defines the address to where the packets of data are sent.

Application Layer (HTTP, WebSockets, SSL, IMAP, POP) – what developers interface with.

HTTP Request And Response – A request model where a client sends a request to the server and the server responds ie. HTML page, ie python Bottle, Flask or Django.

HTTP – Hypertext transfer protocol – stateless. After the initial request is done then the relationship between the two is lost. Ie. flipping a coin, you either get a head or tail and if you flip it again then your next result has nothing to do with the previous result.

In Chrome dev tools you can look at what requests are being made; these are called actions. Here you can see a GET request. Here are some other request actions

ChromeAction-2.png

In 2005 (or like me today) , people realised that the web needed to do more. This is when Ajax was invented which allowed data to be asynchronously sent to the server without refreshing. This is good for something like a chat application, since without AJAX, you would have to refresh and get all the messages constantly – this is a prime example of what I was trying to avoid.

WebSocket’s is built for this, it created a fully duplex bi-directional communication which means that a client and server can communicate without having to request it all the time – thus solving the inherit problem with HTTP. You send a header to the server saying that you want to upgrade from a server using HTTP to websockets. Once this upgrade has done then you do not need to have the overhead of HTTP aka only send headers once.

‘It is easy to implement, and it is standardised’ In the Chrome dev tools you can see the WS.

There are other technologies that can be used as an alternative to WS, ie. polling and long polling.

Ie. every 5 seconds send a request to the server to get new data, here you can get some sort of feel from real time. It should only be used when you want to get data at regular intervals.

Long polling, this is used commonly in webchats. This can be called comet programming; it says to keep a connection open until the server finds it something to give it and after it gives it back it says to give it another request to do the same thing. This achieves dual connection, and this has upsides and down sides.

Server sent events – event source API. Not truly bi-directional as the server sends to the client, this generally requires and event loop and there is no binary message capability.

Using these methods work well with REST API’s and Oauth. WebSocket’s cannot really interface with these well, so you need to consider whether you want to really use these.

Intended use case of websockets (controversial) Not a complete replacement for HTTP but instead it is an upgrade for that channel, you cannot just replace HTTP, since it has a number of benefits that WS won’t give you ie. auto-caching and communicating with REST.
Load balancing the server is a little complicated as well, the intended use case is where you need a real time communication – ie a real time game (turn based), chat application or anything that requires low-latency.

WebSocket Clients

These are built in many languages (including MicroPy and Arduino) and as an upside Python ß this indicates that it is good for IOT, ie controlling a robot. Clients that interface with WebSocket’s don’t need to be websites as seen in the example. If a client tries to request a server without WS then it will not get an upgrade. Something that is quite good is socket.io, there are alternatives for python (my use case).

Although I didn’t use JavaScript to write my Interactive Retro experience, I still have some sample code on what this would look like.

Const socket = new WebSocket(“ws://localhost:8080”) // connect to localhost 

// Functions 
Socket.onopen = (event) =>{ # tenerary operator 
// on connection do something  
Socket.send (“Sushi Gmer is the best”) 
});  

Socket.onmessage = (event) => { 
// Message from server  
Console.log(event.data) 
});

Socket.io is a nice way to interact with WebSocket’s and makes it nice and easy to use ie. auto reconnection and fallbacks (long polling if the web browser doesn’t support WS), gives you the ability to talk to name spaces.

Another JS example of sockets.io

Socket.on(‘connect’, () =>{ 
Socket.emit(‘event_on_my_server’, data = ‘Please share this blog with others!’); 
}); 

Socket.on(‘my custom event’, (data) => { 
// do something 
// pass 
});

Design

I created this website for my high school, I’m in Year 10 and am part of our schools Publications Committee. Every year we have an Award-Winning End Of Year magazine that is printed out and distributed to members of our school community to highlight and praise the achievements of students at our school.

This year (2021), the EOY magazine had to be in the theme of Television (this is what the school voted upon), and since our school never had a website for our publications, I thought this an awesome time to make one. As explained in the idea, this wasn’t going to be a practical website but rather serve as an experiment.

Our School Magazine is here

So I hopped onto Photoshop and started blocking out some ideas. I finally came to the design below

TVMaskGallery-2.png

This was subject to change in the future ie. the background was going to be the headmasters office, however, I lost management of time towards the end of the project, so it stayed as the background.

I also needed to decide what the TV remote was going to look like. After working in Blender for over 2.5 years now, I was well accustomed to ripping images and repositioning them, so I took a photo of various textures around my house and overlayed them onto a real TV remote to get this result:

remote-2.png

Implementation

So, what stack would I create this project in? HTML, CSS, JS? Unity for the web? Godot?

Well, in hindsight using the traditional web stack would have stood me in good stead for this project since I already knew JS (I mean I barely understood arrow functions ATOW but I could read it), I wasn’t a fan of recreating the TV in CSS though. Looking back, I don’t know why this was my main block when I could have just used images?

Unity, well definitely not, I wasn’t accustomed to its’ overly bloated non-open source…. I’ll stop there. I definitely wasn’t going to create a project in a game engine that took my computer at least 10 minutes to load up every time.

Godot, this may have seemed like a strange choice to some of you, but rest assured Godot was the easiest to pick up game engine I’ve ever seemed. You could even compare it to Scratch, its’ everything is a Node concept made it easy to understand and its’ GDScript (basically Python) language was a breeze to pick up.

What is Godot ?

The game engine you waited for. Godot provides a huge set of common tools, so you can just focus on making your game without reinventing the wheel. Godot is completely free and open source under the very permissive MIT license. No strings attached, no royalties, nothing. Your game is yours, down to the last line of engine code.

How did I learn Godot?

I can attribute all my knowledge of the game engine to two sources:

  1. The docs
  2. Godot Tutorials on YouTube

I completed all 79 of the Godot Basics Tutorials Series and learnt everything I needed to for this project. I then made some smaller games in between (in around 3 days each) and felt confident enough to move away from tutorials and create something more unconventional.

Here is a Trello board with tonnes of great resources!

Creation

From down below, I’ll presume some basic Godot knowledge but I try to explain everything in as much detail as possible. I started by creating two separate Godot Project: one for the desktop version, one for the mobile phone.

Here is the finished desktop version in the editor:

DesktopEditor-2.png

Here is the finished mobile version in the editor:

remoteEditor-2.png

Desktop

DesktopNode-2.png

Here, you can see the node tree for the whole setup, I’ve tried to do an explanation of each node below:

Main - all nodes that you can see that are hollow circles are called Node2D’s. This is the parent node for the whole scene. You can see that there is a script attached to it, in this case it is empty and unnecessary. You can see that any node with a symbol on the right-hand side usually denotes its’ own attribute that isn’t enabled by default.

What is a Node2D?

They are a 2D game object, with a transform (position, rotation, and scale). All 2D nodes, including physics objects and sprites, inherit from Node2D. Use Node2D as a parent node to move, scale and rotate children in a 2D project. Also gives control of the node's render order.

Background - this is also a Node2D, but you can rename each “Node” to something easy to understand and remember. By using clear variable names, you ensure maintainable code that can be read by others. (Thanks, first year GCSE Computer Science). The background is exactly what is sounds like, it is the wall behind and doesn’t include the television frame. For those of you reading who have used any photo editing software in the past would be accustomed to setups like this. In games although lots of things can appear to be 2D, in fact they are layers of separate images on top of each other. From experience, this is something that always needs planning beforehand, it helps to mask the correct items as explained below.

Code - this is the picture frame that is denoted by the NA. You can see that it has a signal and script attached to it. Signals in Godot are alike in some python bindings ie. PyQT6 etc… They have ‘subscribers’ that listen to events. In this case, the signals are stated in the code (below) and then used to connect to other signals.

Below I have pasted the code for this, I have commented it afterwards to try and describe what each thing does.

The aim of this picture frame is to display the code that will be received by the python server on Repl.it and is the code that the user puts into their mobile phone. It also relays the buttons pressed to the desktop back-and-forth from the server.

# The below is my websockets server, the commented out one here is when I was developing locally.
#export var websocket_url = "ws://192.168.0.14:8080"
export var websocket_url = "wss://eoyServer.aiyushgupta.repl.co"

# Signals – some nodes in godot have default signals, however, you can create your own signals. This is documented well within the Godot documentation. Each signal represents a button click.
signal fullScreenToggle
signal onePresssed
signal twoPressed
signal threePressed
signal fourPressed
signal fivePressed
signal sixPressed
signal sevenPressed
signal eightPressed
signal ninePressed
signal zeroPressed
signal otherPressed
signal mutePressed
signal volumeUpPressed
signal volumeDownPressed
signal scrollUpPressed
signal scrollDownPressed
signal mediaPressed

# Our WebSocketClient instance
var _client = WebSocketClient.new()
var threeDigitCode = false

func _ready():


    # Connect base signals to get notified of connection open, close, and errors.
    _client.connect("connection_closed", self, "_closed")
    _client.connect("connection_error", self, "_closed")
    _client.connect("connection_established", self, "_connected")
    # This signal is emitted when not using the Multiplayer API every time
    # a full packet is received.
    # Alternatively, you could check get_peer(1).get_available_packets() in a loop.
    _client.connect("data_received", self, "_on_data")

    # Initiate connection to the given URL.
    var err = _client.connect_to_url(websocket_url)
    if err != OK:
        print("Unable to connect")
        set_process(false)

func _closed(was_clean = false):
    # was_clean will tell you if the disconnection was correctly notified
    # by the remote peer before closing the socket.
    print("Closed, clean: ", was_clean)
    set_process(false)

func _connected(proto = ""):
    # This is called on connection, "proto" will be the selected WebSocket
    # sub-protocol (which is optional)
    print("Connected with protocol: ", proto)
    # You MUST always use get_peer(1).put_packet to send data to server,
    # and not put_packet directly when not using the MultiplayerAPI.
    _client.get_peer(1).put_packet("StartSession".to_utf8())

func _on_data():
    # Print the received packet, you MUST always use get_peer(1).get_packet
    # to receive data from server, and not get_packet directly when not
    # using the MultiplayerAPI.
    var serverResponse = _client.get_peer(1).get_packet().get_string_from_utf8()
    print("Got data from server: ", serverResponse)

    if threeDigitCode == false:
        assignThreeCode(serverResponse)

    if serverResponse == "fullScreen":
        emit_signal("fullScreenToggle")

    if serverResponse == "onePressed":
        emit_signal("onePresssed")

    if serverResponse == "twoPressed":
        emit_signal("twoPressed")

    if serverResponse == "threePressed":
        emit_signal("threePressed")

    if serverResponse == "fourPressed":
        emit_signal("fourPressed")

    if serverResponse == "fivePressed":
        emit_signal("fivePressed")

    if serverResponse == "sixPressed":
        emit_signal("sixPressed")

    if serverResponse == "eightPressed":
        emit_signal("eightPressed")

    if serverResponse == "ninePressed":
        emit_signal("ninePressed")

    if serverResponse == "zeroPressed":
        emit_signal("zeroPressed")

    if serverResponse == "otherPressed":
        emit_signal("otherPressed")

    if serverResponse == "mutePressed":
        emit_signal("mutePressed")

    if serverResponse == "volumeUpPressed":
        emit_signal("volumeUpPressed")

    if serverResponse == "volumeDownPressed":
        emit_signal("volumeDownPressed")

    if serverResponse == "scrollUpPressed":
        emit_signal("scrollUpPressed")

    if serverResponse == "scrollDownPressed":
        emit_signal("scrollDownPressed")

    if serverResponse == "mediaPressed":
        emit_signal("mediaPressed")

    if serverResponse == "sevenPressed":
        emit_signal("sevenPressed")




func assignThreeCode(serverResponse):
    threeDigitCode = serverResponse
    get_node("code").text = threeDigitCode
    threeDigitCode = true


func _process(delta):
    # Call this in _process or _physics_process. Data transfer, and signals
    # emission will only happen when calling this function.
    _client.poll()

Here you can see the various signals and the methods that they attach to – these will be triggered when called.

DesktopSignals-2.png

Frame and Code hold the code that will be fetched from the server.

The TV is where you can find the bulk of the content. All of background and frames is for aesthetics and is just the different components of the TV intricately layered up ontop of each other. Shader provides and overlay to the TV and makes it flicker, I have pasted this below.

shader_type canvas_item;

uniform float screen_width = 1024;
uniform float screen_height = 600;

// Curvature
uniform float BarrelPower =1.1;
// Color bleeding
uniform float color_bleeding = 1.2;
uniform float bleeding_range_x = 3;
uniform float bleeding_range_y = 3;
// Scanline
uniform float lines_distance = 4.0;
uniform float scan_size = 2.0;
uniform float scanline_alpha = 0.9;
uniform float lines_velocity = 30.0;

vec2 distort(vec2 p) 
{
    float angle = p.y / p.x;
    float theta = atan(p.y,p.x);
    float radius = pow(length(p), BarrelPower);

    p.x = radius * cos(theta);
    p.y = radius * sin(theta);

    return 0.5 * (p + vec2(1.0,1.0));
}

void get_color_bleeding(inout vec4 current_color,inout vec4 color_left){
    current_color = current_color*vec4(color_bleeding,0.5,1.0-color_bleeding,1);
    color_left = color_left*vec4(1.0-color_bleeding,0.5,color_bleeding,1);
}

void get_color_scanline(vec2 uv,inout vec4 c,float time){
    float line_row = floor((uv.y * screen_height/scan_size) + mod(time*lines_velocity, lines_distance));
    float n = 1.0 - ceil((mod(line_row,lines_distance)/lines_distance));
    c = c - n*c*(1.0 - scanline_alpha);
    c.a = 1.0;
}

void fragment()
{
    vec2 xy = SCREEN_UV * 2.0;
    xy.x -= 1.0;
    xy.y -= 1.0;

    float d = length(xy);
    if(d < 1.5){
        xy = distort(xy);
    }
    else{
        xy = SCREEN_UV;
    }

    float pixel_size_x = 1.0/screen_width*bleeding_range_x;
    float pixel_size_y = 1.0/screen_height*bleeding_range_y;
    vec4 color_left = texture(SCREEN_TEXTURE,xy - vec2(pixel_size_x, pixel_size_y));
    vec4 current_color = texture(SCREEN_TEXTURE,xy);
    get_color_bleeding(current_color,color_left);
    vec4 c = current_color+color_left;
    get_color_scanline(xy,c,TIME);
    COLOR = c;

}

Content is where you will find the bulk of the code; I won’t paste it all below since there is over 600 lines but I have pasted snippets of it to give an idea of how it would look:

extends Node2D

# This code is quite messy so please don't break it future me 
# or other person looking at this/

# Future refactoring:
# Instead of having different functions called depending upon
# fullscreen state, just handle them within main functions.
# Also find a better way to do what I did :(

# Member Variables
var startScreenSize = OS.get_screen_size()
var fullscreen = false

# Scenes
var template = preload("res://Scenes/templateChannel.tscn").instance() 
var moreCategories = preload("res://Scenes/moreChannelsStarting.tscn").instance() 
var startingScreen = preload("res://Scenes/startingScreen.tscn").instance() # Basically sub home page
var templateArticleSelection = preload("res://Scenes/templateArticleSelection.tscn").instance() 
var specialArtifleSelection = preload("res:/

# More and more variables are below with various scenes. In Godot you instance scenes, each scene is a node itself and is alike grouping in other software.

Then we have some utility functions which help to get_child() names and also do the fullscreen calculations – this is very messy and I’m sure there is a better say to do this:

func _ready() -> void:
    ContentChanger.test()

# Zoom with scroll - sometimes works    
func _unhandled_input(event: InputEvent) -> void:
    if event is InputEventMouseButton:
        if event.is_pressed():

            if event.button_index == BUTTON_WHEEL_UP:
                self.position.y += 5

            if event.button_index == BUTTON_WHEEL_DOWN:
                self.position.y -= 5



func fullScreenCalculations():    
    # get child name / background
    var childName = get_children()[0].name
    var bgPath = str(str(childName) + "/background")
    var bg = get_node(bgPath)
    var bgTexture = bg.get_texture()
    var bgSize = bgTexture.get_size() * bg.scale
    var newSize = sqrt(startScreenSize.x / bgSize.x)
    self.set_scale(Vector2(newSize , newSize ))
    self.position.x -= 100 # Just to center, probably there is a better way to do this

    # Shader
    var shader = get_parent().get_node("Shader")
    shader.set_scale(Vector2(3 , 3 ))
    shader.position.x -= 100
    shader.position.y -= 200

func reverseFullScreenCalculations():
    self.set_scale(Vector2(1,1))
    self.position.x += 100

    var shader = get_parent().get_node("Shader")
    shader.set_scale(Vector2(0.7 , 0.8 ))
    shader.position.x = 0
    shader.position.y = -10.402

func reverseShowItems():
    background.show()
    frames.show()

func reverseResizeBackground():
    screenBackground.scale = (Vector2(0.75,0.7))

func hideItems():
    background.hide()
    frames.hide()

func resizeScreenBackground():
    screenBackground.scale = Vector2(3,3)


func _physics_process(_delta: float) -> void:
    scrollInput()
    fullScreenInput()
    channelInput()


func removeChildren():
    for n in self.get_children():
        self.remove_child(n)


func fullScreenInput():
    if Input.is_action_just_pressed("fullscreen"):
        if str(self.get_child(0).get_child(0).name) != "helpVideoRedundant": # Not used at moment    
            if fullscreen == false:
                fullScreenCalculations()
                hideItems()
                resizeScreenBackground()
                fullscreen= true
                self.position.y = 0 # Go to top of content
            else:
                reverseResizeBackground()
                reverseFullScreenCalculations()
                reverseShowItems()
                self.position.y = 0 # Return to top of content 
                fullscreen= false
        else:
            if fullscreen == false:
                self.get_child(0).get_child(0).rect_position = Vector2(0,0)
                self.get_child(0).get_child(0).rect_size = Vector2(1024,600)
                fullscreen = true
                print ("fullScreenHelp")
            else:
                self.get_child(0).get_child(0).rect_position = Vector2(-100,0)
                self.get_child(0).get_child(0).rect_size = Vector2(329,289)
                fullscreen = false

func scrollInput():
    if Input.is_action_pressed("scroll_up"):
        self.position.y -= 2.5

    if Input.is_action_pressed("scroll_down"):
        self.position.y += 2.5


# Called every frame. 'delta' is the elapsed time since the previous frame.
#func _process(delta: float) -> void:
#    pass


func _on_Code_fullScreenToggle() -> void:
    if fullscreen == false:
            fullScreenCalculations()
            hideItems()
            resizeScreenBackground()
            fullscreen= true
            self.position.y = 0 # Go to top of content
    else:
        reverseResizeBackground()
        reverseFullScreenCalculations()
        reverseShowItems()
        self.position.y = 0 # Return to top of content 
        fullscreen= false

# conroller button
func _on_Code_scrollDownPressed() -> void:
    self.position.y += 20


func _on_Code_scrollUpPressed() -> void:
    self.position.y -= 20

func getChildName():
    return (get_child(0).name)

Then for all the signals that we defined earlier, I have functions that connect to them, these decide what screen is shown when the buttons are pressed on the TV. This is an example fo what happens when you click channel 5. I know, don’t say it, there is repeated code at the bottom of each of these and this is easily refactorable, I might do this soon.

func channel5():

    if getChildName() == 'PEArticleSelection':
        removeChildren()
        #specialArticleSelection
        self.add_child(lockdownPE)
        self.position.y = 0
        return


    if getChildName() == 'housesArticleSelection':
        removeChildren()
        #specialArticleSelection
        self.add_child(stamford)
        self.position.y = 0
        return

    if getChildName() == 'moreChannelsStarting':
        removeChildren()
        self.add_child(newBuild)
        self.position.y = 0
        return

    if getChildName() == 'specialArticleSelection':
        removeChildren()
        self.add_child(finalCut)
        self.position.y = 0
        return

    if getChildName() == 'startingScreen':
        removeChildren()
        #specialArticleSelection
        self.add_child(housesSelection)
        self.position.y = 0
        return

Finally, towards the bottom of the script, I have what connects to the signals and then calls the functions above. I separated them out into nested functions as it may appear so it was easier to write.

# Channel Input Keyboard
func channelInput():
    if Input.is_action_just_pressed("channel0"):
        channel0()

    if Input.is_action_just_pressed("channel1"):
        channel1()



        # Make this work here
        #var childName = get_children()[0].name
        #add_child(template)

    if Input.is_action_just_pressed("channel2"):
        channel2()

    if Input.is_action_just_pressed("channel3"):
        channel3()

    if Input.is_action_just_pressed("channel4"):
        channel4()

    ## MORE FUNCTIONS BELOW HERE 



# Channel Input Signals
func _on_Code_onePresssed() -> void:
    channel1()


func _on_Code_zeroPressed() -> void:
    channel0()

func _on_Code_ninePressed() -> void:
    channel9()



func _on_Code_eightPressed() -> void:
    channel8() # Replace with function body.


### … MORE FUNCTIONS HERE…

func _on_Code_threePressed() -> void:
    channel3() # Replace with function body.


func _on_Code_twoPressed() -> void:
    channel2() # Replace with function body.


func _on_frame_pressed() -> void:
    help()

Essentially, all this code does is take the server input and then reroute it towards the desktop and depending upon the input it shows different articles / categories on the screen.

If you’ve read this far, your doing well, this article is quite long but I wanted to try and document the process in as much detail as possible. Once again, if there is anything that you are unsure of feel free to comment down below.

Mobile

remote-2.png

The image shows the screen you would expect to see when you visited the site on a mobile device. When the user wants to type in the number they can just tap on the numbers and then click enter. I also used a AudioStreamPlayer node here so that some sound effect could be played.

I have also shown an image of the node tree and will explain each part briefly. I have removed parts of repetition and highlighted where this is within the code

MobileNode-2.png

Main is a Node2D and have used his as the parent node for the screen to contain the script that is shown below. This script connects to the server after the buttons have been pressed and it is connected to the client. It will then relay any further button presses to the server and then the server will relay them again to the desktop client.

extends Node2D

export var websocket_url = "wss://eoyServer.aiyushgupta.repl.co"
# https://eoyServer.aiyushgupta.repl.co
# Our WebSocketClient instance
var _client = WebSocketClient.new()
var code
var connected = false



func _ready():
    # Connect base signals to get notified of connection open, close, and errors.
    _client.connect("connection_closed", self, "_closed")
    _client.connect("connection_error", self, "_closed")
    _client.connect("connection_established", self, "_connected")
    # This signal is emitted when not using the Multiplayer API every time
    # a full packet is received.
    # Alternatively, you could check get_peer(1).get_available_packets() in a loop.
    _client.connect("data_received", self, "_on_data")

    # Initiate connection to the given URL.
    var err = _client.connect_to_url(websocket_url)
    if err != OK:
        print("Unable to connect")
        set_process(false)

func _closed(was_clean = false):
    # was_clean will tell you if the disconnection was correctly notified
    # by the remote peer before closing the socket.
    print("Closed, clean: ", was_clean)
    set_process(false)

func _connected(proto = ""):
    # This is called on connection, "proto" will be the selected WebSocket
    # sub-protocol (which is optional)
    print("Connected with protocol: ", proto)
    # You MUST always use get_peer(1).put_packet to send data to server,
    # and not put_packet directly when not using the MultiplayerAPI.

    # This is how you 'message'
    # _client.get_peer(1).put_packet("StartSession".to_utf8())

func _on_data():
    # Print the received packet, you MUST always use get_peer(1).get_packet
    # to receive data from server, and not get_packet directly when not
    # using the MultiplayerAPI.
    var dataFromServer = _client.get_peer(1).get_packet().get_string_from_utf8()
    print("Got data from server: ", dataFromServer)

    if connected == false:
        checkIfCodeCorrect(dataFromServer)

func inputCodeViaButtons(num):
    if str(num) != 'other':
        var text = get_node("CodeEntryBox/CodeEntryLineEdit").text
        text = str(text)
        text += str(num)
        get_node("CodeEntryBox/CodeEntryLineEdit").text = text
    else:
        var text = get_node("CodeEntryBox/CodeEntryLineEdit").text
        text.erase(text.length() - 1, 1)
        get_node("CodeEntryBox/CodeEntryLineEdit").text = text


func checkIfCodeCorrect(dataFromServer):
    if 'Correct phone code' in dataFromServer:
        connected = true
        print ("Yay, the code was correct")
    else:
        connected = false
        print ("AWW, the code was incorrect")

func checkConnected():
    if connected: # == true
        return true

func _process(delta):
    # Call this in _process or _physics_process. Data transfer, and signals
    # emission will only happen when calling this function.
    _client.poll()


func _on_Submit_pressed() -> void:
    code = get_node("CodeEntryBox/CodeEntryLineEdit").text

    # Old options select version
    #var option1 = get_node("CodeEntryBox/OptionButton")
    #var option2 = get_node("CodeEntryBox/OptionButton2")
    #var option3 = get_node("CodeEntryBox/OptionButton3")
    #code = str(option1.text + option2.text + option3.text)

    print (code)
    var codeMsg = 'phoneConnect ' + code
    _client.get_peer(1).put_packet(codeMsg.to_utf8())


func _on_1_pressed() -> void:
    if checkConnected():
        print ('Connected')
        code = get_node("CodeEntryBox/CodeEntryLineEdit").text
        var codeMsg = str(code + " onePressed")
        _client.get_peer(1).put_packet(codeMsg.to_utf8())

    else:
        print ('Not connected to anything')
        inputCodeViaButtons(1)


func _on_2_pressed() -> void:
    if checkConnected():
        print ('Connected')
        code = get_node("CodeEntryBox/CodeEntryLineEdit").text
        var codeMsg = str(code + " twoPressed")
        _client.get_peer(1).put_packet(codeMsg.to_utf8())

    else:
        print ('Not connected to anything')
        inputCodeViaButtons(2)

# I continue this for all of the other buttons, I could definitely have used another function here instead of repeating parts here, it would have made the code more modular.

All the other nodes are fairly self-explanatory and the buttons are accessed by the self keyword to interact with the script above. Everything else is pure GUI and could have been solved in several ways. In the interest of not copying and pasting everything again and again I have left that out. Once again, if you are interested in me open sourcing the code and project file then please comment down below!

Server

So far, we have explored the function for both clients and how they work with our mystery server. I have shown the code for how the server works using the techniques that I documented above in my research section. I have commented the code where I felt necessary so you could understand how to use it. The server acts as the middle man between both clients and connects them both.

import asyncio
import websockets
import random

usedCodes = []
phoneConnectedCodes = []

connected = []  # TESTING

# A Handler, doesn't need to be echo.
# Takes the websocket, the connection
# Path could be a slash ie localhost:8080/test

# Goes into websocket gets the message
async def main(websocket, path):

    async for message in websocket:

        if len(usedCodes) > 999:
          usedCodes.clear()
          phoneConnectedCodes.clear()

        print("Connetced", connected)  # TESTING
        # Logic
        # message is the message in byte form
        # websockets is the object # ie <websockets.server.WebSocketServerProtocol object at 0x7fe39a37d450>
        print("Websocket", str(id(websocket)))
        print("Message", str(message))
        print('websocket type', type(websocket))

        # 1st message from Main Client, generate a random code and send
        # back to client
        if 'StartSession' in str(message):
            sessionCode = createSessionCode(websocket)
            print(str(sessionCode))
            print('usedCodes', usedCodes)
            await websocket.send(sessionCode)

        # 1st message from phone client, find if code is in the generated
        # codes list
        if 'phoneConnect' in str(message):

            # Extract Code from String
            code = getCode(message)
            print('Phone attempt to connect', code[0])

            if any(code[0] in sl for sl in usedCodes):
                print('Correct Phone Code Entered')
                await websocket.send(u'Correct phone code'
                                     )  # Don't Change This
            else:
                print('Incorrect Phone Code Entered')
                await websocket.send(u'Incorrect phone code'
                                     )  # Don't Change This

        # Button Presses on remote:
      # I have missed some of the other functions out here just to save more space. They just work for the other possible buttons ie. other numbers.

        if 'onePressed' in str(message):
            print('onePressed')
            code = getCode(message)[0]
            websocketId = findWebsocketObjectID(code, usedCodes)
            websocketId = (usedCodes[int(websocketId[0])][int(websocketId[1])])
            print("WEBSOCKET ID", websocketId)

            await websocketId.send(u'onePressed')

            #websocketId.send(u'FullScreen')
            #websockets.server.WebSocketServerProtocol(websocketId).send(u'FullScreen')

        if 'twoPressed' in str(message):
            print('twoPressed')
            code = getCode(message)[0]
            websocketId = findWebsocketObjectID(code, usedCodes)
            websocketId = (usedCodes[int(websocketId[0])][int(websocketId[1])])
            print("WEBSOCKET ID", websocketId)

            await websocketId.send(u'twoPressed')



        if 'otherPressed' in str(message):
            print('otherPressed')
            code = getCode(message)[0]
            websocketId = findWebsocketObjectID(code, usedCodes)
            websocketId = (usedCodes[int(websocketId[0])][int(websocketId[1])])
            print("WEBSOCKET ID", websocketId)

            await websocketId.send(u'otherPressed')

        if 'mutePressed' in str(message):
            print('mutePressed')
            code = getCode(message)[0]
            websocketId = findWebsocketObjectID(code, usedCodes)
            websocketId = (usedCodes[int(websocketId[0])][int(websocketId[1])])
            print("WEBSOCKET ID", websocketId)

            await websocketId.send(u'mutePressed')

        if 'volumeDownPressed' in str(message):
            print('volumeDownPressed')
            code = getCode(message)[0]
            websocketId = findWebsocketObjectID(code, usedCodes)
            websocketId = (usedCodes[int(websocketId[0])][int(websocketId[1])])
            print("WEBSOCKET ID", websocketId)

            await websocketId.send(u'volumeDownPressed')

        if 'scrollUpPressed' in str(message):
            print('scrollUpPressed')
            code = getCode(message)[0]
            websocketId = findWebsocketObjectID(code, usedCodes)
            websocketId = (usedCodes[int(websocketId[0])][int(websocketId[1])])
            print("WEBSOCKET ID", websocketId)

            await websocketId.send(u'scrollUpPressed'

def findWebsocketObjectID(
    check, usedCodes
):  # Lst = usedCodes, check = code : returns object id of websocket
    index = [
        "{} {}".format(index1, index2)
        for index1, value1 in enumerate(usedCodes)
        for index2, value2 in enumerate(value1) if value2 == check
    ]
    print('index Debugging', index)
    index = str(index)
    index = index[2:-2]
    index = index.split()  # ie ['0', '0']
    index1 = int(index[0])
    index2 = int(index[1]) + 1

    return index1, index2  # returned as a tuple you can access like list

def getCode(message):
    code = [int(i) for i in message.split()
            if i.isdigit()]  # Extracts digits from string
    return code

def createSessionCode(websocket):
    while True:
        sessionCode = random.randint(100, 999)
        if not any(sessionCode in sl for sl in usedCodes):
            usedCodes.append([sessionCode, websocket])
            connected.append(websocket)
            print(connected)  # TESTING
            break

    return str(sessionCode)

start_server = websockets.serve(main, "0.0.0.0", 8080)

asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()

# Recieves a message and sends customised message back
# This is the server

Thanks for reading

Well folks, that is all from me, I recently distributed the website to members of our school community. If you want to check it out: agsb21.tk, thanks for reading and if you have any further questions then please feel free to add comments down below. Also please share this with anyone that you think would be interested!

 
Share this