Home / Articles / FANUC
FANUC KAREL Programming User Socket Msg R648

KAREL Socket Messaging Tutorial: A FANUC Ping/Pong Client

FANUC’s teach pendant programming environment is pretty capable, but one thing it can’t do is low-level socket communication. We need KAREL for that.

I’ve written articles on KAREL before but never one on socket messaging, so let’s dive into the “Hello World” of KAREL socket messaging.

What is socket messaging?

Socket messaging allows the robot to communicate with external devices over the network. We basically create a connection, decide on a message protocol and exchange messages before disconnecting.

When we talk about socket messaging, there is a client and a server. Basically the server is the one who waits around for connections and messages, and the client is the one who connects to the server, sends messages and waits for responses.

What FANUC options are required?

We’ll need both the KAREL (R632) and User Socket Msg (R648) options.

Teach pendant setup

Once the required options have been loaded, we need to set up the socket messaging client and/or server tags. For most applications, the robot is the client (connecting to another device and initiating requests), so we will follow that example here and set up a client tag.

Menu > Setup > Host Comm

F4 [ SHOW ] > Clients

Let’s use the first tag, C1:. Make sure C1: is highlighted and press the DETAIL softkey.

Set Protocol to SM.

Set Startup State to START. This way our tag will automatically start when the controller power cycles.

Set Server IP/Hostname to 127.0.0.1. That’s just our local PC because I’m using ROBOGUIDE here. You’d set this to the IP of wherever your server resides if you’re on a real robot. Make sure you’re on the same subnet!

Set Remote Port to 5000, since that’s what we will implement on our server.

You might think that everything’s configured now, but our client tag will not work until we Define and Start it.

Hit F2 [ACTION] and choose Define, and then hit F2 [ACTION] again and choose Start.

Now our tag is up and running.

The SETUP Tags detail screen for client tag C1: with Protocol SM, Current State STARTED, Startup State START, Server IP 127.0.0.1 and Remote Port 5000

Note that you cannot make changes to the tag when it is defined/started. You need to use [ACTION] > STOP and [ACTION] > Undefine to make changes.

The protocol

In this simple “Hello World” example, we are going to have the client send PING\n, and the server will respond with PONG\n. Note the \n newline characters. These are important and signal to both the client and the server that the message is complete.

By the way, you can get the full source code for this tutorial on GitHub.

Server task

I’m more of a Golang guy, but I know many people prefer Python, so here’s a Python server task pong_server.py:

import socket

HOST = "0.0.0.0"
PORT = 5000

def normalize(buf):
    return buf.replace(b"\r\n", b"\n").replace(b"\r", b"\n")

def handle(conn, addr):
    print(f"Robot connected from {addr}")
    buffer = b""
    while True:
        chunk = conn.recv(1024)
        if not chunk:                      # empty bytes = peer closed
            print("Robot disconnected.")
            return
        buffer = normalize(buffer + chunk)

        # A single recv may contain 0, 1, or several messages - or a
        # partial one. Pull out every complete (newline-terminated) message
        # and leave any partial remainder in the buffer for next time.
        while b"\n" in buffer:
            line, buffer = buffer.split(b"\n", 1)
            msg = line.decode("ascii", errors="replace").strip()
            print(f"Received: {msg!r}")
            if msg == "PING":
                conn.sendall(b"PONG\n")
                print("Sent: PONG")
            else:
                conn.sendall(b"ERR unknown\n")

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind((HOST, PORT))
    server.listen()
    print(f"Listening on {HOST}:{PORT}...")

    while True:                            # keep serving new connections
        conn, addr = server.accept()
        with conn:
            handle(conn, addr)

Assuming you have Python installed, you can run this via py pong_server.py (or python3 pong_server.py on Linux/macOS).

Basically this task will wait for a connection and upon making one, listen for messages and respond appropriately. If the client sends PING\n, we’ll respond with PONG\n. If the client sends anything else, we will respond with ERR unknown\n. When the robot disconnects, the task will wait for a new connection.

Client task

Let’s create ping_client.kl.

PROGRAM ping_client

%NOLOCKGROUP
%NOPAUSE = ERROR + COMMAND + TPENABLE

CONST
	CHR_CLS         = 128
	CHR_HOME_CURSOR = 137

VAR
	f        : FILE
	status   : INTEGER
	response : STRING[128]

ROUTINE assert(status : INTEGER)
VAR
	i : INTEGER
BEGIN
	IF status<>0 THEN
		MSG_DISCO('C1:', i)
		POST_ERR(status, '', 0, 2)
		RETURN
	ENDIF
END assert

