Communicating between Python and PHP/JavaScript with AF_UNIX sockets

Posted on Dec 31, 2011 in Arduino, Electronics, JavaScript, PHP, Programming, Python, UberFridge | 2 comments

I wanted a better and more direct way to communicate between PHP and Python than through files. Socket communication was the answer and this article explains how to set it up!

My router runs a Python script that monitors my fermentation fridge. Only the python script communicates with the Arduino, so all commands and data requests to the Arduino go through this script. To keep loading times low for the web interface, I needed a fast and direct way to communicate between PHP and Python.

Python and PHP run on the same router, so a straight forward way to communicate is through the file system. I use files when there is one-way communication. Python stores the beer name in a text file and logs data in JSON files which are read by PHP.

For commands and two-way communication there is a better solution: AF_UNIX sockets. From the man page:

The AF_UNIX (also known as AF_LOCAL) socket family is used to communicate between processes on the same machine efficiently. Traditionally, UNIX domain sockets can be either unnamed, or bound to a file system pathname (marked as being of type socket). Linux also supports an abstract namespace which is independent of the file system.

I bind the socket to a file system path name: /tmp/BEER_SOCKET

Python code

The python script opens a socket on which is listens for incoming messages from PHP. I set the socket to be blocking, with a timeout of 5 seconds. On a timeout exception, I check the serial port for data.

This makes the script continuously listen for incoming connections from PHP, but check the serial port for data every 5 seconds.

1
2
3
4
5
6
7
8
#create a listening socket to communicate with PHP
if os.path.exists('/tmp/BEERSOCKET'):
	os.remove('/tmp/BEERSOCKET') #if socket already exists, remove it
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.bind("/tmp/BEERSOCKET") #Bind /mnt/BEERSOCKET
s.setblocking(1) # set socket functions to be blocking
s.listen(5) # Create a backlog queue for up to 5 connections.
s.settimeout(serialCheckInterval) # blocking socket functions wait 'serialCheckInterval' seconds.

All messages from PHP start with a unique letter used to identify the type of message. Below is the python code for socket communication. I have removed all irrelevant code snippets for readability.

s.accept() is blocking until a message is received. When no message is received before timeout, the timeout exception is raised. For simple data requests from the script (for which the Arduino doesn’t write anything to the serial port), I immediately jump back to the start of the while loop to process other incoming connections. I do have to check if the previous timeout is more than 5 seconds ago, otherwise incoming messages could prevent the serial port from being checked.

On most messages I process the message and send a reply with conn.send(). A message to set the beer temperature to 21 degrees would be b210. I interpret the string from the second character with newTemp = int(data[1:]).

On messages for which I expect the Arduino to write to the serial port (like setting a new beer/fridge temperature) I don’t jump to the start of the loop. Instead I raise a timeout exception to immediately process the reply from Arduino.

I have to except socket errors, otherwise a socket error will cause the script to crash. A socket error will occur when a browser requests some data, but the page is closed before the answer is received. I log these errors in stderr.txt, but I don’t want them to stop the script.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
while(run):
	#*** removed code snippet not relevant to this article ***
	try: # wait for incoming socket connections.
		conn, addr  = s.accept()
		data = conn.recv(1024)  #blocking receive, times out in 5 minutes
		if(len(data)<=1): #invalid data, too short
			if((time.time() - prevTimeOut) < serialCheckInterval):
				continue
		elif(data[0]=='q'):			#exit instruction received. Stop script.
			run=0
			if((time.time() - prevTimeOut) < serialCheckInterval):
				continue
		elif(data[0]=='a'):			#acknowledge request
			conn.send('ack')
			if((time.time() - prevTimeOut) < serialCheckInterval):
				continue
		elif(data[0]=='m'):			#echo mode setting
			conn.send(mode)
			if((time.time() - prevTimeOut) < serialCheckInterval):
				continue
		elif(data[0]=='t'):			#echo mode setting
			conn.send(str(temperatureSetting))
			if((time.time() - prevTimeOut) < serialCheckInterval):
				continue
		elif(data[0]=='b'):			#new constant beer temperature received
			newTemp = int(data[1:])
				#*** removed code snippet not relevant to this article ***
		elif(data[0]=='f'):			#new constant fridge temperature received
			newTemp = int(data[1:])
				#*** removed code snippet not relevant to this article ***
		elif(data[0]=='p'):			#mode set to profile, read temperatures from currentprofile.csv
				#*** removed code snippet not relevant to this article ***
		elif(data[0]=='l'):			#lcd contents requested
			conn.send(lcdText)
		elif(data[0]=='i'):			#new constant fridge temperature received
			newInterval = int(data[1:])
				#*** removed code snippet not relevant to this article ***+ " seconds"
		elif(data[0]=='n'):			#new beer name
			newName = data[1:]
				#*** removed code snippet not relevant to this article ***
				continue				
 
		else:
			print >> sys.stderr, "Error: Received invalid packet on socket: " + data
 
		raise socket.timeout #raise exception to check serial for data immediately
 
	except socket.timeout: #Do serial communication and update settings every SerialCheckInterval
		prevTimeOut=time.time()
 
		while(1): #read all lines on serial interface
				#*** removed code snippet not relevant to this article ***
 
	except socket.error, e:
		print >>sys.stderr, "socket error: %s" % e
 
