diff --git a/ROSBRIDGE_PROTOCOL.md b/ROSBRIDGE_PROTOCOL.md
index e58850d61..4a7e82468 100644
--- a/ROSBRIDGE_PROTOCOL.md
+++ b/ROSBRIDGE_PROTOCOL.md
@@ -93,12 +93,12 @@ ROS operations:
     * **call_service** - a service call
     * **service_response** - a service response
   * Actions:
-   * **advertise_action** - advertise an external action server
-   * **unadvertise_action** - unadvertise an external action server
-   * **send_action_goal** - a goal sent to an action server
-   * **cancel_action_goal** - cancel an in-progress action goal
-   * **action_feedback** - feedback messages from an action server
-   * **action_result** - an action result
+    * **advertise_action** - advertise an external action server
+    * **unadvertise_action** - unadvertise an external action server
+    * **send_action_goal** - a goal sent to an action server
+    * **cancel_action_goal** - cancel an in-progress action goal
+    * **action_feedback** - feedback messages from an action server
+    * **action_result** - an action result
 
 In general, actions or operations that the client takes (such as publishing and
 subscribing) have opcodes which are verbs (subscribe, call_service, unadvertise
diff --git a/rosapi/package.xml b/rosapi/package.xml
index af550dc72..c81c9b06f 100644
--- a/rosapi/package.xml
+++ b/rosapi/package.xml
@@ -17,7 +17,7 @@
   <maintainer email="jihoonlee.in@gmail.com">Jihoon Lee</maintainer>
   <maintainer email="ros-tooling@foxglove.dev">Foxglove</maintainer>
 
-  <buildtool_depend>ament_cmake_ros</buildtool_depend>
+  <buildtool_depend>ament_python</buildtool_depend>
 
   <exec_depend>rosapi_msgs</exec_depend>
   <exec_depend>builtin_interfaces</exec_depend>
@@ -41,7 +41,7 @@
   <test_depend>rmw_dds_common</test_depend>
 
   <export>
-    <build_type>ament_cmake</build_type>
+    <build_type>ament_python</build_type>
     <ros1_bridge mapping_rules="mapping_rules.yaml"/>
   </export>
 </package>
diff --git a/rosapi/src/rosapi/__init__.py b/rosapi/resource/rosapi
similarity index 100%
rename from rosapi/src/rosapi/__init__.py
rename to rosapi/resource/rosapi
diff --git a/rosapi/rosapi/__init__.py b/rosapi/rosapi/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/rosapi/src/rosapi/glob_helper.py b/rosapi/rosapi/glob_helper.py
similarity index 100%
rename from rosapi/src/rosapi/glob_helper.py
rename to rosapi/rosapi/glob_helper.py
diff --git a/rosapi/src/rosapi/objectutils.py b/rosapi/rosapi/objectutils.py
similarity index 99%
rename from rosapi/src/rosapi/objectutils.py
rename to rosapi/rosapi/objectutils.py
index 0c981c051..7a31a0ad5 100644
--- a/rosapi/src/rosapi/objectutils.py
+++ b/rosapi/rosapi/objectutils.py
@@ -35,9 +35,10 @@
 import logging
 import re
 
-from rosapi.stringify_field_types import stringify_field_types
 from rosbridge_library.internal import ros_loader
 
+from rosapi.stringify_field_types import stringify_field_types
+
 # Keep track of atomic types and special types
 atomics = [
     "bool",
diff --git a/rosapi/src/rosapi/params.py b/rosapi/rosapi/params.py
similarity index 98%
rename from rosapi/src/rosapi/params.py
rename to rosapi/rosapi/params.py
index 988324895..209ab4f90 100644
--- a/rosapi/src/rosapi/params.py
+++ b/rosapi/rosapi/params.py
@@ -40,6 +40,7 @@
 from rclpy.parameter import get_parameter_value
 from ros2node.api import get_absolute_node_name
 from ros2param.api import call_get_parameters, call_set_parameters
+
 from rosapi.proxy import get_nodes
 
 """ Methods to interact with the param server.  Values have to be passed
@@ -75,7 +76,9 @@ def init(parent_node_name):
     parent_node_basename = parent_node_name.split("/")[-1]
     param_node_name = f"{parent_node_basename}_params"
     _node = rclpy.create_node(
-        param_node_name, cli_args=["--ros-args", "-r", f"__node:={param_node_name}"]
+        param_node_name,
+        cli_args=["--ros-args", "-r", f"__node:={param_node_name}"],
+        start_parameter_services=False,
     )
     _parent_node_name = get_absolute_node_name(parent_node_name)
 
diff --git a/rosapi/src/rosapi/proxy.py b/rosapi/rosapi/proxy.py
similarity index 100%
rename from rosapi/src/rosapi/proxy.py
rename to rosapi/rosapi/proxy.py
diff --git a/rosapi/scripts/rosapi_node b/rosapi/rosapi/rosapi_node.py
similarity index 100%
rename from rosapi/scripts/rosapi_node
rename to rosapi/rosapi/rosapi_node.py
diff --git a/rosapi/src/rosapi/stringify_field_types.py b/rosapi/rosapi/stringify_field_types.py
similarity index 100%
rename from rosapi/src/rosapi/stringify_field_types.py
rename to rosapi/rosapi/stringify_field_types.py
diff --git a/rosapi/setup.cfg b/rosapi/setup.cfg
new file mode 100644
index 000000000..7b315f355
--- /dev/null
+++ b/rosapi/setup.cfg
@@ -0,0 +1,4 @@
+[develop]
+script_dir=$base/lib/rosapi
+[install]
+install_scripts=$base/lib/rosapi
diff --git a/rosapi/setup.py b/rosapi/setup.py
new file mode 100644
index 000000000..d86d5febe
--- /dev/null
+++ b/rosapi/setup.py
@@ -0,0 +1,29 @@
+import os
+
+from setuptools import find_packages, setup
+
+package_name = "rosapi"
+
+setup(
+    name=package_name,
+    version="1.3.2",
+    packages=find_packages(exclude=["test"]),
+    data_files=[
+        # Install marker file in the package index
+        ("share/ament_index/resource_index/packages", ["resource/" + package_name]),
+        # Include our package.xml file
+        (os.path.join("share", package_name), ["package.xml"]),
+    ],
+    install_requires=["setuptools"],
+    zip_safe=True,
+    author="Jonathan Mace",
+    author_email="jonathan.c.mace@gmail.com",
+    maintainer="Jihoon Lee, Foxglove",
+    maintainer_email="jihoonlee.in@gmail.com, ros-tooling@foxglove.dev",
+    description="Provides service calls for getting ros meta-information, like list of topics, services, params, etc.",
+    license="BSD",
+    tests_require=["pytest"],
+    entry_points={
+        "console_scripts": ["rosapi_node = rosapi.rosapi_node:main"],
+    },
+)
diff --git a/rosapi/test/test_stringify_field_types.py b/rosapi/test/test_stringify_field_types.py
index ca551bbd0..b6e36bc63 100644
--- a/rosapi/test/test_stringify_field_types.py
+++ b/rosapi/test/test_stringify_field_types.py
@@ -1,9 +1,10 @@
 #!/usr/bin/env python
 import unittest
 
-from rosapi.stringify_field_types import stringify_field_types
 from rosbridge_library.internal.ros_loader import InvalidModuleException
 
+from rosapi.stringify_field_types import stringify_field_types
+
 
 class TestObjectUtils(unittest.TestCase):
     def test_stringify_field_types(self):
diff --git a/rosbridge_library/src/rosbridge_library/internal/message_conversion.py b/rosbridge_library/src/rosbridge_library/internal/message_conversion.py
index d3361cfa3..a0d833ce2 100644
--- a/rosbridge_library/src/rosbridge_library/internal/message_conversion.py
+++ b/rosbridge_library/src/rosbridge_library/internal/message_conversion.py
@@ -90,7 +90,8 @@
 ]
 ros_header_types = ["Header", "std_msgs/Header", "roslib/Header"]
 ros_binary_types = ["uint8[]", "char[]", "sequence<uint8>", "sequence<char>"]
-list_tokens = re.compile("<(.+?)>")
+# Remove the list type wrapper, and length specifier, from rostypes i.e. sequence<double, 3>
+list_tokens = re.compile(r"<(.+?)(, \d+)?>")
 bounded_array_tokens = re.compile(r"(.+)\[.*\]")
 ros_binary_types_list_braces = [
     ("uint8[]", re.compile(r"uint8\[[^\]]*\]")),
@@ -392,7 +393,6 @@ def _to_object_inst(msg, rostype, roottype, clock, inst, stack):
         inst.stamp = clock.now().to_msg()
 
     inst_fields = inst.get_fields_and_field_types()
-
     for field_name in msg:
         # Add this field to the field stack
         field_stack = stack + [field_name]
diff --git a/rosbridge_library/src/rosbridge_library/internal/subscribers.py b/rosbridge_library/src/rosbridge_library/internal/subscribers.py
index 0bb94a809..a64c90b7f 100644
--- a/rosbridge_library/src/rosbridge_library/internal/subscribers.py
+++ b/rosbridge_library/src/rosbridge_library/internal/subscribers.py
@@ -176,6 +176,11 @@ def subscribe(self, client_id, callback):
             # In any case, the first message is handled using new_sub_callback,
             # which adds the new callback to the subscriptions dictionary.
             self.new_subscriptions.update({client_id: callback})
+            infos = self.node_handle.get_publishers_info_by_topic(self.topic)
+            if any(pub.qos_profile.durability == DurabilityPolicy.TRANSIENT_LOCAL for pub in infos):
+                self.qos.durability = DurabilityPolicy.TRANSIENT_LOCAL
+            if any(pub.qos_profile.reliability == ReliabilityPolicy.BEST_EFFORT for pub in infos):
+                self.qos.reliability = ReliabilityPolicy.BEST_EFFORT
             if self.new_subscriber is None:
                 self.new_subscriber = self.node_handle.create_subscription(
                     self.msg_class,
@@ -196,7 +201,7 @@ def unsubscribe(self, client_id):
         with self.rlock:
             if client_id in self.new_subscriptions:
                 del self.new_subscriptions[client_id]
-            else:
+            if client_id in self.subscriptions:
                 del self.subscriptions[client_id]
 
     def has_subscribers(self):
diff --git a/rosbridge_library/test/internal/test_message_conversion.py b/rosbridge_library/test/internal/test_message_conversion.py
index 9ccba4f05..f837e6275 100755
--- a/rosbridge_library/test/internal/test_message_conversion.py
+++ b/rosbridge_library/test/internal/test_message_conversion.py
@@ -316,3 +316,21 @@ def test_float32_msg(rostype, data):
             ints = list(map(int, range(0, 16)))
             ret = test_float32_msg(rostype, ints)
             np.testing.assert_array_equal(ret, np.array(ints))
+
+    # Test a float32 array with a length with non-numeric characters in it
+    def test_float32_complexboundedarray(self):
+        def test_nestedboundedarray_msg(rostype, data):
+            msg = {"data": {"data": data}}
+            inst = ros_loader.get_message_instance(rostype)
+            c.populate_instance(msg, inst)
+            self.validate_instance(inst)
+            return inst.data
+
+        for msgtype in ["TestNestedBoundedArray"]:
+            rostype = "rosbridge_test_msgs/" + msgtype
+
+            # From List[float]
+            floats = list(map(float, range(0, 16)))
+            ret = test_nestedboundedarray_msg(rostype, floats)
+
+            self.assertEqual(c._from_inst(ret, rostype), {"data": floats})
diff --git a/rosbridge_server/package.xml b/rosbridge_server/package.xml
index 07596660c..2278e926e 100644
--- a/rosbridge_server/package.xml
+++ b/rosbridge_server/package.xml
@@ -14,8 +14,7 @@
   <maintainer email="jihoonlee.in@gmail.com">Jihoon Lee</maintainer>
   <maintainer email="ros-tooling@foxglove.dev">Foxglove</maintainer>
 
-  <buildtool_depend>ament_cmake</buildtool_depend>
-  <buildtool_depend>ament_cmake_ros</buildtool_depend>
+  <buildtool_depend>ament_python</buildtool_depend>
 
   <exec_depend>python3-tornado</exec_depend>
   <exec_depend>python3-twisted</exec_depend>
@@ -34,6 +33,6 @@
   <test_depend>std_srvs</test_depend>
 
   <export>
-    <build_type>ament_cmake</build_type>
+    <build_type>ament_python</build_type>
   </export>
 </package>
diff --git a/rosbridge_server/resource/rosbridge_server b/rosbridge_server/resource/rosbridge_server
new file mode 100644
index 000000000..e69de29bb
diff --git a/rosbridge_server/src/rosbridge_server/__init__.py b/rosbridge_server/rosbridge_server/__init__.py
similarity index 100%
rename from rosbridge_server/src/rosbridge_server/__init__.py
rename to rosbridge_server/rosbridge_server/__init__.py
diff --git a/rosbridge_server/src/rosbridge_server/client_manager.py b/rosbridge_server/rosbridge_server/client_manager.py
similarity index 100%
rename from rosbridge_server/src/rosbridge_server/client_manager.py
rename to rosbridge_server/rosbridge_server/client_manager.py
diff --git a/rosbridge_server/scripts/rosbridge_websocket.py b/rosbridge_server/rosbridge_server/rosbridge_websocket.py
similarity index 98%
rename from rosbridge_server/scripts/rosbridge_websocket.py
rename to rosbridge_server/rosbridge_server/rosbridge_websocket.py
index bfa968d15..49d2a52b6 100755
--- a/rosbridge_server/scripts/rosbridge_websocket.py
+++ b/rosbridge_server/rosbridge_server/rosbridge_websocket.py
@@ -337,7 +337,13 @@ def main(args=None):
 
     executor = rclpy.executors.SingleThreadedExecutor()
     executor.add_node(node)
-    spin_callback = PeriodicCallback(lambda: executor.spin_once(timeout_sec=0.01), 1)
+
+    def spin_ros():
+        executor.spin_once(timeout_sec=0.01)
+        if not rclpy.ok():
+            shutdown_hook()
+
+    spin_callback = PeriodicCallback(spin_ros, 1)
     spin_callback.start()
     try:
         start_hook()
diff --git a/rosbridge_server/src/rosbridge_server/websocket_handler.py b/rosbridge_server/rosbridge_server/websocket_handler.py
similarity index 100%
rename from rosbridge_server/src/rosbridge_server/websocket_handler.py
rename to rosbridge_server/rosbridge_server/websocket_handler.py
diff --git a/rosbridge_server/scripts/rosbridge_websocket b/rosbridge_server/scripts/rosbridge_websocket
deleted file mode 120000
index 647069442..000000000
--- a/rosbridge_server/scripts/rosbridge_websocket
+++ /dev/null
@@ -1 +0,0 @@
-rosbridge_websocket.py
\ No newline at end of file
diff --git a/rosbridge_server/setup.cfg b/rosbridge_server/setup.cfg
new file mode 100644
index 000000000..ffe2ab328
--- /dev/null
+++ b/rosbridge_server/setup.cfg
@@ -0,0 +1,4 @@
+[develop]
+script_dir=$base/lib/rosbridge_server
+[install]
+install_scripts=$base/lib/rosbridge_server
diff --git a/rosbridge_server/setup.py b/rosbridge_server/setup.py
new file mode 100644
index 000000000..3f98a8e28
--- /dev/null
+++ b/rosbridge_server/setup.py
@@ -0,0 +1,35 @@
+import os
+from glob import glob
+
+from setuptools import find_packages, setup
+
+package_name = "rosbridge_server"
+
+setup(
+    name=package_name,
+    version="1.3.2",
+    packages=find_packages(exclude=["test"]),
+    data_files=[
+        # Install marker file in the package index
+        ("share/ament_index/resource_index/packages", ["resource/" + package_name]),
+        # Include our package.xml file
+        (os.path.join("share", package_name), ["package.xml"]),
+        # Include all launch files.
+        (
+            os.path.join("share", package_name, "launch"),
+            glob(os.path.join("launch", "*launch.[pxy][yma]*")),
+        ),
+    ],
+    install_requires=["setuptools"],
+    zip_safe=True,
+    author="Jonathan Mace",
+    author_email="jonathan.c.mace@gmail.com",
+    maintainer="Jihoon Lee, Foxglove",
+    maintainer_email="jihoonlee.in@gmail.com, ros-tooling@foxglove.dev",
+    description="A WebSocket interface to rosbridge.",
+    license="BSD",
+    tests_require=["pytest"],
+    entry_points={
+        "console_scripts": ["rosbridge_websocket = rosbridge_server.rosbridge_websocket:main"],
+    },
+)
diff --git a/rosbridge_test_msgs/CMakeLists.txt b/rosbridge_test_msgs/CMakeLists.txt
index e327c419d..4ef1e87df 100644
--- a/rosbridge_test_msgs/CMakeLists.txt
+++ b/rosbridge_test_msgs/CMakeLists.txt
@@ -20,6 +20,7 @@ rosidl_generate_interfaces(${PROJECT_NAME}
   msg/TestUInt8FixedSizeArray16.msg
   msg/TestFloat32Array.msg
   msg/TestFloat32BoundedArray.msg
+  msg/TestNestedBoundedArray.msg
   srv/AddTwoInts.srv
   srv/SendBytes.srv
   srv/TestArrayRequest.srv
diff --git a/rosbridge_test_msgs/msg/TestNestedBoundedArray.msg b/rosbridge_test_msgs/msg/TestNestedBoundedArray.msg
new file mode 100644
index 000000000..a35d82372
--- /dev/null
+++ b/rosbridge_test_msgs/msg/TestNestedBoundedArray.msg
@@ -0,0 +1 @@
+TestFloat32BoundedArray data