How to Write a FANUC KAREL Logging Utility

Filed under: FANUC KAREL Programming

I received a question via email the other day where someone wanted to know how to log timestamped position data to a file. As luck would have it, I’d been meaning to write a post about something like this for quite a while now.

This application ends up being a great example that covers a broad range of KAREL programming fundamentals:

  1. KAREL variables, constants and data types
  2. Custom ROUTINEs
  3. String manipulation
  4. Bitwise operations
  5. TPE parameters
  6. Error-checking and the status convention
  7. File operations
  8. File pipes (the PIP: device)
  9. Data formatting

Let’s get started by considering our desired TPE interface. We want to be able to log Position Register (PR) values (X, Y, Z, W, P, R) to a logfile whenever this KAREL program is called from a TP program. For maximum flexibility, let’s accept two parameters: one will be an INTEGER id for the PR we want to log, and the other will be a STRING filename for the desired logfile.

Here’s how you might use this utility from a TP program:

CALL LOGPR(1, 'PIP:logpr.dt') ;

We’re going to record comma-separated values (CSV) with the following “schema”: timestamp, PR id, X, Y, Z, W, P, R. The output will look something like this when we’re done:

29-OCT-18 07:56:42, 1,  1807.00,     0.00,  1300.00,   180.00,   -90.00,     0.00

At minimum we are going to need three variables for our KAREL program:

  1. A FILE variable to use as a handle for the logfile that we will write to
  2. An INTEGER variable for the target Position Register id (parameter from TP)
  3. A STRING variable for the logfile destination (parameter from TP)

Let’s create a skeleton program with these three variables:

PROGRAM logpr
VAR
    logFile     : FILE
    prmPosregId : INTEGER
    prmLogFile  : STRING[16]
BEGIN
END logpr

Next we’ll add some functionality by grabbing our parameters from the TP environment via the GET_TPE_PRM built-in. The interface for this procedure is as follows:

GET_TPE_PRM(paramNo : INTEGER; dataType : INTEGER; intVal : INTEGER; realVal : REAL; strVal : STRING; status : INTEGER)

The first parameter, paramNo, is the only input parameter. The rest are outputs. As you can see, each call to GET_TPE_PRM will return an INTEGER to dataType (1 for INTEGER, 2 for REAL and 3 for STRING), the actual value in one of intVal, realVal or strVal, and a status output. The procedure follows the standard FANUC KAREL convention of using 0 as a “normal” status; anything non-zero indicates an error.

Here’s the code we need to get the target Position Register id:

GET_TPE_PRM(1, dataType, prmPosregId, realVal, strVal, status)

You may have noticed an issue with our program already. We’ve provided some variables to the procedure that we haven’t defined yet.

The GET_TPE_PRM built-in requires a variables for all three data types, even if we know that we only want one of them. Let’s satisfy the interface by adding a few “temp” or “throw-away” variables to our VAR declaration section.

Your program should look like this and compile just fine:

PROGRAM logpr
VAR
    logFile     : FILE
    prmPosregId : INTEGER
    prmLogFile  : STRING[16]
    dataType    : INTEGER
    status      : INTEGER
    intVal      : INTEGER
    realVal     : REAL
    strVal      : STRING[1]
BEGIN
    GET_TPE_PRM(1, dataType, prmPosregId, realVal, strVal, status)
END logpr

We’ve fixed our program and gotten it to compile, but there are a couple of glaring issues with our use of GET_TPE_PRM:

  1. We are not checking to make sure status is 0 (success) before moving on
  2. We are not validating the dataType return value

These are easy enough to fix. Add a couple of IF-blocks immediately beneath the call to GET_TPE_PRM to check for each error condition. Our error reporting will just be a simple message written to the TP message line before aborting the log program entirely.

GET_TPE_PRM(1, dataType, prmPosregId, realVal, strVal, status)
IF status<>0 THEN
    WRITE TPERROR('[logpr] could not get tpe prm 1', CR)
    ABORT
