Implementing Flask Web Sockets


Description

In this project we’ll implement web sockets into our Flask server. This allows for real-time communication between the browser and the server, eliminating the need to constantly refresh the page to get the latest updates. After discovering this amazing feature I have used it for multiple projects, such as a Smart Home Dashboard in which the display is updated as devices are turned on and off. The other cool thing about web sockets is they can be configured to automatically reconnect if they get disconnected.  So if, for example, your Pi Server loses its network connection, once your self-monitoring program detects the problem and reboots your server and the network connection is reestablished, the socket will reconnect automatically and just start working again. That helps me rest assured that my Smart Home Dashboard is always up to date. Plus, I display the title in red if the socket communication is lost as a visual indicator. Once it’s reconnected, it returns to its normal color.

For this project we’ll expand on our last project, Alternative Methods for Loading Dynamic Data in HTML. We’ll add a new chat route to our server and add a link to our index.html file to access it. Then we’ll update our server (sample.py) to include the web socket library and the related code to handle our new simple chat page. Finally, we’ll create the new chat.html file that will bring it all together.

Parameters

This is some information about this project and the conditions under which it was done. If you try to replicate it in the future and it doesn’t work, you can evaluate these parameters to see if any changes between these and your configuration might have impacted your results. An example might be changes to future versions of Python or Flask.

  • Date: August 9, 2021
  • Skill: Beginner
  • Raspberry Pi Model(s): Zero-W, 3B+, 4B
  • OS: Raspberry Pi OS version 10 (Buster)
  • Python Version: 3.7.3
  • Flask Version: 2.0.1

Steps

  1. First, we need to install web sockets (flask-socketio) on our Pi. Open a Terminal and enter the following commands:
sudo pip3 install flask-socketio
sudo pip3 install eventlet

Notice we also installed eventlet, which is a production-ready WSGI server and also provides better support for concurrent connections in Flask.

  1. To provide a basic example of how this works, we’ll add a new route to our web server that will serve as a simple chat window (/chat). We’ll then use sockets to push a message from the browser to the server and the server will then broadcast the message out to all connected browsers in real-time. This is similar to the way my Smart Home Dashboard is implemented. I have an HTML page for my dashboard that implements sockets and I have a SmartApp that subscribes to a variety of SmartThings capability events that calls my server with updates as things change. My server then broadcasts those updates to all connected dashboards through the use of sockets.

Let’s get started.

Let’s start by updating our index.html file with the new chat route we will be adding. Open index.html and make the following changes:

<!DOCTYPE html>
<html>

<head>
  <style>
    body {
      background-color: lightblue;
      color: blue;
      font-size: 24px;
    }
  </style>
</head>

<body>
  <h1>{{serverData['serverName']}}</h1>
  <h2> [{{serverData['piModel']}}]</h2>
  <p><a href='/chat'>Chat</a></p>
  <p><a href='/healthcheck'>Health Check</a></p>
  <p><a href='/reboot'>Reboot</a></p>
  <p><a href='/shutdown'>Shutdown</a></p>
</body>
</html>

Notice that I also set the font-size to help with readability. Save the file.

  1. Next we’ll make the necessary changes to our server (sample.py):
#!/usr/bin/env python3

#eventlet WSGI server
import eventlet
eventlet.monkey_patch()

#Flask Libs
from flask import Flask, render_template, request

#Web Sockets
from flask_socketio import SocketIO, emit

#Datetime Lib
from datetime import datetime

#System Libs
import io
import os
import subprocess

#JSON Lib
import json

START_DATE = datetime.now().strftime('%m/%d/%y %I:%M:%S %p')
SERVER_NAME = 'Raspberry Pi Server'
PI_MODEL = 'Unknown'

proc = subprocess.Popen(["cat", "/sys/firmware/devicetree/base/model"], stdout=subprocess.PIPE)
(out, err) = proc.communicate()
if not err:
     PI_MODEL = out.decode('utf-8')
else:
    print("Error get Pi Model: %s" % err)


app = Flask(__name__)
socketio = SocketIO(app)
app.config['SECRET_KEY'] = 'abc123'

users = []