BEGIN
	-- initialize file handle for ASCII reads/writes
	SET_FILE_ATR(f, ATR_EOL, 10) -- newline
	SET_FILE_ATR(f, ATR_TIMEOUT, 1000) -- ms

	-- clear user screen
	WRITE (CHR(CHR_CLS),CHR(CHR_HOME_CURSOR))
	-- force user screen
	FORCE_SPMENU(tp_panel, SPI_TPUSER, 1)

	-- just in case
	MSG_DISCO('C1:', status)

	WRITE('Connecting...', CR)
	MSG_CONNECT('C1:', status)
	assert(status)

	WRITE('Opening file...', CR)
	OPEN FILE f ('rw', 'C1:')
	status = IO_STATUS(f)
	assert(status)

	WRITE('Sending PING...', CR)
	WRITE f('PING', CR)
	status = IO_STATUS(f)
	assert(status)

	WRITE('Sent: PING', CR)

	READ f(response)
	status = IO_STATUS(f)
	SELECT status OF
		CASE(0):
			WRITE('Received: ', response, CR)
		CASE(282): -- yes this is an invalid code
			WRITE('Timed out waiting for a response', CR)
		ELSE:
			WRITE('Read failed, status = ', status, CR)
	ENDSELECT

	CLOSE FILE f
	MSG_DISCO('C1:', status)
	WRITE('Disconnect status:', status, CR)
END ping_client

Let’s break it down starting at BEGIN – we’ll get into the constants, variables and routines eventually.

Reading and writing to a socket is very similar to reading and writing to any other file on the robot. We need to have a file handle (variable of type FILE), and we can optionally set up some properties of that file handle with KAREL’s SET_FILE_ATR built-in.

SET_FILE_ATR(f, ATR_EOL, 10) -- newline

Here we are telling KAREL to terminate future READs when it sees CHR(10), or the \n character. By default, KAREL uses CHR(13), or the carriage return \r character, but we specified \n as the end to our messages in the protocol section above.

SET_FILE_ATR(f, ATR_TIMEOUT, 1000) -- ms

Here we are specifying that our READs should timeout after 1000ms if they do not see a \n character. The default value here is 0 which could hang our client up forever. We don’t want that.

-- clear user screen
WRITE (CHR(CHR_CLS),CHR(CHR_HOME_CURSOR))
-- force user screen
FORCE_SPMENU(tp_panel, SPI_TPUSER, 1)

We can write a couple of special characters to the default device (TPDISPLAY, the USER screen) to clear the screen and home the cursor. There are a bunch of other special character codes in the KAREL manual.

Then we use FORCE_SPMENU to bring up the USER screen for our forthcoming debug messages. The USER screen is just a blank screen that WRITEs go to by default. There are some constants here (tp_panel and SPI_TPUSER) that the KAREL translator knows by default when using the basic support files.

Extra steps for sockets

I mentioned earlier that reading and writing to sockets is very similar to reading from and writing to normal files. One of the differences is that we have to first connect to our socket before we can OPEN the socket’s file descriptor.

-- just in case
MSG_DISCO('C1:', status)

WRITE('Connecting...', CR)
MSG_CONNECT('C1:', status)
assert(status)

I use the MSG_DISCO() built-in to disconnect any connections on C1: because MSG_CONNECT() will return a HOST-215 SM: Connection is in use alarm status otherwise.

ROUTINE assert(status : INTEGER)
VAR
	i : INTEGER
BEGIN
	IF status<>0 THEN
		MSG_DISCO('C1:', i)
		POST_ERR(status, '', 0, 2)
		RETURN
	ENDIF
END assert

I use the assert() routine to make sure the status returned is 0, pre-emptively disconnecting the socket, posting an alarm and aborting the program otherwise.

Now it’s just like normal files

WRITE('Opening file...', CR)
OPEN FILE f ('rw', 'C1:')
status = IO_STATUS(f)
assert(status)

We open the file for reading and writing with the special C1: file descriptor that we set up and connected to. Again we use assert(status) to make sure the OPEN operation was successful before moving on.

WRITE('Sending PING...', CR)
WRITE f('PING', CR)
status = IO_STATUS(f)
assert(status)

WRITE('Sent: PING', CR)

Just like writing to any file, we use WRITE with the file handle f and make sure to send a CR at the end so our server knows the message is complete.

We use IO_STATUS() and assert() again to make sure the WRITE operation was successful.

A quick note on CR: CR is a special KAREL keyword. It’s not a constant. It basically tells the KAREL runtime to end “this line / start a new line, however you need to do that.” You might expect CR to send a carriage return \r character (CHR(13)), but in ROBOGUIDE it sends a line feed \n (CHR(10)) over the wire. ATR_EOL doesn’t change that either: it only controls where READs stop, not what CR writes. I’ll have to test on a real robot the next time I have one with the User Socket Msg option. For now the Python server is written to respond correctly to endings of \r, \n or \r\n via normalize().

