Best practice for handshake

Hi there!
Continuing the example given in this discussion about locking a door: How to consume events without using fish

Let’s change the use case a bit and instead of locking and unlocking, we would like it to be opening and closing a door.

Let’s assume that the request to open the door comes from a robot to the machine’s door (e.g. the robot needs to reach something inside the machine and needs to have the door stays open during the process).
After the robot (say, robot A) sends the request and the machine opens the door, the machine somehow needs to reject any request to close the door if it comes from something other than robot A, until robot A itself request it to close (or tell the machine that it no longer needs the door to be opened).

In my understanding we need some kind of handshake or session management between the machine and the robot. My question is, is there any recommended way or best practice on how to implement this using Actyx?

Thank you in advance!

Hey Dipta,

you’d need to track which parties requested the door to be open and only close it once no one requires it to be still open.

So, starting from the example in the last post you linked, we’d change our DoorFish to record a list of who currently wants the door to be open. Whenever we get an open request, we add the requestor to the list and remove it again on close requests. Since we can’t assume that the door opens and closes instantaneously, we also add a new state indicating the door is moving, along with a corresponding event.

type Robot = 'unknown' | 'robot A' | 'robot B' | 'robot C'
type State = {
  state: 'unknown' | 'open' | 'closed' | 'moving',
  requestedOpenDoor: Robot[],
type DoorMovementStartedEvent = { eventType: 'doorMovementStarted', machineId: number }

In the twin, you’d change onEvent to set the new moving state and to track open requests. Note that we also changed the door(Un)Locked state and events to open and close.

    onEvent: (state, event) => {
      switch (event.eventType) {
        case 'doorOpenRequested':
        case 'doorCloseRequested':
          state.requestedOpenDoor = state.requestedOpenDoor.filter(r => r !== event.requestedBy)
        case 'doorMovementStarted':
          state.state = 'moving'
        case 'doorOpened':
          state.state = 'open'
        case 'doorClosed':
          state.state = 'closed'
      return state

In the pond.observe part you’d change the business logic to

  • open the door if it is closed and someone wants it open
  • close the door if it was was open but nobody needs it open anymore

We also log the state so we can see what happens.

pond.observe(DoorFish.of(doorId), ({ state, requestedOpenDoor }) => {
    console.log(state, requestedOpenDoor)
    switch (state) {
      case 'open':
        if (requestedOpenDoor.length === 0) {
          // the door is open but no one needs it anymore, so let's close it
      case 'closed':
        if (requestedOpenDoor.length > 0) {
          // the door is closed, but someone wants it open, so let's open it

The implementation of openDoor and closeDoor will

  • emit the movementStarted event,
  • interface with the actual hardware to open or close the door through its PLC
  • wait for the physical door to be open by polling the PLC
  • emit the open or closed event

The example implementation simply sleeps for some time to simulate the process taking some time. In the real world, you’d write a variable in the machine’s PLC to make it open the door and poll the door state variable to see if it finished.

Now, let’s run this with some mock events.

  let mockEvents: Event[] = [
    { eventType: 'doorClosed', machineId: 1 },
    { eventType: 'doorOpenRequested', machineId: 1, requestedBy: 'robot A' }, // A wants the door open
    { eventType: 'doorOpenRequested', machineId: 1, requestedBy: 'robot C' }, // C also wants the door open
    { eventType: 'doorCloseRequested', machineId: 1, requestedBy: 'robot B' }, // this event should be ignored
    { eventType: 'doorCloseRequested', machineId: 1, requestedBy: 'robot A' }, // A is done, but the door needs to stay open for C
    { eventType: 'doorCloseRequested', machineId: 1, requestedBy: 'robot C' }, // C is done, the door can be closed

This leads to the following output:

closed []
closed [ 'robot A' ]
opening ...
moving [ 'robot A' ]
moving [ 'robot A', 'robot C' ]
moving [ 'robot A', 'robot C' ]
... opened
open [ 'robot A', 'robot C' ]
open [ 'robot C' ]
open []
closing ...
moving []
... closed
closed []

Is this what you where looking for? Does it help, or do you need anything else?

A complete example is attached: (5.8 KB)
Extract and run with npm install && npm run start.

1 Like

@wwerner this is exactly what I was looking for.
So the key point here is to track the parties that requested the door to be opened as a part of the door fish state… That makes a lot of sense. Thank you!! :smiley::+1:t3:

@Benjamin-Actyx and I just discussed another implementation option we’d like to share as well. The key point (as you realized) still is to keep track of requests but in a different fashion.
I case you want the robot to know that the door is opened and kept open for it specifically. Otherwise, the robot might not know whether it can trust the machine to accommodate it as opposed to the door being open coincidentally and may close again.

Choosing the most suitable implementation depends on your specific scenario, so here’s an outline of the second option.

You can also establish a protocol for ‘open door’ requests and track all open requests in a registry. This registry would replace the part of Door twin tracking the requests.

  • Robot requests the door to be open. The request uses the eventId from the openDoorRequested event as its unique identifier.
  • Machine opens the door and sends an event saying the door is open for that specific request.
  • Robot moves through the door and does its thing.
  • Robot emits an event saying it’s done with the specific request, i.e. it does not require the door to be open any longer.
  • Machine decides whether to close the door (if no one is waiting) or to keep it open for other Robots. This also allows the machine additional control, e.g. to give certain requests priority over others.

The trade-off to consider here is that in case the “I’m done” event propagates slowly due to robot connectivity issues, the machine’s door would be kept open too long.
Whether this is only a theoretical concern or a real issue depends on the plant set-up cost of latency: Is it possible/likely that the robot moves out of WiFi coverage for a significant period of time? Does it impact efficiency? If it is a concern in your case, let’s discuss timeouts and consensus in a separate topic.

Thank you for sharing this idea.
What immediately came up to my mind after reading this idea is to track the “opened for whom?” as a part of the door’s fish state. This way the robot can always check whether 1) the door is opened, and 2) for whom the door is opened, just by checking the state of the door’s fish.

What do you think about this approach?

We are still quite faraway from actual implementation so for now connectivity is not a concern. I am currently more focused on designing the general architecture.

The problem with this approach is that the door

  • might be open for the current request, or
  • might be open coincidentally due to a previous request and thus close too early.

This may happen in case the robot’s request didn’t come through due to temporary connectivity issues. While, depending on your setup, this might be highly unlikely, it’s still best to track not only the requesting robot but also the request itself as described in the second scenario.

How much attention these edge cases warrant depends on the actual use case, but we should be aware of them and consciously decide whether or not to handle them.

I see. That makes sense.
Do you recommend to track the request using a separate (registry) fish?
Or do you think it is enough to record it as part of the door’s fish (e.g. the door’s fish will have:

  • the door status,
  • to which request the door is being opened (e.g. using a request GUID)).