diff --git a/blueboat_description/config/blueboat.yaml b/blueboat_description/config/blueboat.yaml index 81c2fbad..ef13c140 100644 --- a/blueboat_description/config/blueboat.yaml +++ b/blueboat_description/config/blueboat.yaml @@ -4,12 +4,7 @@ # composed Gazebo model AND the ros_gz bridge config. To try a config without # rebuilding, pass config_file:= to the launches. # -# The default BlueBoat needs nothing here: every slot of every part fills -# itself with its default (the chassis fits its two outboards, the flag and -# the Ping2 integration kit; the kit's bracket fits the Ping). The entries -# below are how to change that. See bluerobotics_parts/urdf/assembly.xacro -# for the full rules and the site (vehicles/blueboat/configuration) for the slots -# and the catalog. +# The BlueBoat is exactly what parts: lists, because slot_defaults is false: a slot with no entry stays empty, including the slots of fitted parts (the Ping2 kit's bracket brings no Ping unless the Ping is listed too). With slot_defaults true, or left out, every unlisted slot fills with its default instead: the chassis fits its two propellers, the flag and the Ping2 integration kit, and the kit's bracket fits the Ping. See bluerobotics_parts/urdf/assembly.xacro for the full rules and the site (vehicles/blueboat/configuration) for the slots and the catalog. # # Topic namespace: sensor topics default to ///... topic_namespace: blueboat @@ -20,7 +15,10 @@ topic_namespace: blueboat # since graded buoyancy reads only boxes and spheres (docs: design/buoyancy). base: {type: blueboat_chassis, name: base_link} -# parts: overrides and additions. A slot entry picks what goes in a slot +# slot_defaults: false fits only the slots listed under parts; true fills every unlisted slot with its default part. +slot_defaults: false + +# parts: what is fitted. A slot entry picks what goes in a slot # (slot: the slot name, of: the instance carrying it, default base_link; # type: an accepted part type, or none to leave it empty). An entry without # slot: is a free placement: type, name, xyz/rpy relative to parent (default @@ -33,13 +31,17 @@ base: {type: blueboat_chassis, name: base_link} # which is then used as given (and shared by every instance). # bridge: dict of NATIVE ros_gz_bridge keys merged into that part's # generated bridge entries -parts: [] - # - {slot: ping_mount, type: none} # no Ping2 kit - # - {slot: motor_port, type: t200_prop_ccw} # the other propeller option - # - {slot: motor_stbd, type: t200_prop_cw} - # - {slot: mast, type: blueboat_antenna_mast} # slots with no default +parts: + - {slot: prop_port, type: m200_weedless_prop_ccw} + - {slot: prop_stbd, type: m200_weedless_prop_cw} + - {type: ping_singlebeam, name: ping, xyz: "0 -0.259 0", rpy: "0 0 0"} # the Ping where the Ping2 kit puts it, without the bracket + # - {slot: flag, type: blueboat_flag} + # The Ping2 kit instead of the free placed Ping above (remove that entry first): + # - {slot: ping_mount, type: blueboat_ping_singlebeam_mount} # Ping2 integration kit bracket + # - {slot: ping, of: ping_mount, type: ping_singlebeam} # the Ping in it; topic: sonar renames its topics + # - {slot: mast, type: blueboat_antenna_mast} # - {slot: payload, type: blueboat_payload_bracket} - # - {slot: ping, of: ping_mount, type: ping_singlebeam, topic: sonar} # rename the Ping topics + # T200 propellers instead of the M200s: type: t200_prop_ccw / t200_prop_cw on the two prop_ slots above # - {type: basestation_antenna, name: radio, xyz: "-0.35 0 0.5", rpy: "0 0 0"} # - {type: omniscan_450_sidescan, name: sidescan, xyz: "0.2 0.22 -0.03", rpy: "0 0 0"} # - {type: surveyor_multibeam, name: multibeam, xyz: "0.35 0 -0.08", rpy: "0 0 0"} diff --git a/blueboat_description/test/test_description.py b/blueboat_description/test/test_description.py index 85a0aad4..e62e2e93 100644 --- a/blueboat_description/test/test_description.py +++ b/blueboat_description/test/test_description.py @@ -30,13 +30,6 @@ WATER_DENSITY = 1025.0 HULL = yaml.safe_load(DEFAULT_CONFIG)['hull_displacement'] -# What the chassis slots fill with when the config is silent. -DEFAULT_PARTS = {'base_link': 'blueboat_chassis', - 'motor_port': 'm200_weedless_prop_ccw', - 'motor_stbd': 'm200_weedless_prop_cw', - 'flag': 'blueboat_flag', - 'ping_mount': 'blueboat_ping_singlebeam_mount', - 'ping': 'ping_singlebeam'} # The Ping2 transducer face sits this far below the part origin (delivered mesh). PING_FACE_BELOW_ORIGIN = 0.044 @@ -58,7 +51,13 @@ def catalog(): def make_config(parts=(), slots=(), hull=True): - """Vehicle config yaml: chassis base, overrides/additions, hull displacement.""" + """ + Vehicle config yaml: chassis base, overrides/additions, hull displacement. + + The tests' own vehicle, independent of the shipped config: no + slot_defaults key, so every slot fills with its default (the M200 props, + the flag, the Ping2 kit and its Ping) unless an entry says otherwise. + """ cfg = {'base': {'type': 'blueboat_chassis', 'name': 'base_link', 'collision': False}, 'parts': [dict(p) for p in parts]} @@ -142,17 +141,17 @@ def position_in_base(root, link): return pos -def test_default_config_yields_the_default_parts(): - """With no parts configured, the slots fill with their defaults.""" - root = generate_urdf(DEFAULT_CONFIG) - assert manifest(root) == DEFAULT_PARTS +def test_props_spin_and_the_chassis_keeps_contact(): + """Propellers turn on continuous joints; contact stays the chassis's own.""" + cfg = yaml.safe_load(make_config()) + cfg['base']['collision'] = True + root = generate_urdf(yaml.safe_dump(cfg, sort_keys=False)) assert 'hull_displacement' not in link_names(root), 'displacement belongs to the Gazebo side' base = next(li for li in root.findall('link') if li.get('name') == 'base_link') assert base.findall('collision'), 'the chassis keeps its delivered contact geometry' - assert set(DEFAULT_PARTS) <= link_names(root) motors = {j.get('name'): j for j in root.findall('joint') - if j.get('name') in ('motor_port_joint', 'motor_stbd_joint')} - assert set(motors) == {'motor_port_joint', 'motor_stbd_joint'} + if j.get('name') in ('prop_port_joint', 'prop_stbd_joint')} + assert set(motors) == {'prop_port_joint', 'prop_stbd_joint'} for joint in motors.values(): assert joint.get('type') == 'continuous' assert joint.find('axis') is not None @@ -160,7 +159,7 @@ def test_default_config_yields_the_default_parts(): def test_manifest_names_are_links(): """Every manifest instance exists as a link (and vice versa for parts).""" - root = generate_urdf(DEFAULT_CONFIG) + root = generate_urdf(make_config()) links = link_names(root) for name in manifest(root): assert name in links @@ -168,20 +167,20 @@ def test_manifest_names_are_links(): def test_slot_chains_through_the_parent_part(): """Slot occupants hang off the slot frame of the instance carrying it.""" - table = joints_by_child(generate_urdf(DEFAULT_CONFIG)) + table = joints_by_child(generate_urdf(make_config())) assert table['ping'][0] == 'ping_mount_ping' assert table['ping_mount_ping'][0] == 'ping_mount' assert table['ping_mount'][0] == 'base_link_ping_mount' assert table['base_link_ping_mount'][0] == 'base_link' - assert table['motor_port'][0] == 'base_link_motor_port' + assert table['prop_port'][0] == 'base_link_prop_port' assert table['ping_beam'][0] == 'ping', 'the Ping declares its beam frame' def test_slot_override_picks_another_option(): """A slot entry swaps the occupant for another accepted type.""" - root = generate_urdf(make_config([{'slot': 'motor_port', 'type': 't200_prop_ccw'}])) - assert manifest(root)['motor_port'] == 't200_prop_ccw' - assert manifest(root)['motor_stbd'] == 'm200_weedless_prop_cw' + root = generate_urdf(make_config([{'slot': 'prop_port', 'type': 't200_prop_ccw'}])) + assert manifest(root)['prop_port'] == 't200_prop_ccw' + assert manifest(root)['prop_stbd'] == 'm200_weedless_prop_cw' def test_slot_none_leaves_it_empty(): @@ -221,7 +220,7 @@ def test_manifest_records_every_slot_visited(): root = generate_urdf(make_config( slots=[{'of': 'ping_mount', 'name': 'side', 'xyz': '0 0.05 0'}])) visited = assembly.slots(root) - assert {('base_link', 'motor_port'), ('base_link', 'ping_mount'), + assert {('base_link', 'prop_port'), ('base_link', 'ping_mount'), ('ping_mount', 'ping'), ('ping_mount', 'side')} <= visited @@ -257,7 +256,7 @@ def test_check_catches_what_the_expansion_cannot(): def test_slot_rejects_parts_that_do_not_fit(): """A type outside the slot's accepts list fails the expansion.""" - expect_failure(make_config([{'slot': 'motor_port', 'type': 'blueboat_flag'}]), + expect_failure(make_config([{'slot': 'prop_port', 'type': 'blueboat_flag'}]), 'does not fit') @@ -311,7 +310,7 @@ def test_catalog_completeness(): def test_mesh_assets_resolve(): """Every mesh the URDF references ships in bluerobotics_parts.""" - root = generate_urdf(DEFAULT_CONFIG) + root = generate_urdf(make_config()) meshes = list(root.iter('mesh')) assert meshes, 'the parts based model must reference the delivered meshes' prefix = 'package://bluerobotics_parts/' @@ -334,20 +333,26 @@ def test_check_urdf_accepts_generated(): f'--- stdout ---\n{out.stdout}\n--- stderr ---\n{out.stderr}') -def test_echosounder_mounted_below_waterline(): +@pytest.mark.parametrize('config', ['kit', 'shipped']) +def test_echosounder_mounted_below_waterline(config): """ - The default Ping transducer is submerged at rest. + A fitted Ping transducer is submerged at rest. The gpu_lidar returns seabed ranges from anywhere, so a transducer in the air fails silently; lock its face below the static waterline computed from - mass and waterplane area. + mass and waterplane area. Checked on the Ping2 kit (the slot default) and + on whatever the shipped config fits, if it fits a Ping at all. """ - root = generate_urdf(DEFAULT_CONFIG) - face_z = position_in_base(root, 'ping')[2] - PING_FACE_BELOW_ORIGIN - assert position_in_base(root, 'ping_beam')[2] == pytest.approx(face_z, abs=1e-6) + root = generate_urdf(make_config() if config == 'kit' else DEFAULT_CONFIG) + pings = [name for name, ptype in manifest(root).items() if ptype == 'ping_singlebeam'] + if not pings: + pytest.skip(f'the {config} config fits no Ping') area = 2 * HULL['length'] * HULL['width'] draft = total_mass(root) / (WATER_DENSITY * area) waterline_z = HULL['z'] - HULL['height'] / 2 + draft - assert face_z < waterline_z - 0.02, ( - f'ping transducer face at z {face_z:.3f} is not below the static ' - f'waterline {waterline_z:.3f}') + for ping in pings: + face_z = position_in_base(root, ping)[2] - PING_FACE_BELOW_ORIGIN + assert position_in_base(root, f'{ping}_beam')[2] == pytest.approx(face_z, abs=1e-6) + assert face_z < waterline_z - 0.02, ( + f'{ping} transducer face at z {face_z:.3f} is not below the static ' + f'waterline {waterline_z:.3f}') diff --git a/blueboat_description/urdf/blueboat.urdf.xacro b/blueboat_description/urdf/blueboat.urdf.xacro index ba960dd9..aad7645c 100644 --- a/blueboat_description/urdf/blueboat.urdf.xacro +++ b/blueboat_description/urdf/blueboat.urdf.xacro @@ -23,6 +23,7 @@ + slots="${cfg.get('slots') or []}" + defaults="${cfg.get('slot_defaults', True)}"/> diff --git a/blueboat_gazebo/model.sdf.xacro b/blueboat_gazebo/model.sdf.xacro index 3daef0c9..cd9b1ade 100644 --- a/blueboat_gazebo/model.sdf.xacro +++ b/blueboat_gazebo/model.sdf.xacro @@ -324,7 +324,8 @@ - - + + diff --git a/blueboat_gazebo/test/test_bridge_generator.py b/blueboat_gazebo/test/test_bridge_generator.py index a77b1dfb..011391b5 100644 --- a/blueboat_gazebo/test/test_bridge_generator.py +++ b/blueboat_gazebo/test/test_bridge_generator.py @@ -27,7 +27,7 @@ _spec.loader.exec_module(bridge_gen) ALWAYS = {'/clock', '/blueboat/joint_states'} -PROPS = [('m200_weedless_prop_ccw', 'motor_port'), ('m200_weedless_prop_cw', 'motor_stbd')] +PROPS = [('m200_weedless_prop_ccw', 'prop_port'), ('m200_weedless_prop_cw', 'prop_stbd')] def entries_for(cfg, instances): @@ -44,12 +44,12 @@ def test_only_clock_and_joint_states_without_parts(): def test_propellers_bridge_their_thrust_command(): """Each propeller instance gets a ROS_TO_GZ thrust topic named after it.""" entries = entries_for({}, PROPS) - assert set(entries) == ALWAYS | {'/blueboat/motor_port/cmd', '/blueboat/motor_stbd/cmd'} - port = entries['/blueboat/motor_port/cmd'] + assert set(entries) == ALWAYS | {'/blueboat/prop_port/cmd', '/blueboat/prop_stbd/cmd'} + port = entries['/blueboat/prop_port/cmd'] assert port['direction'] == 'ROS_TO_GZ' assert port['ros_type_name'] == 'std_msgs/msg/Float64' # Same name on the gz side: model.sdf.xacro gives the Thruster this . - assert port['gz_topic_name'] == '/blueboat/motor_port/cmd' + assert port['gz_topic_name'] == '/blueboat/prop_port/cmd' # A renamed or swapped propeller follows its instance name. entries = entries_for({}, [('t200_prop_ccw', 'left')]) assert '/blueboat/left/cmd' in entries @@ -83,7 +83,7 @@ def test_topic_override_precedence(): entries = entries_for(cfg, [('ping_singlebeam', 'ping')] + PROPS) ping = entries['/sensors/ping/range'] assert ping['gz_topic_name'] == '/boat_a/ping_raw/range' - assert '/boat_a/motor_port/cmd' in entries + assert '/boat_a/prop_port/cmd' in entries # A renamed occupant is matched by its name, not the slot. cfg = {'parts': [{'slot': 'ping', 'of': 'ping_mount', 'type': 'ping_singlebeam', 'name': 'sonar', 'topic': 'echo'}]} diff --git a/blueboat_gazebo/test/test_configure_vehicle.py b/blueboat_gazebo/test/test_configure_vehicle.py index ec784d00..de83818a 100644 --- a/blueboat_gazebo/test/test_configure_vehicle.py +++ b/blueboat_gazebo/test/test_configure_vehicle.py @@ -38,6 +38,20 @@ def configure(config_path, out_dir): return Path(out_dir) +def kit_config(parts=()): + """ + Return the tests' own vehicle. + + The shipped hull and namespace, every slot filling with its default, plus + parts. Independent of what the shipped config fits, so these tests follow + the tool's behavior rather than the current product. + """ + cfg = yaml.safe_load(DEFAULT_CONFIG.read_text()) + cfg['slot_defaults'] = True + cfg['parts'] = [dict(p) for p in parts] + return cfg + + def links(urdf_path): return {li.get('name') for li in ET.parse(urdf_path).getroot().findall('link')} @@ -67,14 +81,15 @@ def test_default_config_reproduces_the_installed_artifacts(tmp_path): if v.find('geometry/mesh') is not None} ros_rpys = {v.find('origin').get('rpy') for v in ros_root.iter('visual') if v.find('geometry/mesh') is not None} - assert ros_rpys == {'0 0 0'} and gz_rpys == {'1.5708 0 0'} + # legacy exports, and profile axes deliveries (axes="gltf" in part_visual) + assert ros_rpys <= {'0 0 0', '0 0 1.5708'} + assert gz_rpys <= {'1.5708 0 0', '1.5708 0 1.5708'} def test_custom_config_flows_to_every_artifact(tmp_path): """A different config changes URDF, model and bridge consistently.""" - cfg = yaml.safe_load(DEFAULT_CONFIG.read_text()) - cfg['parts'] = [{'slot': 'ping_mount', 'type': 'none'}, - {'slot': 'mast', 'type': 'blueboat_antenna_mast'}] + cfg = kit_config([{'slot': 'ping_mount', 'type': 'none'}, + {'slot': 'mast', 'type': 'blueboat_antenna_mast'}]) config = tmp_path / 'custom.yaml' config.write_text(yaml.safe_dump(cfg, sort_keys=False)) out = configure(config, tmp_path / 'v') @@ -84,14 +99,13 @@ def test_custom_config_flows_to_every_artifact(tmp_path): bridge = yaml.safe_load((out / 'ros_gz_bridge.yaml').read_text()) topics = {e['ros_topic_name'] for e in bridge} assert '/blueboat/ping/range' not in topics - assert '/blueboat/motor_port/cmd' in topics + assert '/blueboat/prop_port/cmd' in topics def test_drivetrain_follows_the_config(tmp_path): """A renamed or removed propeller moves or drops its Thruster and bridge topic.""" - cfg = yaml.safe_load(DEFAULT_CONFIG.read_text()) - cfg['parts'] = [{'slot': 'motor_port', 'type': 'none'}, - {'slot': 'motor_stbd', 'type': 't200_prop_cw', 'name': 'right'}] + cfg = kit_config([{'slot': 'prop_port', 'type': 'none'}, + {'slot': 'prop_stbd', 'type': 't200_prop_cw', 'name': 'right'}]) config = tmp_path / 'custom.yaml' config.write_text(yaml.safe_dump(cfg, sort_keys=False)) out = configure(config, tmp_path / 'v') @@ -102,7 +116,7 @@ def test_drivetrain_follows_the_config(tmp_path): topics = {e['ros_topic_name'] for e in yaml.safe_load((out / 'ros_gz_bridge.yaml').read_text())} assert '/blueboat/right/cmd' in topics - assert not any(t.endswith('motor_port/cmd') for t in topics) + assert not any(t.endswith('prop_port/cmd') for t in topics) def test_base_name_flows_to_the_model(tmp_path): @@ -125,8 +139,7 @@ def test_base_name_flows_to_the_model(tmp_path): def test_mismatched_config_fails_with_the_reason(tmp_path): """A slot entry that matches nothing fails the tool (and so the launch).""" - cfg = yaml.safe_load(DEFAULT_CONFIG.read_text()) - cfg['parts'] = [{'slot': 'pingg', 'of': 'ping_mount', 'type': 'none'}] + cfg = kit_config([{'slot': 'pingg', 'of': 'ping_mount', 'type': 'none'}]) config = tmp_path / 'custom.yaml' config.write_text(yaml.safe_dump(cfg, sort_keys=False)) out = subprocess.run([str(TOOL), '--config', str(config), '--out-dir', str(tmp_path / 'v')], @@ -171,7 +184,7 @@ def test_name_flows_to_every_artifact(tmp_path): # The bridge agrees, so gz and ROS sides stay in sync. bridge = yaml.safe_load((out_dir / 'ros_gz_bridge.yaml').read_text()) gz_topics = {e['gz_topic_name'] for e in bridge} - assert any(t.startswith('/boat_b/motor_port/') for t in gz_topics), gz_topics + assert any(t.startswith('/boat_b/prop_port/') for t in gz_topics), gz_topics assert not any(t.startswith('/blueboat/') for t in gz_topics), gz_topics @@ -204,9 +217,8 @@ def test_two_instances_from_one_config_share_no_topic(tmp_path): a command for one instance never reaches the other; only the fixed /clock remains shared. """ - cfg = yaml.safe_load(DEFAULT_CONFIG.read_text()) - cfg['parts'] = [{'slot': 'ping', 'of': 'ping_mount', 'type': 'ping_singlebeam', - 'topic': 'sonar'}] + cfg = kit_config([{'slot': 'ping', 'of': 'ping_mount', 'type': 'ping_singlebeam', + 'topic': 'sonar'}]) config = tmp_path / 'custom.yaml' config.write_text(yaml.safe_dump(cfg, sort_keys=False)) topics = {} diff --git a/blueboat_gazebo/test/test_gz_launch.py b/blueboat_gazebo/test/test_gz_launch.py index 32847604..95400913 100644 --- a/blueboat_gazebo/test/test_gz_launch.py +++ b/blueboat_gazebo/test/test_gz_launch.py @@ -141,10 +141,10 @@ def test_model_loaded(sim): def test_interfaces_advertised(sim): """Motor commands, speed feedback and the world clock are advertised.""" - needed = ('/blueboat/motor_port/cmd', - '/blueboat/motor_stbd/cmd', - '/blueboat/motor_port/cmd/ang_vel', - '/blueboat/motor_stbd/cmd/ang_vel', + needed = ('/blueboat/prop_port/cmd', + '/blueboat/prop_stbd/cmd', + '/blueboat/prop_port/cmd/ang_vel', + '/blueboat/prop_stbd/cmd/ang_vel', f'/world/{WORLD_NAME}/clock') poll_until( lambda: all(t in gz(sim, 'topic', '-l')[1] for t in needed), 30, diff --git a/blueboat_gazebo/test/test_model_generation.py b/blueboat_gazebo/test/test_model_generation.py index 6c14f99a..4431ae4b 100644 --- a/blueboat_gazebo/test/test_model_generation.py +++ b/blueboat_gazebo/test/test_model_generation.py @@ -55,6 +55,23 @@ def catalog(): return BLUEBOAT_PARTS +def kit_config(parts=(), extra=''): + """ + Return the tests' own vehicle. + + The shipped hull and namespace, every slot filling with its default, plus + parts. Independent of what the shipped config fits, so these tests follow + the assembly rules rather than the current product. + """ + cfg = yaml.safe_load(DEFAULT_CONFIG) + cfg['slot_defaults'] = True + cfg['parts'] = [dict(p) for p in parts] + return yaml.safe_dump(cfg, sort_keys=False) + extra + + +KIT_CONFIG = kit_config() + + # Types the chassis slots fit by default; everything else in the catalog is # added explicitly below so FULL_CONFIG exercises the whole library. DEFAULT_TYPES = {'blueboat_chassis', 'm200_weedless_prop_ccw', 'm200_weedless_prop_cw', @@ -70,19 +87,15 @@ def full_config_text(extra=''): slots; every remaining type is free placed. test_full_config_covers_catalog enforces that nothing is left out. """ - cfg = yaml.safe_load(DEFAULT_CONFIG) parts = [{'slot': slot, 'type': t} for t, slot in SLOTTED_EXTRAS.items()] free = [t for t in catalog() if t not in DEFAULT_TYPES and t not in SLOTTED_EXTRAS] parts += [{'type': t, 'name': f'acc_{t}', 'xyz': f'{0.1 * i - 0.6:.2f} 0 0.5', 'rpy': '0 0 0'} for i, t in enumerate(free)] - cfg['parts'] = parts - return yaml.safe_dump(cfg, sort_keys=False) + extra + return kit_config(parts, extra) FULL_CONFIG = full_config_text() -NO_SENSOR_CONFIG = yaml.safe_dump( - yaml.safe_load(DEFAULT_CONFIG) | {'parts': [{'slot': 'ping_mount', 'type': 'none'}]}, - sort_keys=False) +NO_SENSOR_CONFIG = kit_config([{'slot': 'ping_mount', 'type': 'none'}]) def xacro(top_file, config_text, *extra_args): @@ -130,37 +143,29 @@ def test_model_generation_follows_config(): def test_thrusters_follow_the_propeller_parts(): """One Thruster per propeller instance, on its joint, with the part's drive data.""" - root, _ = xacro(MODEL_XACRO, DEFAULT_CONFIG) + root, _ = xacro(MODEL_XACRO, KIT_CONFIG) thrusters = {t.find('joint_name').text: t for t in plugins(root, 'gz-maritime-thruster-system')} - assert set(thrusters) == {'motor_port_joint', 'motor_stbd_joint'} - port, stbd = thrusters['motor_port_joint'], thrusters['motor_stbd_joint'] + assert set(thrusters) == {'prop_port_joint', 'prop_stbd_joint'} + port, stbd = thrusters['prop_port_joint'], thrusters['prop_stbd_joint'] # Counter rotating pair: opposite coefficient signs; same limits. assert float(port.find('thrust_coefficient').text) == \ -float(stbd.find('thrust_coefficient').text) assert float(port.find('max_thrust_cmd').text) > 0 > float(port.find('min_thrust_cmd').text) - assert port.find('topic').text == 'blueboat/motor_port/cmd' + assert port.find('topic').text == 'blueboat/prop_port/cmd' # Empty a motor slot and rename the other: the plugins follow. - cfg = yaml.safe_dump(yaml.safe_load(DEFAULT_CONFIG) | {'parts': [ - {'slot': 'motor_port', 'type': 'none'}, - {'slot': 'motor_stbd', 'type': 't200_prop_cw', 'name': 'right', 'topic': 'r'}]}, - sort_keys=False) + cfg = kit_config([ + {'slot': 'prop_port', 'type': 'none'}, + {'slot': 'prop_stbd', 'type': 't200_prop_cw', 'name': 'right', 'topic': 'r'}]) root, _ = xacro(MODEL_XACRO, cfg) thrusters = plugins(root, 'gz-maritime-thruster-system') assert [t.find('joint_name').text for t in thrusters] == ['right_joint'] assert thrusters[0].find('topic').text == 'blueboat/r/cmd' -def test_default_config_has_the_ping_sensor(): - """The zero configuration model ships the echosounder, through the slot default.""" - root, _ = xacro(MODEL_XACRO, DEFAULT_CONFIG) - sensors = list(root.iter('sensor')) - assert [s.get('name') for s in sensors] == ['ping'] - - def test_sensors_follow_their_part_frame(): """A sensor link is posed at the part's declared sensing frame.""" - root, _ = xacro(MODEL_XACRO, DEFAULT_CONFIG) + root, _ = xacro(MODEL_XACRO, KIT_CONFIG) sensor_links = [li for li in root.iter('link') if li.get('name') == 'ping_sensor'] assert len(sensor_links) == 1 assert sensor_links[0].find('pose').get('relative_to') == 'ping_beam' @@ -326,7 +331,9 @@ def test_installed_model_merges_the_gazebo_flavoured_urdf(): if v.find('geometry/mesh') is not None} ros_rpys = {v.find('origin').get('rpy') for v in ros_urdf.iter('visual') if v.find('geometry/mesh') is not None} - assert gz_rpys == {'1.5708 0 0'} and ros_rpys == {'0 0 0'} + # legacy exports, and profile axes deliveries (axes="gltf" in part_visual) + assert gz_rpys <= {'1.5708 0 0', '1.5708 0 1.5708'} + assert ros_rpys <= {'0 0 0', '0 0 1.5708'} def test_installed_model_sdf_carries_the_specs_comment(): @@ -396,10 +403,10 @@ def test_sensor_and_bridge_topics_agree(): 'ros_topic_name': '/diagnostics', 'gz_topic_name': '/diagnostics', 'ros_type_name': 'std_msgs/msg/Empty', 'gz_type_name': 'gz.msgs.Empty', 'direction': 'GZ_TO_ROS'} - renamed = yaml.safe_dump(yaml.safe_load(DEFAULT_CONFIG) | {'parts': [ + renamed = kit_config([ {'slot': 'ping', 'of': 'ping_mount', 'type': 'ping_singlebeam', - 'name': 'sonar', 'topic': 'echo'}]}, sort_keys=False) - for config in (FULL_CONFIG, DEFAULT_CONFIG, NO_SENSOR_CONFIG, renamed, + 'name': 'sonar', 'topic': 'echo'}]) + for config in (FULL_CONFIG, DEFAULT_CONFIG, KIT_CONFIG, NO_SENSOR_CONFIG, renamed, full_config_text('topic_namespace: boat_a\n')): cfg = yaml.safe_load(config) cfg.setdefault('extra_bridge_topics', []).append(extra) @@ -458,8 +465,6 @@ def test_installed_artifacts_match_shipped_config(): links = {li.get('name') for li in urdf.findall('link')} for part in urdf.findall('assembly_part'): assert part.get('name') in links, f'{part.get("name")} missing from shipped URDF' - assert any(p.get('type') == 'ping_singlebeam' for p in urdf.findall('assembly_part')), \ - 'the shipped default must carry the Ping' def test_model_name_follows_the_topic_namespace(): @@ -477,16 +482,15 @@ def test_ardupilot_channels_command_the_thrusters_topics(): the ArduPilot control block alike; otherwise ArduRover would command a topic nobody listens on. """ - cfg = DEFAULT_CONFIG.replace('parts: []', ( - 'parts:\n' - ' - {slot: motor_stbd, type: t200_prop_cw, gz_topic: /fleet/stbd}\n' - ' - {slot: motor_port, type: t200_prop_ccw, topic: left}\n')) + cfg = kit_config([ + {'slot': 'prop_stbd', 'type': 't200_prop_cw', 'gz_topic': '/fleet/stbd'}, + {'slot': 'prop_port', 'type': 't200_prop_ccw', 'topic': 'left'}]) root, _ = xacro(MODEL_XACRO, cfg, 'ardupilot:=true') thrusters = {t.find('joint_name').text: t.find('topic').text for t in plugins(root, 'gz-maritime-thruster-system')} controls = {c.find('jointName').text: c.find('cmd_topic').text for c in root.iter('control')} - assert controls == {'motor_stbd_joint': '/fleet/stbd/cmd', - 'motor_port_joint': '/blueboat/left/cmd'} + assert controls == {'prop_stbd_joint': '/fleet/stbd/cmd', + 'prop_port_joint': '/blueboat/left/cmd'} for joint, topic in controls.items(): assert topic == '/' + thrusters[joint].lstrip('/') diff --git a/blueboat_gazebo/test/test_ros_launch.py b/blueboat_gazebo/test/test_ros_launch.py index b9b8ffe4..93e8a384 100644 --- a/blueboat_gazebo/test/test_ros_launch.py +++ b/blueboat_gazebo/test/test_ros_launch.py @@ -75,7 +75,7 @@ def test_joint_states_flow(sim): """Motor joint states cross the bridge under the instance name, for RViz.""" code, out, err = ros(sim, 'topic', 'echo', '/blueboat/joint_states', '--once', timeout=30) - assert code == 0 and 'motor_port_joint' in out, f'no joint states\n{err}' + assert code == 0 and 'prop_port_joint' in out, f'no joint states\n{err}' def test_tf_frames_carry_the_instance_name(sim): @@ -88,8 +88,8 @@ def test_tf_frames_carry_the_instance_name(sim): def test_thruster_and_ping_topics_bridged(sim): """The thrust command and echosounder topics exist on the ROS graph.""" - needed = ('/blueboat/motor_port/cmd', - '/blueboat/motor_stbd/cmd', + needed = ('/blueboat/prop_port/cmd', + '/blueboat/prop_stbd/cmd', '/blueboat/ping/range') poll_until( lambda: all(t in ros(sim, 'topic', 'list')[1] for t in needed), 30, diff --git a/bluerobotics_parts/bluerobotics_parts/assembly.py b/bluerobotics_parts/bluerobotics_parts/assembly.py index ebddaedf..70234d43 100644 --- a/bluerobotics_parts/bluerobotics_parts/assembly.py +++ b/bluerobotics_parts/bluerobotics_parts/assembly.py @@ -34,15 +34,15 @@ # Keys each kind of config entry may carry. A key outside its set is a typo # until proven otherwise; the sets grow with the features. -TOP_KEYS = {'topic_namespace', 'base', 'parts', 'slots', 'hull_displacement', - 'buoyancy', 'extra_bridge_topics'} +TOP_KEYS = {'topic_namespace', 'base', 'slot_defaults', 'parts', 'slots', + 'hull_displacement', 'buoyancy', 'extra_bridge_topics'} BASE_KEYS = {'type', 'name', 'collision'} PART_COMMON_KEYS = {'type', 'name', 'xyz', 'rpy', 'collision', 'joint', 'axis', 'topic', 'gz_topic', 'ros_topic', 'bridge'} SLOT_ENTRY_KEYS = PART_COMMON_KEYS | {'slot', 'of'} FREE_ENTRY_KEYS = PART_COMMON_KEYS | {'parent'} ADHOC_SLOT_KEYS = {'of', 'name', 'xyz', 'rpy', 'accepts', 'default', 'joint', - 'collision'} + 'axis', 'collision'} def rpy_matrix(r, p, y): diff --git a/bluerobotics_parts/models/blueboat_chassis/blueboat_chassis_visual.glb b/bluerobotics_parts/models/blueboat_chassis/blueboat_chassis_visual.glb index 860ddd9b..a6d37147 100644 Binary files a/bluerobotics_parts/models/blueboat_chassis/blueboat_chassis_visual.glb and b/bluerobotics_parts/models/blueboat_chassis/blueboat_chassis_visual.glb differ diff --git a/bluerobotics_parts/models/m200_weedless_prop_ccw/m200_weedless_prop_ccw_visual.glb b/bluerobotics_parts/models/m200_weedless_prop_ccw/m200_weedless_prop_ccw_visual.glb index 79183525..1a4508fe 100644 Binary files a/bluerobotics_parts/models/m200_weedless_prop_ccw/m200_weedless_prop_ccw_visual.glb and b/bluerobotics_parts/models/m200_weedless_prop_ccw/m200_weedless_prop_ccw_visual.glb differ diff --git a/bluerobotics_parts/models/m200_weedless_prop_cw/m200_weedless_prop_cw_visual.glb b/bluerobotics_parts/models/m200_weedless_prop_cw/m200_weedless_prop_cw_visual.glb index 13b1380a..2d5b7b37 100644 Binary files a/bluerobotics_parts/models/m200_weedless_prop_cw/m200_weedless_prop_cw_visual.glb and b/bluerobotics_parts/models/m200_weedless_prop_cw/m200_weedless_prop_cw_visual.glb differ diff --git a/bluerobotics_parts/scripts/sdf_to_part.py b/bluerobotics_parts/scripts/sdf_to_part.py index 50f3c23d..2759bdfe 100755 --- a/bluerobotics_parts/scripts/sdf_to_part.py +++ b/bluerobotics_parts/scripts/sdf_to_part.py @@ -33,7 +33,7 @@ sdf_to_part.py models/t200_thruster --mass 0.344 --axis "1 0 0" sdf_to_part.py models/blueboat_chassis --mass 12 - --slot "motor_port=-0.52,0.301,-0.117;accepts=m200_weedless_prop_ccw; + --slot "prop_port=-0.52,0.301,-0.117;accepts=m200_weedless_prop_ccw; default=m200_weedless_prop_ccw;joint=continuous" sdf_to_part.py models/ping_singlebeam --frame beam=0,0,-0.044 sdf_to_part.py --all models # first version of every delivered part diff --git a/bluerobotics_parts/urdf/assembly.xacro b/bluerobotics_parts/urdf/assembly.xacro index d6f88b80..a69528fe 100644 --- a/bluerobotics_parts/urdf/assembly.xacro +++ b/bluerobotics_parts/urdf/assembly.xacro @@ -13,7 +13,7 @@ base: {type: blueboat_chassis, name: base_link} parts: - - {slot: motor_port, type: t200_prop_ccw} # another option for a slot + - {slot: prop_port, type: t200_prop_ccw} # another option for a slot - {slot: ping_mount, type: none} # leave a slot empty - {slot: mast, type: blueboat_antenna_mast} # fill a slot that has no default - {slot: ping, of: ping_mount, type: ping_singlebeam, topic: sonar} # a slot of another part @@ -35,9 +35,13 @@ to parent (default: the base). Their slots fill too. * The occupant's joint is the entry's `joint:`, else the slot's, else the part's nature: a propeller (a part with a `drive` table) turns on a - continuous joint, anything else is fixed. + continuous joint, anything else is fixed. Its axis is the entry's + `axis:`, else the slot's, else the part's own spin axis. * `slots:` declares ad hoc slots on any instance, usable like the part's own (accepts and default work the same). + * `defaults:=false` (a config's `slot_defaults: false`) turns the slot + defaults off: a slot with no config entry stays empty, so the config + lists every part that is fitted, the base's and the deeper ones alike. * Mistakes the expansion can see fail it loudly: an unknown slot on the base, a type a slot does not accept, a slot configured twice, a bare `on:` key. An unknown part type fails as "unknown macro name". What @@ -110,7 +114,8 @@ - + @@ -119,7 +124,7 @@ xyz="${entry.get('xyz', '0 0 0')}" rpy="${entry.get('rpy', '0 0 0')}" collision="${entry.get('collision', spec.get('collision', True))}" joint="${entry.get('joint', spec.get('joint', ''))}" - axis="${entry.get('axis', '')}" + axis="${entry.get('axis', spec.get('axis', ''))}" entries="${entries}" adhoc="${adhoc}" emit="${emit}" emit_slot="${emit_slot}"/> @@ -185,8 +190,9 @@ + params="base parts slots:='' emit:=emit_part emit_slot:=part_slot manifest:=true defaults:=true"> + diff --git a/bluerobotics_parts/urdf/blueboat_chassis.urdf.xacro b/bluerobotics_parts/urdf/blueboat_chassis.urdf.xacro index 11ef1830..8f2169c3 100644 --- a/bluerobotics_parts/urdf/blueboat_chassis.urdf.xacro +++ b/bluerobotics_parts/urdf/blueboat_chassis.urdf.xacro @@ -11,8 +11,8 @@ slots, frames, drive); instantiates the link, its mounting joint and its slot / frame links. Slots other parts fit into, as frames _: - motor_port: m200_weedless_prop_ccw, t200_prop_ccw; default m200_weedless_prop_ccw - motor_stbd: m200_weedless_prop_cw, t200_prop_cw; default m200_weedless_prop_cw + prop_port: m200_weedless_prop_ccw, t200_prop_ccw; default m200_weedless_prop_ccw + prop_stbd: m200_weedless_prop_cw, t200_prop_cw; default m200_weedless_prop_cw mast: blueboat_antenna_mast; default none flag: blueboat_flag; default blueboat_flag payload: blueboat_payload_bracket; default none @@ -26,12 +26,13 @@ The position is a placeholder at the chassis origin: the real offset inside the enclosure has not been measured, and the lever arm only starts to matter once accelerations are compared against the real boat. --> + - + diff --git a/bluerobotics_parts/urdf/m200_weedless_prop_ccw.urdf.xacro b/bluerobotics_parts/urdf/m200_weedless_prop_ccw.urdf.xacro index 1baf793c..6c244003 100644 --- a/bluerobotics_parts/urdf/m200_weedless_prop_ccw.urdf.xacro +++ b/bluerobotics_parts/urdf/m200_weedless_prop_ccw.urdf.xacro @@ -32,7 +32,7 @@ iyy="3.11438e-05" iyz="0" izz="3.11438e-05"/> - + diff --git a/bluerobotics_parts/urdf/m200_weedless_prop_cw.urdf.xacro b/bluerobotics_parts/urdf/m200_weedless_prop_cw.urdf.xacro index a01de9bc..0d30ed9e 100644 --- a/bluerobotics_parts/urdf/m200_weedless_prop_cw.urdf.xacro +++ b/bluerobotics_parts/urdf/m200_weedless_prop_cw.urdf.xacro @@ -36,7 +36,7 @@ iyy="3.11438e-05" iyz="0" izz="3.11438e-05"/> - + diff --git a/bluerobotics_parts/urdf/parts.xacro b/bluerobotics_parts/urdf/parts.xacro index 53a66167..c91fedb1 100644 --- a/bluerobotics_parts/urdf/parts.xacro +++ b/bluerobotics_parts/urdf/parts.xacro @@ -26,6 +26,7 @@ default the type fitted when the config is silent, or 'none' joint joint type for the occupant (default fixed) + axis joint axis for the occupant, in its own frame (default: the part's own spin axis) collision false to fit occupants without contact geometry (a propeller spinning inside a duct or hull volume would grind on it) @@ -81,10 +82,13 @@ - - + + + + + - + diff --git a/bluerobotics_parts/worlds/parts_check.sdf b/bluerobotics_parts/worlds/parts_check.sdf index 8381c13a..341419f9 100644 --- a/bluerobotics_parts/worlds/parts_check.sdf +++ b/bluerobotics_parts/worlds/parts_check.sdf @@ -118,14 +118,14 @@ 0.021265900000000001 0 0.072499800000000003 0 0 0 - 12 + 16.800000000000001 - 1.6043000000000001 - -1.22202e-18 - 0.0037280299999999998 - 0.98831400000000003 - 4.88808e-18 - 2.5308899999999999 + 1.51 + 0 + 0 + 2.3100000000000001 + 0 + 3.23 @@ -179,11 +179,11 @@ - 0 0 0 1.5708 0 0 + 0 0 0 1.5708 -5.5511151231257827e-17 1.5708 1 1 1 - model://bluerobotics_parts/models/blueboat_chassis/blueboat_chassis.visual.glb + model://bluerobotics_parts/models/blueboat_chassis/blueboat_chassis_visual.glb @@ -194,22 +194,16 @@ 0 0 0 0 0 0 - - -0.29799999999999999 0 0.14699999999999999 0 0 0 - - + 0 0 0 0 0 0 - - -0.52000000000000002 0.30099999999999999 -0.11700000000000001 0 0 0 - - + 0 0 0 0 0 0 - - -0.52000000000000002 -0.30099999999999999 -0.11700000000000001 0 0 0 + + -0.29799999999999999 0 0.14699999999999999 0 0 0 - + 0 0 0 0 0 0 @@ -224,6 +218,18 @@ 0 0 0 0 0 0 + + -0.51959999999999995 0.30159999999999998 -0.1268 0 0 -3.1415853071795872 + + + 0 0 0 0 0 0 + + + -0.51959999999999995 -0.30159999999999998 -0.127 0 0 -3.1415853071795872 + + + 0 0 0 0 0 0 + true @@ -344,14 +350,14 @@ 0.0097937300000000005 -0.00022557600000000001 0.010999999999999999 0 0 0 - 11.229699999999999 + 9.4000000000000004 - 0.16716700000000001 - -6.7739100000000003e-06 - -0.00060175400000000003 - 0.208312 - 2.46933e-05 - 0.26392199999999999 + 0.13992979999999999 + -5.6702100000000002e-06 + -0.0005037078 + 0.1743709 + 2.0669920000000001e-05 + 0.22092010000000001 @@ -456,6 +462,12 @@ 0 0 0 0 0 0 + + 0 0 0 0 0 0 + + + 0 0 0 0 0 0 + 0 0 -0.17000000000000001 0 0 0 @@ -499,17 +511,53 @@ 0 0 0 0 0 0 - 0 -0.109 0.076999999999999999 3.1415926535897931 1.5707926536359933 3.1415926535897931 + 0.014999999999999999 -0.109 0.050000000000000003 3.1415926535897931 1.5707926536359933 3.1415926535897931 0 0 0 0 0 0 - 0 0.109 0.076999999999999999 3.1415926535897931 1.5707926536359933 3.1415926535897931 + 0.014999999999999999 0.109 0.050000000000000003 3.1415926535897931 1.5707926536359933 3.1415926535897931 0 0 0 0 0 0 + + 0.14000000000000001 -0.091999999999999998 0 0 0 -2.3561999999999999 + + + 0 0 0 0 0 0 + + + 0.14000000000000001 0.091999999999999998 0 0 0 2.3561999999999999 + + + 0 0 0 0 0 0 + + + -0.14999999999999999 -0.091999999999999998 0 0 0 -0.7854000000000001 + + + 0 0 0 0 0 0 + + + -0.14999999999999999 0.091999999999999998 0 0 0 0.7854000000000001 + + + 0 0 0 0 0 0 + + + 0.014999999999999999 -0.109 0.050000000000000003 3.1415926535897931 -1.5707926536359933 3.1415926535897931 + + + 0 0 0 0 0 0 + + + 0.014999999999999999 0.109 0.050000000000000003 3.1415926535897931 -1.5707926536359933 3.1415926535897931 + + + 0 0 0 0 0 0 + true @@ -517,14 +565,14 @@ 0.0097937300000000005 -0.00022557600000000001 0.010999999999999999 0 0 0 - 15.1896 + 12.75 - 0.22611899999999999 - -9.1623300000000008e-06 - -0.00081394599999999996 - 0.28177400000000002 - 3.3402100000000003e-05 - 0.35701300000000002 + 0.1898021 + -7.6907689999999995e-06 + -0.00068321820000000004 + 0.23651829999999999 + 2.803739e-05 + 0.29967319999999997 @@ -695,6 +743,54 @@ 0 0 0 0 0 0 + + 0.14000000000000001 -0.091999999999999998 0 0 0 -2.3561999999999999 + + + 0 0 0 0 0 0 + + + 0.14000000000000001 0.091999999999999998 0 0 0 2.3561999999999999 + + + 0 0 0 0 0 0 + + + -0.14999999999999999 -0.091999999999999998 0 0 0 -0.7854000000000001 + + + 0 0 0 0 0 0 + + + -0.14999999999999999 0.091999999999999998 0 0 0 0.7854000000000001 + + + 0 0 0 0 0 0 + + + 0.11799999999999999 -0.215 0.064000000000000001 3.1415926535897931 -1.5707926536359933 3.1415926535897931 + + + 0 0 0 0 0 0 + + + 0.11799999999999999 0.215 0.064000000000000001 3.1415926535897931 -1.5707926536359933 3.1415926535897931 + + + 0 0 0 0 0 0 + + + -0.11799999999999999 -0.215 0.064000000000000001 3.1415926535897931 -1.5707926536359933 3.1415926535897931 + + + 0 0 0 0 0 0 + + + -0.11799999999999999 0.215 0.064000000000000001 3.1415926535897931 -1.5707926536359933 3.1415926535897931 + + + 0 0 0 0 0 0 + true @@ -744,7 +840,7 @@ - 0 0 0 0 0 0 + 0 0 0 0 1.5707963267948966 0 0.12 @@ -784,11 +880,11 @@ - 0 0 0 1.5708 0 0 + 0 0 0 1.5708 -5.5511151231257827e-17 1.5708 1 1 1 - model://bluerobotics_parts/models/m200_weedless_prop_ccw/m200_weedless_prop_ccw.visual.glb + model://bluerobotics_parts/models/m200_weedless_prop_ccw/m200_weedless_prop_ccw_visual.glb @@ -800,7 +896,7 @@ 0.00033599999999999998 3.0000000000000001e-05 -5.0000000000000004e-06 0 0 0 - 0.074471999999999997 + 0.01 1.41779e-05 0 @@ -820,11 +916,11 @@ - 0 0 0 1.5708 0 0 + 0 0 0 1.5708 -5.5511151231257827e-17 1.5708 1 1 1 - model://bluerobotics_parts/models/m200_weedless_prop_cw/m200_weedless_prop_cw.visual.glb + model://bluerobotics_parts/models/m200_weedless_prop_cw/m200_weedless_prop_cw_visual.glb @@ -1004,39 +1100,9 @@ - - true - 3.20 -3.20 0.8 0 0 0 - - - 0 0 0 0 0 0 - 0.44 - - 0.00015084666666666661 - 0 - 0 - 0.0010381800000000001 - 0 - 0.0011091666666666669 - - - - 0 0 0 0 0 0 - - - 0.16500000000000001 0.055 0.033000000000000002 - - - - 0.625 0.625 0.6875 1 - 0.625 0.625 0.6875 1 - - - - true - -3.20 -4.80 0.8 0 0 0 + 3.20 -3.20 0.8 0 0 0 0 -0.001397 0.019345299999999999 0 0 0 @@ -1103,7 +1169,7 @@ true - -1.60 -4.80 0.8 0 0 0 + -3.20 -4.80 0.8 0 0 0 0 0 0 0 0 0 @@ -1133,7 +1199,7 @@ true - 0.00 -4.80 0.8 0 0 0 + -1.60 -4.80 0.8 0 0 0 0 0 0 0 0 0 @@ -1160,7 +1226,7 @@ true - 1.60 -4.80 0.8 0 0 0 + 0.00 -4.80 0.8 0 0 0 -9.9999999999999995e-07 -0.00080216900000000004 0.000201108 0 0 0 @@ -1211,7 +1277,7 @@ true - 3.20 -4.80 0.8 0 0 0 + 1.60 -4.80 0.8 0 0 0 0 0 0 0 0 0 @@ -1241,7 +1307,7 @@ true - -3.20 -6.40 0.8 0 0 0 + 3.20 -4.80 0.8 0 0 0 0 0 0 0 0 0 @@ -1409,39 +1475,9 @@ - - true - -1.60 -6.40 0.8 0 0 0 - - - 0 0 0 0 0 0 - 0.75 - - 0.00073525000000000012 - 0 - 0 - 0.0015165000000000001 - 0 - 0.0020312500000000001 - - - - 0 0 0 0 0 0 - - - 0.14999999999999999 0.10000000000000001 0.042000000000000003 - - - - 0.625 0.625 0.6875 1 - 0.625 0.625 0.6875 1 - - - - true - 0.00 -6.40 0.8 0 0 0 + -3.20 -6.40 0.8 0 0 0 -0.0025521900000000002 -0.000106891 -0.00198043 0 0 0 @@ -1502,7 +1538,7 @@ true - 1.60 -6.40 0.8 0 0 0 + -1.60 -6.40 0.8 0 0 0 5.1999999999999997e-05 0 0 0 0 0 @@ -1538,7 +1574,7 @@ true - 3.20 -6.40 0.8 0 0 0 + 0.00 -6.40 0.8 0 0 0 5.1999999999999997e-05 0 0 0 0 0 @@ -1574,18 +1610,18 @@ true - -3.20 -8.00 0.8 0 0 0 + 1.60 -6.40 0.8 0 0 0 -0.0068830699999999998 0.0028555199999999998 -0.00016627300000000001 0 0 0 - 0.34399999999999997 + 0.30494660000000001 - 0.00031312000000000001 - -1.3050300000000001e-08 - -2.5610200000000003e-07 - 0.00029150999999999998 - -3.5035900000000001e-10 - 0.00029150299999999998 + 0.00027757230000000001 + -1.1568729999999999e-08 + -2.2702740000000001e-07 + 0.0002584156 + -3.1058370000000002e-10 + 0.0002584094 diff --git a/bluerobotics_teleop/config/blueboat/mixer.yaml b/bluerobotics_teleop/config/blueboat/mixer.yaml index ceec01a3..c7a374de 100644 --- a/bluerobotics_teleop/config/blueboat/mixer.yaml +++ b/bluerobotics_teleop/config/blueboat/mixer.yaml @@ -4,10 +4,10 @@ twist_to_thrust: ros__parameters: # Relative to the instance: the teleop launch runs the mixer in the - # / namespace, so these resolve to //motor_port/cmd and so on. + # / namespace, so these resolve to //prop_port/cmd and so on. thruster_topics: - - motor_port/cmd - - motor_stbd/cmd + - prop_port/cmd + - prop_stbd/cmd gains_linear_x: [ 1.0, 1.0] gains_linear_y: [ 0.0, 0.0] gains_linear_z: [ 0.0, 0.0] diff --git a/bluerobotics_teleop/test/test_configs.py b/bluerobotics_teleop/test/test_configs.py index 705eac7e..f8375d63 100644 --- a/bluerobotics_teleop/test/test_configs.py +++ b/bluerobotics_teleop/test/test_configs.py @@ -164,14 +164,14 @@ def test_default_names_are_the_sim_launch_names(): def test_under_puts_a_relative_topic_in_the_instance(): - assert under('boat_b', 'motor_port/cmd') == '/boat_b/motor_port/cmd' - assert under('boat_b', '/motor_port/cmd') == '/boat_b/motor_port/cmd' + assert under('boat_b', 'prop_port/cmd') == '/boat_b/prop_port/cmd' + assert under('boat_b', '/prop_port/cmd') == '/boat_b/prop_port/cmd' def test_node_params_reads_the_bare_node_key(): """The launch hands a file's values to the node, whatever its namespace.""" params = node_params(TELEOP / 'config' / 'blueboat' / 'mixer.yaml', 'twist_to_thrust') - assert params['thruster_topics'] == ['motor_port/cmd', 'motor_stbd/cmd'] + assert params['thruster_topics'] == ['prop_port/cmd', 'prop_stbd/cmd'] params = node_params(TELEOP / 'config' / 'pad' / 'joystick.config.yaml', 'teleop_twist_joy_node') assert params['enable_button'] == 5 diff --git a/docs/design/architecture.md b/docs/design/architecture.md index 9662029c..21e095fc 100644 --- a/docs/design/architecture.md +++ b/docs/design/architecture.md @@ -10,7 +10,7 @@ flowchart LR GLB["models/part.visual.glb"] PART["urdf/part.urdf.xacro
the part: link, inertia, visual, collision,
attach, slots, frames"] GLB -. "referenced by the visual" .-> PART - CFG["config/blueboat.yaml
base, overrides, hull displacement"] --> ASM["assembly.xacro
slots fill with defaults,
config overrides and adds"] + CFG["config/blueboat.yaml
base, overrides, hull displacement"] --> ASM["assembly.xacro
slots fill from the config
and their defaults"] PART --> ASM ASM --> URDF["blueboat.urdf
+ assembly_part manifest"] URDF --> ROS["ROS: robot_state_publisher, RViz, TF"] @@ -58,7 +58,7 @@ through a merge include, so both sides always see identical geometry. "standalone" vehicle and a separate programmatic path and declared them deliberately out of sync. Slots with defaults remove the split: the zero configuration vehicle and the configurable one are the same resolution, -with an empty config. +with an empty config. A config can instead turn the defaults off (`slot_defaults: false`) and list every fitted part. **The structure is Clearpath's.** [`clearpath_common`](https://github.com/clearpathrobotics/clearpath_common) is the closest analogue, a family of configurable robots for ROS and Gazebo diff --git a/docs/design/slots.md b/docs/design/slots.md index ae253eff..c43218b8 100644 --- a/docs/design/slots.md +++ b/docs/design/slots.md @@ -9,8 +9,8 @@ goes; the Ping declares the frame its beam leaves from. ```{mermaid} flowchart TB C["blueboat_chassis"] - C -->|"slot motor_port
accepts M200 ccw, T200 ccw
default M200 ccw, continuous"| MP["m200_weedless_prop_ccw"] - C -->|"slot motor_stbd
default M200 cw, continuous"| MS["m200_weedless_prop_cw"] + C -->|"slot prop_port
accepts M200 ccw, T200 ccw
default M200 ccw, continuous"| MP["m200_weedless_prop_ccw"] + C -->|"slot prop_stbd
default M200 cw, continuous"| MS["m200_weedless_prop_cw"] C -->|"slot flag
default blueboat_flag"| F["blueboat_flag"] C -->|"slot mast
accepts antenna mast
default none"| M(("empty")) C -->|"slot payload
accepts payload bracket
default none"| P(("empty")) @@ -27,7 +27,7 @@ flowchart TB flowchart TD A["instantiate the base part"] --> B["for each slot of the instance
(its own, plus ad hoc slots from the config)"] B --> C{"config entry
for this slot?"} - C -- "none" --> D["fit the slot's default
(nothing if default is none)"] + C -- "none" --> D["fit the slot's default
(nothing if default is none
or slot_defaults is false)"] C -- "type: none" --> E["leave it empty;
the slot frame stays"] C -- "type: X" --> F{"X in accepts?"} F -- "no" --> G["ASSEMBLY ERROR"] @@ -41,10 +41,8 @@ flowchart TD Rules, in words: - The **base** part is the root; its link is the named root link (no joint). -- Every slot fills itself with its **default**, recursively, unless the - config has an entry for it. An entry names the slot (`slot:`), the - instance carrying it (`of:`, default the base), and `type:` an accepted - part or `none`. The occupant's instance name defaults to the slot name. +- Every slot fills itself with its **default**, recursively, unless the config has an entry for it. An entry names the slot (`slot:`), the instance carrying it (`of:`, default the base), and `type:` an accepted part or `none`. The occupant's instance name defaults to the slot name. +- `slot_defaults: false` in the config turns the defaults off: a slot with no entry stays empty, so the config lists every fitted part, including the deeper ones (the Ping in the kit's bracket needs its own entry). - **Free placements** (no `slot:`) give `type`, `name`, `xyz`/`rpy` relative to `parent`. - **Ad hoc slots** declared under `slots:` on any instance behave like the diff --git a/docs/getting-started/first-simulation.md b/docs/getting-started/first-simulation.md index bdd9b84e..7cecd9f9 100644 --- a/docs/getting-started/first-simulation.md +++ b/docs/getting-started/first-simulation.md @@ -8,18 +8,13 @@ ros2 launch blueboat_gazebo sim.launch.xml ``` The simulation starts running, with the camera a few meters off the boat's -port quarter. The boat is spawned at its static waterline and floats there: -two hulls, the two outboard propellers, the flag and the Ping2 echosounder -fitted under the starboard hull. That is the default configuration; no -configuration was involved. +port quarter. The boat is spawned at its static waterline and floats there: the chassis and its two outboard propellers, which is what the shipped config lists. -In a second terminal (also sourced), watch the echosounder and spin the -propellers: +In a second terminal (also sourced), spin the propellers: ```bash -ros2 topic echo /blueboat/ping/range --once -ros2 topic pub /blueboat/motor_port/cmd std_msgs/msg/Float64 "data: 0.2" -1 & -ros2 topic pub /blueboat/motor_stbd/cmd std_msgs/msg/Float64 "data: 0.2" -1 & +ros2 topic pub /blueboat/prop_port/cmd std_msgs/msg/Float64 "data: 0.2" -1 & +ros2 topic pub /blueboat/prop_stbd/cmd std_msgs/msg/Float64 "data: 0.2" -1 & wait ``` diff --git a/docs/how-to/change-parts.md b/docs/how-to/change-parts.md index 0e911f64..00fa9fbe 100644 --- a/docs/how-to/change-parts.md +++ b/docs/how-to/change-parts.md @@ -12,16 +12,30 @@ cp $(ros2 pkg prefix --share blueboat_description)/config/blueboat.yaml my_vehic and try it without rebuilding: `ros2 launch blueboat_gazebo sim.launch.xml config_file:=$PWD/my_vehicle.yaml`. +The shipped config sets `slot_defaults: false`, so the vehicle is exactly its `parts:` list and each recipe below adds entries to it. With `slot_defaults: true` every unlisted slot fills with its default instead, and `type: none` is how a part is left off. + +## Fit the Ping2 kit + +```yaml +parts: + - {slot: ping_mount, type: blueboat_ping_singlebeam_mount} + - {slot: ping, of: ping_mount, type: ping_singlebeam} +``` + +The Ping fills a slot of the bracket, so with `slot_defaults: false` it needs its own entry. + ## Fit the other propellers ```yaml parts: - - {slot: motor_port, type: t200_prop_ccw} - - {slot: motor_stbd, type: t200_prop_cw} + - {slot: prop_port, type: t200_prop_ccw} + - {slot: prop_stbd, type: t200_prop_cw} ``` ## Leave the Ping2 kit off +With `slot_defaults: true`: + ```yaml parts: - {slot: ping_mount, type: none} diff --git a/docs/how-to/teleop.md b/docs/how-to/teleop.md index 12920325..f84290af 100644 --- a/docs/how-to/teleop.md +++ b/docs/how-to/teleop.md @@ -6,7 +6,7 @@ normalized `cmd_vel`, and the `twist_to_thrust` mixer turns that Twist into per thruster thrust commands through a per vehicle gain matrix. The stack binds to one vehicle instance, ``: its three nodes and their topics run under `/`, and the mixer's thruster topics, relative in -its config, land on `//motor_port/cmd` and the like. Those are the +its config, land on `//prop_port/cmd` and the like. Those are the topics documented in each vehicle's Actuators page ([BlueBoat](../vehicles/blueboat/actuators.md), [BlueROV2](../vehicles/bluerov2/actuators.md)), so the same launch works @@ -124,11 +124,11 @@ ros2 topic list | grep cmd ```text /boat_a/cmd_vel -/boat_a/motor_port/cmd -/boat_a/motor_stbd/cmd +/boat_a/prop_port/cmd +/boat_a/prop_stbd/cmd /boat_b/cmd_vel -/boat_b/motor_port/cmd -/boat_b/motor_stbd/cmd +/boat_b/prop_port/cmd +/boat_b/prop_stbd/cmd ``` ## Safety behavior diff --git a/docs/reference/parts-catalog.md b/docs/reference/parts-catalog.md index 66b8909d..6e85247b 100644 --- a/docs/reference/parts-catalog.md +++ b/docs/reference/parts-catalog.md @@ -43,8 +43,8 @@ after the slot by default. | Part | Slot | Accepts | Default | Joint | |---|---|---|---|---| -| `blueboat_chassis` | `motor_port` | `m200_weedless_prop_ccw`, `t200_prop_ccw` | `m200_weedless_prop_ccw` | continuous | -| | `motor_stbd` | `m200_weedless_prop_cw`, `t200_prop_cw` | `m200_weedless_prop_cw` | continuous | +| `blueboat_chassis` | `prop_port` | `m200_weedless_prop_ccw`, `t200_prop_ccw` | `m200_weedless_prop_ccw` | continuous | +| | `prop_stbd` | `m200_weedless_prop_cw`, `t200_prop_cw` | `m200_weedless_prop_cw` | continuous | | | `flag` | `blueboat_flag` | `blueboat_flag` | fixed | | | `mast` | `blueboat_antenna_mast` | none | fixed | | | `payload` | `blueboat_payload_bracket` | none | fixed | @@ -78,8 +78,9 @@ for both vehicles: |---|---|---| | `topic_namespace` | no (the vehicle name) | prefix of every part topic: `///...` | | `base` | yes | the root part: `{type, name (default base_link), collision (default true)}` | +| `slot_defaults` | no (`true`) | `false` leaves every slot without an entry empty, so `parts` lists every fitted part | | `parts` | no (`[]`) | slot entries and free placements, see below | -| `slots` | no | ad hoc slots: `{of (instance, default base_link), name, xyz, rpy (default "0 0 0"), accepts (list, optional), default (type or none), joint}` | +| `slots` | no | ad hoc slots: `{of (instance, default base_link), name, xyz, rpy (default "0 0 0"), accepts (list, optional), default (type or none), joint, axis}` | | `hull_displacement` | BlueBoat | the box pontoons the Gazebo composition places on the `hull_displacement` link: `{length, width, height, x, y, z, segments}` | | `buoyancy` | BlueROV2 | `{net_buoyancy (kg), cob_offset, cob_frame (com/base_link), fluid_density, footprint}`, realized as a dedicated displacement link solved from the assembled mass; see [Buoyancy](../design/buoyancy.md) | | `extra_bridge_topics` | no | list appended verbatim to the generated ros_gz bridge config (native syntax) | @@ -94,7 +95,7 @@ A **slot entry** in `parts` (has `slot`): | `name` | instance name (default: the slot name); must be unique across the vehicle | | `xyz`, `rpy` | offset from the slot pose (default zero) | | `joint` | joint type for the occupant (default: the slot's, else `continuous` for a propeller, else `fixed`) | -| `axis` | joint axis override (default: the part's own spin axis) | +| `axis` | joint axis override (default: the slot's, else the part's own spin axis) | | `collision` | `false` to fit the part without its contact geometry | | `topic`, `gz_topic`, `ros_topic`, `bridge` | parts with topics (propellers, sensors): topic base for both sides / one side, under the namespace unless it starts with a slash; native bridge keys merged into the entry | @@ -121,8 +122,8 @@ frames follow the fitted parts, with the same naming patterns on both vehicles: | Pattern | Kind | Meaning | |---|---|---| | `base_link` | root link | the chassis part: origin at the mesh centroid, x forward, y left, z up ([REP 103](https://www.ros.org/reps/rep-0103.html)); floats with its origin at the waterline | -| `` | link | one frame per fitted part, named after its slot or its `name` (`motor_port`, `thruster_1`, `ping`, `camera`); spinning parts sit on their continuous joint `_joint` | -| `_` | massless link | one per slot a part declares, whether filled or not (`base_link_motor_port`, `base_link_camera`, `ping_mount_ping`) | +| `` | link | one frame per fitted part, named after its slot or its `name` (`prop_port`, `thruster_1`, `ping`, `camera`); spinning parts sit on their continuous joint `_joint` | +| `_` | massless link | one per slot a part declares, whether filled or not (`base_link_prop_port`, `base_link_camera`, `ping_mount_ping`) | | `_` | massless link | frames the part itself declares (`ping_beam`, the transducer face); sensor messages carry them as `frame_id`, resolved by TF | The displacement volume is not in the URDF or TF: each Gazebo package adds diff --git a/docs/vehicles/blueboat/actuators.md b/docs/vehicles/blueboat/actuators.md index 128dd13b..59be7c1f 100644 --- a/docs/vehicles/blueboat/actuators.md +++ b/docs/vehicles/blueboat/actuators.md @@ -8,10 +8,10 @@ following the [Add a part](../../how-to/add-part.md) guide. ## Available actuators -| Actuator | Part | Slot | Fitted by default | +| Actuator | Part | Slot | Fitted in the shipped config | |---|---|---|---| -| M200 weedless propellers | `m200_weedless_prop_ccw` / `_cw` | `motor_port`, `motor_stbd` | yes | -| T200 propellers | `t200_prop_ccw` / `_cw` | `motor_port`, `motor_stbd` | no | +| M200 weedless propellers | `m200_weedless_prop_ccw` / `_cw` | `prop_port`, `prop_stbd` | yes | +| T200 propellers | `t200_prop_ccw` / `_cw` | `prop_port`, `prop_stbd` | no | ## Actuators ROS API @@ -25,8 +25,8 @@ below use, or whatever the boat was spawned as | ROS Topic | Description | Message type | |---|---|---| -| `//motor_port/cmd` | Normalized thrust command in [-1, 1]: +1 full ahead, -1 full astern, 0 stop | [std_msgs/msg/Float64](https://docs.ros.org/en/rolling/p/std_msgs/interfaces/msg/Float64.html) | -| `//motor_stbd/cmd` | Normalized thrust command in [-1, 1]: +1 full ahead, -1 full astern, 0 stop | [std_msgs/msg/Float64](https://docs.ros.org/en/rolling/p/std_msgs/interfaces/msg/Float64.html) | +| `//prop_port/cmd` | Normalized thrust command in [-1, 1]: +1 full ahead, -1 full astern, 0 stop | [std_msgs/msg/Float64](https://docs.ros.org/en/rolling/p/std_msgs/interfaces/msg/Float64.html) | +| `//prop_stbd/cmd` | Normalized thrust command in [-1, 1]: +1 full ahead, -1 full astern, 0 stop | [std_msgs/msg/Float64](https://docs.ros.org/en/rolling/p/std_msgs/interfaces/msg/Float64.html) | The command is a fraction of the propeller's own limits, which the model takes from the part's drive table - 51.5 N ahead and -40.2 N astern for a @@ -37,8 +37,8 @@ Equal command drives ahead; differential command yaws (more starboard turns to port and vice versa). To manually send thruster commands via ROS: ```bash -ros2 topic pub /blueboat/motor_port/cmd std_msgs/msg/Float64 "data: 0.4" -1 & -ros2 topic pub /blueboat/motor_stbd/cmd std_msgs/msg/Float64 "data: 0.4" -1 & +ros2 topic pub /blueboat/prop_port/cmd std_msgs/msg/Float64 "data: 0.4" -1 & +ros2 topic pub /blueboat/prop_stbd/cmd std_msgs/msg/Float64 "data: 0.4" -1 & wait ``` @@ -68,8 +68,8 @@ under the same names), plus a speed feedback per motor: | `//motor_/cmd/ang_vel` | Propeller speed feedback (rad/s) | `gz.msgs.Double` | ```bash -gz topic -t /blueboat/motor_port/cmd -m gz.msgs.Double -p 'data: 0.2' & -gz topic -t /blueboat/motor_stbd/cmd -m gz.msgs.Double -p 'data: 0.2' & +gz topic -t /blueboat/prop_port/cmd -m gz.msgs.Double -p 'data: 0.2' & +gz topic -t /blueboat/prop_stbd/cmd -m gz.msgs.Double -p 'data: 0.2' & wait ``` diff --git a/docs/vehicles/blueboat/configuration.md b/docs/vehicles/blueboat/configuration.md index a3356e62..c976c857 100644 --- a/docs/vehicles/blueboat/configuration.md +++ b/docs/vehicles/blueboat/configuration.md @@ -1,9 +1,6 @@ # Configuration -The BlueBoat needs no configuration: every slot the parts declare fills -itself with its default. The vehicle config -(`blueboat_description/config/blueboat.yaml`, or any file passed as -`config_file:=`) states differences only. +The vehicle config (`blueboat_description/config/blueboat.yaml`, or any file passed as `config_file:=`) lists the fitted parts. The shipped config sets `slot_defaults: false` and fits the chassis and its two propellers only. With `slot_defaults: true`, or the key left out, every slot fills itself with the default in the table below, and the config states differences only. ## Hull displacement @@ -24,7 +21,7 @@ config entry each. The fitted options and their topics live in | Slot | Accepts | Default | |---|---|---| -| `motor_port`, `motor_stbd` | `m200_weedless_prop_ccw`/`_cw`, `t200_prop_ccw`/`_cw` | the M200 weedless props | +| `prop_port`, `prop_stbd` | `m200_weedless_prop_ccw`/`_cw`, `t200_prop_ccw`/`_cw` | the M200 weedless props | | `flag` | `blueboat_flag` | `blueboat_flag` | | `mast` | `blueboat_antenna_mast` | none | | `payload` | `blueboat_payload_bracket` | none | diff --git a/docs/vehicles/blueboat/images/bleuboat_gz_baseviz_cs.png b/docs/vehicles/blueboat/images/bleuboat_gz_baseviz_cs.png new file mode 100644 index 00000000..a172085d Binary files /dev/null and b/docs/vehicles/blueboat/images/bleuboat_gz_baseviz_cs.png differ diff --git a/docs/vehicles/blueboat/images/blueboat_gz_base_cs.png b/docs/vehicles/blueboat/images/blueboat_gz_base_cs.png new file mode 100644 index 00000000..f5ae91bd Binary files /dev/null and b/docs/vehicles/blueboat/images/blueboat_gz_base_cs.png differ diff --git a/docs/vehicles/blueboat/images/blueboat_rviz_cs.png b/docs/vehicles/blueboat/images/blueboat_rviz_cs.png new file mode 100644 index 00000000..3fd0b880 Binary files /dev/null and b/docs/vehicles/blueboat/images/blueboat_rviz_cs.png differ diff --git a/docs/vehicles/blueboat/index.md b/docs/vehicles/blueboat/index.md index c6800034..77e3b10c 100644 --- a/docs/vehicles/blueboat/index.md +++ b/docs/vehicles/blueboat/index.md @@ -19,9 +19,4 @@ visual_asset_commission ## The default vehicle -The default configuration is the chassis (pontoons, crossbeams, hatches and the -outboard thruster bodies) with its two M200 weedless propellers, the flag -on the aft crossbeam and the Ping2 echosounder on its integration kit -bracket, face below the waterline. Everything else in the catalog can be -fitted, and the defaults swapped or left off -([Configuration](configuration.md)). +The shipped config fits the chassis (pontoons, crossbeams, hatches and the outboard thruster bodies) and its two M200 weedless propellers, nothing else. With `slot_defaults: true` the slots fill with their own defaults as well: the flag on the aft crossbeam and the Ping2 echosounder on its integration kit bracket, face below the waterline. Everything else in the catalog can be fitted ([Configuration](configuration.md)). diff --git a/docs/vehicles/blueboat/running.md b/docs/vehicles/blueboat/running.md index df99ba83..510998cd 100644 --- a/docs/vehicles/blueboat/running.md +++ b/docs/vehicles/blueboat/running.md @@ -11,6 +11,12 @@ config, into a directory under `$ROS_HOME`), the model is spawned as `[kdl_parser] root link ... inertia` warning from `robot_state_publisher` is expected and harmless. +This image shows the visualization of the base_link coordinate system to verify that the visual model has the correct placement and orientation, that the base_link coordinate system origin is located at the correct place, as specified by the datum specification in the part model (see commissioning) and that the orientation of the coordinate system relative the semantic front-left-up convention of ISO and ROS-REP. +![Blueboat in gz with base frame](./images/blueboat_gz_base_cs.png) + +This image shows the same scenario with botht the base_link and the base_link_visual coordinate systems. You can see that the base_link_visual coordinate system complies with the gltf 2.0 convention. +![Blueboat in gz with base and visual](./images/bleuboat_gz_baseviz_cs.png) + To run a custom vehicle instead, pass its config with `config_file:=`; the [configuration page](configuration.md) lists the slots and [Change the fitted parts](../../how-to/change-parts.md) walks through writing one. @@ -39,6 +45,9 @@ anything else about the simulation: pass any vehicle free world SDF ros2 launch blueboat_gazebo sim.launch.xml world:=/path/my_world.sdf ``` + + + ## In RViz To see the model in RViz: @@ -55,3 +64,6 @@ custom config needs no rebuild. In a simulation the frames carry the instance na and joint states arrive as `/blueboat/joint_states`, so RViz next to `sim.launch.xml` needs its fixed frame, its RobotModel TF prefix and its joint states pointed at the instance. + +Image showing expected RVIZ results with TF frames visualied to show that transforms from gltf 2.0 to RVIZ have resulted in geometry consistent with ISO/ROS-REP coordinate convention. +![BB in rviz](./images/blueboat_rviz_cs.png) \ No newline at end of file diff --git a/docs/vehicles/blueboat/sensors.md b/docs/vehicles/blueboat/sensors.md index 4f388a96..884eaf40 100644 --- a/docs/vehicles/blueboat/sensors.md +++ b/docs/vehicles/blueboat/sensors.md @@ -8,9 +8,9 @@ sensors can be added following the ## Available sensors -| Sensor | Part | Slot | Fitted by default | +| Sensor | Part | Slot | Fitted in the shipped config | |---|---|---|---| -| Ping2 echosounder | `ping_singlebeam` | `ping` (on the integration kit bracket, itself in the chassis `ping_mount` slot) | yes | +| Ping2 echosounder | `ping_singlebeam` | `ping` (on the integration kit bracket, itself in the chassis `ping_mount` slot) | no | ## Sensors ROS API diff --git a/docs/vehicles/blueboat/verify-sitl-boat.md b/docs/vehicles/blueboat/verify-sitl-boat.md index 72ff75bf..8c4d1a36 100644 --- a/docs/vehicles/blueboat/verify-sitl-boat.md +++ b/docs/vehicles/blueboat/verify-sitl-boat.md @@ -62,8 +62,8 @@ rc 3 1700 To see what the autopilot is actually commanding while you move the sticks, echo the same topics: ```bash -gz topic -e -t /blueboat/motor_port/cmd -gz topic -e -t /blueboat/motor_stbd/cmd +gz topic -e -t /blueboat/prop_port/cmd +gz topic -e -t /blueboat/prop_stbd/cmd ```