READ f(response)
status = IO_STATUS(f)
SELECT status OF
	CASE(0):
		WRITE('Received: ', response, CR)
	CASE(282): -- yes this is an invalid code
		WRITE('Timed out waiting for a response', CR)
	ELSE:
		WRITE('Read failed, status = ', status, CR)
ENDSELECT

Here we use READ with the f file handle into the response STRING variable and check the operation status with IO_STATUS(). Rather than using assert(), we SELECT on status and output a debug message depending on the result.

Interesting note: It seems that a read timeout returns a status of 282 for some reason, which is very unusual given that it’s not a valid FANUC error code. I almost always use POST_ERR() eventually with statuses returned by FANUC built-ins and this is one case where the error code is invalid. If you do attempt to use POST_ERR() with the 282 status, you’ll end up with an OS-XXX alarm with what looks like a memory address dump. This seems to happen any time you use POST_ERR() with an invalid error code.

CLOSE FILE f
MSG_DISCO('C1:', status)
WRITE('Disconnect status:', status, CR)

Lastly we close the file and disconnect the socket.

I won’t go into the details of translating the KAREL source file, loading it onto the robot or running the program. (See my Introduction to KAREL Programming for that information.)

But you should see something like this in the Python task’s terminal:

$ py pong_server.py
Listening on 0.0.0.0:5000...
Robot connected from ('127.0.0.1', 50555)
Received: 'PING'
Sent: PONG
Robot disconnected.

On the robot USER screen:

Connecting...
Opening file...
Sending PING...
Sent: PING
Received: PONG
Disconnect status: 0

More testing

To test the timeout behavior, add a short delay to our server:

import socket
import time

...

            if msg == "PING":
                time.sleep(2)
                conn.sendall(b"PONG\n")
                print("Sent: PONG")

We set our ATR_TIMEOUT to 1000ms, so our 2-second sleep on the server-side should trigger this code path:

Connecting...
Opening file...
Sending PING...
Sent: PING
Timed out waiting for a response
Disconnect status: 0

The problem now is that our server crashes because the robot aborted the connection. Let’s add some error handling to handle():

def handle(conn, addr):
    print(f"Robot connected from {addr}")
    buffer = b""
    try:
        while True:
            chunk = conn.recv(1024)
            if not chunk:                      # empty bytes = peer closed
                print("Robot disconnected.")
                return
            buffer = normalize(buffer + chunk)
            while b"\n" in buffer:
                line, buffer = buffer.split(b"\n", 1)
                msg = line.decode("ascii", errors="replace").strip()
                print(f"Received: {msg!r}")
                if msg == "PING":
                    time.sleep(2)              # simulated stall
                    conn.sendall(b"PONG\n")
                    print("Sent: PONG")
                else:
                    conn.sendall(b"ERR unknown\n")
    except OSError as e:                       # recv or send failed: robot hung up
        print(f"Connection error with {addr}: {e}")

What if the protocol does not have a termination character?

Maybe you’re sending and receiving binary data with no EOL character. In this case, we need some other way to know when a message is complete. The simplest is a fixed-length message: both sides agree that every response is exactly N bytes. (Another common approach is a length prefix: a fixed-size header that says how many bytes of payload follow.)

We don’t want to change our server, and it always sends PONG\n, so every response is exactly 5 bytes. We can set the FILE up such that READs only read a specified number of bytes off the wire. For this to work, we’ll have to use the BYTES_AHEAD() built-in to see what’s on the wire and roll our own timeout:

ROUTINE readBinary(response : STRING; msgLen : INTEGER; timeoutMs : INTEGER; status : INTEGER)
VAR
	nBytes : INTEGER
	us     : INTEGER
BEGIN
	us = GET_USEC_TIM
	REPEAT
		IF (GET_USEC_SUB(GET_USEC_TIM, us)>timeoutMs*1000) THEN
			status = 282 -- yes i know this is an invalid code
			RETURN
		ENDIF

		BYTES_AHEAD(f, nBytes, status)
		IF status<>0 THEN RETURN; ENDIF
		IF nBytes<msgLen THEN
			DELAY(32)
		ENDIF
	UNTIL nBytes>=msgLen

	READ f(response::msgLen)
	status = IO_STATUS(f)
END readBinary

...

-- before we OPEN the file
	SET_FILE_ATR(f, ATR_FIELD)
-- instead of SET_FILE_ATR(f, ATR_EOL, 10)