ENDIF
IF dataType<>1 THEN
    WRITE TPERROR('[logpr] expected INTEGER for prm 1', CR)
    ABORT
ENDIF

The IF-statement and associated block should be easy enough to follow (though the <> operator for “not equal” may look strange to you).

The WRITE built-in is used as follows:

WRITE <file_var> (data_item {, data_item})

The file_var is optional (TPDISPLAY is the default, the USER screen), and you only need one data_item, but the routine will accept more separated by commas.

Our statement writes a simple message to the pre-defined TPERROR file (again, TP message line) followed by the predefined CR (carriage return) constant. (If you don’t send a carriage return, your subsequent error messages won’t clear the line and will pile up.)

I don’t like the dataType<>1 expression. What does the 1 represent? I prefer to define constants for situations like this rather than just having random integers lying around with no meaning behind them.

CONST
    TPE_TYP_INT  = 1
    TPE_TYP_REAL = 2
    TPE_TYP_STR  = 3
...
IF dataType<>TPE_TYP_INT THEN
...

After adding some code to get the second logfile name parameter, your program should look like this:

PROGRAM logpr
CONST
    TPE_TYP_INT  = 1
    TPE_TYP_REAL = 2
    TPE_TYP_STR  = 3
VAR
    logFile     : FILE
    prmPosregId : INTEGER
    prmLogFile  : STRING[16]
    dataType    : INTEGER
    status      : INTEGER
    intVal      : INTEGER
    realVal     : REAL
    strVal      : STRING[1]
BEGIN
    -- clear the TPERROR screen
    WRITE TPERROR(chr(128))

    GET_TPE_PRM(1, dataType, prmPosregId, realVal, strVal, status)
    IF status<>0 THEN
        WRITE TPERROR('[logpr] could not get tpe prm 1', CR)
        ABORT
    ENDIF
    IF dataType<>TPE_TYP_INT THEN
        WRITE TPERROR('[logpr] expected INTEGER for prm 1', CR)
        ABORT
    ENDIF

    GET_TPE_PRM(2, dataType, intVal, realVal, prmLogFile, status)
    IF status<>0 THEN
        WRITE TPERROR('[logpr] could not get tpe prm 2', CR)
        ABORT
    ENDIF
    IF dataType<>TPE_TYP_STR THEN
        WRITE TPERROR('[logpr] expected STRING for prm 2', CR)
        ABORT
    ENDIF
END logpr

NOTE: I added a line using the CHR built-in to send the special 128 character code to TPERROR which clears the window.

This will work, but there’s a lot of duplication here. Let’s take this opportunity to refactor into dedicated subroutines for getting INTEGERs and STRINGs from TPE parameters:

PROGRAM logpr
CONST
    TPE_TYP_INT  = 1
    TPE_TYP_REAL = 2
    TPE_TYP_STR  = 3
VAR
    logFile     : FILE
    prmPosregId : INTEGER
    prmLogFile  : STRING[16]

ROUTINE GET_TPE_PRM2(paramNo : INTEGER; expType : INTEGER; intVal : INTEGER; realVal : REAL; strVal : STRING)
VAR
    dataType  : INTEGER
    status    : INTEGER
BEGIN
    GET_TPE_PRM(paramNo, dataType, intVal, realVal, strVal, status)
    IF status<>0 THEN
        WRITE TPERROR('[logpr] could not get tpe prm', paramNo, CR)
        ABORT
    ENDIF

    IF dataType<>expType THEN
        WRITE TPERROR('[logpr] bad data type for prm', paramNo, CR)
        ABORT
    ENDIF
END GET_TPE_PRM2

ROUTINE GET_TPE_INT(paramNo : INTEGER; intVal : INTEGER)
VAR
    realVal : REAL
    strVal  : STRING[1]
BEGIN
    GET_TPE_PRM2(paramNo, TPE_TYP_INT, intVal, realVal, strVal)
END GET_TPE_INT