#Called when the browser sucessfully connects via a web socket
@socketio.on('connect')
def socket_connect():
	data = json.dumps({'status': 'connected'})
	emit('conn', data, broadcast=False) #Emitted back only to the new connection

#Called by the browser once the user has entered their username
@socketio.on('join')
def socket_join(msg):
    print('User: %s' % msg['username']) 
    if len(msg['username']) > 0:
        users.append({'username': msg['username'], 'sid': request.sid}) #Keep track of the connected users
        #Emitted to all connections to announce the new user
        emit('joined', json.dumps({'msg': '%s joined! (%d total users connected)' % (msg['username'], len(users))}), broadcast=True)
        user_list = ''
        for user in users:
            if len(user_list) > 0:
                user_list += ', '
            user_list += user['username']
        #Emitted only to the new connection to announce who is already connected
        emit('chat', json.dumps({'username': 'System', 'msg': 'Connected users: %s' % user_list}), broadcast=False)

#Called when the user types a message and presses Send            
@socketio.on('chat_send')
def socket_chat(msg):
    print('Chat: %s' % msg)
    #Message is emitted to all connected users
    emit('chat', json.dumps(msg), broadcast=True)

#Called when a socket disconnects
@socketio.on('disconnect')
def socket_disconnect():
    print('Disconnecting: %s' % request.sid)
    for user in users:
        if user['sid'] == request.sid:
            print('Disconnecting: %s' % user['username'])
            #Emmitted to all remained connected users to let them know a user has disconnected
            emit('disconnected', json.dumps({'msg': '%s disconnected! (%d total users connected)' % (user['username'], len(users)-1)}), broadcast=True)
            users.remove(user) #Removed the disconnected user from the users list

@app.route('/')
def index():
    serverData = {'serverName': SERVER_NAME, 'piModel': PI_MODEL}
    return render_template('index.html', serverData=serverData)

@app.route('/healthcheck')
def healthcheck():
    return render_template('health.html')

@app.route('/health-data')
def healthData():
    curDateTime = datetime.now().strftime('%m/%d/%y %-I:%M:%S %p')

    resp = {'serverName': SERVER_NAME, 'piModel': PI_MODEL, 'startDate': START_DATE, 'curDate': curDateTime}

    #Get ip.txt contents
    try:
        f = open('/home/pi/ip.txt', 'r')
        fdata = f.readlines()
        fcnt = 'Fail Cnt: %s' % (fdata[0].strip())
        rcnt = 'Reboot Cnt: %s' % (fdata[1].strip())
        f.close()
    except:
        fcnt = 'Fail Cnt: No Data'
        rcnt = 'Reboot Cnt: No Data'
    resp['fcnt'] = fcnt
    resp['rcnt'] = rcnt

    #Get reboot.txt contents
    try:
        f = open('/home/pi/reboot.txt', 'r')
        fdata = f.readlines()
        rhist = ''
        for x in range(len(fdata)):
            rhist += fdata[x]
        f.close()
    except:
        rhist = 'None'
    resp['rhist'] = rhist

    # Report available disk space
    stat = os.statvfs('/home/pi')
    gbFree = '%0.2f GB' % (stat.f_bfree*stat.f_bsize/1024/1024/1024)
    resp['gbFree'] = gbFree

    # Report CPU Temp
    try:
        tFile = open('/sys/class/thermal/thermal_zone0/temp')
        temp = float(tFile.read())
        tempC = '%0.1f C' % (temp/1000)
        tempF = '%0.1f F' % ((temp/1000) * 1.8 + 32)
    except:
        tempC = 'ERR'
        tempF = 'ERR'
    resp['tempC'] = tempC
    resp['tempF'] = tempF

    tFile.close()

    return json.dumps(resp)

@app.route('/chat')
def chat():
    return render_template('chat.html')

@app.route('/reboot')
def reboot():
    ret = os.system('sudo reboot')
    return 'Rebooting'

@app.route('/shutdown')
def shutdown():
    ret = os.system('sudo shutdown -h now')
    return 'Shutting down'