ser.close()						# close	port
conn.shutdown(socket.SHUT_RDWR) # close socket
conn.close()

PHP code

All socket communication in PHP is done via POST requests to socketmessage.php, which is displayed below. I use a 15 second timeout, because the python script could be busy writing a large data table to a JSON file. I disable warnings, because otherwise lots of warnings will be generated if the script is not running. The message type and the message is sent with the post request. The PHP file echos the answer. Because the byte value for a degree symbol on the LCD is not the same as in ASCII, I do a string replace before echoing the data.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
<?php
	error_reporting(E_ALL ^ E_WARNING);
	$messageType = $_POST["messageType"];
	$data = $_POST["message"];
 
	$sock = open_socket();
	if($sock){
		switch($messageType){
			case "checkScript":
				socket_write($sock, "ack", 1024);
				$answer = socket_read($sock, 1024);
				if($answer = "ack"){
					echo 1;
				}
				else{
					echo 0;
				}
				break;
			case "getmode":
				socket_write($sock, "mode", 1024);
				switch(socket_read($sock, 1024)){
					case "profile":
						echo 1;
						break;
					case "beer":
						echo 2;
						break;
					case "fridge":
						echo 3;
						break;
					default:
						echo "\"error\"";
				}
				break;
			case "setprofile":
				socket_write($sock, "profile", 1024);
				break;
			case "setbeer":
				$modestring = "b" . $data;
				socket_write($sock, $modestring, 1024);
				break;
			case "setfridge":
				$modestring = "f" . $data;
				socket_write($sock, $modestring, 1024);
				break;
			case "stopscript":
				socket_write($sock, "quit", 1024);
				break;
			case "lcd":
				socket_write($sock, "lcd", 1024);
				$lcdText = socket_read($sock, 1024);
				echo str_replace(chr(0xDF), "&deg;", $lcdText);
				break;
			case "interval":
				$modestring = "i" . $data;
				socket_write($sock, $modestring, 1024);
				break;
			case "name":
				$modestring = "n" . $data;
				socket_write($sock, $modestring, 1024);
				break;
			case "gettemp":
				socket_write($sock, "temperature", 1024);
				echo socket_read($sock, 1024);
				break;
		}
		socket_close($sock);
	}
	else{
		echo false;
	}
?>
<?php
function open_socket()
{
	$sock = socket_create(AF_UNIX, SOCK_STREAM, 0);
	if ($sock === false) {
    return false;
	}
	else {
		if(socket_connect($sock, '/tmp/BEERSOCKET')){
			socket_set_option($sock, SOL_SOCKET, SO_RCVTIMEO, array('sec' => 15, 'usec' => 0));
			return $sock;
		}
		else{
			return false;
		}
	}
}

 JavaScript code

I can communicate with python from JavaScript now through a simple jQuery POST request:

1
2
3
4
5
6
7
8
9
10
11
function refreshLcd(){
    $.post('socketmessage.php', {messageType: "lcd", message: ""}, function(lcdText){
        if(lcdText != false){
            $('#lcd').html(lcdText);
        }
        else{
            $('#lcd').html("Error: script not responding");
        }
        window.setTimeout(checkScriptStatus,5000);
    });
}
1
$.post('socketmessage.php', {messageType: "setbeer", message: String(beerTemp)}, function(){    });

2 Comments

  1. Hi, your article is very useful!! I’m trying to build a web interface for some python scripts. Is there any chance you have a zip file with the demo?

Leave a Reply

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