ROUTINE GET_TPE_STR(paramNo : INTEGER; strVal : STRING)
VAR
    intVal  : INTEGER
    realVal : REAL
BEGIN
    GET_TPE_PRM2(paramNo, TPE_TYP_STR, intVal, realVal, strVal)
END GET_TPE_STR

BEGIN
    -- clear the TPERROR screen
    WRITE TPERROR(chr(128))

    GET_TPE_INT(1, prmPosregId)
    GET_TPE_STR(2, prmLogFile)
END logpr

Ok so we did a lot in this step. Let’s break it down:

  1. Created a routine called GET_TPE_PRM2 that essentially just wraps the built-in GET_TPE_PRM before checking the status and dataType values for us.
  2. Created two other routines, GET_TPE_INT and GET_TPE_STR, that are essentially just shortcuts to GET_TPE_PRM2.

While our program did get longer, the all-important “main” section of our code is only two lines long (outside of clearing the TPERROR screen), and it’s easy to tell what it’s doing. (If we had more than 12 characters available for identifiers, I’d probably rename each routine to something like GET_TPE_INT_OR_ABORT to make the functionality more clear. Alternatively, you could leave the status checking to the main part of the routine to keep it obvious.)

By separating this other functionality into separate ROUTINES, we’ve reduced our complexity and increased the “testability” of our program. (i.e. we could easily test the GET_TPE_PRM2 routine to make sure that it writes an error on a non-zero status, but it might be more difficult to validate that when all that logic just exists within our main code block.)

The next major bit of functionality is getting the current time and formatting it appropriately. The good news is FANUC provides a couple of built-in procedures for this; the bad news is that one of them doesn’t work quite right.

The GET_TIME built-in accepts one INTEGER parameter which is where the current time will be stored. You might expect a UNIX timestamp here (number of seconds since January 1, 1970), but a FANUC timestamp is a little funky. The year, month, day, hour, minute and second (in two-second intervals) are stored in bits 31-25, 24-21, 20-16, 15-11, 10-5 and 4-0 respectively.

It’s great that we can get the current time, but we want to log in a more readable format (e.g. 29-OCT-18 07:56:42). Luckily FANUC provides another built-in to do just this: CNV_TIME_STR. Unfortunately CNV_TIME_STR seems to only return a readable timestamp to the nearest minute.

We’ll need to write another wrapper routine to add the seconds functionality:

ROUTINE CNV_TIME_ST2(timeIn : INTEGER; timeOut : STRING)
VAR
    secondsI : INTEGER
    secondsS : STRING[4]
BEGIN
    -- use FANUC built-in to do most of the work
    CNV_TIME_STR(timeIn, timeOut)
    -- chop off trailing spaces, if any
    timeOut = SUB_STR(timeOut, 1, 15)
    -- add trailing :
    timeOut = timeOut + ':'

    -- get seconds
    secondsI = timeIn AND 31
    secondsI = secondsI * 2

    -- convert to string
    CNV_INT_STR(secondsI, 2, 0, secondsS)
    -- get rid of leading space
    secondsS = SUB_STR(secondsS, 2, 2)
    -- add leading zero if required
    IF timeIn < 10 THEN
        secondsS = '0' + secondsS
    ENDIF

    timeOut = timeOut + secondsS
END CNV_TIME_ST2

First we use FANUC’s built-in to get our timestamp in this format: 29-OCT-18 07:56 (note the trailing space). The SUB_STR built-in is used to only grab the first 15 characters of the string, eliminating that pesky trailing space before concatenating a ‘:’ onto the end. (STRING concatenation is easy in KAREL, just use the + operator between two STRINGs.)

Because FANUC stores the seconds of a timestamp in bits 4-0, we can get the value of just those bits by bitwise AND-ing our timestamp with the number 31 (binary 11111). We then multiply by two since those seconds are actually stored in two-second increments.

We convert this new integer value to a string via the CNV_INT_STR built-in. Unfortunately this built-in puts an annoying leading space on our string, so we get rid of it with the SUB_STR built-in. We add on a leading 0 if we have to in order to keep things consistent with how FANUC’s built-in reports months and days. Finally we concatenate the original timestamp with our seconds STRING as the final output value.