if __name__ == '__main__':
    #app.run(debug=True, host='0.0.0.0', port=5000)
	socketio.run(app, debug=True, host='0.0.0.0', port=5000)

Notice that we no longer use app.run() to run our server. We changed to socketio.run(app), which not only wraps our app with the socket layer, but also runs the WSGI server instead of the default Flask server. Also note that the server doesn’t initiate the web socket connection, the browser does. The server simply makes it available and then can respond to events and can emit messages to connections as appropriate.

Also notice that we emit messages as either broadcast=False, which is the default if not specified, and broadcast=True. When set to False, it will only emit the message to the browser socket that triggered the socket event (i.e., in socketio.on(‘connect’) we emit a ‘conn’ message only to the browser that is connecting). When set to True, it well broadcast the message to all connected sockets (i.e., in socketio.on(‘chat-send’) we emit a ‘chat’ message to all connected browsers).

Note that you can further segment connections into groups, called rooms (think of them as separate chat rooms), and messages can be emitted only to specific rooms if needed. I’ll include a link to the documentation for flask-socketio below so you can learn more about all of the available features.

  1. Now create a new file and save it to /home/pi/sample/templates/chat.html. Add the following code:
<!DOCTYPE html>
<html>

<head>
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<meta name="apple-mobile-web-app-capable" content="yes">	
	<title>Simple Chat</title>
	<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/socket.io/4.1.3/socket.io.min.js"></script>
	<style>
	body {
	  background-color: lightblue;
	  color: blue;
	  font-size: 24px;
	}
	
	#chat {
		background-color: white;
		border: solid 1px black;
		height: 500px;
		width: 500px;
		font-size: 18px;
		overflow: scroll;
	}
	
	#chat-text {
		margin-top: 20px;
	}
	
	#text-area {
		width: 410px;
		height: 30px;
	}
	
	#send {
		height: 35px;
		width: 90px;
		font-size: 20px;
	}
	
	</style>
</head>

<body>
	<h3 id="chat-title">Chat Window</h3>
	<div id="chat"></div>
	<div id="chat-text">
		<label for="text-area">Send your message!</label><br />
		<input type="text" id="text-area" autofocus placeholder="Enter message here...">
		<button type="button" id="send" onclick="sendClick()">Send</button>
	</div>
	<p><a href="/">Index</a></p>

