TP Programming With Interfaces

Filed under: FANUC TP Programming

I worked on a palletizing project recently that required serialization. In effect, the product must be tracked through every stop of the process:

  1. Product picked
  2. Inspection passed
  3. Inspection failed
  4. Product moving to pallet
  5. Product placed on pallet
  6. Product dropped
  7. Product rejected

The robot simply sends a Digital Output signal to the PLC when any of these events takes place, and the PLC is responsible for sending signals on to the customer’s serialization service.

It’s tempting to simply sprinkle DO[x]=PULSE,0.1sec statements in the appropriate places and be done with it, but I think it is better to design a single interface that the robot will use to handle all serialization requirements.

Separation of Concerns

Robot programmers already have plenty to worry about: safety, motion, logic, I/O, tooling, etc. The list goes on. Now we have to worry about data tracking as well?

Writing clean code is challenging regardless of your development environment, though it is especially difficult in TPE.

Over the years I’ve found that the best thing I can do is separate my concerns into small programs, allowing my high-level routines to be very readable and descriptive.

I like Wikipedia’s definition for Separation of Concerns:

In computer science, separation of concerns (SoC) is a design principle for separating a computer program into distinct sections, so that each section addresses a separate concern. A concern is a set of information that affects the code of a computer program.

Instead of adding a bunch of I/O-statements to our high-level routines, let’s think about this serialization requirement as a single component. We can encapsulate the functionality with a small interface, hiding the details from the calling program.

The Interface

The customer specification already numbers our serialization signals, so it makes sense to use these unique identifiers as the argument to our interface.

SERIALIZE(signalId : INTEGER)

The high-level routines can simply CALL this interface at the appropriate times and trust that the underlying implementation does what it’s supposed to do.

It does not matter if SERIALIZE is a TP or KAREL program. It does not matter if the signal is sent with a handshake or a PULSE. We are free to bypass serialization entirely with one line of code or swap in new signal definitions behind the scenes. It’s also trivial to find every routine that performs serialization by simply searching for CALLs to SERIALIZE.

Here’s a simple TP implementation:

! SERIALIZE(signalId : INTEGER) ;
! ----------------------------- ;
JMP LBL[AR[1]] ;
 ;
LBL[1] ;
  ! Robot picked product ;
  DO[1:productPicked]=PULSE,0.1sec ;
  END ;
 ;
LBL[2] ;
  ! Inspection passed ;
  DO[2:inspectionPassed]=PULSE,0.1sec ;
  END ;
 ;
! ...

The JMP LBL[AR[1]] statement does a “good enough” job of making sure that any invalid calls to SERIALIZE are caught with a runtime exception. Normally I recommend handling errors gracefully, but i’m ok with the equivalent of a panic here since it would only occur when the programmer makes an error.


This is somewhat contrived, but what if there are two serialization devices: one for testing and one for production? We can implement them separately and delegate to the appropriate one in the main SERIALIZE routine.

! SERIALIZE(signalId : INTEGER) ;
! ----------------------------- ;
SELECT R[1:serializeMethod]=1,CALL SERIALIZER_A(AR[1]) ;
       =2,CALL SERIALIZER_B(AR[1]) ;


! SERIALIZER_A(signalId : INTEGER) ;
! -------------------------------- ;
JMP LBL[AR[1]] ;
 ;
LBL[1] ;
  ! Robot picked product ;
  DO[1:productPicked]=PULSE,0.1sec ;
  END ;
 ;
! ... ;


! SERIALIZER_B(signalId : INTEGER) ;
! -------------------------------- ;
JMP LBL[AR[1]] ;
 ;
LBL[1] ;
  ! Robot picked product ;
  DO[101:productPicked]=ON ;
  WAIT (DI[101:productPickAck]) ;
  DO[101:productPicked]=OFF ;
  WAIT (!DI[101:productPickAck]) ;
  END ;
 ;
! ... ;

It does not matter to the calling routines how (or even if) the serialization actually gets done. They’ve done their job by calling the interface at the appropriate time, and that’s all that matters to them.


Quick Note: Finding Dependencies

If you have unix-like development environment (I highly recommend msys2 for Windows), you can quickly find all of your serialization calls with the following grep command:

> grep "CALL SERIALIZE" src/*.ls

Side-note: Don’t Repeat Yourself

I can’t pass up the opportunity to discuss the Don’t Repeat Yourself (DRY) principle.

Had I written the entire SERIALIZE* routines, you would have noticed a ton of duplication: lots of PULSE statements and handshakes. Let’s refactor:

! PULSE_DOUT(doutID : INTEGER) ;
! ------------------------- ;
DO[AR[1]]=PULSE,0.1sec ;


! HANDSHAKE(id : INTEGER) ;
! ----------------------- ;
DO[AR[1]]=ON ;
WAIT (DI[AR[1]]) ;
DO[AR[1]]=OFF ;
WAIT (!DI[AR[1]]) ;


! SERIALIZER_A(signalId : INTEGER) ;
! -------------------------------- ;
JMP LBL[AR[1]] ;
 ;
LBL[1] ;
  ! Robot picked product ;
  CALL PULSE_DOUT(1) ;
  END ;
 ;
! ... ;


! SERIALIZER_B(signalId : INTEGER) ;
! -------------------------------- ;
JMP LBL[AR[1]] ;
 ;
LBL[1] ;
  ! Robot picked product ;
  CALL HANDSHAKE(101) ;
  END ;
 ;
! ... ;

Not only do we significantly reduce duplication, but we also provide the opportunity to make sweeping changes in one place. We can make all pulses 200msec by changing one line of code in PULSE_DOUT. We could support handshake timeouts by adding a few lines to HANDSHAKE.

Again, the serlialization programs don’t care about the implementation details of the IO-pulse or handshake mechanisms. They simply call the interface and let those concerns take care of themselves.


Side-side-note: Readability

My only concern with the above refactoring is a slight reduction in code readability, but I would argue that the benefits Concern Separation and DRY-ness outweigh the small hit. This is due to a limitation in the TP programming language itself: a lack of named constants.

I’d much rather write something like this:

SIGNAL_ROBOT_PICKED = 101 ;

CALL HANDSHAKE(SIGNAL_ROBOT_PICKED) ;

As a programmer I don’t really care that SIGNAL_ROBOT_PICKED happens to be 101. I just want to call the correct handshake. (Constants are supported in TP+, by the way.)

The best I can do for now is add a comment to indicate that handshake’s intent. I prefer descriptive code over comments, but when we cannot write descriptive code we must comment.

…but I digress. Separate your concerns appropriately, and you will find that your core functionality is easier to compose. I often start by writing my ideal high-level programs first, implementing the details later.


Want posts like this delivered right to your inbox?

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