From da4cc5483d9466a457fd3b49a76708132943ba83 Mon Sep 17 00:00:00 2001
From: Caileigh Fitzgerald <cfitzgerald@whoi.edu>
Date: Wed, 10 Jul 2024 20:41:58 +0000
Subject: [PATCH 1/2] acomms_driver and modem_sim new default minimum
 ping_timeout_sec.

---
 ros_acomms/src/acomms_driver_node.py          |   4 +-
 ros_acomms/src/tdma_advanced_node.py          |  24 ++-
 ros_acomms/src/tdma_node.py                   |  30 +++
 ros_acomms_modeling/src/modem_sim_node.py     |   4 +-
 ros_acomms_tests/launch/tdma_fast_ping.launch | 173 ++++++++++++++++++
 5 files changed, 223 insertions(+), 12 deletions(-)
 create mode 100644 ros_acomms_tests/launch/tdma_fast_ping.launch

diff --git a/ros_acomms/src/acomms_driver_node.py b/ros_acomms/src/acomms_driver_node.py
index 4b1d86f6..21b4204b 100755
--- a/ros_acomms/src/acomms_driver_node.py
+++ b/ros_acomms/src/acomms_driver_node.py
@@ -522,8 +522,8 @@ class AcommsDriverNode(object):
         """
         rospy.loginfo("Requesting modem send ping")
         self.um.send_fdp_ping(request.dest, request.rate, request.cdr, request.hexdata)
-        if request.reply_timeout <= 0:
-            request.reply_timeout = 5
+        if request.reply_timeout < 1:
+            request.reply_timeout = 1
         ping_reply, cst = self.um.wait_for_fdp_ping_reply(
             include_cst=True, timeout=request.reply_timeout
         )
diff --git a/ros_acomms/src/tdma_advanced_node.py b/ros_acomms/src/tdma_advanced_node.py
index 0f82a1cd..74dfeba7 100755
--- a/ros_acomms/src/tdma_advanced_node.py
+++ b/ros_acomms/src/tdma_advanced_node.py
@@ -48,12 +48,12 @@ class TdmaAdvancedMacNode(TdmaMacNode):
     """
     def __init__(self, subclass=False):
         if not subclass:
-            rospy.init_node('tdma_advanced_mac')
+            rospy.init_node('tdma_advanced_mac') #, log_level=rospy.DEBUG)
             self.tdma_status_publisher = rospy.Publisher('~tdma_advanced_status' if rospy.get_param('~publish_private_status', False) else 'tdma_advanced_status', TdmaAdvancedStatus, queue_size=0)
         super().__init__(subclass=True)
         # take supers active_slots list of ints and generate comms_slot & nav_slots masks
         self.nav_slots = self.config_slot_mask(slots=rospy.get_param('~nav_slots', None), allow_empty=True)
-        self.comms_slots = self.config_slot_mask(slots=rospy.get_param('~comms_slots', None))
+        self.comms_slots = self.config_slot_mask(slots=rospy.get_param('~comms_slots', None), allow_empty=True)
         self.cache_slot_ranges_for_status()
 
         # do advanced stuff
@@ -82,6 +82,11 @@ class TdmaAdvancedMacNode(TdmaMacNode):
         # set up subscriber to toggle software_mute without overhead of dynamic_reconfigure
         rospy.Subscriber('~toggle_software_mute', Header, self.on_toggle_software_mute)
 
+        # this method is called everytime the slot changes. We reset modem ping flags here
+        self.sent_modem_pings = False
+        self.sent_transponder_ping = False
+        self.register_on_slot_change_callback(self.on_slot_changed)
+
         if not subclass:
             # setup dynamic reconf for periodic ping params
             self.first_dynamic_reconf = True
@@ -182,6 +187,7 @@ class TdmaAdvancedMacNode(TdmaMacNode):
             config['packet_length_seconds'] = self.packet_length_seconds
             config['miniframe_rate'] = self.miniframe_rate
             config['dataframe_rate'] = self.dataframe_rate
+
             rospy.logdebug(f'DEBUG: First dynamic_reconfigure call, syncing config from init')
             return config
 
@@ -307,12 +313,13 @@ class TdmaAdvancedMacNode(TdmaMacNode):
             
         return sent_modem_pings, sent_transponder_ping, update_last_tx
 
+    def on_slot_changed(self):
+        self.sent_modem_pings, self.sent_transponder_ping = False, False
+
     def spin(self):
-        rate = rospy.Rate(5)
+        rate = rospy.Rate(15)
         last_tx_time = 0
         msg = None
-        sent_modem_pings = False
-        sent_transponder_ping = False
 
         while not rospy.is_shutdown():
             msg = self.get_current_slot_info(software_mute=self.software_mute)
@@ -320,16 +327,17 @@ class TdmaAdvancedMacNode(TdmaMacNode):
 
             if self.can_transmit_now(msg=msg, last_tx_time=last_tx_time):
                 # we are active and have atleast enough time to send another packet