Once this routine is added, we simply add a couple of lines to our main program to get our nicely formatted timestamp (after adding the timeInt and timeStr variables as well):

PROGRAM logpr
CONST
    TPE_TYP_INT  = 1
    TPE_TYP_REAL = 2
    TPE_TYP_STR  = 3
VAR
    logFile     : FILE
    prmPosregId : INTEGER
    prmLogFile  : STRING[16]
    timeInt     : INTEGER
    timeStr     : STRING[18]

ROUTINE GET_TPE_PRM2(paramNo : INTEGER; expType : INTEGER; intVal : INTEGER; realVal : REAL; strVal : STRING)
VAR
    dataType  : INTEGER
    status    : INTEGER
BEGIN
    GET_TPE_PRM(paramNo, dataType, intVal, realVal, strVal, status)
    IF status<>0 THEN
        WRITE TPERROR('[logpr] could not get tpe prm', paramNo, CR)
        ABORT
    ENDIF

    IF dataType<>expType THEN
        WRITE TPERROR('[logpr] bad data type for prm', paramNo, CR)
        ABORT
    ENDIF
END GET_TPE_PRM2

ROUTINE GET_TPE_INT(paramNo : INTEGER; intVal : INTEGER)
VAR
    realVal : REAL
    strVal  : STRING[1]
BEGIN
    GET_TPE_PRM2(paramNo, TPE_TYP_INT, intVal, realVal, strVal)
END GET_TPE_INT

ROUTINE GET_TPE_STR(paramNo : INTEGER; strVal : STRING)
VAR
    intVal  : INTEGER
    realVal : REAL
BEGIN
    GET_TPE_PRM2(paramNo, TPE_TYP_STR, intVal, realVal, strVal)
END GET_TPE_STR

ROUTINE CNV_TIME_ST2(timeIn : INTEGER; timeOut : STRING)
VAR
    secondsI : INTEGER
    secondsS : STRING[4]
BEGIN
    -- use FANUC built-in to do most of the work
    CNV_TIME_STR(timeIn, timeOut)
    -- chop off trailing spaces, if any
    timeOut = SUB_STR(timeOut, 1, 15)
    -- add trailing :
    timeOut = timeOut + ':'

    -- get seconds
    secondsI = timeIn AND 31
    secondsI = secondsI * 2

    -- convert to string
    CNV_INT_STR(secondsI, 2, 0, secondsS)
    -- get rid of leading space
    secondsS = SUB_STR(secondsS, 2, 2)
    -- add leading zero if required
    IF timeIn < 10 THEN
        secondsS = '0' + secondsS
    ENDIF

    timeOut = timeOut + secondsS
END CNV_TIME_ST2

BEGIN
    -- clear the TPERROR screen
    WRITE TPERROR(chr(128))

    GET_TPE_INT(1, prmPosregId)
    GET_TPE_STR(2, prmLogFile)

    GET_TIME(timeInt)
    CNV_TIME_ST2(timeInt, timeStr)
END logpr

Next we have to get the value of the target Position Register. For this we’ll use the GET_POS_REG built-in which only takes an INTEGER id input and an INTEGER status output.

NOTE: Be sure to add an XYZWPR variable named posreg and an INTEGER status variable.

posreg = GET_POS_REG(prmPosregId, status)
IF status<>0 THEN
    WRITE TPERROR('[logpr] could not get PR', prmPosregId, CR)
    ABORT
ENDIF

We’ll also want to check to make sure no part of our Position Register is uninitialized before trying to write those components to our logfile. (You’ll get an error otherwise if a component is UNINIT.)

IF UNINIT(posreg) THEN
    WRITE TPERROR('[logpr] PR', prmPosregId, 'is UNINIT', CR)
    ABORT
ENDIF

We’re almost done. Here’s what we have so far:

PROGRAM logpr
CONST
    TPE_TYP_INT  = 1
    TPE_TYP_REAL = 2
    TPE_TYP_STR  = 3
VAR
    logFile     : FILE
    prmPosregId : INTEGER
    prmLogFile  : STRING[16]
    timeInt     : INTEGER
    timeStr     : STRING[18]
    status      : INTEGER
    posreg      : XYZWPR

ROUTINE GET_TPE_PRM2(paramNo : INTEGER; expType : INTEGER; intVal : INTEGER; realVal : REAL; strVal : STRING)
VAR
    dataType  : INTEGER
    status    : INTEGER
BEGIN
    GET_TPE_PRM(paramNo, dataType, intVal, realVal, strVal, status)
    IF status<>0 THEN
        WRITE TPERROR('[logpr] could not get tpe prm', paramNo, CR)
        ABORT
    ENDIF

    IF dataType<>expType THEN
        WRITE TPERROR('[logpr] bad data type for prm', paramNo, CR)
        ABORT
    ENDIF
END GET_TPE_PRM2

ROUTINE GET_TPE_INT(paramNo : INTEGER; intVal : INTEGER)
VAR
    realVal : REAL
    strVal  : STRING[1]
BEGIN
    GET_TPE_PRM2(paramNo, TPE_TYP_INT, intVal, realVal, strVal)
END GET_TPE_INT

ROUTINE GET_TPE_STR(paramNo : INTEGER; strVal : STRING)
VAR
    intVal  : INTEGER
    realVal : REAL
BEGIN
    GET_TPE_PRM2(paramNo, TPE_TYP_STR, intVal, realVal, strVal)
END GET_TPE_STR

ROUTINE CNV_TIME_ST2(timeIn : INTEGER; timeOut : STRING)
VAR
    secondsI : INTEGER
    secondsS : STRING[4]
BEGIN
    -- use FANUC built-in to do most of the work
    CNV_TIME_STR(timeIn, timeOut)
    -- chop off trailing spaces, if any
    timeOut = SUB_STR(timeOut, 1, 15)
    -- add trailing :
    timeOut = timeOut + ':'

    -- get seconds
    secondsI = timeIn AND 31
    secondsI = secondsI * 2

    -- convert to string
    CNV_INT_STR(secondsI, 2, 0, secondsS)
    -- get rid of leading space
    secondsS = SUB_STR(secondsS, 2, 2)
    -- add leading zero if required
    IF timeIn < 10 THEN
        secondsS = '0' + secondsS
    ENDIF

    timeOut = timeOut + secondsS
END CNV_TIME_ST2

BEGIN
    -- clear the TPERROR screen
    WRITE TPERROR(chr(128))

    GET_TPE_INT(1, prmPosregId)
    GET_TPE_STR(2, prmLogFile)

    GET_TIME(timeInt)
    CNV_TIME_ST2(timeInt, timeStr)

    posreg = GET_POS_REG(prmPosregId, status)
    IF status<>0 THEN
        WRITE TPERROR('[logpr] could not get PR', prmPosregId, CR)
        ABORT
    ENDIF
    IF UNINIT(posreg) THEN
        WRITE TPERROR('[logpr] PR', prmPosregId, ' is UNINIT', CR)
        ABORT
    ENDIF
END logpr

The last bit of functionality is actually writing data to our logfile. In order to do this we need to 1) open the file for writing, 2) write our data and 3) close the file when we’re done.

For reference, here’s how we want the data to look again:

29-OCT-18 07:56:42, 1,  1807.00,     0.00,  1300.00,   180.00,   -90.00,     0.00

We can open files with KAREL with the appropriately named OPEN FILE statement. The interface is as follows:

OPEN FILE file_var (usage_string : STRING; file_string : STRING)

The usage string determines how the file will be accessed:

  • “RO” - Read only
  • “RW” - Read and write
  • “AP” - Append
  • “UD” - Update

