ROS2 Action over Routing service

8 posts / 0 new
Last post
Offline
Last seen: 7 months 3 weeks ago
Joined: 01/29/2021
Posts: 4
ROS2 Action over Routing service

Hello,

i want to route certian topics from a docker network to an outside server, which shall perform a task. For that Ros2 has designed an action, wich i want to use,

For testing (avoiding building an docker image for each new try) i build up a test network with 3 different virtual machines:

VM A: Host Only Network (192.168.64.20/24)
VM B: Host Only Network (192.168.80.20/24)
VM Router: Two Host Only Networks (192.168.64.1/24 AND 192.168.80.24/24) which is the connection of the two networks.

So far i have managed to set up the asymmetric routing scenario, wich seems to work fine with ros topics and ros services, but not for ros actions. I cannot route an action request to the action server.

I used the xml files given from the example, wich i have edited for my network. And i have edited <creation_mode> to IMMEDIATE which seems to work much better with ROS topics and services.

I also tried setting the tag <publish_with_ original_timestamp> to true (perhaps actions are sensitive for the timestamp), but this does not change anything.

I also tried the versions 5.3.1 and 6.0.1 of the routing service, but this does not change anything too.

If somebody has experience in setting up Ros and the rti routing service, i would very appreciate any tips.

Kind regards,

Christoph

 

AttachmentSize
File Config for VM B5.29 KB
File Config for VM A5.24 KB
Organization:
Offline
Last seen: 2 days 3 hours ago
Joined: 11/14/2017
Posts: 26

Hello Christoph,

Just a quick note to say that I've set this up on a local UDP network (starting with small steps), to do a RS transform between different DDS domains -- all good so far.
I'll next test it across a TCP connection, it might be a day before I can get to it.

Thanks -
Neil

Offline
Last seen: 2 days 3 hours ago
Joined: 11/14/2017
Posts: 26

Hi Christoph,


My test configuration was a little different -- but it worked as expected.
The chain was:

LocalPC[
   DDS application with ROS2 action topics for TurtleSim Rotate_Absolute on UDP domain 1
   Routing Service(UDP domain 1 to TCP domain 2 at cloud IP address, port 7501)
]
CloudPC[
   Routing Service(TCP domain 2/port 7501 : TCP domain 1/port 7500)
]
LocalPC[
   Routing Service(TCP domain 1/port 7500 to UDP domain 0)
   ROS2 TurtleSim application on domain 0
]

So it's actually a 3 routing service connection, but it works.

A few questions:
  Does any part of the action topic get through?
  As in: does the action request get through, but no response?    Does the feedback topic come through?
  What does Admin Console show you on each end?

  Are there any specific QoS settings being made on one end or the other, that are not also being set in the RS configuration?
  -- note that these QoS settings need to be set for the TCP connection (to match the QoS on the UDP side).

  Can you replicate this setup outside of Docker? (to separate the Docker issues from the DDS issues).

Please let me know if the above test setup is of interest, I can share the configuration files.

Thanks!
Neil

Offline
Last seen: 7 months 3 weeks ago
Joined: 01/29/2021
Posts: 4

Hello Neil,

first of all thank you very much for your effort!
Somehow the problem must be something with the requested/offered QoS by my used demo application. I have used the action_tutorials node (https://github.com/ros2/demos/tree/foxy/action_tutorials) for testing. Apparently, the actions of turtlesim do work!
Back to the action_tutorials node: Here i can see an QoS Error in the Match Analyses: The topic "rt/fibonacci/_action/status" and "ros_discovery_info" do have an discovery error, where the offered and requested "Durability.kind" setting are not compatible to each other. (See the attached Screenshot)

Offered: VOLATILE_DURABILITY_QOS
Requested: TRANSIENT_LOCAL_DURABILITY_QOS

Now i have to figure out how i can set the durability in the ROS application.

To your questions:

  Does any part of the action topic get through? 
  - The Topics are created on each side. One topic seems to have errors in the Match creation phase related to the QoS settings. (See the attached screenshot)

  As in: does the action request get through, but no response?    Does the feedback topic come through?
  - The action server does not get any message. It does not know that there is a request from the other side.

  What does Admin Console show you on each end?
  - See the attached Screenshot. It looks the same at both ends.

  Are there any specific QoS settings being made on one end or the other, that are not also being set in the RS configuration?
  - Not by me. I used the standard QoS Settings provided by the demo nodes from ROS (Action tutorials and turtle sim)

So finally i think the problem lays on the ROS side and the provided QoS settings. I will dig deeper into this and let you know if i can get the action tutorials node run.

Thank you again for your help :)

