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:
- KAREL variables, constants and data types
- Custom
ROUTINE
s - String manipulation
- Bitwise operations
- TPE parameters
- Error-checking and the
status
convention - File operations
- File pipes (the
PIP:
device) - 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:
- A
FILE
variable to use as a handle for the logfile that we will write to - An
INTEGER
variable for the target Position Register id (parameter from TP) - 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
:
- We are not checking to make sure
status
is 0 (success) before moving on - 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 INTEGER
s and STRING
s 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:
- Created a routine called
GET_TPE_PRM2
that essentially just wraps the built-inGET_TPE_PRM
before checking thestatus
anddataType
values for us. - Created two other routines,
GET_TPE_INT
andGET_TPE_STR
, that are essentially just shortcuts toGET_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 STRING
s.)
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:
- The files are stored in-memory, so access is very efficient (low latency logging)
- 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.