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.
Note that you cannot make changes to the tag when it is defined/started. You need to use
[ACTION] > STOPand[ACTION] > Undefineto 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_clientLet’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) -- newlineHere 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) -- msHere 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 assertI 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:CRis 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 expectCRto send a carriage return\rcharacter (CHR(13)), but in ROBOGUIDE it sends a line feed\n(CHR(10)) over the wire.ATR_EOLdoesn’t change that either: it only controls whereREADs stop, not whatCRwrites. 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,\nor\r\nvianormalize().
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)
ENDSELECTHere 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
282for some reason, which is very unusual given that it’s not a valid FANUC error code. I almost always usePOST_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 usePOST_ERR()with the282status, you’ll end up with anOS-XXXalarm with what looks like a memory address dump. This seems to happen any time you usePOST_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 readResponseAdd this to the very beginning to make sure it at least has a value:
IF UNINIT(modeBinary) THEN modeBinary=false; ENDIFThen 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.
initand use our new readResponse() routine which will use the correct read* routine based on the chosen mode:
readResponse(response, 1000, status)
SELECT status OFIf 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 readBinaryWhat 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:
- Define and start the tag.
- Set
ATR_EOLandATR_TIMEOUT(orATR_FIELDfor fixed-length reads withBYTES_AHEAD()) before opening the file.ATR_TIMEOUTseems to be OK to adjust on the fly afterOPEN. - Use
MSG_CONNECT()andMSG_DISCO()(these are only needed for sockets). - Use
OPEN,CLOSE,READandWRITEas normal and, as per usual, make sure to useIO_STATUS()to ensure the operation succeeded. Remember to expect the weird282return 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.