If you specify “RO”, you wont’ be able to write to the file (not good for logging). “RW” will allow you to write to the file, but it will clear the contents each time the file is accessed (not good for logging). “UD” won’t clear the contents of the file, but it will overwrite your existing data (not good for logging). Finally, “AP” will simply append new data to the end of the file (good for logging).

Remember that the file name for our logfile is passed via a TPE parameter, prmLogFile:

OPEN FILE logFile ('AP', prmLogFile)

We can then use the IO_STATUS built-in to make sure the file was opened succesfully:

OPEN FILE logFile ('AP', prmLogFile)
status = IO_STATUS(logFile)
IF status<>0 THEN
    WRITE TPERROR('[logpr] could not open logFile', prmLogFile, CR)
    ABORT
ENDIF

Now that the file has been opened, we can write some data. We’ve already seen the WRITE statement; we just need to change the file_var to logFile so the operation works on the file we just opened for appending.

A first pass to include all our data might look like this:

WRITE logFile (timeStr, ',',
    prmPosregId, ',',
    posreg.x, ',',
    posreg.y, ',',
    posreg.z, ',',
    posreg.w, ',',
    posreg.p, ',',
    posreg.r, CR)

This would work just fine, but I dont’ like the default REAL number formatting when writing (scientific notation). KAREL allows you to add formatting specifiers to items within READ and WRITE statements.

For REAL numbers, the first number indicates how many characters will be written. The second number indicates how many numbers after the decimal will be included. To make sure we don’t lose data, we’ll use 9 as the first format specifier and a 2 for the second format specifier to only include two numbers after the decimal:

WRITE logFile (timeStr, ',',
    prmPosregId, ',',
    posreg.x::9::2, ',',
    posreg.y::9::2, ',',
    posreg.z::9::2, ',',
    posreg.w::9::2, ',',
    posreg.p::9::2, ',',
    posreg.r::9::2, CR)

Let’s check the IO_STATUS again to make sure our WRITE operation was succesfull:

WRITE logFile (timeStr, ',',
    prmPosregId, ',',
    posreg.x::9::2, ',',
    posreg.y::9::2, ',',
    posreg.z::9::2, ',',
    posreg.w::9::2, ',',
    posreg.p::9::2, ',',
    posreg.r::9::2, CR)
status = IO_STATUS(logFile)
IF status<>0 THEN
    WRITE TPERROR('[logpr] error writing to logFile', status, CR)
    ABORT
ENDIF

You can always debug the status value and use the Error Code manual to find out what’s wrong. The status codes are output like so: FFCCC where FF is the facility code and CCC is the actual error code. For example, a status code of 66013 corresponds to facility code 66, HRTL, code 13: HRTL-013 Access permission denied. (Interestingly enough, I get this error code when attempting to WRITE to a read-only FILE handle. I would have expected to get a code of 02040: FILE-040 Illegal file access mode. Oh well.)

Lastly, let’s be a good citizen and close the file before returning to the TP program:

CLOSE FILE logFile

Here’s the KAREL logging utility program in its entirety:

PROGRAM logpr
CONST
    TPE_TYP_INT  = 1
    TPE_TYP_REAL = 2
    TPE_TYP_STR  = 3
VAR
    logFile     : FILE
    prmPosregId : INTEGER
    prmLogFile  : STRING[16]
    timeInt     : INTEGER
    timeStr     : STRING[18]
    status      : INTEGER
    posreg      : XYZWPR

ROUTINE GET_TPE_PRM2(paramNo : INTEGER; expType : INTEGER; intVal : INTEGER; realVal : REAL; strVal : STRING)
VAR
    dataType  : INTEGER
    status    : INTEGER
BEGIN
    GET_TPE_PRM(paramNo, dataType, intVal, realVal, strVal, status)
    IF status<>0 THEN
        WRITE TPERROR('[logpr] could not get tpe prm', paramNo, CR)
        ABORT
    ENDIF

    IF dataType<>expType THEN
        WRITE TPERROR('[logpr] bad data type for prm', paramNo, CR)
        ABORT
    ENDIF