-                sent_modem_pings, sent_transponder_ping, update_last_tx = self.handle_queuing_packet(
+                self.sent_modem_pings, self.sent_transponder_ping, update_last_tx = self.handle_queuing_packet(
                         msg.current_slot,
                         msg.remaining_active_seconds,
-                        sent_modem_pings, 
-                        sent_transponder_ping
+                        self.sent_modem_pings, 
+                        self.sent_transponder_ping
                     )
                 if update_last_tx:
                     last_tx_time = rospy.get_time()
                 else:
                     # nothing sent, sleep then check again
+                    rospy.logdebug(f'DEBUG: Nothing sent during tdma... remaining_active_seconds: {msg.remaining_active_seconds}')
                     rate.sleep()
                     continue
             else:
diff --git a/ros_acomms/src/tdma_node.py b/ros_acomms/src/tdma_node.py
index 7ca3e8d9..b7c55818 100755
--- a/ros_acomms/src/tdma_node.py
+++ b/ros_acomms/src/tdma_node.py
@@ -11,6 +11,7 @@ import datetime
 import dateutil.parser
 import json
 import logging
+from threading import Thread
 
 from mac_utils import PSK_ROS_FDP_Rates, ManualTransmitRequestQueue # pure python class for non-ros functionality
 from mac_utils import validate_slot_range
@@ -56,10 +57,18 @@ class TdmaMacNode(object):
         self.manual_transmit_queue.add_manual_transmit_topic(topic='nmea_to_modem', msg_type=String)
         self.allow_manual_tx_during_priority_slots = rospy.get_param('~allow_manual_tx_during_priority_slots', False)
 
+        # mechanism for registering callbacks in child classes on slot change
+        self.on_slot_change_callbacks = []
+        self.on_slot_change_thread = Thread(target=self.on_slot_change, daemon=True)
+        self.on_slot_change_thread.start()
+
         if not subclass:
             rospy.loginfo("~*+ tdma_mac node running.")
             self.spin()
 
+    def register_on_slot_change_callback(self, callback):
+        self.on_slot_change_callbacks.append(callback)
+
     def wait_for_services(self, timeout=120):
         rospy.loginfo("INFO: tdma waiting for queue_tx_packet service")
         rospy.wait_for_service('queue_tx_packet', timeout=timeout)
@@ -334,6 +343,27 @@ class TdmaMacNode(object):
 
         return True
 
+    def on_slot_change(self):
+        rate = rospy.Rate(1)
+        last_slot = None
+
+        while not rospy.is_shutdown():
+            rate.sleep()            
+            
+            current_slot, remaining_slot_seconds = self.get_current_slot()
+            if last_slot is None:
+                last_slot = current_slot
+
+            if last_slot != current_slot:
+                for callback in self.on_slot_change_callbacks:
+                    try:
+                        callback()
+                    except:
+                        rospy.logwarn(f'WARNING: Exception thrown during on_slot_change()')
+
+                last_slot = current_slot
+                rospy.sleep(self.slot_duration_seconds / 2)
+
     def spin(self):
         rate = rospy.Rate(5)
         last_tx_time = 0
diff --git a/ros_acomms_modeling/src/modem_sim_node.py b/ros_acomms_modeling/src/modem_sim_node.py
index eefcbcae..8d24b04e 100755
--- a/ros_acomms_modeling/src/modem_sim_node.py
+++ b/ros_acomms_modeling/src/modem_sim_node.py
@@ -378,8 +378,8 @@ class ModemSimNode(object):
             # TODO should handle this error
             return PingModemResponse(timed_out=True)
 
-        if request.reply_timeout < 5:
-            request.reply_timeout = 5
+        if request.reply_timeout < 1:
+            request.reply_timeout = 1
 
         this_transaction, response, sim_packet_reply = None, None, None
         try:
diff --git a/ros_acomms_tests/launch/tdma_fast_ping.launch b/ros_acomms_tests/launch/tdma_fast_ping.launch
new file mode 100644
index 00000000..3bd53278
--- /dev/null
+++ b/ros_acomms_tests/launch/tdma_fast_ping.launch
@@ -0,0 +1,173 @@
+<launch>
+    <arg name="num_slots" default="2" doc="" />
+    <arg name="slot_duration_seconds" default="6" doc="" />
+    <arg name="tdma_type" default="tdma_advanced" doc="type of TDMA node to use: tdma_advanced, or tdma_scripted or tdma_slotted_aloha" />
+    <arg name="test_cycling_rates" default="true" doc="" />
+    <arg name="use_hw_modem" default="true" doc="" />
+
+    <env name="ROSCONSOLE_FORMAT" value="[${severity}] [${time:%Y-%m-%dT%H:%M:%S}] [${node}]: ${message}"/>
+
+    <param name="use_sim_time" value="false"/>
+
+    <group ns="modem0">
+        <group if="$(arg use_hw_modem)">
+            <node name="acomms_driver" pkg="ros_acomms" type="acomms_driver_node.py" respawn="true" respawn_delay="10" >
+                <param name="modem_serial_port" value="/dev/ttyUSB2" type="str" />
+                <param name="modem_baud_rate" value="19200" />
+                <param name="set_modem_time" value="true"/>
+                <param name="pwramp_txlevel" value="2" type="int" />
+                <rosparam param="modem_config" subst_value="True">
+                    ALL: 0
+                    BND: 3
+                    FC0: 25000
+                    BW0: 5000
+                    SST: 1
+                    DTP: 50
+                    pwramp.txlevel: 2
+                    psk.packet.mod_hdr_version: 1
+                    SRC: 0
+                </rosparam>
+            </node>
+        </group>
+
+        <group unless="$(arg use_hw_modem)">
+            <param name="random_seed" value="1" />
+
+            <node name="modem_sim_node" pkg="ros_acomms_modeling" type="modem_sim_node.py" output="screen" >
+                <param name="center_frequency_hz" value="10000" type="int" />
+                <param name="bandwidth_hz" value="5000" type="int" />
+                <param name="SRC" value="0" type="int" />
+            </node>
+
+            <node name="modem_location_sim" pkg="ros_acomms_modeling" type="modem_location_sim_node.py" >
+                <param name="simple_path" value="true" />
+            </node>
+
+        </group>
+
+        <node name="acoustic_range_node" pkg="ros_acomms" type="acoustic_range_node.py" output="screen" >
+            <param name="sound_speed_max_age_sec" value="30" type="int" />
+        </node>
+
+        <node name="mac_switcher" pkg="ros_acomms" type="mac_switcher_node.py" output="screen" required="true" respawn="false" clear_params="true" >
+            <rosparam subst_value="True">
+                default_mac_namespace: 'tdma_fast_ping'
+                managed_mac_namespaces:
+                    - 'tdma'
+
+                # TODO: require managed mac nodes to continually publish the select message at minimum every managed_mac_heartbeat_timeout_sec
+                # .. of the mac manager will revert to the default (or prior mac if configured that way)
+                # .. mac manager select topic name: mac_switcher/MANAGED_MAC_NAMESPACE/select
+                # ..              select topic type: std_msgs/Bool (True for select, False for deselect)
+                managed_mac_heartbeat_timeout_sec: -1 # if set to -1, it will not timeout
+            </rosparam>
+        </node>
+
+        <node name="tdma_fast_ping" pkg="ros_acomms" type="$(arg tdma_type)_node.py" output="screen" required="true" respawn="false">
+            <param name="num_slots" value="$(arg num_slots)"/>
+            <param name="slot_duration_seconds" value="$(arg slot_duration_seconds)" />
+            <param name="active_slots" value="0,1"/>
+            <param name="comms_slots" value=""/>
+            <param name="pings_per_slot" value="1"/>
+            <param name="ping_modem_src" value="1"/>
+            <param name="ping_modem_timeout_sec" value="4" />
+            <param name="packet_length_seconds" value="0"/>
+            <param name="guard_time_seconds" value="0"/>
+            <param name="always_send_test_data" value="true"/>
+            <param name="publish_private_status" value="true"/>
+            <!-- <param name="software_mute" value="true"/> -->
+        </node>
+
+        <node name="tdma" pkg="ros_acomms" type="$(arg tdma_type)_node.py" output="screen" required="true" respawn="false">
+            <param name="num_slots" value="$(arg num_slots)"/>
+            <param name="slot_duration_seconds" value="$(arg slot_duration_seconds)" />
+            <param name="active_slots" value="0,1"/>
+            <param name="packet_length_seconds" value="3"/>
+            <param name="guard_time_seconds" value="2.5"/>
+            <param name="publish_private_status" value="true"/>
+            <param name="software_mute" value="true"/>
+        </node>
+
+        <node name="message_queue_node" pkg="ros_acomms" type="message_queue_node.py" output="screen" >
+            <param name="packet_codec_file" value="$(find ros_acomms_tests)/launch/message_codec_config.yaml"/>
+        </node>
+
+        <node name="packet_dispatch" pkg="ros_acomms" type="packet_dispatch_node.py" output="screen" >
+            <param name="packet_codec_file" value="$(find ros_acomms_tests)/launch/message_codec_config.yaml"/>
+        </node>
+    </group>
+
+    <group ns="modem1">
+        <group if="$(arg use_hw_modem)">
+            <node name="acomms_driver" pkg="ros_acomms" type="acomms_driver_node.py" respawn="true" respawn_delay="10" >
+                <param name="modem_serial_port" value="/dev/ttyUSB3" type="str" />
+                <param name="modem_baud_rate" value="19200" />
+                <param name="set_modem_time" value="true"/>
+                <param name="pwramp_txlevel" value="2" type="int" />
+                <rosparam param="modem_config" subst_value="True">
+                    ALL: 0
+                    BND: 3
+                    FC0: 25000
+                    BW0: 5000
+                    SST: 1
+                    DTP: 50
+                    pwramp.txlevel: 2
+                    psk.packet.mod_hdr_version: 1
+                    SRC: 1
+                </rosparam>
+            </node>
+        </group>
+
+        <group unless="$(arg use_hw_modem)">
+            <param name="random_seed" value="2" />
+
+            <node name="modem_sim_node" pkg="ros_acomms_modeling" type="modem_sim_node.py" output="screen" >
+                <param name="center_frequency_hz" value="10000" type="int" />
+                <param name="bandwidth_hz" value="5000" type="int" />
+                <param name="SRC" value="1" type="int" />
+            </node>
+
+            <node name="modem_location_sim" pkg="ros_acomms_modeling" type="modem_location_sim_node.py" >
+                <param name="simple_path" value="true" />
+            </node>
+
+        </group>
+
+        <node name="acoustic_range_node" pkg="ros_acomms" type="acoustic_range_node.py" output="screen" >
+            <param name="sound_speed_max_age_sec" value="30" type="int" />
+        </node>
+
+        <node name="tdma" pkg="ros_acomms" type="$(arg tdma_type)_node.py" output="screen" required="true" respawn="false">
+            <param name="software_mute" value="true"/>
+            <param name="num_slots" value="$(arg num_slots)"/>
+            <param name="slot_duration_seconds" value="$(arg slot_duration_seconds)" />
+            <param name="active_slots" value="0,1"/>
+            <param name="comms_slots" value=""/>
+            <param name="pings_per_slot" value="1"/>
+            <param name="ping_modem_src" value="0"/>
+            <param name="always_send_test_data" value="True"/>
+            <param name="packet_length_seconds" value="2.5"/>
+            <param name="guard_time_seconds" value="2.5"/>
+        </node>
+
+        <node name="message_queue_node" pkg="ros_acomms" type="message_queue_node.py" output="screen" >
+            <param name="packet_codec_file" value="$(find ros_acomms_tests)/launch/message_codec_config.yaml"/>
+        </node>
+
+        <node name="packet_dispatch" pkg="ros_acomms" type="packet_dispatch_node.py" output="screen" >
+            <param name="packet_codec_file" value="$(find ros_acomms_tests)/launch/message_codec_config.yaml"/>
+        </node>
+    </group>
+
+    <param name="random_seed" value="4" />
+    <param name="multiplier" value="1" />
+    <node name="sim_packet_performance_node" pkg="ros_acomms_modeling" type="sim_packet_performance_node.py" />
+
+    <node name="sim_transmission_loss_node" pkg="ros_acomms_modeling" type="sim_transmission_loss_node.py"  >
+        <param name="water_depth" value="20" type="int" />
+        <param name="bellhop_arrivals" value="false" type="bool" />
+    </node>
+
+    <node name="clock_generator" pkg="ros_acomms_modeling" type="clock_generator.py"/>
+
+</launch>
-- 
GitLab


From 86494088eeb5c92745bfadf2184d2367e51a776b Mon Sep 17 00:00:00 2001
From: Caileigh Fitzgerald <cfitzgerald@whoi.edu>
Date: Tue, 30 Jul 2024 23:22:07 +0000
Subject: [PATCH 2/2] acomms_driver_node and modem_sim_node can now send ROS
 msg traffic in FDP ping payloads

---
 .gitlab-ci.yml                                |  32 ++++
 ros_acomms/cfg/acomms_driver.cfg              |  15 +-
 ros_acomms/src/acomms_driver_node.py          | 137 ++++++++++++++----
 ros_acomms_modeling/src/modem_sim_node.py     |  46 +++++-
 .../launch/test_use_ping_payload.launch       | 115 +++++++++++++++
 ros_acomms_tests/src/test_use_ping_payload.py | 108 ++++++++++++++
 6 files changed, 415 insertions(+), 38 deletions(-)
 create mode 100644 ros_acomms_tests/launch/test_use_ping_payload.launch
 create mode 100755 ros_acomms_tests/src/test_use_ping_payload.py

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 2dba1bba..7f0d67de 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -334,6 +334,37 @@ test-mac_switcher:
     reports:
       junit: '*.results.xml'
 
+test-use_ping_payload:
+  stage: test
+  image: ros_acomms-tests:pipeline_$CI_PIPELINE_ID
+  script:
+    - source /ros_entrypoint.sh
+    - mkdir -p ~/catkin_ws/src
+    - cd ../ && cp -r $CI_PROJECT_NAME ~/catkin_ws/src/
+    - cd ~/catkin_ws/src/ros_acomms && pip install -U -r requirements.txt
+    - cd ~/catkin_ws && catkin_make
+    - roscore &
+    - sleep 5
+    - rosparam set /clock_generator/multiplier $CLOCK_MULTIPLIER
+    - rosparam set /clock_generator/publish_rate $CLOCK_PUBLISH_RATE
+    - source devel/setup.bash
+    - TESTS_PATH=$(rospack find ros_acomms_tests)/src
+    - ROS_ACOMMS_PATH=$(rospack find ros_acomms)/src
+    - ROS_ACOMMS_MODELING_PATH=$(rospack find ros_acomms_modeling)/src
+    - cd ${CI_PROJECT_DIR}
+    - pytest --capture=no --junitxml=test_use_ping_payload.results.xml --cov=$ROS_ACOMMS_PATH --cov-report=  $TESTS_PATH/test_use_ping_payload.py
+    - mv .coverage test_use_ping_payload.coverage  # Note that the next call to pytest will delete anything that starts with .coverage*
+    - coverage combine --keep *.coverage
+    - coverage report
+    - mv .coverage "$CI_JOB_NAME_SLUG.job.coverage"
+  except:
+    - dev/autodocs
+  artifacts:
+    paths:
+      - '*.coverage'
+    reports:
+      junit: '*.results.xml'
+
 test-coverage:
   image: ros_acomms-tests:pipeline_$CI_PIPELINE_ID
   except:
@@ -354,6 +385,7 @@ test-coverage:
     - "test-tdma-slotted-aloha"
     - "test-tdma-scripted"
     - "test-mac_switcher"
+    - "test-use_ping_payload"
   script:
     - mkdir -p ~/catkin_ws/src
     - cp -r ../$CI_PROJECT_NAME ~/catkin_ws/src/
diff --git a/ros_acomms/cfg/acomms_driver.cfg b/ros_acomms/cfg/acomms_driver.cfg
index 475e7e59..e48e2f03 100644
--- a/ros_acomms/cfg/acomms_driver.cfg
+++ b/ros_acomms/cfg/acomms_driver.cfg
@@ -1,6 +1,6 @@
 #! /usr/bin/env python
 import roslib
-from dynamic_reconfigure.parameter_generator_catkin import ParameterGenerator, str_t, int_t, bool_t, double_t
+from dynamic_reconfigure.parameter_generator_catkin import ParameterGenerator, str_t, int_t, bool_t
 
 PACKAGE = 'ros_acomms'
 
@@ -9,10 +9,19 @@ roslib.load_manifest(PACKAGE)
 gen = ParameterGenerator()
 
 # Name, Type, Level, Description, Default, Min, Max
+gen.add("use_ping_payload", bool_t, 0, "Use the payload of an FDP ping for ROS message transport", False)
+gen.add("ping_maximum_miniframe_bytes", int_t, 0, "Ping payload max size", 32, 0, 32) # default size is entire miniframe 32 bytes
 gen.add("tx_inhibit", bool_t, 0, "Disable modem transmissions", False)
-gen.add("pwramp_txlevel", int_t, 0, "pwramp.txlevel setting", 3, 0, 3)
+
+pwramp_txlevel_enum = gen.enum([gen.const("most_loud", int_t, 0, "Highest TX volume"),
+			                    gen.const("more_loud", int_t, 1, "Mid-Upper TX volume"),
+			                    gen.const("less_loud", int_t, 2, "Mid-Lower TX volume"),
+			                    gen.const("least_loud", int_t, 3, "Lowest TX volume")],
+			                    "Modem Transmit Volume")
+
+gen.add("pwramp_txlevel", int_t, 0, "pwramp.txlevel setting", 3, 0, 3, edit_method=pwramp_txlevel_enum)
 
 # The second parameter is the name of a node this could run in (used to generate documentation only),
 # the third parameter is a name prefix the generated files will get (e.g. "<name>Config.h" for c++, or "<name>Config.py"
 # for python.
-exit(gen.generate(PACKAGE, "acomms_driver", "acomms_driver"))
\ No newline at end of file
+exit(gen.generate(PACKAGE, "acomms_driver", "acomms_driver"))
diff --git a/ros_acomms/src/acomms_driver_node.py b/ros_acomms/src/acomms_driver_node.py
index 21b4204b..a4cd2946 100755
--- a/ros_acomms/src/acomms_driver_node.py
+++ b/ros_acomms/src/acomms_driver_node.py
@@ -40,7 +40,7 @@ from ros_acomms_msgs.msg import (
 from ros_acomms_msgs.srv import QueryModemParam, QueryModemParamRequest, QueryModemParamResponse
 from ros_acomms_msgs.srv import PingModem, PingModemResponse
 from ros_acomms_msgs.srv import PingTransponders, PingTranspondersResponse
-from ros_acomms_msgs.srv import QueueTxPacket, QueueTxPacketResponse
+from ros_acomms_msgs.srv import QueueTxPacket, QueueTxPacketResponse, GetNextPacketData
 from ros_acomms_msgs.srv import QueryModemSrcRequest, QueryModemSrcResponse, QueryModemSrc
 from ros_acomms.cfg import acomms_driverConfig
 from std_msgs.msg import Header, String, Time
@@ -121,7 +121,24 @@ class AcommsDriverNode(object):
         modem_serial_port = rospy.get_param("~modem_serial_port", "/dev/ttyS3")
         modem_baud_rate = rospy.get_param("~modem_baud_rate", 19200)
         set_modem_time = rospy.get_param("~set_modem_time", False)
-        modem_config = rospy.get_param("~modem_config", {})
+        self.modem_config = rospy.get_param("~modem_config", {})
+
+        # pull modem_config params used in dynamic_reconfigure (if there, otherwise use default and add to modem_config dict)
+        self.pwramp_txlevel = self.modem_config.get('pwramp.txlevel', None)
+        self.tx_inhibit = self.modem_config.get('xmit.txinhibit', None)
+
+        if self.pwramp_txlevel is None:
+            self.pwramp_txlevel = rospy.get_param("~pwramp_txlevel", 3) # default is lowest volume (checks top level param)
+            self.modem_config['pwramp.txlevel'] = self.pwramp_txlevel
+
+        if self.tx_inhibit is None:
+            self.tx_inhibit = False # default is false
+            self.modem_config['xmit.txinhibit'] = 1 # when 1:False, 2:True. 0:floating
+
+        # set the updated modem_config dict and pwramp_txlevel on the param server
+        rospy.set_param("~modem_config", self.modem_config)
+        rospy.set_param("~pwramp_txlevel", self.pwramp_txlevel)
+
         self.publish_partial_packets = rospy.get_param("~publish_partial_packets", True)
 
         port = modem_serial_port
@@ -194,7 +211,7 @@ class AcommsDriverNode(object):
         if set_modem_time:
             self.um.set_time()
 
-        for p, v in modem_config.items():
+        for p, v in self.modem_config.items():
             self.um.set_config(p, v)
 
         # Request all the parameters so they're saved in the log file.
@@ -243,13 +260,61 @@ class AcommsDriverNode(object):
             "query_modem_param", QueryModemParam, self.handle_query_modem_param
         )
 
+        # check if we should use the ping payload for message transport
+        self.use_ping_payload = rospy.get_param("~use_ping_payload", False)
+        # the default is the max bytes we can fit in a miniframe
+        self.ping_maximum_miniframe_bytes = rospy.get_param("~ping_maximum_miniframe_bytes", 32)
+
+        # if using the ping payload (or want to use later), configure the service proxy to message_queue
+        rospy.loginfo("acomms_driver_node creating get_next_packet_data service proxy")
+        self.get_next_packet_data = rospy.ServiceProxy('get_next_packet_data', GetNextPacketData)
+
+        # set flag for first callback only. This syncs dynamic reconf with params passed at launch
+        self.first_dynamic_reconf = True
         self.reconfigure_server = DynamicReconfigureServer(
             acomms_driverConfig, self.reconfigure
         )
+
         self.publish_nodestatus(True, "acomms_driver_node initialized.")
 
         rospy.loginfo("Acomms driver node initialized.")
 
+    def reconfigure(self, config, level):
+        if self.first_dynamic_reconf:
+            self.first_dynamic_reconf = False
+            # due to way rosparam of typle list doesn't properly map to param server in launch file
+            # ..meaning the defaults set in the .cfg file will always use those values at first call
+            # ..so to avoid having defaults set (when ros handles this by calling all non-list type ros params from param server),
+            # ..set values from init in config
+            config["tx_inhibit"] = self.tx_inhibit
+            config["pwramp_txlevel"] = self.pwramp_txlevel
+
+            config["use_ping_payload"] = self.use_ping_payload
+            config["ping_maximum_miniframe_bytes"] = self.ping_maximum_miniframe_bytes
+
+            rospy.logdebug(f'DEBUG: First dynamic_reconfigure call, syncing config from init')
+            return config
+            
+        rospy.loginfo("Acomms driver reconfigure request received.")
+
+        self.pwramp_txlevel = config["pwramp_txlevel"]
+        self.tx_inhibit = config["tx_inhibit"]
+            
+        self.use_ping_payload = config["use_ping_payload"]
+        self.ping_maximum_miniframe_bytes = config["ping_maximum_miniframe_bytes"] # dynamic reconf has a min:0 max:32
+    
+        # we always set all the params passed rather than checking state. 
+        # .. e.g., If the modem is booted into another slot etc.. we may need to set the same value again
+        rospy.loginfo(f"Acomms driver reconfigure request received. config={config}")
+
+        um_response = self.um.set_config("xmit.txinhibit", 2 if self.tx_inhibit else 1, response_timeout=5)
+        rospy.loginfo(f"Acomms driver reconfigure xmit.txinhibit um_response: {um_response}")
+
+        um_response = self.um.set_config("pwramp.txlevel", self.pwramp_txlevel, response_timeout=5)
+        rospy.loginfo(f"Acomms driver reconfigure pwramp.txlevel um_response: {um_response}")
+
+        return config
+
     def verify_modem_connection(self, missed_checkin_factor: int = 5):
         secs_since_checkin = rospy.get_rostime().secs - self.carev_checkin.secs
         cycle_timeout_factor = self.um.config_data['CTO'] * missed_checkin_factor
@@ -403,9 +468,37 @@ class AcommsDriverNode(object):
                 if "CATXF" in incoming_string:
                     txf_msg = Header(stamp=rospy.get_rostime())
                     self.txf_publisher.publish(txf_msg)
+
+                # if any params we set with dynamic reconfigure flow through, update our value
+                # .. so future calls to dynamic reconfigure set the latest value
+                if "CACFG" in incoming_string:
+                    self.handle_config_update(cacfg_nmea=incoming_string)
+
             except Exception as e:
                 rospy.logerr_throttle(1, "Error in NMEA RX handler: {}".format(e))
 
+    def handle_config_update(self, cacfg_nmea):
+        # any params added to dynamic reconfigure should add a conditional to this method
+        # this methods intent is to capture those values as they are read in a $CACFG
+        is_dynamic_reconf_param = False
+        try:
+            value = cacfg_nmea.split('*')[0].split(',')[-1]
+            if 'pwramp.txlevel' in cacfg_nmea:
+                is_dynamic_reconf_param = True
+                self.pwramp_txlevel = int(value)
+                
+            elif 'xmit.txinhibit' in cacfg_nmea:
+                is_dynamic_reconf_param = True
+                if int(value) == 2:
+                    self.tx_inhibit = True
+                else:
+                    self.tx_inhibit = False
+        except ValueError:
+            rospy.logwarn(f'WARNING: ValueError when updating modem config value managed by dynamic reconfigure! modem msg: {cacfg_nmea}')
+            rospy.logwarn(f'WARNING: Leaving config value as-is. A call to dynamic reconfigure will use the old value.')
+        else:
+            if is_dynamic_reconf_param: rospy.logdebug(f'DEBUG: Got a CACFG for a param used in dynamic_reconfigure! Updating: {cacfg_nmea}')
+
     def handle_ping_transponders(self, request):
         """handler for rospy PingTransponders service class
 
@@ -521,7 +614,20 @@ class AcommsDriverNode(object):
             (PingModemResponse): ping response from the modem
         """
         rospy.loginfo("Requesting modem send ping")
-        self.um.send_fdp_ping(request.dest, request.rate, request.cdr, request.hexdata)
+        ping_payload = request.hexdata
+
+        if self.use_ping_payload and self.get_next_packet_data:
+            rospy.loginfo(f"Attempting to fill ping payload for ROS msg transport. payload max size: {self.ping_maximum_miniframe_bytes} bytes")
+            try:
+                packet_data_response = self.get_next_packet_data(num_miniframe_bytes=self.ping_maximum_miniframe_bytes, num_dataframe_bytes=0)
+            except:
+                rospy.logwarn(f"ERROR: problem with get_next_packet_data() service. Not sending a payload with this ping...")
+            else:
+                if packet_data_response.num_messages > 0:
+                    rospy.loginfo(f"Received data to send with ping request length: {len(packet_data_response.miniframe_bytes)}")
+                    ping_payload = packet_data_response.miniframe_bytes
+
+        self.um.send_fdp_ping(request.dest, request.rate, request.cdr, ping_payload)
         if request.reply_timeout < 1:
             request.reply_timeout = 1
         ping_reply, cst = self.um.wait_for_fdp_ping_reply(
@@ -605,29 +711,6 @@ class AcommsDriverNode(object):
             queue_tx_packet_resp.success = True
         return queue_tx_packet_resp
 
-    def reconfigure(self, config, level):
-        rospy.loginfo("Acomms driver reconfigure request received.")
-        tx_inhibit = config["tx_inhibit"]
-        pwramp_txlevel = config["pwramp_txlevel"]
-        rospy.loginfo(
-            "Acomms driver reconfigure request received. tx_inhibit={}".format(
-                tx_inhibit
-            )
-        )
-
-        if tx_inhibit:
-            value = 2
-        else:
-            value = 1
-        # Err on the side of sending configs too often for now.  TODO: track state and clean this up
-        um_response = self.um.set_config("xmit.txinhibit", value, response_timeout=5)
-        um_response = self.um.set_config(
-            "pwramp.txlevel", pwramp_txlevel, response_timeout=5
-        )
-        # um_response = self.um.set_config('psk.packet.mod_hdr_version', pwramp_txlevel, response_timeout=5)
-        #
-        return config
-
     def _send_packet(self, packet: Packet):
         rospy.loginfo("Sending message via acomms")
 
diff --git a/ros_acomms_modeling/src/modem_sim_node.py b/ros_acomms_modeling/src/modem_sim_node.py
index 8d24b04e..8e58aa0a 100755
--- a/ros_acomms_modeling/src/modem_sim_node.py
+++ b/ros_acomms_modeling/src/modem_sim_node.py
@@ -5,7 +5,7 @@ from rospy import AnyMsg
 from std_msgs.msg import Header, Float32
 from ros_acomms_msgs.msg import PingReply, CST, XST, TransmittedPacket, ReceivedPacket, Packet, SST, AcousticRange
 from ros_acomms_msgs.srv import PingModem, PingModemResponse, QueueTxPacket, QueueTxPacketResponse, \
-    QueryModemSrc, QueryModemSrcRequest, QueryModemSrcResponse
+    QueryModemSrc, QueryModemSrcRequest, QueryModemSrcResponse, GetNextPacketData
 from ros_acomms_modeling.msg import SimPacket
 from ros_acomms_modeling.srv import SimTransmissionLoss, SimTransmissionLossRequest, SimPacketPerformance, \
     SimPacketPerformanceRequest, SimPacketPerformanceResponse, SimTravelTime, SimTravelTimeRequest, SimTravelTimeResponse
@@ -252,6 +252,15 @@ class ModemSimNode(object):
             "query_modem_src", QueryModemSrc, self.handle_query_modem_src
         )
 
+        # check if we should use the ping payload for message transport
+        self.use_ping_payload = rospy.get_param("~use_ping_payload", False)
+        # the default is the max bytes we can fit in a miniframe
+        self.ping_maximum_miniframe_bytes = rospy.get_param("~ping_maximum_miniframe_bytes", 32)
+
+        # if using the ping payload (or want to use later), configure the service proxy to message_queue
+        rospy.loginfo("modem_sim_node creating get_next_packet_data service proxy")
+        self.get_next_packet_data = rospy.ServiceProxy('get_next_packet_data', GetNextPacketData)
+
         rospy.loginfo("Modem sim node running{}. SRC={}, FC={} Hz, BW={} Hz".format(
                       " with sim ticks active" if self.use_sim_tick else "", self.src, self.fc, self.bw))
 
@@ -324,11 +333,8 @@ class ModemSimNode(object):
 
         return tx_packet
 
-    def send_fdp_ping_sim(self, dest, rate=1, cdr=0, hexdata=None):
+    def send_fdp_ping_sim(self, dest, rate=1, cdr=0, ping_payload=bytes()):
         queue_tx_packet_resp, sim_packet = None, None
-        if not hexdata:
-            hexdata = bytes()
-
         start_t = rospy.Time.now()
 
         queue_tx_packet_req = self.format_ping_for_tx_queue(dest=dest)
@@ -338,8 +344,9 @@ class ModemSimNode(object):
                 'ping_reply': self.create_ping_reply_msg(dest=dest),
                 'ping_type': PingReplyTransaction.PING_TYPE_REQUEST,
                 'transaction_start_t': start_t,
-            })
-
+                'ping_payload': ping_payload,
+            }
+        )
         return queue_tx_packet_resp, sim_packet
 
     def create_ping_transaction(self, sim_packet):