Kind regards,
Christoph

File Attachments: 
Offline
Last seen: 2 days 3 hours ago
Joined: 11/14/2017
Posts: 26

Hello Christoph,

Thank you for the detailed response!
There are a few things happening here:

The feedback topic for a ROS2 ACTION gets its own separate QoS setting; you can see an example of where that happens in the ROS2 RCL code at:
https://github.com/ros2/rcl/blob/0ca8c6162136594e490ba69fbf288de1b5f7073d/rcl_action/include/rcl_action/default_qos.h#L26
This is where it's getting set to TRANSIENT_LOCAL volatility, causing the mismatch.

To resolve this with RTI Routing Service, the configuration file will need to define a route with a matching QoS setting for this topic; something like:

<auto_topic_route name="AllForward">
<publish_with_original_info>true</publish_with_original_info>
<input participant="1">
<deny_topic_name_filter>rti/*, */_action/status, ros_discovery_info</deny_topic_name_filter>
<creation_mode>IMMEDIATE</creation_mode>
</input>
<output>
<deny_topic_name_filter>rti/*, */_action/status, ros_discovery_info</deny_topic_name_filter>
<creation_mode>IMMEDIATE</creation_mode>
</output>
</auto_topic_route>
<auto_topic_route name="AllForwardTransientLocal">
<publish_with_original_info>true</publish_with_original_info>
<input participant="1">
<allow_topic_name_filter>*/_action/status, ros_discovery_info</allow_topic_name_filter>
<creation_mode>IMMEDIATE</creation_mode>
<durability>
<kind>TRANSIENT_LOCAL_DURABILITY_QOS</kind>
</durability>
</input>
<output>
<allow_topic_name_filter>*/_action/status, ros_discovery_info</allow_topic_name_filter>
<creation_mode>IMMEDIATE</creation_mode>
<durability>
<kind>TRANSIENT_LOCAL_DURABILITY_QOS</kind>
</durability>
</output>
</auto_topic_route>
<auto_topic_route name="AllBackward">
<publish_with_original_info>true</publish_with_original_info>
<input participant="2">
<deny_topic_name_filter>rti/*, */_action/status, ros_discovery_info</deny_topic_name_filter>
<creation_mode>IMMEDIATE</creation_mode>
</input>
<output>
<deny_topic_name_filter>rti/*, */_action/status, ros_discovery_info</deny_topic_name_filter>
<creation_mode>IMMEDIATE</creation_mode>
</output>
</auto_topic_route>
<auto_topic_route name="AllBackwardTransientLocal">
<publish_with_original_info>true</publish_with_original_info>
<input participant="2">
<allow_topic_name_filter>*/_action/status, ros_discovery_info</allow_topic_name_filter>
<creation_mode>IMMEDIATE</creation_mode>
<durability>
<kind>TRANSIENT_LOCAL_DURABILITY_QOS</kind>
</durability>
</input>
<output>
<allow_topic_name_filter>*/_action/status, ros_discovery_info</allow_topic_name_filter>
<creation_mode>IMMEDIATE</creation_mode>
<durability>
<kind>TRANSIENT_LOCAL_DURABILITY_QOS</kind>
</durability>
</output>
</auto_topic_route>

 

This is a sketched (untested) example, it might have some issue.

This config example also provides for routing the 'ros_discovery_info' topic, which helps ROS2 nodes get additional information to construct a node graph of the system, in case you need that capability in your system.

Please let me know how this works out :)

Thanks!

Neil

Offline
Last seen: 7 months 3 weeks ago
Joined: 01/29/2021
Posts: 4

Hello Neil,

your suggestion does not work out for me. Te possible reliability tags inside <input> or <output> must be set within <datareader_qos> or <datawriter_qos>.
The <reliability> tag within this tag allows "BEST_EFFORT_RELIABILITY_QOS" and  "RELIABLE_RELIABILITY_QOS".
A Test with these tags do not give me the wanted result