END GET_TPE_PRM2

ROUTINE GET_TPE_INT(paramNo : INTEGER; intVal : INTEGER)
VAR
    realVal : REAL
    strVal  : STRING[1]
BEGIN
    GET_TPE_PRM2(paramNo, TPE_TYP_INT, intVal, realVal, strVal)
END GET_TPE_INT

ROUTINE GET_TPE_STR(paramNo : INTEGER; strVal : STRING)
VAR
    intVal  : INTEGER
    realVal : REAL
BEGIN
    GET_TPE_PRM2(paramNo, TPE_TYP_STR, intVal, realVal, strVal)
END GET_TPE_STR

ROUTINE CNV_TIME_ST2(timeIn : INTEGER; timeOut : STRING)
VAR
    secondsI : INTEGER
    secondsS : STRING[4]
BEGIN
    -- use FANUC built-in to do most of the work
    CNV_TIME_STR(timeIn, timeOut)
    -- chop off trailing spaces, if any
    timeOut = SUB_STR(timeOut, 1, 15)
    -- add trailing :
    timeOut = timeOut + ':'

    -- get seconds
    secondsI = timeIn AND 31
    secondsI = secondsI * 2

    -- convert to string
    CNV_INT_STR(secondsI, 2, 0, secondsS)
    -- get rid of leading space
    secondsS = SUB_STR(secondsS, 2, 2)
    -- add leading zero if required
    IF timeIn < 10 THEN
        secondsS = '0' + secondsS
    ENDIF

    timeOut = timeOut + secondsS
END CNV_TIME_ST2

BEGIN
    -- clear the TPERROR screen
    WRITE TPERROR(chr(128))

    GET_TPE_INT(1, prmPosregId)
    GET_TPE_STR(2, prmLogFile)

    GET_TIME(timeInt)
    CNV_TIME_ST2(timeInt, timeStr)

    posreg = GET_POS_REG(prmPosregId, status)
    IF status<>0 THEN
        WRITE TPERROR('[logpr] could not get PR', prmPosregId, CR)
        ABORT
    ENDIF
    IF UNINIT(posreg) THEN
        WRITE TPERROR('[logpr] PR', prmPosregId, ' is UNINIT', CR)
        ABORT
    ENDIF

    OPEN FILE logFile ('AP', prmLogFile)
    status = IO_STATUS(logFile)
    IF status<>0 THEN
        WRITE TPERROR('[logpr] could not open logFile', prmLogFile, CR)
        ABORT
    ENDIF
    WRITE logFile (timeStr, ',',
        prmPosregId, ',',
        posreg.x::9::2, ',',
        posreg.y::9::2, ',',
        posreg.z::9::2, ',',
        posreg.w::9::2, ',',
        posreg.p::9::2, ',',
        posreg.r::9::2, CR)
    status = IO_STATUS(logFile)
    IF status<>0 THEN
        WRITE TPERROR('[logpr] error writing to logFile', status, CR)
        ABORT
    ENDIF

    CLOSE FILE logFile
END logpr

Not too bad, right?

The easiest way to view your logfile is with a web browser: http://robot.host/pip/logpr.dt. You could also grab it via FTP (be sure to cd pip: to change to the PIP: device).

One last thing:

You may be wondering why I chose to write to a file on the PIP: device in my example usage:

CALL LOGPR(1, 'PIP:logpr.dt') ;

We could written to UD1: or UT1: just as easily.

Here are two reasons why the PIP: device is perfect for this application:

  1. The files are stored in-memory, so access is very efficient (low latency logging)
  2. File pipes are of a fixed size circular buffer (8k by default), so we don’t have to worry about our logfile getting too big. (If our logfile is at the maximum size, the next time we log a position to the file, the first log entry will be overwritten.)

I hope you’ve found this KAREL programming tutorial helpful. As always, let me know if you have any questions.


Want posts like this delivered right to your inbox?

If you liked this post, please sign up for my mailing list!