I worked on a palletizing project recently that required serialization. In effect, the product must be tracked through every stop of the process:
- Product picked
- Inspection passed
- Inspection failed
- Product moving to pallet
- Product placed on pallet
- Product dropped
- 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 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
Here’s a simple TP implementation:
! SERIALIZE(signalId : INTEGER) ; ! ----------------------------- ; JMP LBL[AR] ; ; LBL ; ! Robot picked product ; DO[1:productPicked]=PULSE,0.1sec ; END ; ; LBL ; ! Inspection passed ; DO[2:inspectionPassed]=PULSE,0.1sec ; END ; ; ! ...
JMP LBL[AR] 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(signalId : INTEGER) ; ! ----------------------------- ; SELECT R[1:serializeMethod]=1,CALL SERIALIZER_A(AR) ; =2,CALL SERIALIZER_B(AR) ; ! SERIALIZER_A(signalId : INTEGER) ; ! -------------------------------- ; JMP LBL[AR] ; ; LBL ; ! Robot picked product ; DO[1:productPicked]=PULSE,0.1sec ; END ; ; ! ... ; ! SERIALIZER_B(signalId : INTEGER) ; ! -------------------------------- ; JMP LBL[AR] ; ; LBL ; ! 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 "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]=PULSE,0.1sec ; ! HANDSHAKE(id : INTEGER) ; ! ----------------------- ; DO[AR]=ON ; WAIT (DI[AR]) ; DO[AR]=OFF ; WAIT (!DI[AR]) ; ! SERIALIZER_A(signalId : INTEGER) ; ! -------------------------------- ; JMP LBL[AR] ; ; LBL ; ! Robot picked product ; CALL PULSE_DOUT(1) ; END ; ; ! ... ; ! SERIALIZER_B(signalId : INTEGER) ; ! -------------------------------- ; JMP LBL[AR] ; ; LBL ; ! 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
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.
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.