My session config is looking like this now:
Notice, that i uncommented the tag  <kind>TRANSIENT_LOCAL_DURABILITY_QOS</kind>, which does not work.

              <session name="Session">
                <!-- Copy Paste -->
                <auto_topic_route name="AllForward">
                    <publish_with_original_info>true</publish_with_original_info>
                    <input participant="1">
                        <deny_topic_name_filter>rti/*,*/_action/status,ros_discovery_info</deny_topic_name_filter>
                        <creation_mode>IMMEDIATE</creation_mode>
                    </input>
                    <output>
                        <deny_topic_name_filter>rti/*,*/_action/status,ros_discovery_info</deny_topic_name_filter>
                        <creation_mode>IMMEDIATE</creation_mode>
                    </output>
                </auto_topic_route>
                <auto_topic_route name="AllForwardTransientLocal">
                    <publish_with_original_info>true</publish_with_original_info>
                    <input participant="1">
                        <allow_topic_name_filter>*/_action/status,ros_discovery_info</allow_topic_name_filter>
                        <creation_mode>IMMEDIATE</creation_mode>
                        <datareader_qos> 
                            <!-- <kind>TRANSIENT_LOCAL_DURABILITY_QOS</kind> -->
                            <reliability>
                                <kind>BEST_EFFORT_RELIABILITY_QOS</kind>
                            </reliability>
                            <history>
                                <kind>KEEP_ALL_HISTORY_QOS</kind>
                            </history>
                        </datareader_qos> 
                    </input>
                    <output>
                        <allow_topic_name_filter>*/_action/status,ros_discovery_info</allow_topic_name_filter>
                        <creation_mode>IMMEDIATE</creation_mode>
                        <datawriter_qos>
                            <!-- <kind>TRANSIENT_LOCAL_DURABILITY_QOS</kind> -->
                            <reliability>
                                <kind>BEST_EFFORT_RELIABILITY_QOS</kind>
                            </reliability>
                            <history>
                                <kind>KEEP_ALL_HISTORY_QOS</kind>
                            </history>
                        </datawriter_qos>
                    </output>
                </auto_topic_route>
                <auto_topic_route name="AllBackward">
                    <publish_with_original_info>true</publish_with_original_info>
                    <input participant="2">
                        <deny_topic_name_filter>rti/*,*/_action/status,ros_discovery_info</deny_topic_name_filter>
                    <creation_mode>IMMEDIATE</creation_mode>
                    </input>
                    <output>
                        <deny_topic_name_filter>rti/*,*/_action/status,ros_discovery_info</deny_topic_name_filter>
                        <creation_mode>IMMEDIATE</creation_mode>
                    </output>
                </auto_topic_route>
                <auto_topic_route name="AllBackwardTransientLocal">
                    <publish_with_original_info>true</publish_with_original_info>
                    <input participant="2">
                        <allow_topic_name_filter>*/_action/status,ros_discovery_info</allow_topic_name_filter>
                        <creation_mode>IMMEDIATE</creation_mode>

                        <datareader_qos> 
                            <!-- <kind>TRANSIENT_LOCAL_DURABILITY_QOS</kind> -->
                            <reliability>
                                <kind>BEST_EFFORT_RELIABILITY_QOS</kind>
                            </reliability>
                            <history>
                                <kind>KEEP_ALL_HISTORY_QOS</kind>
                            </history>
                        </datareader_qos> 

                    </input>
                    <output>
                        <allow_topic_name_filter>*/_action/status,ros_discovery_info</allow_topic_name_filter>
                        <creation_mode>IMMEDIATE</creation_mode>

                        <datawriter_qos>
                            <!-- <kind>TRANSIENT_LOCAL_DURABILITY_QOS</kind> -->
                            <reliability>
                                <kind>BEST_EFFORT_RELIABILITY_QOS</kind>
                            </reliability>
                            <history>
                                <kind>KEEP_ALL_HISTORY_QOS</kind>
                            </history>
                        </datawriter_qos>

                    </output>
                </auto_topic_route>
            </session>

Well it seems that the issue is at the ros side.

EDIT:

Digging deeper into the docs... Aparantly i can set TRANSIENT_LOCAL_DURABILITY_QOS for the datareader/writer:

<datawriter_qos>
<reliability>
<kind>RELIABLE_RELIABILITY_QOS</kind>
</reliability>
<durability>
<kind>TRANSIENT_LOCAL_DURABILITY_QOS</kind>
</durability>
</datawriter_qos>