Here we first grab the current system time in microseconds with the GET_USEC_TIM built-in. Then, in a loop, we check to see if we’ve timed out before using BYTES_AHEAD() to see how many bytes are waiting on the wire. Once the whole message has arrived, we read exactly msgLen bytes. If the server sent two messages back-to-back, the second one stays on the wire for the next READ instead of getting mixed into this one. Just make sure msgLen fits in the response string (128 characters here).

Just for kicks, let’s add a modeBinary variable to our program so we can try out the two different read modes.

VAR
	modeBinary : BOOLEAN
	f          : FILE
	status     : INTEGER
	response   : STRING[128]

Along with readBinary() from above, let’s create routines for each mode:

ROUTINE readASCII(response : STRING; status : INTEGER)
BEGIN
	READ f(response)
	status = IO_STATUS(f)
END readASCII

ROUTINE initBinary
BEGIN
	SET_FILE_ATR(f, ATR_FIELD)
END initBinary

ROUTINE initASCII
BEGIN
	SET_FILE_ATR(f, ATR_EOL, 10) -- newline
END initASCII

ROUTINE init
BEGIN
	IF modeBinary THEN
		initBinary
	ELSE
		initASCII
	ENDIF
END init

ROUTINE readResponse(response : STRING; timeoutMs : INTEGER; status : INTEGER)
BEGIN
	IF modeBinary THEN
		readBinary(response, 5, timeoutMs, status)
	ELSE
		SET_FILE_ATR(f, ATR_TIMEOUT, timeoutMs) -- ms
		readASCII(response, status)
	ENDIF
END readResponse

Add this to the very beginning to make sure it at least has a value:

IF UNINIT(modeBinary) THEN modeBinary=false; ENDIF

Then we can call init after forcing the USER screen to set up the FILE var. This replaces the two SET_FILE_ATR() lines at the top of the program.

init

and use our new readResponse() routine which will use the correct read* routine based on the chosen mode:

readResponse(response, 1000, status)
SELECT status OF

If you want to switch the mode, just select PING_CLIENT in the SELECT menu, and then hit DATA > F1 [TYPE] > KAREL Vars. You can change MODEBINARY to TRUE to use the new binary read mode or FALSE to use the original one that requires the \n message endings.

One thing to note is that the binary mode now actually brings the \n into our response:

Connecting...
Opening file...
Sending PING...
Sent: PING
Received: PONG

Disconnect status: 0

Since we know every message ends with \n, we can check for it with SUB_STR() and strip it off at the end of readBinary():

	READ f(response::msgLen)
	status = IO_STATUS(f)
	IF status<>0 THEN RETURN; ENDIF

	-- strip the trailing newline
	IF SUB_STR(response, msgLen, 1)=CHR(10) THEN
		response = SUB_STR(response, 1, msgLen-1)
	ENDIF
END readBinary

What if the server goes away?

If you run PING_CLIENT without the server running, MSG_CONNECT() will fail, and assert() will post the HOST-216 SM: Invalid Socket alarm from a 67216 status code.

What happens if the server dies immediately after accepting the connection?

If the server closes the connection cleanly right after accepting it, the robot gets a status of 67213, or HOST-213 SM: Read Direction shut down by Peer. If the server process is killed instead, the socket WRITE returns 1152838, another invalid code, and passing it to POST_ERR() raises INTP-322 Invalid arg val for builtin. Interestingly, POST_ERR() rejects this out-of-range value, but invalid values under 1000 (like 282 on a READ timeout) apparently aren’t checked.

If the server dies after receiving our message but before sending a response, we hit the ELSE case of the SELECT branch with status 2021, or FILE-021 End of file.

The takeaway

Socket messaging is a powerful tool for communicating with third-party devices over Ethernet. If you know the device’s message protocol, you can implement it on the robot.

Sockets are basically just slightly special files. We just need to set them up correctly and look out for connection errors:

  1. Define and start the tag.
  2. Set ATR_EOL and ATR_TIMEOUT (or ATR_FIELD for fixed-length reads with BYTES_AHEAD()) before opening the file. ATR_TIMEOUT seems to be OK to adjust on the fly after OPEN.
  3. Use MSG_CONNECT() and MSG_DISCO() (these are only needed for sockets).
  4. Use OPEN, CLOSE, READ and WRITE as normal and, as per usual, make sure to use IO_STATUS() to ensure the operation succeeded. Remember to expect the weird 282 return value on a timeout.

This introductory example doesn’t make any effort to repair broken connections, but perhaps that’s a good idea for another post.

The full source code for this tutorial is available on GitHub.

Robot Whispering
Want the whole picture?
Frames, offsets, KAREL and more — all in Robot Whispering.
Get the book →
// (ALMOST) EVERY TUESDAY

Get the next article in your inbox.