@@ -358,12 +365,28 @@ class ModemSimNode(object):
         Equivalent with a real modem would be sending $CCCMD,PNG,...
         '''
         rospy.logdebug("MODEM {}: Requesting modem send ping".format(self.src))
+        ping_payload = request.hexdata
+
+        # handle case where user wants to fill the ping payload with ros msgs
+        if self.use_ping_payload and self.get_next_packet_data:
+            rospy.loginfo(f"MODEM {self.src}: Attempting to fill ping payload for ROS msg transport. payload max size: {self.ping_maximum_miniframe_bytes} bytes")
+            try:
+                packet_data_response = self.get_next_packet_data(num_miniframe_bytes=self.ping_maximum_miniframe_bytes, num_dataframe_bytes=0)
+            except:
+                rospy.logwarn(f"ERROR: problem with get_next_packet_data() service. Not sending a payload with this ping...")
+            else:
+                if packet_data_response.num_messages > 0:
+                    rospy.logdebug(f"Received data to send with ping request length: {len(packet_data_response.miniframe_bytes)}")
+                    ping_payload = packet_data_response.miniframe_bytes
+                else:
+                    rospy.logdebug(f"Received NO data to send with ping request \npacket_data_response: {packet_data_response}\nlength: {len(packet_data_response.miniframe_bytes)}")
+
         try:
             # Equivalent with a real modem would be sending $CACMD,PNG,...
             queue_tx_packet_resp, sim_packet = self.send_fdp_ping_sim(request.dest,
                                                                       request.rate,
                                                                       request.cdr,
-                                                                      request.hexdata)
+                                                                      ping_payload)
         except TypeError:
             import traceback
             rospy.logwarn("MODEM {}: Threw exception\r\n{}".format(
@@ -884,11 +907,18 @@ class ModemSimNode(object):
             sim_packet.ping_reply = ping['ping_reply']
             sim_packet.ping_type = ping['ping_type']
             sim_packet.ping_transaction_start = ping['transaction_start_t']
+            # optionally user can include a payload the size of a miniframe
+            miniframe_bytes = ping.get('ping_payload', None)
         else:
             sim_packet.ping_type = PingReplyTransaction.PING_TYPE_NONE
+            miniframe_bytes = None # unset, use packet from req
 
         sim_packet.packet = queue_tx_packet_req.packet
         sim_packet.packet.src = self.src
+        if miniframe_bytes is not None:
+            # only set if explicit ping_payload key in req
+            sim_packet.packet.miniframe_bytes = miniframe_bytes
+
         if queue_tx_packet_req.requested_src_level_db == 0:
             queue_tx_packet_req.requested_src_level_db = 180
         queue_tx_packet_resp.actual_src_level_db = self.get_actual_src_level_db(
diff --git a/ros_acomms_tests/launch/test_use_ping_payload.launch b/ros_acomms_tests/launch/test_use_ping_payload.launch
new file mode 100644
index 00000000..3a5a455d
--- /dev/null
+++ b/ros_acomms_tests/launch/test_use_ping_payload.launch
@@ -0,0 +1,115 @@
+<launch>
+    <env name="ROSCONSOLE_FORMAT" value="[${severity}] [${time}] [${node}]: ${message}"/>
+
+    <arg name="sim" default="true" doc="Sim or Hardware modems, default: sim" />
+    <arg name="tdma_type" default="tdma_advanced" doc="type of TDMA node to use: tdma_advanced, or tdma_scripted or tdma_slotted_aloha" />
+
+    <!-- <node pkg="rostopic" type="rostopic" name="modem0_talker" args="pub /modem0/test_msg std_msgs/UInt8 1 -r 1" output="screen"/>
+    <node pkg="rostopic" type="rostopic" name="modem1_talker" args="pub /modem1/test_msg std_msgs/UInt8 1 -r 1" output="screen"/> -->
+
+    <group ns="modem0">
+        <node name="acoustic_range_node" pkg="ros_acomms" type="acoustic_range_node.py" output="screen" />
+
+        <node unless="$(arg sim)" name="acomms_driver_node" pkg="ros_acomms" type="acomms_driver_node.py" output="screen" >
+            <param name="modem_serial_port" value="/dev/ttyUSB1" type="str" />
+            <param name="modem_baud_rate" value="19200" type="int" />
+            <param name="pwramp_txlevel" value="3" type="int" />
+            <param name="use_ping_payload" value="false" type="bool" />
+            <rosparam param="modem_config">
+                SRC: 0
+                BND: 0
+                FC0: 25000
+                BW0: 5000
+                pwramp.txlevel: 2
+                psk.packet.mod_hdr_version: 1
+            </rosparam>
+        </node>
+
+        <node if="$(arg sim)" name="modem_sim_node" pkg="ros_acomms_modeling" type="modem_sim_node.py" output="screen" >
+            <param name="center_frequency_hz" value="25000" type="int" />
+            <param name="bandwidth_hz" value="5000" type="int" />
+            <param name="SRC" value="0" type="int" />
+            <param name="use_ping_payload" value="false" type="bool" />
+        </node>
+
+        <node name="tdma" pkg="ros_acomms" type="$(arg tdma_type)_node.py" output="screen" required="true" respawn="false">
+            <rosparam>
+                active_slots: 0
+                num_slots: 2
+                slot_duration_seconds: 10
+                comms_slots: ''
+                guard_time_seconds: 4
+                packet_length_seconds: 5
+                ping_modem: True
+                ping_modem_src: 1
+                pings_per_slot: 1
+            </rosparam>
+        </node>
+
+        <node name="message_queue_node" pkg="ros_acomms" type="message_queue_node.py" output="screen" >
+            <param name="packet_codec_file" value="$(find ros_acomms_tests)/launch/message_codec_config.yaml"/>
+        </node>
+
+        <node name="packet_dispatch" pkg="ros_acomms" type="packet_dispatch_node.py" output="screen" >
+            <param name="packet_codec_file" value="$(find ros_acomms_tests)/launch/message_codec_config.yaml"/>
+        </node>
+
+        <node name="modem_location_sim" pkg="ros_acomms_modeling" type="modem_location_sim_node.py" />
+    </group>
+
+    <group ns="modem1">
+        <node name="acoustic_range_node" pkg="ros_acomms" type="acoustic_range_node.py" output="screen" />
+
+        <node unless="$(arg sim)" name="acomms_driver_node" pkg="ros_acomms" type="acomms_driver_node.py" output="screen" >
+            <param name="modem_serial_port" value="/dev/ttyUSB2" type="str" />
+            <param name="modem_baud_rate" value="19200" type="int" />
+            <param name="pwramp_txlevel" value="3" type="int" />
+            <param name="use_ping_payload" value="true" type="bool" />
+            <rosparam param="modem_config">
+                SRC: 1
+                BND: 0
+                FC0: 25000
+                BW0: 5000
+                psk.packet.mod_hdr_version: 1
+            </rosparam>
+        </node>
+
+        <node if="$(arg sim)" name="modem_sim_node" pkg="ros_acomms_modeling" type="modem_sim_node.py" output="screen" >
+            <param name="center_frequency_hz" value="25000" type="int" />
+            <param name="bandwidth_hz" value="5000" type="int" />
+            <param name="SRC" value="1" type="int" />
+            <param name="use_ping_payload" value="true" type="bool" />
+        </node>
+
+        <node name="tdma" pkg="ros_acomms" type="$(arg tdma_type)_node.py" output="screen" required="true" respawn="false">
+            <rosparam>
+                active_slots: 1
+                num_slots: 2
+                slot_duration_seconds: 10
+                comms_slots: ''
+                guard_time_seconds: 4
+                packet_length_seconds: 5
+                ping_modem: True
+                ping_modem_src: 0
+                pings_per_slot: 1
+            </rosparam>
+        </node>
+
+        <node name="message_queue_node" pkg="ros_acomms" type="message_queue_node.py" output="screen" >
+            <param name="packet_codec_file" value="$(find ros_acomms_tests)/launch/message_codec_config.yaml"/>
+        </node>
+
+        <node name="packet_dispatch" pkg="ros_acomms" type="packet_dispatch_node.py" output="screen" >
+            <param name="packet_codec_file" value="$(find ros_acomms_tests)/launch/message_codec_config.yaml"/>
+        </node>
+
+        <node name="modem_location_sim" pkg="ros_acomms_modeling" type="modem_location_sim_node.py" />
+    </group>
+
+    <node name="sim_packet_performance_node" pkg="ros_acomms_modeling" type="sim_packet_performance_node.py" />
+
+    <node name="sim_transmission_loss_node" pkg="ros_acomms_modeling" type="sim_transmission_loss_node.py"  >
+        <param name="water_depth" value="20" type="int" />
+        <param name="bellhop_arrivals" value="false" type="bool" />
+    </node>
+</launch>
diff --git a/ros_acomms_tests/src/test_use_ping_payload.py b/ros_acomms_tests/src/test_use_ping_payload.py
new file mode 100755
index 00000000..0aea4f7f
--- /dev/null
+++ b/ros_acomms_tests/src/test_use_ping_payload.py
@@ -0,0 +1,108 @@
+#! /usr/bin/env python3
+
+'''
+tests for using ping payload for ros msg transport
+'''
+import hashlib
+import os
+import secrets
+import sys
+import time
+import traceback
+import threading
+import logging
+
+import pytest
+import rospy
+import rospkg
+import roslaunch
+
+from std_msgs.msg import Header, Bool, UInt8
+
+class TestUsePingPayload:
+    '''tdma Extended test class
+
+    test cases:
+    '''
+    VALID_TDMA_TYPES = ['tdma_advanced', 'tdma_scripted', 'tdma_slotted_aloha']
+    @classmethod
+    def setup_class(self):
+        """
+        setup function for tests
+        """
+        self.test_counter = 0
+        rospy.init_node("test_use_ping_payload", log_level=rospy.DEBUG)
+        self.logger = logging.getLogger("rosout")
+
+        self.tdma_type = rospy.get_param("/tdma_type", "tdma_advanced")
+        self.sim = rospy.get_param("/sim", True)
+        if self.tdma_type not in TestUsePingPayload.VALID_TDMA_TYPES:
+            raise NotImplementedError(f'This test module does not work with {self.tdma_type}. Valid tdma_types: {TestUsePingPayload.VALID_TDMA_TYPES}')
+
+        self.roslaunch_dict = dict(
+                sim=self.sim,
+                tdma_type=self.tdma_type,
+            )
+        self.rospack = rospkg.RosPack()
+
+        uuid = roslaunch.rlutil.get_or_generate_uuid(None, False)
+        roslaunch.configure_logging(uuid)
+
+        launch_file = os.path.join(
+            self.rospack.get_path("ros_acomms_tests"),
+            "launch/test_use_ping_payload.launch",
+        )
+
+        roslaunch_args = [f"{k}:={v}" for k, v in self.roslaunch_dict.items()]
+        full_cmd_list = [launch_file] + roslaunch_args
+        roslaunch_file_param = [(roslaunch.rlutil.resolve_launch_arguments(full_cmd_list)[0], roslaunch_args)]
+
+        self.launch = roslaunch.parent.ROSLaunchParent(uuid, roslaunch_file_param)
+        self.launch.start()
+
+        rospy.sleep(30.0)
+        self.logger.warning('Node: test_use_ping_payload running!')
+
+    def test_modem1_enabled(self):
+        self.logger.warning(f'Testing use ping payload on modem1 which has ~use_ping_payload set to true')
+
+        publisher = rospy.Publisher('/modem1/test_msg', UInt8, queue_size=5)
+        time.sleep(2)
+        msg = UInt8(data=1)
+        publisher.publish(msg)
+
+        try:
+            ret = rospy.wait_for_message(f'/modem0/from_acomms/test_msg', UInt8, timeout=30)
+        except rospy.exceptions.ROSException:
+            self.logger.warning("Timed-out waiting for msg on /modem0/from_acomms/test_msg from modem1")
+            pytest.fail("Timed-out waiting for msg on /modem0/from_acomms/test_msg from modem1")
+        else:
+            if ret.data == 1:
+                self.logger.warning("Success! Got msg on /modem0/from_acomms/test_msg from modem1 which has ~use_ping_payload:True and no comms_slots")
+
+    def test_modem0_disabled(self):
+        self.logger.warning(f'Testing use ping payload on modem0 which has ~use_ping_payload set to false')
+
+        publisher = rospy.Publisher('/modem0/test_msg', UInt8, queue_size=5)
+        time.sleep(2)
+        msg = UInt8(data=1)
+        publisher.publish(msg)
+
+        try:
+            ret = rospy.wait_for_message(f'/modem1/from_acomms/test_msg', UInt8, timeout=30)
+        except rospy.exceptions.ROSException:
+            self.logger.warning("Success! timed out while waiting for /modem1/from_acomms/test_msg")
+        else:
+            self.logger.warning("Got test_msg from modem0 which has ~use_ping_payload:False and no comms_slots.")
+            pytest.fail("Got test_msg from modem0 which has ~use_ping_payload:False and no comms_slots.")
+
+
+    @classmethod
+    def teardown_class(self):
+        """
+        teardown function for tests
+        """
+        self.launch.shutdown()
+
+if __name__ == "__main__":
+    sys.exit(pytest.main(['--capture=no', __file__]))
-- 
GitLab