I will test this and let you know if this works :)

Kind regards
Christoph

Offline
Last seen: 7 months 3 weeks ago
Joined: 01/29/2021
Posts: 4

Hello Neil,

it works!

Thank you very much for you help, i was almost giving up on this project...
As described, i have to set the durability of the Routing service to TRANSIENT_LOCAL_DURABILITY_QOS. Here are the full xml files i used:

<?xml version="1.0"?>
<dds xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
     xsi:noNamespaceSchemaLocation="http://community.rti.com/schema/6.0.1/6.0.0/rti_routing_service.xsd">
    
     <!-- ********************************************************************** -->
    <!--                                                                        -->
    <!-- RTI Routing service examples for TCP over WAN                          -->
    <!--                                                                        -->
    <!-- Note: Change the IPs and ports below to fit yours                      -->
    <!--                                                                        -->
    <!-- ********************************************************************** -->

    <!-- ********************************************************************** -->
    <!-- UDP/Shared memory (domain 0) => TCP (domain 1)                         -->
    <!-- ********************************************************************** -->
    <routing_service name="TCP_TestFromNeil">

        <annotation>
            <documentation>
                In a WAN, routes from UDP or shared memory to TCP.
                Remember to edit the file and change the public IP address.
            </documentation>
        </annotation>

        <domain_route name="DR_UDPLAN_TCPWAN">

            <participant name="1">
                <domain_id>0</domain_id>
            </participant>

            <participant name="2">
                <domain_id>1</domain_id>
                <participant_qos>
                    <transport_builtin>
                        <mask>MASK_NONE</mask>
                    </transport_builtin>
                    <discovery>
                      <initial_peers>
                        <element>tcpv4_wan://192.168.80.20:7400</element>
                      </initial_peers>
                    </discovery>
                    <property>
                        <value>
                            <element>
                                <name>dds.transport.load_plugins</name>
                                <value>dds.transport.TCPv4.tcp1</value>
                            </element>
                            <element>
                                <name>dds.transport.TCPv4.tcp1.library</name>
                                <value>nddstransporttcp</value>
                            </element>
                            <element>
                                <name>dds.transport.TCPv4.tcp1.create_function</name>
                                <value>NDDS_Transport_TCPv4_create</value>
                            </element>
                            <element>
                                <name>
                                    dds.transport.TCPv4.tcp1.parent.classid
                                </name>
                                <value>
                                    NDDS_TRANSPORT_CLASSID_TCPV4_WAN
                                </value>
                            </element>
                            <element>
                                <name>dds.transport.TCPv4.tcp1.public_address</name>
                                <value>0.0.0.0:0</value>
                            </element>
                            <element>
                                <name>
                                    dds.transport.TCPv4.tcp1.server_bind_port
                                </name>
                                <value>0</value>
                            </element>
                            <element>
                                <name>
                                    dds.transport.TCPv4.tcp1.disable_nagle
                                </name>
                                <value>1</value>
                            </element>
                        </value>
                    </property>
                </participant_qos>
            </participant>
        

            <session name="Session">
                <!-- Copy Paste -->
                <auto_topic_route name="AllForward">
                    <publish_with_original_info>true</publish_with_original_info>
                    <input participant="1">
                        <deny_topic_name_filter>rti/*,*/_action/status,ros_discovery_info</deny_topic_name_filter>
                        <creation_mode>IMMEDIATE</creation_mode>
                    </input>
                    <output>
                        <deny_topic_name_filter>rti/*,*/_action/status,ros_discovery_info</deny_topic_name_filter>
                        <creation_mode>IMMEDIATE</creation_mode>
                    </output>
                </auto_topic_route>
                <auto_topic_route name="AllForwardTransientLocal">
                    <publish_with_original_info>true</publish_with_original_info>
                    <input participant="1">
                        <deny_topic_name_filter>rti/*</deny_topic_name_filter>
                        <allow_topic_name_filter>*/_action/status,ros_discovery_info</allow_topic_name_filter>
                        <creation_mode>IMMEDIATE</creation_mode>
                        <datareader_qos>
                            <durability>
                                <kind>TRANSIENT_LOCAL_DURABILITY_QOS</kind>
                            </durability>                           
                        </datareader_qos>
                    </input>
                    <output>
                        <deny_topic_name_filter>rti/*</deny_topic_name_filter>
                        <allow_topic_name_filter>*/_action/status,ros_discovery_info</allow_topic_name_filter>
                        <creation_mode>IMMEDIATE</creation_mode>
                        <datawriter_qos>
                            <durability>
                                <kind>TRANSIENT_LOCAL_DURABILITY_QOS</kind>
                            </durability>                            
                        </datawriter_qos>
                    </output>
                </auto_topic_route>
                <auto_topic_route name="AllBackward">
                    <publish_with_original_info>true</publish_with_original_info>
                    <input participant="2">
                        <deny_topic_name_filter>rti/*,*/_action/status,ros_discovery_info</deny_topic_name_filter>
                        <creation_mode>IMMEDIATE</creation_mode>
                    </input>
                    <output>
                        <deny_topic_name_filter>rti/*,*/_action/status,ros_discovery_info</deny_topic_name_filter>
                        <creation_mode>IMMEDIATE</creation_mode>
                    </output>
                </auto_topic_route>
                <auto_topic_route name="AllBackwardTransientLocal">
                    <publish_with_original_info>true</publish_with_original_info>
                    <input participant="2">
                        <deny_topic_name_filter>rti/*</deny_topic_name_filter>
                        <allow_topic_name_filter>*/_action/status,ros_discovery_info</allow_topic_name_filter>
                        <creation_mode>IMMEDIATE</creation_mode>
                        <datareader_qos>
                            <durability>
                                <kind>TRANSIENT_LOCAL_DURABILITY_QOS</kind>
                            </durability>
                        </datareader_qos>
                    </input>
                    <output>
                        <deny_topic_name_filter>rti/*</deny_topic_name_filter>
                        <allow_topic_name_filter>*/_action/status,ros_discovery_info</allow_topic_name_filter>
                        <creation_mode>IMMEDIATE</creation_mode>
                        <datawriter_qos>
                            <durability>
                                <kind>TRANSIENT_LOCAL_DURABILITY_QOS</kind>
                            </durability>                            
                        </datawriter_qos>
                    </output>
                </auto_topic_route>
            </session>
        </domain_route>
    </routing_service>    
    