<script>
	var chatTitle = document.getElementById("chat-title");
	var msgBox = document.getElementById("text-area");
	var chatWindow = document.getElementById("chat");
	var sendButton = document.getElementById("send");

	//Listen for the user pressing Enter while in the message box
	msgBox.addEventListener("keyup", function(event) {
		if (event.keyCode === 13) {
		event.preventDefault();
		sendButton.click();
	}});

	const DOW_SHORT = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];

	function getTimeDisplay(dt) {
		var time_str = "";
		var hrs = "";
		var mins = "";
		var ap = "AM";
		var val = 0;
		
		val = dt.getHours();
		if (val >=12) {
			ap = "PM";
			if (val > 12) {
				val -= 12;
			}
		}
		else if (val == 0) {
			val = 12;
		}
		hrs = val.toString();
		
		val = dt.getMinutes();
		mins = ("0" + val).slice(-2);
		
		time_str = hrs + ":" + mins + " " + ap;
		return time_str;
	}
	
	var protocol = window.location.protocol;
	var socketURL = protocol + '//' + document.domain + ':' + location.port;
	console.log("Socket URL: " + socketURL);

	//Request a web socket connection with the server
	var socket = io.connect(socketURL, {
		reconnection: true,
		reconnectionDelay: 1000,
		reconnectionDelayMax: 10000,
		reconnectionAttempts: 99999
	});

	var username = "";

	//Called by the server once it accepts the connection
	socket.on('conn', function(msg) {
		chatTitle.style.color = "blue"; //Set title to Blue to indicate a good connection
		
		var dt = new Date();
		var dtDisp = DOW_SHORT[dt.getDay()] + " " + getTimeDisplay(dt);
		var data = JSON.parse(msg);
		console.log(dtDisp + " - " + data.status);

		//Only prompt for the username if we don't have it already.
		//If we lose the connection and get it back, we don't need to ask again.
		if (!username) {
			username = prompt("Please enter your username");

			if (username != null) {
			  console.log("Hello " + username + "!");
			}
		}
		if (username) {
			//Tell the server our username
			socket.emit('join', {'username': username}); 
			//Update our title with our username
			chatTitle.textContent = "Chat Window [" + username + "]";
		}	
	});

	//Called when our socket is disconnected
	socket.on('disconnect', function(msg) {
		chatTitle.style.color = "red"; //Set the title to Red to indicate our connection has been lost
		console.log("disconnect: " + msg);
	});

	//Call by the server whenever a new user joins
	socket.on('joined', function(msg) {
		console.log("joined: " + msg);
		var dt = new Date();
		var dtDisp = DOW_SHORT[dt.getDay()] + " " + getTimeDisplay(dt);
		var data = JSON.parse(msg);
		var dataDisp = dtDisp + ": " + data.msg;
		console.log(dataDisp);
		//Show join messages with a yellow background
		chatWindow.innerHTML += "<span style='background-color:yellow'>" + dataDisp + "</span><br />";
	});

	//Called by the server to announce that a user has disconnected
	socket.on('disconnected', function(msg) {
		console.log("disconnected: " + msg);
		var dt = new Date();
		var dtDisp = DOW_SHORT[dt.getDay()] + " " + getTimeDisplay(dt);
		var data = JSON.parse(msg);
		var dataDisp = dtDisp + ": " + data.msg;
		console.log(dataDisp);
		//Show disconnection messages with a red background
		chatWindow.innerHTML += "<span style='background-color:red'>" + dataDisp + "</span><br />";
	});

	//Called by the server to broadcast a message that a user has sent
	socket.on('chat', function(msg) {
		console.log("chat: " + msg);
		var dt = new Date();
		var dtDisp = DOW_SHORT[dt.getDay()] + " " + getTimeDisplay(dt);
		var data = JSON.parse(msg);
		//If this is our message, display the background as light gray
		var style = username == data.username ? "background-color: lightgray" : "";
		var dataDisp = `<span style='${style}'>${dtDisp}: [${data.username}] ${data.msg}</span>`;
		console.log(dataDisp);
		chatWindow.innerHTML += dataDisp + "<br />";
		chatWindow.scrollBy(0,30); //Scroll the window down to keep last items in view
	});

	//Called when the Send button is pressed or the user presses Enter in the message box
	function sendClick() {
		if (msgBox.value.length > 0) {
			socket.emit('chat_send', {'username': username, 'msg': msgBox.value}); //Emit the message
		}
		msgBox.value = ""; //Clear the current text
		msgBox.focus(); //Set focus back to the message box
	}
	
</script>
</body>
</html>

I’ve highlighted the specific code segments that are related to web sockets. In essence, we first have to include the Javascript that includes the web socket library we need. Modern browser have web socket functionality built in to them, but I have been using this library for quite a while and have had very good success with it.

The browser initiates the web socket connection and creates a socket object (var socket = io.connect()).

Then we have to define callback functions for the specific events we want to handle (socket.on()). These are the events that are triggered from the server and they must match the name of the event from the server exactly. There are a few reserved ones, such as connect and disconnect, so if you have a message that doesn’t seem to be working as expected, try changing the name.

And last we need to emit messages back to the server (socket.emit()).

  1. Now start your server and open a few browsers and click on the new Chat link. You should see something similar to this:

If you stop the server while chatting, you’ll see the title text (Chat Window for [username]) turn red. Then if you restart the server, you’ll see it turn back to blue.

If you close one of the chat browsers, you’ll see the disconnect message in the other.

Summary

Overall, it’s really not that complicated to create a basic web socket connection. Of course there are a lot of other features built-in to flask-socketio that will allow you to do much more (see below), but hopefully this serves as a good introduction to some of the basic features available.

I think after you try it, you’ll love it as much as I do!

Learn More

Raspberry Pi / Raspberry Pi OS

Python / HTML / CSS

Flask

Flask-SocketIO

Leave a Comment

Your email address will not be published. Required fields are marked *