</dds> 

 

 
<?xml version="1.0"?>
<dds xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
     xsi:noNamespaceSchemaLocation="http://community.rti.com/schema/6.0.1/6.0.0/rti_routing_service.xsd">
    <!-- ********************************************************************** -->
    <!-- TCP (domain 1) => UDP/Shared memory (domain 0)                         -->
    <!--                                                                        -->       
    <!-- ********************************************************************** -->
    <routing_service name="TCP_TestFromNeil">

        <annotation>
            <documentation>
                In a WAN, routes from TCP to UDP or shared memory.
                Remember to edit the file and change the public IP address.
            </documentation>
        </annotation>

        <domain_route name="DR_TCPWAN_UPDLAN">

            <participant name="1">
                <domain_id>1</domain_id>
                <participant_qos>
                    <transport_builtin>
                        <mask>MASK_NONE</mask>
                    </transport_builtin>
                    <property>
                        <value>
                            <element>
                                <name>dds.transport.load_plugins</name>
                                <value>dds.transport.TCPv4.tcp1</value>
                            </element>
                            <element>
                                <name>dds.transport.TCPv4.tcp1.library</name>
                                <value>nddstransporttcp</value>
                            </element>
                            <element>
                                <name>dds.transport.TCPv4.tcp1.create_function</name>
                                <value>NDDS_Transport_TCPv4_create</value>
                            </element>
                            <element>
                                <name>
                                    dds.transport.TCPv4.tcp1.parent.classid
                                </name>
                                <value>
                                    NDDS_TRANSPORT_CLASSID_TCPV4_WAN
                                </value>
                            </element>
                            
                            <element>
                                <name>dds.transport.TCPv4.tcp1.public_address</name>
                                <value>192.168.80.20:7400</value>
                            </element>
                            <element>
                                <name>
                                    dds.transport.TCPv4.tcp1.server_bind_port
                                </name>
                                <value>7400</value>
                            </element>
                            <element>
                                <name>
                                    dds.transport.TCPv4.tcp1.disable_nagle
                                </name>
                                <value>1</value>
                            </element>
                        </value>
                    </property>
                </participant_qos>
            </participant>

            <participant name="2">
                <domain_id>0</domain_id>  
            </participant>

            <session name="Session">
                <!-- Copy Paste -->
                <auto_topic_route name="AllForward">
                    <publish_with_original_info>true</publish_with_original_info>
                    <input participant="1">
                        <deny_topic_name_filter>rti/*,*/_action/status,ros_discovery_info</deny_topic_name_filter>
                        <creation_mode>IMMEDIATE</creation_mode>
                    </input>
                    <output>
                        <deny_topic_name_filter>rti/*,*/_action/status,ros_discovery_info</deny_topic_name_filter>
                        <creation_mode>IMMEDIATE</creation_mode>
                    </output>
                </auto_topic_route>
                <auto_topic_route name="AllForwardTransientLocal">
                    <publish_with_original_info>true</publish_with_original_info>
                    <input participant="1">
                        <deny_topic_name_filter>rti/*</deny_topic_name_filter>
                        <allow_topic_name_filter>*/_action/status,ros_discovery_info</allow_topic_name_filter>                        
                        <creation_mode>IMMEDIATE</creation_mode>
                        <datareader_qos> 
                            <!-- <kind>TRANSIENT_LOCAL_DURABILITY_QOS</kind> -->
                            <durability>
                                <kind>TRANSIENT_LOCAL_DURABILITY_QOS</kind>
                            </durability>
                        </datareader_qos> 
                    </input>
                    <output>
                        <deny_topic_name_filter>rti/*</deny_topic_name_filter>
                        <allow_topic_name_filter>*/_action/status,ros_discovery_info</allow_topic_name_filter>
                        <creation_mode>IMMEDIATE</creation_mode>
                        <datawriter_qos>
                            <!-- <kind>TRANSIENT_LOCAL_DURABILITY_QOS</kind> -->
                            <durability>
                                <kind>TRANSIENT_LOCAL_DURABILITY_QOS</kind>
                            </durability>
                        </datawriter_qos>
                    </output>
                </auto_topic_route>
                <auto_topic_route name="AllBackward">
                    <publish_with_original_info>true</publish_with_original_info>
                    <input participant="2">
                        <deny_topic_name_filter>rti/*,*/_action/status,ros_discovery_info</deny_topic_name_filter>
                    <creation_mode>IMMEDIATE</creation_mode>
                    </input>
                    <output>
                        <deny_topic_name_filter>rti/*,*/_action/status,ros_discovery_info</deny_topic_name_filter>
                        <creation_mode>IMMEDIATE</creation_mode>
                    </output>
                </auto_topic_route>
                <auto_topic_route name="AllBackwardTransientLocal">
                    <publish_with_original_info>true</publish_with_original_info>
                    <input participant="2">
                        <deny_topic_name_filter>rti/*</deny_topic_name_filter>
                        <allow_topic_name_filter>*/_action/status,ros_discovery_info</allow_topic_name_filter>
                        <creation_mode>IMMEDIATE</creation_mode>

                        <datareader_qos> 
                            <!-- <kind>TRANSIENT_LOCAL_DURABILITY_QOS</kind> -->
                            <durability>
                                <kind>TRANSIENT_LOCAL_DURABILITY_QOS</kind>
                            </durability>  
                        </datareader_qos> 
                    </input>
                    <output>
                        <deny_topic_name_filter>rti/*</deny_topic_name_filter>
                        <allow_topic_name_filter>*/_action/status,ros_discovery_info</allow_topic_name_filter>
                        <creation_mode>IMMEDIATE</creation_mode>
                        <datawriter_qos>
                            <!-- <kind>TRANSIENT_LOCAL_DURABILITY_QOS</kind> -->
                            <durability>
                                <kind>TRANSIENT_LOCAL_DURABILITY_QOS</kind>
                            </durability>
                        </datawriter_qos>
                    </output>
                </auto_topic_route>
            </session>
        </domain_route>
    </routing_service>
</dds>

 

Kind regards
Christoph

Offline
Last seen: 2 days 3 hours ago
Joined: 11/14/2017
Posts: 26

Hi Christoph -- that's great news!

Apologies for the overly terse pseudocode, great persistence on your part :) 

Thanks again for the detailed answer.

- Neil