From 90dda96d6c2763e56d2e49cacd86b2fa02075ec6 Mon Sep 17 00:00:00 2001
From: andermi The Monterey Bay Aquarium Research Institute (MBARI) Wave-Energy Converter is a point-absorber type wave-energy converter that has been operating in Monterey Bay, CA since 2014. This system was developed as part of MBARI's goals of advancing and demonstrating an autonomous and persistent presence of oceanographic instrumentation in the worlds oceans. This project is complemented by developments in autonomous underwater vehicles, underwater vehicle docking, oceanographic instrumentation, autonomy, and science use. The MBARI-WEC is currently maintained by MBARI and operates for six-month periods near the MBARI facility in Moss Landing, California, and averages about 250 Watts of power capture, averaged through the weather cycles and seasons. The MBARI-WEC is a complete system with a four-quadrant electro-hydraulic power-take-off device, board battery storage, control-computers, sensors and instrumentation, and an always-on cell-modem connection to the internet. The architecture of the system is such that critical functions are performed by micro-controllers throughout the system that implement default behaviors and stream sensor data continuously. A Linux computer on the buoy performs data-logging and provides a command interface to the underlying micro-controllers. The system is designed such that the Linux computer is not necessary for safe behavior, if this computer re-boots or goes offline, the system will default to safe behaviors. Additionally, the micro-controllers will ignore damaging commands from the Linux computer. This architecture allows control algorithms running on the Linux computer to be started, stopped, and changed while the device is at sea through the cell-modem connection. This project provides a software interface to the system to allow such algorithms to be efficiently developed, tested, and executed. Using this interface, MBARI intends to make the system available to external researchers. By providing access to the hardware during the ongoing MBARI deployments of this system, the intention is to provide access to hardware that is often otherwise unavailable. To facilitate this, the project has developed a simulator that provides the same interface as the real hardware, allowing projects the ability to develop and test their work independently, before deployment on the real system which will occur under MBARI supervision. The following sections of this documentation outlines the physical system, describes the software- interfaces available, describes the simulation environment, and provides all information needed to interact with this project. The software interface is built upon the ROS 2 framework, and the simulation environment uses Gazebo Simulator. In addition to descriptions of these systems, the documentation provides a number of tutorials intended to lead a new user through the installation of the necessary tools, basic operation of the system, and provide guidance on implement new algorithms to run on the simulator and ultimately the buoy. This is an open-source project with all necessary code and resources freely available. The MBARI Wave-Energy-Converter is a small point absorber design that includes a surface expression, an electro-hydraulic PTO, and a submerged heave-cone device. The system is moored to the seafloor (typically in 80m of water) through a chain-catenary mooring connected to an anchor. As waves excite the system, a differential motion results between the buoy at the surface and the submerged heave cone. Resisting this motion results in energy being absorbed by the system, and this energy is converted to electrical form and stored in a battery bank on the buoy. The rest of this section provides details about the various components of the system The buoy in the MBARI-WEC has a diameter of 2.6m, a water-plane area of 5 m^2, and a mass of 1400kg. This buoy houses the system battery and compute infrastructure, described below. The heave-cone component sits at about 30m depth and provides inertia and drag for the surface-buoy to pull against. The heave-cone has operable doors that can be opened to reduce the drag and inertial of this component in high sea-states. When the doors are open, the heave-cone has added-mass of about 10,000kg, in addition to it's own 600kg mass. When opened, the added-mass reduces to about 3,000kg, which reduces the inertial forcing and increases the natural frequency of the buoy -- heave-cone pair. A chain-catenary mooring and anchor connects to heave-cone to the ocean floor, keeping the buoy on-station. The system loading due to the mooring increases in higher winds and currents, but remains relatively low compared with the inertia forces the heave-cone creates. The power take-off device is located below the buoy and converts the differential motion and forces between the buoy and heave-cone from mechanical to electrical energy. This device is an electro-hydraulic device in which a piston pumps oil through a hydraulic motor, causing an electrical motor/generator to spin and generate electrical energy. In parallel to the hydraulic ram, a pneumatic piston charged with an inert gas provides a sprint returning force for the system. The combination of the hydraulic and pneumatic pistons creates a spring-damper system for which the spring constant is set by the amount of gas in the system, and the damping behavior is adjustable electronically by varying the torque on the hydraulic motor in response to conditions. The electrical-drive is a four-quadrant device in which the electric motor can operate as a generator in which energy flows into the battery, or as a motor in which energy is drawn from the battery. The winding-currents (and resulting torque) can be set arbitrarily, but by default the power take-off device acts as a generator, i.e. a damper resisting motion. The electrical system of this buoy consists of a 325V battery system for energy storage, and a 24V system for powering ancillary instrumentation. The 325V battery is connected directly to the power take-off device motor drive electronics. In normal use, the motor-drive device is generating electrical energy at 325V which charges the battery system. In the case the battery is full (or dis-connected), the motor-drive system directs excess energy to an electrical load-dump device. This submerged heater plays a critical role in maintaining a load on the power take-off device at all times. The electrical system also includes 300V-24V power supplies that provides 24 volts to the compute and instrumentation infrastructure in the system. In the case of low battery voltage due to an extended period of calm seas, the The compute and control architecture of the system is such that critical functions are performed by micro-controllers throughout the system that implement default behaviors and stream sensor data continuously. A Linux computer on the buoy performs data-logging and provides a command interface to the underlying micro-controllers. See figure. The system is designed such that the Linux computer is not necessary for safe behavior, if this computer re-boots or goes offline, the system will default to safe behaviors. Additionally, the micro-controllers will ignore damaging commands from the Linux computer. This architecture allows control algorithms running on the Linux computer to be started, stopped, and changed while the device is at sea through the cell-modem connection. The fundamental system behaviors are performed by a network of micro-controller based compute nodes, that communicate with the buoy Linux computer and with one-another through a Controller Area Network (CAN) bus. There are four of these controllers as follows: Battery Controller (BC_): This micro-controller monitors battery voltage, currents, state-of-charge, cell-balance, and environmental conditions inside the battery enclosure. This controller is largely a data-telemetry gathering item, but also includes an important low-voltage disconnect features which shuts down the 24V battery bus during periods of low-battery state-of-charge. During these periods the system continues to convert wave-energy and charge the batteries, but all sources of significant battery drain are disconnected which allows the battery to re-charge to a serviceable level, even in calm conditions. Spring Controller (SC_): This micro-controller primarily monitors the piston position and a load-cell located between the buoy and power take-off component. Additionally, this controller responds to commands to change the gas pressure in each chamber of the pneumatic spring, a pump to move gas from the lower pressure chamber to the higher pressure chamber, and a valve to do the opposite. Additionally, this controller can turn power on and off to the heave-cone. Power Converter (PC_): This controller implements the field-oriented control of the winding current in the generator. It's core function is to control the winding current to a set target, but it performs a number of other specialized functions as well. In particular, the converter monitors and reports motor RPM, and by default sets the motor winding current target as a pre-defined function of RPM. This default behavior can be modified (scale and offset), or over-ridden entirely. In this later case the power converter only over-rides the default winding-current for a short period of time (2 seconds). If a new winding-current command does not arrive in that time, the system reverts to the built-in default behavior. The power converter also can switch power on and off to the batteries, divert power to an internal load-dump if the batteries are full or disconnected, and monitors the bus voltage and other health functions. The power converter also obeys current draw/charge limits specified depending on the size and capability of the attached battery pack. Heave-cone Controller (TC_): This controller is responsible for opening and closing the heave-cone doors when commanded. For storm-safety, the controller will open the doors one hour after power from the surface is lost, or a watchdog timer expires. Battery back-up of the system enables this, and the buoy system must issue a watchdog reset at least every hour to keep the doors closed. In addition, this controller has an IMU and pressure sensor that provides attitude and depth information about the heave-cone. Note: This is denoted \"TC_\", the T stands for trefoil, which is French for clover and refers to the heave-cone doors themselves. The on-board linux computer is accessible from shore over a radio link (cell-modem, satellite, or line-of-site radio). This link enables enabling data-telemetry, real-time control, and software-updates to be applied to the Linux computer. ** Load Cell: ** This is a load cell between the buoy and the power take-off device, and has a range of up to 20,000lbs. ** Piston Position: ** Inside the pneumatic spring there is a laser range-finder that continuously monitors the position of the power take-off piston. ** Pneumatic Pressures: ** The spring controller monitors the pressures of the two gas-chambers that make up the pneumatic spring. ** Hydraulic Compensator Pressure ** The hydraulic system has a small pressurized accumulator attached to the low-pressure side of the hydraulic circuit. This applies a small pressure (3psi) to ensure seawater does not enter the hydraulic system. An abrupt drop in this pressure indicates a hydraulic leak and is therefore monitored and reported. ** Buoy Inertial Measurement Unit: ** On-board the buoy there is a GPS disciplined six DOF attitude-heading and reference system. This unit monitors and reports the buoys attitude and GPS location. ** Heave Cone Inertial Measurement Unit: ** On-board the heave-cone there is an attitude-heading and reference system with a magnetometer and pressure sensor that reports the heave-cones orientation and depth. ** Electrical System Sensors: ** Voltages and currents are monitored throughout the electrical system and reported by various controllers. In particular the motor winding currents, the load-dump current, and the battery current are all independently monitored and reported. Under Construction If you use this software, please cite it as below: Copyright 2022 Open Source Robotics Foundation, Inc. and Monterey Bay Aquarium Research Institute Licensed under the Apache License, Version 2.0 This project maintains a discussion forum at TBD and we try to respond as quickly as possible to all questions and discussion. This is the best way to reach the developers and maintainers of this project The simulator used in this project utilizes the Gazebo simulator as the base, with a number of custom PlugIns developed by this project to implement specific features needed to simulate this WEC. Gazebo and this projects plugins are open source projects and the source code for these can be accessed at https://github.com/gazebosim/gz-sim and https://github.com/osrf/mbari_wec. Documentation in these respositories is intended for developers but does provide some detailed information about how the simulation works. Under Construction Under Construction A computer simulation environment has created that that emulates the ROS 2 messages used on the buoy system, and connects them to a numerical simulation of the system dynamics and behavior of the electro-hydraulic Power-Take-Off device. The aim of this simulation is to provide a computer based simulation that for most behaviors, interacts with computer code in the same way the physical buoy system does. The aim is that code running on a desktop Linux machine can run unchanged on the physical buoy, and that the response of the simulated buoy is indistinguishable from the real system. The following sections provide some details of this simulator, and additional details are in the theory section. The numerical modeling used in this simulator relies upon the Gazebo simulators ability to solve for the motion of a collection of rigid bodies connected by various types of joints. Gazebo can use several different physics solvers to perform this solution, and the wave-energy buoy simulator in this project uses the DART physics engine. The physics engine solves the multi-body six degree-of-freedom problem for the motion of the connected buoy, power-take-off device, and heave cone, subject to initial conditions and forces that act on the various components as the simulation progresses. In the Gazebo simulator, these forces are provided by plugin code that applies forces to each body based on the state (position and velocity) of each component in the system at each timestep. The Gazebo simulator already includes several plugins that provide relevant forces such as buoyancy and hydrodynamic drag. Additionally, several additional plugins have been created for this simulator that provide the forcings on the system due to the electro-hydraulic power-take-off system, the pneumatic spring system, the tether connecting the PTO to the heave-cone, the mooring system that anchors the system, and the forcing on the buoy from the ocean surface waves. The sections below outline some details about how each of these forcings are modelled and computed. First however, the specific physical characteristics of the MBARI WEC are tabulated for reference. Each rigid body in the simulation has a \"Link Frame\" coordinate system in which all other characteristics of the body are defined in for computational purposes. This link-frame coordinate system is often selected to be at the location of a joint that connects the various bodies (which are also called links in the vernacular of Gazebo). Also, the description below includes alternative notation for added mass, e.g. , in the style of Newman. Under compression and expansion, the pressure, volume and temperature of the Nitrogen in each chamber evolves according to Ideal Gas Law: and a polytropic process: with hysteresis, there are two values for the polytropic index, and , to capture behavior when the gas is compressing or expanding. Using this quasi-static solution and discrete time steps, and also incorporating hysteresis, the process becomes: where is the current time step. Whenever the piston velocity is slow enough, the process is dominated by heat loss and modeled with Newton's Law of Cooling (using forward difference) followed by an update of pressure using Ideal Gas Law: The mass of the Nitrogen in each chamber is determined from inputs in the SDF: and is used for mass flow between chambers in simulating the pump/valve. Linear regression was used to determine the polytropic indices for each chamber using empirical data from the physical system. Using pressure vs volume curves, is determined from increasing volume, and is determined from decreasing volume. The data is then preconditioned by taking the logarithm to linearize and perform regression to find the parameters. For a polytropic process: so, in block matrix notation where and are the arrays of volume and pressure data, respectively. The other parameters in the system are taken from CAD or empirically determined by comparing logged data from prescribed motion between simulation and the physical test bench. Docker images that include the neccessary software and dependencies have been created for convenience. Install Docker using installation instructions. Complete the Linux Postinstall steps to allow you to manage Docker as a non-root user. If you have an NVIDIA graphics card, it can help speed up rendering. Install nvidia-docker. MBARI maintains Docker images for the two most recent releases on their DockerHub: -
Hamilton, A., Anderson, M., Poubel, L., Dutia, D., Carroll, M., Zhang, M., McEwen, R., & Mayans, J. (2023). MBARI Wave Energy Conversion Simulation (Version 1.0.0) [Computer software]. https://github.com/osrf/mbari_wec\n
"},{"location":"license/","title":"License","text":"@software{Hamilton_MBARI_Wave_Energy_2023,\n author = {Hamilton, Andrew and Anderson, Michael and Poubel, Louise and Dutia, Dharini and Carroll, Michael and Zhang, Mabel and McEwen, Rob and Mayans, Joan},\n month = may,\n title = {{MBARI Wave Energy Conversion Simulation}},\n url = {https://github.com/osrf/mbari_wec},\n version = {1.0.0},\n year = {2023}\n}\n
"},{"location":"theory/#power-take-off-device","title":"Power Take-Off Device","text":"Description Units PTO Mass 605 kg PTO Displacement .205 m Center of Gravity in Link Frame (x,y,z) (0.0, 0.0, -4.0) m Center of Buoyancy in Link Frame (x,y,z) (0.0, 0.0, -3.0) m Roll Moment of Inertia about center of mass 3525 kg m Pitch Moment of Inertia about center of mass 3525 kg m Yaw Moment of Inertia about center of mass 10 kg m Surge Added Mass () 310 kg Sway Added Mass () 310 kg Heave Added Mass () 10 kg Roll Added Mass MOI () 2030 kg m Pitch Added Mass MOI () 2030 kg m Surge Quadratic Drag -1140 kg/m Sway Quadratic Drag -1140 kg/m Heave Quadratic Drag -50 kg/m Roll Quadratic Drag -195400 kg m Pitch Quadratic Drag -195400 kg m Yaw Quadratic Drag -50 kg m
"},{"location":"theory/#piston","title":"Piston","text":"Description Units Piston Mass 48.0 kg Center of Gravity in Link Frame (x,y,z) (0.0, 0.0, -2.58) m Roll Moment of Inertia 100.0 kg m Pitch Moment of Inertia 100.0 kg m Yaw Moment of Inertia 5.0 kg m
"},{"location":"theory/#heave-cone","title":"Heave Cone","text":"Description Units Heave Cone Mass 820 kg Heave Cone Displacement .12 m Center of Gravity in Link Frame (x,y,z) (0.0, 0.0, -1.25) m Center of Buoyancy in Link Frame (x,y,z) (0.0, 0.0, -1.21) m Roll Moment of Inertia about the center of mass 340 kg m Pitch Moment of Inertia about the center of mass 340 kg m Yaw Moment of Inertia about the center of mass 610 kg m Surge Added Mass () 720 kg Sway Added Mass () 720 kg Sway-Roll Added Mass MOI () -900 kg m Heave Added Mass: Doors Closed () 9330 kg Heave Added Mass: Doors Open 3000 kg Roll Added Mass MOI () 2870 kg m Pitch Added Mass MOI() 2870 kg m Yaw Added Mass MOI () 10 kg m Surge Quadratic Drag -1580 kg/m Sway Quadratic Drag -1580 kg/m Vertical Quadratic Drag: Doors Open -3200 kg/m Vertical Quadratic Drag: Doors Closed -3900 kg/m Roll Quadratic Drag: -4620 kg m Pitch Quadratic Drag: -4620 kg m Yaw Quadratic Drag: -50 kg m
"},{"location":"theory/#electro-hydraulic-pto-forces","title":"Electro-Hydraulic PTO Forces","text":""},{"location":"theory/#pneumatic-spring-forces","title":"Pneumatic Spring Forces","text":""},{"location":"theory/#definitions","title":"Definitions","text":"Description Units Piston position m Piston velocity m/s Mass of gas in chamber kg Temperature of gas K Gas pressure Pa Chamber volume (dependent on piston position) m Specific Gas Constant N J/kg/K Chamber dead volume (fully compressed) m Specific Heat Capacity N (constant pressure) J/kg/K Surface area of piston head m Polytropic index (Adiabatic if for N) N/A Coefficient of heat transfer (Newton's Law of Cooling) 1/s"},{"location":"theory/#model","title":"Model","text":"
"},{"location":"tutorials/#running-the-simulator","title":"Running the Simulator","text":"
"},{"location":"tutorials/#adding-control-code","title":"Adding Control Code","text":"
"},{"location":"Tutorials/Install/Install_docker/","title":"Install using Docker","text":"
"},{"location":"Tutorials/Install/Install_docker/#use-existing-image-on-dockerhub","title":"Use Existing Image on DockerHub","text":"mbari/mbari_wec:latest
- mbari/mbari_wec:previous
run.bash
script.
Or git clone -b main https://github.com/osrf/mbari_wec.git\ncd ~/mbari_wec/docker/\n
wget https://raw.githubusercontent.com/osrf/mbari_wec/main/docker/run.bash\nchmod +x run.bash\n
If you have an NVIDIA graphics card
./run.bash mbari/mbari_wec:latest\n
Otherwise ./run.bash mbari/mbari_wec:latest --no-nvidia\n
"},{"location":"Tutorials/Install/Install_docker/#build-from-dockerfile","title":"Build from Dockerfile","text":"An alternative to using the images from MBARI's DockerHub would be to build from a Dockerfile. This is convenient if you would like to make any changes.
git clone -b main https://github.com/osrf/mbari_wec.git\ncd ~/mbari_wec/docker/\n
If you have an NVIDIA graphics card
./build.bash nvidia_opengl_ubuntu22\n./build.bash mbari_wec\n
Otherwise ./build.bash mbari_wec --no-nvidia\n
If you have an NVIDIA graphics card
./run.bash mbari_wec\n
Otherwise ./run.bash mbari_wec --no-nvidia\n
./join.bash mbari_wec\n
"},{"location":"Tutorials/Install/Install_docker/#quick-start","title":"Quick start","text":"Quick start scripts are provided in the home directory:
This sources the compiled workspace:
. setup.bash\n
This sources the compiled workspace and launches the simulation:
./run_simulation.bash\n
"},{"location":"Tutorials/Install/Install_docker/#run-an-example-to-test","title":"Run an example to test","text":"In a new terminal (whether on host machine or in Docker container), source the workspace
. ~/mbari_wec_ws/install/setup.bash\n
Launch the simulation
ros2 launch buoy_gazebo mbari_wec.launch.py\n
The simulation software should now be available. To run and test, proceed to the Run the Simulator tutorial series.
"},{"location":"Tutorials/Install/Install_source/","title":"Install from source","text":""},{"location":"Tutorials/Install/Install_source/#requirements","title":"Requirements","text":"Use Ubuntu 22.04.
Install ROS 2 Humble MBARI WEC is tested against the cyclonedds rmw implementation, so set that up as follows:
sudo apt install -y ros-humble-rmw-cyclonedds-cpp\nexport RMW_IMPLEMENTATION=rmw_cyclonedds_cpp\n
Install Gazebo Garden
Install necessary tools
sudo apt install python3-vcstool python3-colcon-common-extensions python3-pip git wget\n
Install necessary libraries
curl -s --compressed \"https://hamilton8415.github.io/ppa/KEY.gpg\" | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/ppa.gpg >/dev/null\nsudo curl -s --compressed -o /etc/apt/sources.list.d/my_list_file.list \"https://hamilton8415.github.io/ppa/my_list_file.list\"\nsudo apt update\nsudo apt install libfshydrodynamics=1.3.1\n
Create a workspace, for example:
mkdir -p ~/mbari_wec_ws/src\ncd ~/mbari_wec_ws/src\n
Clone all source repos with the help of vcstool
:
wget https://raw.githubusercontent.com/osrf/mbari_wec/main/mbari_wec_all.yaml\nvcs import < mbari_wec_all.yaml\ncd ~/mbari_wec_ws\n
Set the Gazebo version to Garden. This is needed because we're not using an official ROS + Gazebo combination (place this in ~/.bashrc for convenience if rebuilding often):
export GZ_VERSION=garden\n
Install ROS dependencies
sudo pip3 install -U rosdep\nsudo rosdep init\nrosdep update\nrosdep install --from-paths src --ignore-src -r -y -i\n
Build and install
source /opt/ros/humble/setup.bash\ncd ~/mbari_wec_ws\ncolcon build\n
The simulation software should build without errors. To run and test, proceed to the Run the Simulator tutorial series. Or run a quick test as described below to confirm all has worked as expected.
"},{"location":"Tutorials/Install/Install_source/#run-an-example-to-test","title":"Run an example to test","text":"In a new terminal, source the workspace
. ~/mbari_wec_ws/install/setup.bash\n
Set SDF_PATH
to allow robot_state_publisher
parse the robot description from the sdformat model (place this in ~/.bashrc for convenience if rebuilding often):
export SDF_PATH=$GZ_SIM_RESOURCE_PATH\n
Launch the simulation
ros2 launch buoy_gazebo mbari_wec.launch.py\n
In this tutorial you will implement a simple linear damper controller for the piston in the WEC Power-Take-Off (PTO). Given motor RPM, it outputs desired motor winding current (interpolated from RPM->Torque lookup table) to generate a torque to resist piston velocity with a damping force. Configurable gains (scale/retract factor) are applied before output. In the end, you will have a working linear damper controller that is very close to the controller running on both the physical and simulated buoy.
"},{"location":"Tutorials/ROS2/CppLinearDamperExample/#prerequisite","title":"Prerequisite","text":"This tutorial assumes you are familiar the steps from the previous tutorial and have built your own custom C++ ROS 2 controller package from the mbari_wec_template_cpp template repository which we will use to implement a simple linear damper controller.
To begin, you should have a C++ ROS 2 controller package that looks similar to:
mbari_wec_linear_damper_cpp\n \u251c\u2500\u2500 CMakeLists.txt\n \u251c\u2500\u2500 config\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 controller.yaml\n \u251c\u2500\u2500 include\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 mbari_wec_linear_damper_cpp\n \u2502\u00a0\u00a0 \u251c\u2500\u2500 controller.hpp\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 control_policy.hpp\n \u251c\u2500\u2500 launch\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 controller.launch.py\n \u251c\u2500\u2500 LICENSE\n \u251c\u2500\u2500 package.xml\n \u251c\u2500\u2500 README.md\n \u2514\u2500\u2500 src\n \u2514\u2500\u2500 controller.cpp\n
with the files modified from the previous tutorial. If you haven't already, follow the steps in the above mentioned link to create a package for this tutorial named mbari_wec_linear_damper_cpp
.
A complete example starting from the template may be found here. Line numbers in this tutorial correspond to the lines in relevant files in the full example.
"},{"location":"Tutorials/ROS2/CppLinearDamperExample/#parameters","title":"Parameters","text":"Parameters for the controller are:
torque_constant
: Motor Torque Constant (N-m/Amp) Constant to convert desired torque to applied motor winding current n_spec
: Input Motor Speed (RPM) Breakpoints (RPM) is the input to the controller and n_spec
are the x-components of the breakpoints for the interpolant, torque_spec
: Desired Output Motor Torque (N-m) Breakpoints Torque (N-m) is the eventual desired output of the controller given an input N
(motor RPM) and torque_spec
/ torque_constant
(Amps) are the y-components of the breakpoints for the interpolant. The controller actually outputs motor winding current (Amps) to generate a torque in the opposite direction of piston velocity to generate a damping force.These can be configured using the config/controller.yaml
file.
/linear_damper:\n ros__parameters:\n torque_constant: 0.438\n n_spec: [0.0, 300.0, 600.0, 1000.0, 1700.0, 4400.0, 6790.0]\n torque_spec: [0.0, 0.0, 0.8, 2.9, 5.6, 9.8, 16.6]\n
As you can see, as motor speed increases, so does the damping torque. For low RPM (up to 300), there is no damping.
Initialize these variables and create the interpolator, winding_current
, in ControlPolicy
in include/mbari_wec_linear_damper_cpp/control_policy.hpp
. This example makes use of <simple_interp/interp1d.hpp>
from mbari_wec_utils
, so don't forget to include that as well as <algorithm>
and <vector>
.
#include <algorithm>\n#include <vector>\n\n#include <mbari_wec_linear_damper_cpp/controller.hpp>\n\n// interp1d for rpm->winding current\n#include <simple_interp/interp1d.hpp>\n\n\n/* Simple Linear Damper Control Policy.\n Implements a simple linear damper controller for the piston in the WEC\n Power-Take-Off (PTO). Given motor RPM, outputs desired motor winding current (interpolated\n from RPM->Torque lookup table) to resist piston velocity. Configurable gains\n (scale/retract factor) are applied before output.\n*/\nstruct ControlPolicy\n{\n // declare/init any parameter variables here\n double Torque_constant; // N-m/Amps\n std::vector<double> N_Spec; // RPM\n std::vector<double> Torque_Spec; // N-m\n std::vector<double> I_Spec; // Amps\n\n // interpolator for rpm -> winding current\n simple_interp::Interp1d winding_current;\n\n ControlPolicy()\n : Torque_constant(0.438F),\n N_Spec{0.0F, 300.0F, 600.0F, 1000.0F, 1700.0F, 4400.0F, 6790.0F},\n Torque_Spec{0.0F, 0.0F, 0.8F, 2.9F, 5.6F, 9.8F, 16.6F},\n I_Spec(Torque_Spec.size(), 0.0F),\n winding_current(N_Spec, I_Spec)\n {\n update_params();\n }\n
Update the dependent variable, I_Spec
, as well as the interpolator, winding_current
.
// Update dependent variables after reading in params\n void update_params()\n {\n std::transform(\n Torque_Spec.cbegin(), Torque_Spec.cend(),\n I_Spec.begin(),\n [tc = Torque_constant](const double & ts) {return ts / tc;});\n\n winding_current.update(N_Spec, I_Spec);\n }\n
Finally, define the set_params
function of the Controller
class and declare/get/set/update these parameters from ROS 2 (as set in config/controller.yaml
).
// Use ROS2 declare_parameter and get_parameter to set policy params\nvoid Controller::set_params()\n{\n this->declare_parameter(\"torque_constant\", policy_->Torque_constant);\n policy_->Torque_constant = this->get_parameter(\"torque_constant\").as_double();\n\n this->declare_parameter(\n \"n_spec\", std::vector<double>(\n policy_->N_Spec.begin(),\n policy_->N_Spec.end()));\n std::vector<double> temp_double_arr = this->get_parameter(\"n_spec\").as_double_array();\n policy_->N_Spec.assign(temp_double_arr.begin(), temp_double_arr.end());\n\n this->declare_parameter(\n \"torque_spec\", std::vector<double>(\n policy_->Torque_Spec.begin(),\n policy_->Torque_Spec.end()));\n temp_double_arr = this->get_parameter(\"torque_spec\").as_double_array();\n policy_->Torque_Spec.assign(temp_double_arr.begin(), temp_double_arr.end());\n\n // recompute any dependent variables\n policy_->update_params();\n RCLCPP_INFO_STREAM(rclcpp::get_logger(this->get_name()), *policy_);\n}\n
Add a helper function, for the ControlPolicy
class for this example to report the parameters used.
// Helper function to print policy parameters\nstd::ostream & operator<<(std::ostream & os, const ControlPolicy & policy)\n{\n os << \"ControlPolicy:\" << std::endl;\n\n os << \"\\tTorque_constant: \" << policy.Torque_constant << std::endl;\n\n os << \"\\tN_Spec: \" << std::flush;\n std::copy(policy.N_Spec.cbegin(), policy.N_Spec.cend(), std::ostream_iterator<double>(os, \",\"));\n os << \"\\b \\b\" << std::endl;\n\n os << \"\\tTorque_Spec: \" << std::flush;\n std::copy(\n policy.Torque_Spec.cbegin(),\n policy.Torque_Spec.cend(),\n std::ostream_iterator<double>(os, \",\"));\n os << \"\\b \\b\" << std::endl;\n\n os << \"\\tI_Spec: \" << std::flush;\n std::copy(policy.I_Spec.cbegin(), policy.I_Spec.cend(), std::ostream_iterator<double>(os, \",\"));\n os << \"\\b \\b\" << std::endl;\n\n return os;\n}\n
"},{"location":"Tutorials/ROS2/CppLinearDamperExample/#control-policy-target","title":"Control Policy Target","text":"To implement the torque control control policy, we use the target
function in ControlPolicy
. This is where we accept feedback data and return a command value. In this case, we need the motor rpm
, and the gains applied to the winding current damping, scale_factor
and retract_factor
. Typical values for these gains are
// Calculate target value from feedback inputs\n double target(\n const double & rpm,\n const double & scale_factor,\n const double & retract_factor)\n {\n double N = fabs(rpm);\n double I = winding_current.eval(N);\n\n // apply damping gain\n I *= scale_factor;\n\n // Hysteresis due to gravity / wave assist\n if (rpm > 0.0F) {\n I *= -retract_factor;\n }\n\n return I;\n }\n
So, as you can see we apply a positive damping torque when RPM is negative (piston extending), and a positive damping torque when RPM is positive (piston retracting). The damping torque required is reduced when retracting.
"},{"location":"Tutorials/ROS2/CppLinearDamperExample/#controller","title":"Controller","text":"All that is left is to connect the necessary feedback data to the ControlPolicy
. In this case, rpm
, scale
, and retract
are present in buoy_interfaces.msg.PCRecord
on the /power_data
topic published by the Power Controller running on the buoy.
To access the data, all that is required is to define the callback void Controller::power_callback(const buoy_interfaces::msg::PCRecord & data)
in the Controller
class, and pass the data to this->policy_->target
to get the desired winding current command. Various commands are available, and this time we will be using this->send_pc_wind_curr_command(wind_curr_amps);
// Callback for '/power_data' topic from Power Controller\nvoid Controller::power_callback(const buoy_interfaces::msg::PCRecord & data)\n{\n // Update class variables, get control policy target, send commands, etc.\n // get target value from control policy\n double wind_curr = this->policy_->target(data.rpm, data.scale, data.retract);\n\n RCLCPP_INFO_STREAM(\n rclcpp::get_logger(\n this->get_name()),\n \"WindingCurrent: f(\" << data.rpm << \", \" << data.scale << \", \" << data.retract << \") = \" <<\n wind_curr);\n\n auto future = this->send_pc_wind_curr_command(wind_curr);\n}\n
Finally, let's set the Power Controller's publish rate to the maximum of 50Hz. Uncomment the line to set the PC Pack Rate in Controller
constructor:
Controller::Controller(const std::string & node_name)\n: buoy_api::Interface<Controller>(node_name),\n policy_(std::make_unique<ControlPolicy>())\n{\n this->set_params();\n\n // set packet rates from controllers here\n // controller defaults to publishing @ 10Hz\n // call these to set rate to 50Hz or provide argument for specific rate\n // this->set_sc_pack_rate(); // set SC publish rate to 50Hz\n this->set_pc_pack_rate(); // set PC publish rate to 50Hz\n}\n
In this tutorial, we've named this controller linear_damper
. Don't forget to update controller names along with other changes according to the previous tutorial.
Make sure to build and source your workspace. This tutorial assumes you cloned your package to ~/controller_ws/src
and you have sourced mbari_wec_gz
and mbari_wec_utils
$ cd ~/controller_ws\n$ colcon build\n$ source install/local_setup.bash\n
We will be using ros2 launch
and launch/controller.launch.py
to run our new controller.
To run the controller along with the simulation, launch your controller: $ ros2 launch mbari_wec_linear_damper_cpp controller.launch.py
Then, in another terminal (with mbari_wec_gz
sourced), launch the sim: $ ros2 launch buoy_gazebo mbari_wec.launch.py
and click the play button.
You should see output similar to:
[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.623306255] [mbari_wec_linear_damper_cpp]: Found all required services.\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.623437968] [mbari_wec_linear_damper_cpp]: ControlPolicy:\n[mbari_wec_linear_damper_cpp-1] Torque_constant: 0.438\n[mbari_wec_linear_damper_cpp-1] N_Spec: 0,300,600,1000,1700,4400,6790\n[mbari_wec_linear_damper_cpp-1] Torque_Spec: 0,0,0.8,2.9,5.6,9.8,16.6\n[mbari_wec_linear_damper_cpp-1] I_Spec: 0,0,1.82648,6.621,12.7854,22.3744,37.8995\n[mbari_wec_linear_damper_cpp-1]\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.623585767] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2911.62, 1, 0.6) = -10.2531\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.623769091] [mbari_wec_linear_damper_cpp]: Successfully set publish_rate for power_controller\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.723139301] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2881.34, 1, 0.6) = -10.1886\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.723199020] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2881.34, 1, 0.6) = -10.1886\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.743295542] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2864.91, 1, 0.6) = -10.1535\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.763406662] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2845.41, 1, 0.6) = -10.112\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.783518884] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2822.43, 1, 0.6) = -10.063\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.803625212] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2796.29, 1, 0.6) = -10.0073\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.823736947] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2767.06, 1, 0.6) = -9.94502\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.843817290] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2734.81, 1, 0.6) = -9.87631\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.863931284] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2699.67, 1, 0.6) = -9.80143\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.884041064] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2661.74, 1, 0.6) = -9.7206\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.904159386] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2621.09, 1, 0.6) = -9.63398\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.924232170] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2577.85, 1, 0.6) = -9.54184\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.944361837] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2532.15, 1, 0.6) = -9.44445\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.964467851] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2484.08, 1, 0.6) = -9.34203\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.984588134] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2433.76, 1, 0.6) = -9.23479\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215529.004697490] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2381.3, 1, 0.6) = -9.12301\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215529.024807195] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2326.81, 1, 0.6) = -9.00691\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215529.044928940] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2270.4, 1, 0.6) = -8.88669\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215529.065039660] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2212.17, 1, 0.6) = -8.76262\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215529.085145551] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2152.24, 1, 0.6) = -8.63491\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215529.105256007] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2090.71, 1, 0.6) = -8.50379\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215529.125363653] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2027.67, 1, 0.6) = -8.36947\n
"},{"location":"Tutorials/ROS2/CppOpenLoopControl/","title":"Open-Loop Force Command Example (C++)","text":""},{"location":"Tutorials/ROS2/CppTemplate/","title":"Quick Start \u2014 Writing External Controller With GitHub Template Repository","text":"In this tutorial, you will make and customize a GitHub repository from a GitHub Template with a ROS 2 C++ package and code ready to implement your own external controller utilizing the buoy_api_cpp
interface. This interface may be used with the both the simulated and physical buoy.
There are two GitHub template repositories set up (C++/Python) for a quick start on writing a custom controller utilizing buoy_api_cpp and buoy_api_py. Please see C++ examples and Python examples for example controller implementations.
You may also refer to GitHub's template documentation
To start using the C++ GitHub template
Navigate to mbari_wec_template_cpp and click the green button with the text Use this template
and select Create a new repository
Next, set up the repository like you would any new GitHub repository choosing the owner, repository name, public/private, etc.
Make a ROS 2 workspace
$ mkdir -p ~/controller_ws/src\n$ cd ~/controller_ws/src\n
Now that your new repository is set up, clone it to your local machine, make a branch, etc.
$ git clone https://github.com/<owner>/<repo_name>.git\n$ cd ~/controller_ws\n
You should now have a C++ ROS 2 package with the following structure in your workspace src
:
<repo_name>\n \u251c\u2500\u2500 CMakeLists.txt\n \u251c\u2500\u2500 config\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 controller.yaml\n \u251c\u2500\u2500 include\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 mbari_wec_template_cpp\n \u2502\u00a0\u00a0 \u251c\u2500\u2500 controller.hpp\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 control_policy.hpp\n \u251c\u2500\u2500 launch\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 controller.launch.py\n \u251c\u2500\u2500 LICENSE\n \u251c\u2500\u2500 package.xml\n \u251c\u2500\u2500 README.md\n \u2514\u2500\u2500 src\n \u2514\u2500\u2500 controller.cpp\n
"},{"location":"Tutorials/ROS2/CppTemplate/#customizing-the-controller","title":"Customizing the controller","text":"You may also refer to the README.md
in your newly cloned repository.
Replace mbari_wec_template_cpp
with your package name and modify other fields as necessary in:
package.xml
(lines 4-8)<?xml version=\"1.0\"?>\n<?xml-model href=\"http://download.ros.org/schema/package_format3.xsd\" schematypens=\"http://www.w3.org/2001/XMLSchema\"?>\n<package format=\"3\">\n <name>repo_name</name> <!-- Update package name -->\n <version>3.14</version> <!-- Update version -->\n <description>Your Controller Description</description> <!-- Update description -->\n <maintainer email=\"your@email\">Your Name</maintainer> <!-- Update email and name -->\n <license>Your License</license> <!-- Update license -->\n
CMakeLists.txt
(line 2)cmake_minimum_required(VERSION 3.8)\nproject(mbari_wec_template_cpp) # Update ${PROJECT_NAME}\n\nif(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES \"Clang\")\n add_compile_options(-Wall -Wextra -Wpedantic)\nendif()\n\n# find dependencies\nfind_package(ament_cmake REQUIRED)\nfind_package(rclcpp REQUIRED)\nfind_package(buoy_interfaces REQUIRED)\nfind_package(buoy_api_cpp REQUIRED COMPONENTS buoy_api)\n\nadd_executable(${PROJECT_NAME} src/controller.cpp)\ntarget_link_libraries(${PROJECT_NAME} PUBLIC buoy_api_cpp::buoy_api)\nament_target_dependencies(${PROJECT_NAME} PUBLIC rclcpp buoy_interfaces)\ntarget_include_directories(${PROJECT_NAME} PUBLIC\n $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>\n $<INSTALL_INTERFACE:include>)\ntarget_compile_features(${PROJECT_NAME} PUBLIC c_std_99 cxx_std_17) # Require C99 and C++17\n
launch/controller.launch.py
(line 22) Update package_name
and node name with your controller name (same as the name in config/controller.yaml
)package_name = 'your_package_name' # Update package name (same as in CMakeLists.txt)\n\ndef generate_launch_description():\n ld = LaunchDescription()\n config = os.path.join(\n get_package_share_directory(package_name),\n 'config',\n 'controller.yaml'\n )\n\n node = Node(\n package=package_name,\n name='your_controller_name', # ensure same as name in config.yaml\n executable=package_name,\n
config/controller.yaml
(line 1) Update first line with your controller name (same as node name in launch file)/your_controller_name:\n ros__parameters:\n foo: 1.0\n
and rename the folder:
include/mbari_wec_template_cpp
(containing controller.hpp
and control_policy.hpp
) to your package nameresulting in the following folder structure:
<your_package_name>\n \u251c\u2500\u2500 CMakeLists.txt\n \u251c\u2500\u2500 config\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 controller.yaml\n \u251c\u2500\u2500 include\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 <your_package_name>\n \u2502\u00a0\u00a0 \u251c\u2500\u2500 controller.hpp\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 control_policy.hpp\n \u251c\u2500\u2500 launch\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 controller.launch.py\n \u251c\u2500\u2500 LICENSE\n \u251c\u2500\u2500 package.xml\n \u251c\u2500\u2500 README.md\n \u2514\u2500\u2500 src\n \u2514\u2500\u2500 controller.cpp\n
Update the include paths in:
controller.cpp
(lines 18-19) src/controller.cpp
#include <mbari_wec_template_cpp/control_policy.hpp> // update include path\n#include <mbari_wec_template_cpp/controller.hpp> // update include path\n
control_policy.hpp
(line 22) include/your_package_name/control_policy.hpp
#include <mbari_wec_template_cpp/controller.hpp> // update include path\n
Also, update include guards:
control_policy.hpp
include/your_package_name/control_policy.hpp
#ifndef YOUR_PACKAGE_NAME__CONTROL_POLICY_HPP_\n#define YOUR_PACKAGE_NAME__CONTROL_POLICY_HPP_\n
...
\u200b#endif // YOUR_PACKAGE_NAME__CONTROL_POLICY_HPP_\n
controller.hpp
include/your_package_name/controller.hpp
#ifndef YOUR_PACKAGE_NAME__CONTROLLER_HPP_\n#define YOUR_PACKAGE_NAME__CONTROLLER_HPP_\n
...
\u200b#endif // YOUR_PACKAGE_NAME__CONTROLLER_HPP_\n
Modify CMakeLists.txt
as desired and add any dependencies in package.xml
following standard ROS 2 documentation.
Assuming you have followed the above,
include/<your_package_name>/control_policy.hpp
src/controller.cpp
are stubbed out to implement your custom external controller. You may also use config/controller.yaml
for any policy parameters.
You may use the struct ControlPolicy
in control_policy.hpp
to implement your controller.
struct ControlPolicy\n{\n // declare/init any parameter variables here\n double foo{1.0};\n double bar{10.0*foo};\n\n ControlPolicy()\n : foo{1.0},\n bar{10.0*foo}\n {\n update_params();\n }\n\n // Update dependent variables after reading in params\n void update_params()\n {\n bar = 10.0*foo;\n }\n\n // Modify function inputs as desired\n // Calculate target value from feedback inputs\n double target(\n const double & /*some*/,\n const double & /*feedback*/,\n const double & /*values*/)\n {\n\n // secret sauce\n\n return 0.0; // obviously, modify to return proper target value\n }\n};\n
// declare/init any parameter variables here\n double foo{1.0};\n double bar{10.0*foo};\n\n ControlPolicy()\n : foo{1.0},\n bar{10.0*foo}\n
update_params
on line 39 // Update dependent variables after reading in params\n void update_params()\n {\n bar = 10.0*foo;\n }\n
set_params
function of the Controller
class on line 58// Use ROS2 declare_parameter and get_parameter to set policy params\nvoid Controller::set_params()\n{\n this->declare_parameter(\"foo\", policy_->foo);\n policy_->foo = this->get_parameter(\"foo\").as_double();\n\n // recompute any dependent variables\n policy_->update_params();\n}\n
target
function on line 46. Modify the input args as well as the return value as necessary // Modify function inputs as desired\n // Calculate target value from feedback inputs\n double target(\n const double & /*some*/,\n const double & /*feedback*/,\n const double & /*values*/)\n {\n\n // secret sauce\n\n return 0.0; // obviously, modify to return proper target value\n }\n
"},{"location":"Tutorials/ROS2/CppTemplate/#controller","title":"Controller","text":"The Controller
class contains an instance of ControlPolicy
as the member variable, this->policy
. The this->policy->target
function may be called anywhere within the Controller
class. You may call it inside any of the data callbacks to enable feedback control (for example):
// To subscribe to any topic, simply declare & define the specific callback, e.g. power_callback\n\n // Callback for '/power_data' topic from Power Controller\n void power_callback(const buoy_interfaces::msg::PCRecord & data)\n {\n // get target value from control policy\n double wind_curr = policy_->target(data.rpm, data.scale, data.retract);\n\n auto future = this->send_pc_wind_curr_command(wind_curr);\n }\n
Or, set up a loop in main
and run open-loop:
int main(int argc, char ** argv)\n{\n rclcpp::init(argc, argv);\n\n auto controller = std::make_shared<Controller>(\"controller\");\n rclcpp::Rate rate(50.0);\n while (rclcpp::ok()) {\n rclcpp::spin_once(controller);\n rate.sleep();\n }\n rclcpp::shutdown();\n\n return 0;\n}\n
You may get feedback data from any of the buoy topics by simply creating a specific callback listed below. For feedback data you'd like to use in another area of the class, feel free to assign them to class variables.
(Delete any callbacks you don't need in the Controller
class)
Available callback functions:
/ahrs_data
\u2192 void ahrs_callback(const buoy_interfaces::msg::XBRecord & data){}
/battery_data
\u2192 void battery_callback(const buoy_interfaces::msg::BCRecord & data){}
/spring_data
\u2192 void spring_callback(const buoy_interfaces::msg::SCRecord & data){}
/power_data
\u2192 void power_callback(const buoy_interfaces::msg::PCRecord & data){}
/trefoil_data
\u2192 void trefoil_callback(const buoy_interfaces::msg::TFRecord & data){}
/powerbuoy_data
\u2192 void powerbuoy_callback(const buoy_interfaces::msg::PBRecord & data){}
You may also send commands from within the Controller
class:
this->send_pump_command(duration_mins);
this->send_valve_command(duration_sec);
this->send_pc_wind_curr_command(wind_curr_amps);
this->send_pc_bias_curr_command(bias_curr_amps);
this->send_pc_scale_command(scale_factor);
this->send_pc_retract_command(retract_factor);
In the Controller
constructor, you may also uncomment lines 31 or 32 to set the publish rates for the Spring or Power Controllers on the buoy. These controllers default to publishing feedback at 10Hz to conserve data/bandwidth (on the physical buoy). For feedback control, you have the option to increase the publish rate. You can call commands to set the rates anywhere from 10Hz to 50Hz (default argument is 50Hz).
Controller::Controller(const std::string & node_name)\n: buoy_api::Interface<Controller>(node_name),\n policy_(std::make_unique<ControlPolicy>())\n{\n this->set_params();\n\n // set packet rates from controllers here\n // controller defaults to publishing @ 10Hz\n // call these to set rate to 50Hz or provide argument for specific rate\n // this->set_sc_pack_rate(); // set SC publish rate to 50Hz\n // this->set_pc_pack_rate(); // set PC publish rate to 50Hz\n
The Controller
is also capable of synchronizing its clock from the sim /clock
by uncommenting line 36. Since the Controller
inherits from rclcpp::Node
, you may use this->get_clock()
and other various time-related functions of rclcpp::Node
.
// Use this to set node clock to use sim time from /clock (from gazebo sim time)\n // Access node clock via this->get_clock() or other various time-related functions of rclcpp::Node\n // this->use_sim_time();\n
"},{"location":"Tutorials/ROS2/CppTemplate/#build-test-run","title":"Build, Test, Run","text":"At this point, your new package should build, pass tests, and run against the sim (will connect but do nothing).
It is assumed that you have already installed or built the buoy packages.
From your workspace (e.g. ~/controller_ws
) build your package:
$ colcon build\nStarting >>> mbari_wec_template_cpp\nFinished <<< mbari_wec_template_cpp [25.0s]\n\nSummary: 1 package finished [25.2s]\n
You may also build only your new controller package (if you have other packages in the workspace) using: $ colcon build --packages-up-to <your_package_name>
Then, source and test:
$ source install/local_setup.bash\n$ colcon test\nStarting >>> mbari_wec_template_cpp\nFinished <<< mbari_wec_template_cpp [1.38s]\n\nSummary: 1 package finished [1.54s]\n
Or, you may test only your new controller package using: $ colcon test --packages-select <your_package_name>
Next, in another terminal run the sim (after sourcing the sim packages of course): $ ros2 launch buoy_gazebo mbari_wec.launch.py
Now, in the previous terminal, launch the empty controller:
$ ros2 launch <your_package_name> controller.launch.py\n
And you should see something similar to:
[INFO] [launch]: Default logging verbosity is set to INFO\n[INFO] [mbari_wec_template_cpp-1]: process started with pid [1297902]\n[mbari_wec_template_cpp-1] [INFO] [1678127525.594948064] [mbari_wec_template_cpp]: Subscribing to XBRecord on '/ahrs_data' and '/xb_record'\n[mbari_wec_template_cpp-1] [INFO] [1678127525.595508167] [mbari_wec_template_cpp]: Subscribing to BCRecord on '/battery_data' and '/bc_record'\n[mbari_wec_template_cpp-1] [INFO] [1678127525.595795098] [mbari_wec_template_cpp]: Subscribing to SCRecord on '/spring_data' and '/sc_record'\n[mbari_wec_template_cpp-1] [INFO] [1678127525.596027219] [mbari_wec_template_cpp]: Subscribing to PCRecord on '/power_data' and '/pc_record'\n[mbari_wec_template_cpp-1] [INFO] [1678127525.596275007] [mbari_wec_template_cpp]: Subscribing to TFRecord on '/trefoil_data' and '/tf_record'\n[mbari_wec_template_cpp-1] [INFO] [1678127525.596593805] [mbari_wec_template_cpp]: Subscribing to PBRecord on '/powerbuoy_data'\n[mbari_wec_template_cpp-1] [INFO] [1678127525.697067297] [mbari_wec_template_cpp]: /pc_pack_rate_command not available\n[mbari_wec_template_cpp-1] [INFO] [1678127525.797309937] [mbari_wec_template_cpp]: /sc_pack_rate_command not available\n[mbari_wec_template_cpp-1] [INFO] [1678127525.797524439] [mbari_wec_template_cpp]: Found all required services.\n
"},{"location":"Tutorials/ROS2/CppTemplate/#example","title":"Example","text":"An example using this interface will follow in the next tutorial: Linear Damper Example (C++)
"},{"location":"Tutorials/ROS2/MessagesAndServices/","title":"ROS 2 Messages and Services","text":""},{"location":"Tutorials/ROS2/PythonLinearDamperExample/","title":"Quick Start -- Simple Linear Damper Controller (Python)","text":"In this tutorial you will implement a simple linear damper controller for the piston in the WEC Power-Take-Off (PTO). Given motor RPM, it outputs desired motor winding current (interpolated from RPM->Torque lookup table) to generate a torque to resist piston velocity with a damping force. Configurable gains (scale/retract factor) are applied before output. In the end, you will have a working linear damper controller that is very close to the controller running on both the physical and simulated buoy.
"},{"location":"Tutorials/ROS2/PythonLinearDamperExample/#prerequisite","title":"Prerequisite","text":"This tutorial assumes you are familiar the steps from the previous tutorial and have built your own custom Python ROS 2 controller package from the mbari_wec_template_py template repository which we will use to implement a simple linear damper controller.
To begin, you should have a Python ROS 2 controller package that looks similar to:
mbari_wec_linear_damper_py\n\u251c\u2500\u2500 config\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 controller.yaml\n\u251c\u2500\u2500 CONTRIBUTING.md\n\u251c\u2500\u2500 launch\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 controller.launch.py\n\u251c\u2500\u2500 LICENSE\n\u251c\u2500\u2500 mbari_wec_linear_damper_py\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 controller.py\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 __init__.py\n\u251c\u2500\u2500 package.xml\n\u251c\u2500\u2500 README.md\n\u251c\u2500\u2500 resource\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 mbari_wec_linear_damper_py\n\u251c\u2500\u2500 setup.cfg\n\u251c\u2500\u2500 setup.py\n\u2514\u2500\u2500 test\n \u251c\u2500\u2500 test_copyright.py\n \u251c\u2500\u2500 test_flake8.py\n \u2514\u2500\u2500 test_pep257.py\n
with the files modified from the previous tutorial. If you haven't already, follow the steps in the above mentioned link to create a package for this tutorial named mbari_wec_linear_damper_py
.
A complete example starting from the template may be found here. Line numbers in this tutorial correspond to the lines in relevant files in the full example.
"},{"location":"Tutorials/ROS2/PythonLinearDamperExample/#parameters","title":"Parameters","text":"Parameters for the controller are:
torque_constant
: Motor Torque Constant (N-m/Amp) Constant to convert desired torque to applied motor winding current n_spec
: Input Motor Speed (RPM) Breakpoints (RPM) is the input to the controller and n_spec
are the x-components of the breakpoints for the interpolant, torque_spec
: Desired Output Motor Torque (N-m) Breakpoints Torque (N-m) is the eventual desired output of the controller given an input N
(motor RPM) and torque_spec
/ torque_constant
(Amps) are the y-components of the breakpoints for the interpolant. The controller actually outputs motor winding current (Amps) to generate a torque in the opposite direction of piston velocity to generate a damping force.These can be configured using the config/controller.yaml
file.
/linear_damper:\n ros__parameters:\n torque_constant: 0.438\n n_spec: [0.0, 300.0, 600.0, 1000.0, 1700.0, 4400.0, 6790.0]\n torque_spec: [0.0, 0.0, 0.8, 2.9, 5.6, 9.8, 16.6]\n
As you can see, as motor speed increases, so does the damping torque. For low RPM (up to 300), there is no damping.
Initialize these variables in ControlPolicy
in mbari_wec_linear_damper_py/controller.py
. This example makes use of numpy.array
as well as scipy.interpolate.interp1d
, so don't forget to include those.
import numpy as np\nfrom scipy import interpolate\n\n\nclass ControlPolicy(object):\n \"\"\"\n Simple Linear Damper Control Policy.\n Implements a simple linear damper controller for the piston in the WEC\n Power-Take-Off (PTO). Given motor RPM, outputs desired motor winding current (interpolated\n from RPM->Torque lookup table) to resist piston velocity. Configurable gains\n (scale/retract factor) are applied before output.\n \"\"\"\n\n def __init__(self):\n # Define any parameter variables here\n self.Torque_constant = 0.438 # N-m/Amps\n # Desired damping Torque vs RPM relationship\n self.N_Spec = np.array([0.0, 300.0, 600.0, 1000.0, 1700.0, 4400.0, 6790.0]) # RPM\n self.Torque_Spec = np.array([0.0, 0.0, 0.8, 2.9, 5.6, 9.8, 16.6]) # N-m\n
Update the dependent variable, I_Spec
, and create the interpolator, windcurr_interp1d
, which uses interp1d
from scipy.interpolate
.
def update_params(self):\n \"\"\"Update dependent variables after reading in params.\"\"\"\n # Convert to Motor Winding Current vs RPM and generate interpolator for f(RPM) = I\n self.I_Spec = self.Torque_Spec / self.Torque_constant # Amps\n self.windcurr_interp1d = interpolate.interp1d(self.N_Spec, self.I_Spec,\n fill_value=self.I_Spec[-1],\n bounds_error=False)\n
Finally, in the Controller
class, declare/get/set/update these parameters from ROS 2 (as set in config/controller.yaml
).
def set_params(self):\n \"\"\"Use ROS2 declare_parameter and get_parameter to set policy params.\"\"\"\n self.declare_parameter('torque_constant', self.policy.Torque_constant)\n self.policy.Torque_constant = \\\n self.get_parameter('torque_constant').get_parameter_value().double_value\n\n self.declare_parameter('n_spec', self.policy.N_Spec.tolist())\n self.policy.N_Spec = \\\n np.array(self.get_parameter('n_spec').get_parameter_value().double_array_value)\n\n self.declare_parameter('torque_spec', self.policy.Torque_Spec.tolist())\n self.policy.Torque_Spec = \\\n np.array(self.get_parameter('torque_spec').get_parameter_value().double_array_value)\n\n # recompute any dependent variables\n self.policy.update_params()\n self.get_logger().info(str(self.policy))\n
Add a helper function, __str__
, in the ControlPolicy
class for this example to report the parameters used.
def __str__(self):\n return \"\"\"ControlPolicy:\n\\tTorque_constant: {tc}\n\\tN_Spec: {nspec}\n\\tTorque_Spec: {tspec}\n\\tI_Spec: {ispec}\"\"\".format(tc=self.Torque_constant,\n nspec=self.N_Spec,\n tspec=self.Torque_Spec,\n ispec=self.I_Spec)\n
"},{"location":"Tutorials/ROS2/PythonLinearDamperExample/#control-policy-target","title":"Control Policy Target","text":"To implement the torque control control policy, we use the target
function in ControlPolicy
. This is where we accept feedback data and return a command value. In this case, we need the motor rpm
, and the gains applied to the winding current damping, scale_factor
and retract_factor
. Typical values for these gains are
def target(self, rpm, scale_factor, retract_factor):\n \"\"\"Calculate target value from feedback inputs.\"\"\"\n N = abs(rpm)\n I = self.windcurr_interp1d(N)\n\n # Apply damping gain\n I *= scale_factor\n\n # Hysteresis due to gravity / wave assist\n if rpm > 0.0:\n I *= -retract_factor\n\n return float(I)\n
So, as you can see we apply a positive damping torque when RPM is negative (piston extending), and a positive damping torque when RPM is positive (piston retracting). The damping torque required is reduced when retracting.
"},{"location":"Tutorials/ROS2/PythonLinearDamperExample/#controller","title":"Controller","text":"All that is left is to connect the necessary feedback data to the ControlPolicy
. In this case, rpm
, scale
, and retract
are present in buoy_interfaces.msg.PCRecord
on the /power_data
topic published by the Power Controller running on the buoy.
To access the data, all that is required is to define the callback def power_callback(self, data)
in the Controller
class, and pass the data to self.policy.target
to get the desired winding current command. Various commands are available, and this time we will be using self.send_pc_wind_curr_command(wind_curr, blocking=False)
def power_callback(self, data):\n \"\"\"Provide feedback of '/power_data' topic from Power Controller.\"\"\"\n # Update class variables, get control policy target, send commands, etc.\n wind_curr = self.policy.target(data.rpm, data.scale, data.retract)\n\n self.get_logger().info('WindingCurrent:' +\n f' f({data.rpm:.02f}, {data.scale:.02f}, {data.retract:.02f})' +\n f' = {wind_curr:.02f}')\n\n self.send_pc_wind_curr_command(wind_curr, blocking=False)\n
Finally, let's set the Power Controller's publish rate to the maximum of 50Hz. Uncomment the line to set the PC Pack Rate in Controller.__init__
:
def __init__(self):\n super().__init__('linear_damper')\n\n self.policy = ControlPolicy()\n self.set_params()\n\n # set packet rates from controllers here\n # controller defaults to publishing feedback @ 10Hz\n # call these to set rate to 50Hz or provide argument for specific rate\n self.set_pc_pack_rate(blocking=False) # set PC feedback publish rate to 50Hz\n
In this tutorial, we've named this controller linear_damper
. Don't forget to update controller names along with other changes according to the previous tutorial.
Make sure to build and source your workspace. This tutorial assumes you cloned your package to ~/controller_ws/src
and you have sourced mbari_wec_gz
and mbari_wec_utils
$ cd ~/controller_ws\n$ colcon build\n$ source install/local_setup.bash\n
We will be using ros2 launch
and launch/controller.launch.py
to run our new controller.
To run the controller along with the simulation, launch your controller: $ ros2 launch mbari_wec_linear_damper_cpp controller.launch.py
Then, in another terminal (with mbari_wec_gz
sourced), launch the sim: $ ros2 launch buoy_gazebo mbari_wec.launch.py
and click the play button.
You should see output similar to:
[linear_damper-1] [INFO] [1677864397.617058507] [linear_damper]: Found all required services.\n[linear_damper-1] [INFO] [1677864397.618426488] [linear_damper]: ControlPolicy:\n[linear_damper-1] Torque_constant: 0.438\n[linear_damper-1] N_Spec: [ 0. 300. 600. 1000. 1700. 4400. 6790.]\n[linear_damper-1] Torque_Spec: [ 0. 0. 0.8 2.9 5.6 9.8 16.6]\n[linear_damper-1] I_Spec: [ 0. 0. 1.82648402 6.62100457 12.78538813 22.37442922\n[linear_damper-1] 37.89954338]\n[linear_damper-1] [INFO] [1677864197.432679525] [linear_damper]: WindingCurrent: f(4962.91, 1.00, 0.60) = -15.62\n[linear_damper-1] [INFO] [1677864197.532727531] [linear_damper]: WindingCurrent: f(7764.73, 1.00, 0.60) = -22.74\n[linear_damper-1] [INFO] [1677864197.632748699] [linear_damper]: WindingCurrent: f(10504.88, 1.00, 0.60) = -22.74\n[linear_damper-1] [INFO] [1677864197.732851121] [linear_damper]: WindingCurrent: f(11491.33, 1.00, 0.60) = -22.74\n[linear_damper-1] [INFO] [1677864197.833078440] [linear_damper]: WindingCurrent: f(11075.84, 1.00, 0.60) = -22.74\n[linear_damper-1] [INFO] [1677864197.933050356] [linear_damper]: WindingCurrent: f(9546.51, 1.00, 0.60) = -22.74\n[linear_damper-1] [INFO] [1677864198.033185882] [linear_damper]: WindingCurrent: f(7499.68, 1.00, 0.60) = -22.74\n[linear_damper-1] [INFO] [1677864198.133197926] [linear_damper]: WindingCurrent: f(5190.35, 1.00, 0.60) = -16.51\n[linear_damper-1] [INFO] [1677864198.233322713] [linear_damper]: WindingCurrent: f(2353.02, 1.00, 0.60) = -9.06\n[linear_damper-1] [INFO] [1677864198.333507127] [linear_damper]: WindingCurrent: f(-257.59, 1.00, 0.60) = 0.00\n[linear_damper-1] [INFO] [1677864198.433489830] [linear_damper]: WindingCurrent: f(-2185.58, 1.00, 0.60) = 14.51\n[linear_damper-1] [INFO] [1677864198.533538450] [linear_damper]: WindingCurrent: f(-2987.98, 1.00, 0.60) = 17.36\n[linear_damper-1] [INFO] [1677864198.633671249] [linear_damper]: WindingCurrent: f(-3513.15, 1.00, 0.60) = 19.22\n[linear_damper-1] [INFO] [1677864198.733703803] [linear_damper]: WindingCurrent: f(-3738.12, 1.00, 0.60) = 20.02\n[linear_damper-1] [INFO] [1677864198.833889518] [linear_damper]: WindingCurrent: f(-3751.64, 1.00, 0.60) = 20.07\n[linear_damper-1] [INFO] [1677864198.933993414] [linear_damper]: WindingCurrent: f(-3595.71, 1.00, 0.60) = 19.52\n[linear_damper-1] [INFO] [1677864199.034078009] [linear_damper]: WindingCurrent: f(-3306.87, 1.00, 0.60) = 18.49\n[linear_damper-1] [INFO] [1677864199.134273438] [linear_damper]: WindingCurrent: f(-3012.52, 1.00, 0.60) = 17.45\n[linear_damper-1] [INFO] [1677864199.234371669] [linear_damper]: WindingCurrent: f(-2617.97, 1.00, 0.60) = 16.05\n[linear_damper-1] [INFO] [1677864199.334275962] [linear_damper]: WindingCurrent: f(-2269.58, 1.00, 0.60) = 14.81\n[linear_damper-1] [INFO] [1677864199.434369620] [linear_damper]: WindingCurrent: f(-1893.56, 1.00, 0.60) = 13.47\n[linear_damper-1] [INFO] [1677864199.534461914] [linear_damper]: WindingCurrent: f(-1513.34, 1.00, 0.60) = 11.14\n[linear_damper-1] [INFO] [1677864199.634556815] [linear_damper]: WindingCurrent: f(-1128.46, 1.00, 0.60) = 7.75\n[linear_damper-1] [INFO] [1677864199.734798736] [linear_damper]: WindingCurrent: f(-825.91, 1.00, 0.60) = 4.53\n[linear_damper-1] [INFO] [1677864199.834753871] [linear_damper]: WindingCurrent: f(-586.78, 1.00, 0.60) = 1.75\n[linear_damper-1] [INFO] [1677864199.934809041] [linear_damper]: WindingCurrent: f(-393.25, 1.00, 0.60) = 0.57\n[linear_damper-1] [INFO] [1677864200.035109715] [linear_damper]: WindingCurrent: f(-132.04, 1.00, 0.60) = 0.00\n[linear_damper-1] [INFO] [1677864200.134981992] [linear_damper]: WindingCurrent: f(92.19, 1.00, 0.60) = -0.00\n[linear_damper-1] [INFO] [1677864200.235094219] [linear_damper]: WindingCurrent: f(338.10, 1.00, 0.60) = -0.14\n[linear_damper-1] [INFO] [1677864200.335164181] [linear_damper]: WindingCurrent: f(636.96, 1.00, 0.60) = -1.36\n[linear_damper-1] [INFO] [1677864200.435227880] [linear_damper]: WindingCurrent: f(863.33, 1.00, 0.60) = -2.99\n
"},{"location":"Tutorials/ROS2/PythonOpenLoopControl/","title":"Open-Loop Force Command Example (Python)","text":""},{"location":"Tutorials/ROS2/PythonTemplate/","title":"Quick Start \u2014 Writing External Controller With GitHub Template Repository","text":"In this tutorial, you will make and customize a GitHub repository from a GitHub Template with a ROS 2 Python package and code ready to implement your own external controller utilizing the buoy_api_py
interface. This interface may be used with the both the simulated and physical buoy.
There are two GitHub template repositories set up (C++/Python) for a quick start on writing a custom controller utilizing buoy_api_cpp and buoy_api_py. Please see C++ examples and Python examples for example controller implementations.
You may also refer to GitHub's template documentation
To start using the Python GitHub template
Navigate to mbari_wec_template_py and click the green button with the text Use this template
and select Create a new repository
Next, set up the repository like you would any new GitHub repository choosing the owner, repository name, public/private, etc.
$ mkdir -p ~/controller_ws/src\n$ cd ~/controller_ws/src\n
$ git clone https://github.com/<owner>/<repo_name>.git\n$ cd ~/controller_ws\n
You should now have a Python ROS 2 package with the following structure:
<repo_name>\n \u251c\u2500\u2500 config\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 controller.yaml\n \u251c\u2500\u2500 launch\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 controller.launch.py\n \u251c\u2500\u2500 LICENSE\n \u251c\u2500\u2500 mbari_wec_template_py\n \u2502\u00a0\u00a0 \u251c\u2500\u2500 controller.py\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 __init__.py\n \u251c\u2500\u2500 package.xml\n \u251c\u2500\u2500 README.md\n \u251c\u2500\u2500 resource\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 mbari_wec_template_py\n \u251c\u2500\u2500 setup.cfg\n \u251c\u2500\u2500 setup.py\n \u2514\u2500\u2500 test\n \u251c\u2500\u2500 test_copyright.py\n \u251c\u2500\u2500 test_flake8.py\n \u2514\u2500\u2500 test_pep257.py\n
"},{"location":"Tutorials/ROS2/PythonTemplate/#customizing-the-controller","title":"Customizing the controller","text":"You may also refer to the README.md
in your newly cloned repository.
Replace mbari_wec_template_py
with your package name and modify other fields as necessary in:
<?xml version=\"1.0\"?>\n<?xml-model href=\"http://download.ros.org/schema/package_format3.xsd\" schematypens=\"http://www.w3.org/2001/XMLSchema\"?>\n<package format=\"3\">\n <name>repo_name</name> <!-- Update package name -->\n <version>3.14</version> <!-- Update version -->\n <description>Your Controller Description</description> <!-- Update description -->\n <maintainer email=\"your@email\">Your Name</maintainer> <!-- Update email and name -->\n <license>Your License</license> <!-- Update license -->\n
package_name = 'your_package_name' # Update package name\n\nsetup(\n name=package_name,\n version='3.14', # Update version\n packages=[f'{package_name}'],\n data_files=[\n ('share/ament_index/resource_index/packages',\n ['resource/' + package_name]),\n ('share/' + package_name, ['package.xml']),\n (os.path.join('share', package_name, 'launch'), glob('launch/*.launch.py')),\n (os.path.join('share', package_name, 'config'), glob('config/*.yaml'))\n ],\n install_requires=['setuptools'],\n zip_safe=True,\n maintainer='Your Name', # Update name\n maintainer_email='your@email', # Update email\n description='Your package description', # Update package description\n license='Your License', # Update license\n tests_require=['pytest'],\n entry_points={\n 'console_scripts': [\n f'your_controller_name = {package_name}.controller:main', # Update controller executable name\n
[develop]\nscript_dir=$base/lib/your_package_name\n[install]\ninstall_scripts=$base/lib/your_package_name\n
package_name = 'your_package_name' # Update package name\n\n\ndef generate_launch_description():\n ld = LaunchDescription()\n config = os.path.join(\n get_package_share_directory(package_name),\n 'config',\n 'controller.yaml'\n )\n\n node = Node(\n package=package_name,\n name='your_controller_name', # Update controller name (same as name in config.yaml)\n executable='your_controller_name', # Update controller executable name from setup.py\n
/your_controller_name:\n ros__parameters:\n foo: 1.0\n
and rename two files/folders
resource/mbari_wec_template_py
mbari_wec_template_py
containing controller.py
resulting in the following folder structure:
repo_name\n \u251c\u2500\u2500 config\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 controller.yaml\n \u251c\u2500\u2500 launch\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 controller.launch.py\n \u251c\u2500\u2500 LICENSE\n \u251c\u2500\u2500 your_package_name\n \u2502\u00a0\u00a0 \u251c\u2500\u2500 controller.py\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 __init__.py\n \u251c\u2500\u2500 package.xml\n \u251c\u2500\u2500 README.md\n \u251c\u2500\u2500 resource\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 your_package_name\n \u251c\u2500\u2500 setup.cfg\n \u251c\u2500\u2500 setup.py\n \u2514\u2500\u2500 test\n \u251c\u2500\u2500 test_copyright.py\n \u251c\u2500\u2500 test_flake8.py\n \u2514\u2500\u2500 test_pep257.py\n
Modify setup.py
as desired and add any dependencies in package.xml
following standard ROS 2 documentation.
Assuming you have followed the above and renamed the Python package mbari_wec_template_py
to your package name, <your_package_name>/controller.py
is stubbed out to implement your custom external controller. You may also use config/controller.yaml
for any policy parameters.
You may use the class ControlPolicy
in <your_package_name>/controller.py
to implement your controller.
class ControlPolicy(object):\n\n def __init__(self):\n # Define any parameter variables here\n self.foo = 1.0\n\n self.update_params()\n\n def update_params(self):\n \"\"\"Update dependent variables after reading in params.\"\"\"\n self.bar = 10.0 * self.foo\n\n pass # remove if there's anything to set above\n\n # Modify function inputs as desired\n def target(self, *args, **kwargs):\n \"\"\"Calculate target value from feedback inputs.\"\"\"\n\n # secret sauce\n\n return 0.0 # obviously, modify to return proper target value\n
__init__
on line 23 def __init__(self):\n # Define any parameter variables here\n self.foo = 1.0\n\n self.update_params()\n
update_params
on line 29 def update_params(self):\n \"\"\"Update dependent variables after reading in params.\"\"\"\n self.bar = 10.0 * self.foo\n\n pass # remove if there's anything to set above\n
set_params
function of the Controller
class on line 118 def set_params(self):\n \"\"\"Use ROS2 declare_parameter and get_parameter to set policy params.\"\"\"\n self.declare_parameter('foo', self.policy.foo)\n self.policy.foo = \\\n self.get_parameter('foo').get_parameter_value().double_value\n\n # recompute any dependent variables\n self.policy.update_params()\n
target
function on line 36. Modify the input args as well as the return value as necessary # Modify function inputs as desired\n def target(self, *args, **kwargs): # noqa: D202\n \"\"\"Calculate target value from feedback inputs.\"\"\"\n\n # secret sauce\n\n return 0.0 # obviously, modify to return proper target value\n
"},{"location":"Tutorials/ROS2/PythonTemplate/#controller","title":"Controller","text":"The Controller
class contains an instance of ControlPolicy
as the member variable, self.policy
. The self.policy.target
function may be called anywhere within the Controller
class. You may call it inside any of the data callbacks to enable feedback control (for example):
# To subscribe to any topic, simply define the specific callback, e.g. power_callback\n def power_callback(self, data):\n '''Callback for '/power_data' topic from Power Controller'''\n # get target value from control policy\n target_value = self.policy.target(data.rpm, data.scale, data.retract)\n\n # send a command, e.g. winding current\n self.send_pc_wind_curr_command(target_value, blocking=False)\n
Or, set up a loop in main()
and run open-loop:
def main():\n rclpy.init()\n controller = Controller()\n rate = controller.create_rate(50.0) # Hz\n while rclpy.ok():\n\n # Open-loop control logic\n\n rclpy.spin_once(controller)\n rate.sleep()\n rclpy.shutdown()\n
You may get feedback data from any of the buoy topics by simply creating a specific callback listed below. For feedback data you'd like to use in another area of the class, feel free to assign them to class variables.
(Delete any callbacks you don't need in the Controller
class)
Available callback functions:
/ahrs_data
\u2192 def ahrs_callback(self, data):
/battery_data
\u2192 def battery_callback(self, data):
/spring_data
\u2192 def spring_callback(self, data):
/power_data
\u2192 def power_callback(self, data):
/trefoil_data
\u2192 def trefoil_callback(self, data):
/powerbuoy_data
\u2192 def powerbuoy_callback(self, data):
You may also send commands from within the Controller
class:
self.send_pump_command(duration_mins, blocking=False)
self.send_valve_command(duration_sec, blocking=False)
self.send_pc_wind_curr_command(wind_curr_amps, blocking=False)
self.send_pc_bias_curr_command(bias_curr_amps, blocking=False)
self.send_pc_scale_command(scale_factor, blocking=False)
self.send_pc_retract_command(retract_factor, blocking=False)
In the Controller
constructor, you may also uncomment lines 55 or 56 to set the publish rates for the Spring or Power Controllers on the buoy. These controllers default to publishing at 10Hz. You can call commands to set the rates anywhere from 10Hz to 50Hz (default argument is 50Hz).
def __init__(self):\n super().__init__('controller')\n\n self.policy = ControlPolicy()\n self.set_params()\n\n # set packet rates from controllers here\n # controller defaults to publishing @ 10Hz\n # call these to set rate to 50Hz or provide argument for specific rate\n # self.set_pc_pack_rate(blocking=False) # set PC publish rate to 50Hz\n # self.set_sc_pack_rate(blocking=False) # set SC publish rate to 50Hz\n
The Controller
is also capable of synchronizing its clock from the sim /clock
by uncommenting line 61. Since the Controller
inherits from rclpy.Node
, you may use self.get_clock()
and other various time-related functions of rclpy.Node
.
# Use this to set node clock to use sim time from /clock (from gazebo sim time)\n # Access node clock via self.get_clock() or other various\n # time-related functions of rclpy.Node\n # self.use_sim_time()\n
"},{"location":"Tutorials/ROS2/PythonTemplate/#build-test-run","title":"Build, Test, Run","text":"At this point, your new package should build, pass tests, and run against the sim (will connect but do nothing).
It is assumed that you have already installed or built the buoy packages.
From your workspace (e.g. ~/controller_ws
) build your package:
$ colcon build\nStarting >>> mbari_wec_template_py\n--- stderr: mbari_wec_template_py\n/usr/lib/python3/dist-packages/setuptools/command/install.py:34: SetuptoolsDeprecationWarning: setup.py install is deprecated. Use build and pip and other standards-based tools.\n warnings.warn(\n---\nFinished <<< mbari_wec_template_py [0.74s]\n\nSummary: 1 package finished [0.89s]\n 1 package had stderr output: mbari_wec_template_py\n
You may also build only your new controller package (if you have other packages in the workspace) using: $ colcon build --packages-up-to <your_package_name>
Then, source and test:
$ source install/local_setup.bash\n$ colcon test\nStarting >>> mbari_wec_template_py\n--- stderr: mbari_wec_template_py\n\n=============================== warnings summary ===============================\ntest/test_flake8.py::test_flake8\ntest/test_flake8.py::test_flake8\n Warning: SelectableGroups dict interface is deprecated. Use select.\n\n-- Docs: https://docs.pytest.org/en/stable/warnings.html\n---\nFinished <<< mbari_wec_template_py [0.74s]\n\nSummary: 1 package finished [0.87s]\n 1 package had stderr output: mbari_wec_template_py\n
Or, you may test only your new controller package using: $ colcon test --packages-select <your_package_name>
Next, in another terminal run the sim (after sourcing the sim packages of course): $ ros2 launch buoy_gazebo mbari_wec.launch.py
Now, in the previous terminal, launch the empty controller:
$ ros2 launch <your_package_name> controller.launch.py\n
And you should see something similar to:
[INFO] [launch]: Default logging verbosity is set to INFO\n[INFO] [controller-1]: process started with pid [1409887]\n[controller-1] [INFO] [1678130539.867493131] [controller]: Subscribing to <class 'buoy_interfaces.msg._xb_record.XBRecord'> on '/ahrs_data'\n[controller-1] [INFO] [1678130540.031500810] [controller]: Subscribing to <class 'buoy_interfaces.msg._bc_record.BCRecord'> on '/battery_data'\n[controller-1] [INFO] [1678130540.031972332] [controller]: Subscribing to <class 'buoy_interfaces.msg._sc_record.SCRecord'> on '/spring_data'\n[controller-1] [INFO] [1678130540.032390456] [controller]: Subscribing to <class 'buoy_interfaces.msg._pc_record.PCRecord'> on '/power_data'\n[controller-1] [INFO] [1678130540.032810815] [controller]: Subscribing to <class 'buoy_interfaces.msg._tf_record.TFRecord'> on '/trefoil_data'\n[controller-1] [INFO] [1678130540.033268687] [controller]: Subscribing to <class 'buoy_interfaces.msg._xb_record.XBRecord'> on '/xb_record'\n[controller-1] [INFO] [1678130540.033703510] [controller]: Subscribing to <class 'buoy_interfaces.msg._bc_record.BCRecord'> on '/bc_record'\n[controller-1] [INFO] [1678130540.034091374] [controller]: Subscribing to <class 'buoy_interfaces.msg._sc_record.SCRecord'> on '/sc_record'\n[controller-1] [INFO] [1678130540.034467140] [controller]: Subscribing to <class 'buoy_interfaces.msg._pc_record.PCRecord'> on '/pc_record'\n[controller-1] [INFO] [1678130540.034868686] [controller]: Subscribing to <class 'buoy_interfaces.msg._tf_record.TFRecord'> on '/tf_record'\n[controller-1] [INFO] [1678130540.035298496] [controller]: Subscribing to <class 'buoy_interfaces.msg._pb_record.PBRecord'> on '/powerbuoy_data'\n[controller-1] [INFO] [1678130540.286577653] [controller]: /pc_pack_rate_command not available\n[controller-1] [INFO] [1678130540.537643441] [controller]: /sc_pack_rate_command not available\n[controller-1] [INFO] [1678130540.538230613] [controller]: Found all required services.\n
"},{"location":"Tutorials/ROS2/PythonTemplate/#example","title":"Example","text":"An example using this interface will follow in the next tutorial: Linear Damper Example (Python)
"},{"location":"Tutorials/Simulation/RunSimulator/","title":"Running the Simulator","text":""},{"location":"Tutorials/Simulation/RunSimulator/#introduction","title":"Introduction","text":"This tutorial will illustrate how to start the buoy simulation in Gazebo, when the simulation is running, a rendering of the buoy system motions will be visible, and ROS2 messages will be published that represent the buoy systems state. The simulation also provides the same ROS2 services the real buoy does, so will respond to ROS2 messages appropriately.
Subsequent tutorials will illustrate how to view and plot data being published by the simulation, view data logs being generated, and control the simulated buoy with the command-line tool that is available on the buoy.
"},{"location":"Tutorials/Simulation/RunSimulator/#how-to-run","title":"How to Run","text":"To run the simulator, it is necessary to source the workspace in a separate terminal than was used to build the application. Therefore, open a new terminal window and do the following:
$ . ~/mbari_wec_ws/install/setup.bash\n
$ ros2 launch buoy_gazebo mbari_wec.launch.py \n
The Gazebo rendering of the buoy system should become visible and appear as follows:
To start the simulation, press the \"play\" arrow in the lower left, the buoy should start to move in response to incoming waves.
It is also possible to adjust various parameters such as the sea-state, visibility of the rendering, and speed the simulation will run relative to real-time. These topics are covered in a later tutorial.
To view the ROS2 messages and associated data while the simulation runs, proceed to the next tutorial: View ROS2 Messages
"},{"location":"Tutorials/Simulation/RunSimulator/#notes-on-workspaces","title":"Notes on Workspaces","text":"The step of sourcing the workspace is important and must be done in every window that the simulation and associated tools are accessed from. It is possible, and possibly convenient, to put the source command from step 1 above in the .bashrc or similar place if a different shell is being used. This will automatically perform this step anytime a new window is opened. Beware however, the software can not be built from source in a window where this workspace has previously been sourced. So when re-compiling the source often it can be problematic to have this line in .bashrc. When primarily running the simulations without frequent re-building of the software, it is then appropriate to use .bashrc in this way, just remember to take it out before opening a window where you want to do a \"colcon build\".
"},{"location":"Tutorials/Simulation/SimulatorInteractionPbcmd/","title":"Run-Time Control using pbcmd","text":""},{"location":"Tutorials/Simulation/SimulatorInteractionPbcmd/#introduction","title":"Introduction","text":"There is a command interpreter running on the physical buoy that provides some control over the behavior of the buoy while it is deployed. This is a Linux executable that implements a number of commands, accepts appropriate arguments, and issues commands over the CANbus or ROS 2 on the buoy to effect a change of buoy behavior. These commands can also be issued programmatically as described in subsequent tutorials, but pbcmd is the human interface.
This same command and interface is implemented in the simulation environment, so it is possible to change the behavior of the simulated buoy from the command line in the same way as can be done on the physical buoy. However many of the possible commands are not sensible in the simulated environment, so are not implemented, but instead return a message indicating they aren't relevant in simulation. A key example is the command to open and close the heave-cone doors, because the simulator can not change this behavior while running, the command to do so is inactive. Similarly, it makes no sense to turn on and off the electrical ground fault detector that exists on the buoy, but does not exist in simulation.
"},{"location":"Tutorials/Simulation/SimulatorInteractionPbcmd/#pbcmd-usage","title":"pbcmd Usage","text":"Issuing 'pbcmd' at the command prompt provides guidance on the possible commands. In simulation it also indicates which commands are supported in simulation, on the at-sea system, all commands have an effect.
First source the workspace as usual:
$ cd /path/to/workspace\n$ source install/setup.bash\n
Then, run pbcmd
or any of the commands listed in its output:
$ pbcmd\n\npbcmd: Multi-call command Power Buoy dispatcher\nCommands currently supported in Simulation:\n* pump - Spring Controller pump off or on for a time in minutes <= 10\n* valve - Spring Controller valve off or on for a time in seconds <= 12\n* sc_pack_rate - Set the CANBUS packet rate from the spring controller\n* pc_Scale - Set the scale factor\n* pc_Retract - Set the retract factor\n* pc_WindCurr - Set the winding current target\n* pc_BiasCurr - Set the winding current bias\n* pc_PackRate - Set the CANBUS packet rate\n\nCommands currently not supported in Simulation:\n* bender - Sets the state of the bender module\n* reset_battery - Reset battery controller (caution - no args)\n\n* tether - Spring Controller tether power on or off\n* reset_spring - Reset Spring Controller (caution - no args)\n\n* pc_VTargMax - Set the max target voltage\n* pc_ChargeCurrLim - Set the maximum battery charge current\n* pc_DrawCurrLim - Set the maximum battery current draw\n* pc_BattSwitch - Set the battery switch state\n* pc_Gain - Set the gain scheduler gain\n* pc_StdDevTarg - Set the target RPM standard deviation\n\n* tf_SetPos - Open/close the doors in the heave-cone\n* tf_SetActualPos - Open/close the doors in the heave-cone\n* tf_SetMode - Set controller mode\n* tf_SetChargeMode - Set Battery Charge mode\n* tf_SetStateMachine - Set Battery Charge mode\n* tf_SetCurrLim - Set controller current limit\n* tf_WatchDog - Toggle controller watchdog (caution - no args)\n* tf_Reset - Reset Controller (caution - no args)\n\n\nFor help on a command use the command name, i.e. \"bender\";\n\nExcept the reset commands which take no arguments.\n\nDO NOT enter reset_battery and expect to get help. The command will execute!\n
Note that at the end of this usage message, there is a hint that typing most commands without an argument will supply some further help.
"},{"location":"Tutorials/Simulation/SimulatorInteractionPbcmd/#command-descriptions","title":"Command descriptions","text":"pump - This command turns on and off the gas pump that pumps Nitrogen from the upper pneumatic spring chamber to the lower spring chamber. The result of this is the mean position of the position will slowly rise. The rate is about 1 inch per minute of pump action, a required argument for this command specifies how long the pump will run for, in minutes, and must be between 0 and 10. After this timeout the pump will stop even if no further commands are issued. A \"pump 0\" command stops the pump immediately.
valve - The valve command releases Nitrogen gas from the lower chamber to the upper chamber, resulting in the mean position of the piston lowering. This process is much faster so the required argument for this command is in seconds, and must be between 0 and 10. After this timeout the valve closes even if no further commands are issued. A \"valve 0\" command closes the pump immediately.
sc_pack_rate - This command sets the data packet rate for data coming from the spring controller, the required argument between 10 and 50 indicates the desired data rate in Hz. This controls the rate of the ROS 2 messages from the spring controller on the buoy, and in the simulator.
pc_scale - This command adjusts the multiplier that is applied to the default motor-current/RPM relationship that is programmed into the power converter. This value can be from 0.4 to 1.5, allowing a range of damping to be applied.
pc_Retract - This command adjusts an additional multiplier that is applied to the default motor-current/RPM relationship during piston retraction. This value can be from 0.4 to 1.0, and allows the damping behavior of the system to be asymmetrical, promoting retraction since the pneumatic spring can not pull as hard as the waves can.
pc_WindCurr - This command directly sets the winding current in the electric motor and accepts a value between -35 Amps and +35 Amps. This value is the quadrature current in the permanent magnet electric motor, and therefore corresponds directly with applied torque. A positive winding current produces a torque that applies a force that retracts the piston. The controller applies this specified torque for two seconds. After that time, if no new pc_WindCurr command is executed, the system returns to following the default motor-current/RPM relationship (adjusted by the Scale and Retract factor as described). This allows safety in the case of a communication failure, and makes it a bit impractical to manipulate the winding current manually from the keyboard. As described in subsequent tutorials, programmatically adjusting this value in response to the behavior of the wave-energy converter is the primary automated external control mechanism.
pc_BiasCurr - This command applies an offset to the default motor-current/RPM relationship. This value can be between -15 Amps and +15 Amps and is applied for 10 seconds before the system reverts to the default motor-current/RPM relationship. A positive current corresponds to a torque that tends to retract the piston. This command is useful for temporarily changing the equilibrium point of the piston at sea.
pc_pack_rate - This command sets the data packet rate for data coming from the power converter, the required argument between 10 and 50 indicates the desired data rate in Hz. This controls the rate of the ROS 2 messages from the power converter on the buoy, and in the simulator.
As an example, issue the following commands in a terminal where the workspace has been sourced:
Launch the simulation without incident waves by issuing the following commands, the first command over-rides the default sea-state and results in no incident wave-forcing on the buoy when the \"regenerate_models\" flag is set to false:
$ empy -D 'inc_wave_spectrum_type=\"None\"' -o ~/mbari_wec_ws/install/buoy_description/share/buoy_description/models/mbari_wec/model.sdf ~/mbari_wec_ws/install/buoy_description/share/buoy_description/models/mbari_wec/model.sdf.em\n\n$ ros2 launch buoy_gazebo mbari_wec.launch.py regenerate_models:=false\n
Start the simulation in the GUI by pressing the play button.
Start PlotJuggler
$ ros2 run plotjuggler plotjuggler &\n
Select messages wcurrent and rpm from the /power_data topic and the range_finder message from the /spring_data topic, and then create plots to display these messages in separate windows.
Issue the following command to introduce a 10A winding current offset in the motor-current/RPM relationship.
$ pc_BiasCurr 10\n
After 20 seconds of simulation time or so, the plotjuggler window should look approximately as below. One can see that the command resulted in an additional 10 Amps of motor winding current being present (time = 6 seconds in plot), this additional torque spins the motor and a force is applied to retract the piston, which is evident in the range_finder data.
After a 10 second timeout, because no new current over-ride command is issued, the system reverts to the default motor-current/RPM relationship that doesn't include the offset (time = 16 seconds in plot). After this extra torque is removed, the weight of the heave cone causes the piston to extend back to it's nominal mean position causing the motor to spin in the opposite direction (time = 16-20 seconds in the plot). The default motor-current/RPM relationship is programmed to resist this motion and a smaller positive motor-current is applied during this time.
After time = 21 seconds, the energy of the raised heave-cone has been dissipated into the PTO system and the system comes mostly to rest. The slow creep of the piston beyond 21 seconds is due to the heating effects of the pneumatic spring, heat has been created in the gas spring which slowly dissipates to the environment and the lower spring pressure drops slowly and lowers the piston gradually.
To extend the previous example, some interesting exercises to try are the following:
Repeat the above example but also plot the battery voltage and current. Observe how current flows from the battery to raise the piston and heave cone when the pc_BiasCurr 10 command is issued, and then when the timeout occurs, the potential energy of the raised heave cone is converted to electrical energy and current flows into the battery, with a commensurate change in battery voltage.
Plot the upper and lower spring pressures, observer how they change relative to piston position, and their decay in time after the piston comes to rest.
Repeatedly issue the \"pc_BiasCurr 10\" command before the timeout expires, note how the timeout is extended to 10 seconds beyond the last command issued.
Issue a \"pc_WindCurr 10\" command instead of the pc_BiasCurr command. This command directly sets the winding current, so the resulting winding current is not affected by the motor RPM until after the timeout has expired. For this command, the timeout is two seconds.
Plot other messages while manipulating the winding current. Note how the buoy and heave-cone positions as well as the load cell value respond to the dynamics of the floating bodies.
\"PlotJuggler\" is a plotting program that includes support for ROS 2 messages, and allows real-time plotting of data from ROS 2 messages while the simulator runs, as well as plotting of logged data.
"},{"location":"Tutorials/Simulation/SimulatorOutputPlotjuggler/#installation-and-running","title":"Installation and Running","text":"To install, see instructions here. If using the supplied docker images, this step is not necessary as the software is already installed.
To start, issue the following command in a window where the environment has already been sourced using $ . ~/mbari_wec_ws/install/setup.bash
:
$ ros2 run plotjuggler plotjuggler \n
In the window on the left that shows the available topics, select the /arhs_data, /power_data, /spring_data, battery_data, heavecone_data and /xb_data topics and click \"OK\". Note that for these topics to be available, the simulation needs to be running.
The selected topics will appear in the \"Timeseries List\" window, and selecting the carrot to the left of each topic will expand them and show the data that can be plotted. Note that these topics and data are the same as are visible using the $ ros2 topic list
and $ ros2 topic echo
commands from the command-line.
Dragging any data item into the plot field on the right will plot that data on a scrolling graph. The time-extent of the graph can be changed using the \"Buffer\" text-box under the \"Streaming\" box in the upper left. Graphs can be split horizontally and vertically to make room for more data items, see this guide for information on manipulating the PlotJuggler windows.
After a bit of data selection, the window can look like the example below and show many data items in real-time. Under the \"File\" box in the upper left, there are options to save and retrieve this layout to avoid setting up the windows at each invocation of PlotJuggler. PlotJuggler will continue to run through re-starts of the simulator, so it is often not necessary to re-start PlotJuggler often.
While running, the simulator generates exactly the same ROS 2 messages that the buoy hardware does during operation. These are grouped into ROS 2 topics that corresponds to data being produced by each micro-controller or instrument on the buoy. To see all ROS 2 topics being published to on the system, issue the following command (after sourcing the workspace if needed in a new terminal $ . ~/mbari_wec_ws/install/setup.bash
$ ros2 topic list \n/ahrs_data\n/clock\n/joint_states\n/parameter_events\n/power_data\n/rosout\n/spring_data\n/tf\n/tf_static\n/xb_imu\n
The topics /ahrs_data, /battery_data, /spring_data, /power_data, and /heavecone_data corresponds to the buoy-based instrumentation (AHRS), battery controller, spring controller, power-converter controller, and heave-cone controller. Several of these topics are only available in simulation, and only /ahrs_data, /battery_data, /spring_data, /power_data, and /heavecone_data will be present on the real buoy.
To see the data being published in these topics, issue the following command and the data will scroll by, for example:
$ ros2 topic echo power_data\n---\nheader:\n stamp:\n sec: 712\n nanosec: 710000000\n frame_id: ''\nseq_num: 6703\nrpm: 369.927978515625\nsd_rpm: 0.0\nvoltage: 313.98431396484375\ndraw_curr_limit: 0.0\nbcurrent: -0.14509780704975128\nwcurrent: -0.2554447054862976\ntorque: 0.0\ndiff_press: 2.9100000858306885\nbias_current: 0.0\nloaddc: 0.0\nscale: 1.0\nretract: 0.6000000238418579\ntarget_v: 0.0\ntarget_a: -0.2554447054862976\ncharge_curr_limit: 0.0\nstatus: 0\n---\n
The data in each topic corresponds to the message descriptions which can be seen here along with a description of each field.
The next tutorial \"View Messages with Plotjuggler\" shows how to conveniently plot these data items while the simulator is running.
"},{"location":"Tutorials/Simulation/SimulatorParameters/","title":"Parameters and Batch Runs","text":""},{"location":"Tutorials/Simulation/SimulatorParameters/#introduction","title":"Introduction","text":"When running a simple instance of the simulator as described in the Run the Simulator Tutorial. i.e. using:
$ ros2 launch buoy_gazebo mbari_wec.launch.py\n
the simulation uses a number of defaults for a range of parameters. In most cases, one may want to run the simulator with different values for these parameters, and/or run a number of simulations that iterate across a range of values for particular parameters. For instance, one may want to run the simulator in a batch mode that runs the same buoy and controller set-up in a range of sea-states.
To facilitate this, a batch tool is provided that allows one to specify ranges for a number of parameters, and then run a number of simulations for all combinations of parameters, saving the results separately.
This tutorial describes these capabilities, demonstrates with some examples, and discusses how this tool can be used.
"},{"location":"Tutorials/Simulation/SimulatorParameters/#parameters","title":"Parameters","text":"There are a number of parameters that impact the behavior of the simulator, and must be specified at start-up:
The above parameters are specified in a .yaml file that the batch-run tool reads in before execution begins. A commented example is below and illustrates the use of the above parameters. Lines that begin with # are comments and have no impact.
#\n# Batch-Specific Scalar Parameters\n#\nduration: 300\nseed: 42\nphysics_rtf: 11\n#\n# Run-Specific Parameters (Test Matrix)\n#\nphysics_step: [0.001, 0.01, 0.1]\ndoor_state: ['closed', 'open']\nscale_factor: [0.5, 0.75, 1.0, 1.3, 1.4]\nenable_gui: False\n# May specify vector/scalar battery_soc (0.0 to 1.0) or battery_emf (270V to 320V)\nbattery_soc: [0.25, 0.5, 0.75, 1.0]\n# battery_emf: [282.5, 295.0, 307.5, 320.0]\nIncidentWaveSpectrumType:\n - MonoChromatic:\n # A & T may be vector/scalar in pairs (A & T same length)\n A: [1.0, 2.0, 3.0]\n T: [12.0, 14.0, 15.0]\n - Bretschneider:\n # Hs & Tp may be vector/scalar in pairs (Hs & Tp same length)\n Hs: 3.0\n Tp: 14.0\n # Multiple Custom Spectra must be listed individually (f & Szz are already vectors of same size)\n - Custom:\n f: [0.0, 0.2, 0.4, 0.6, 2.0]\n Szz: [0.0, 0.4, 1.0, 1.0, 0.0]\n
As seen in this example, some parameters are enforced to be scalar values and apply to the entire batch of specified runs. These are the specification of simulation duration (300), random seed (42), and the physics real-time factor (11).
The remaining run-specific parameters can be specified as arrays, and the batch-run tool then executes simulations for all possible combinations of these values. Note that some values are specified in pairs. For instance, three mono-chromatic waves are specified by this file with a specification of (A=1.0m, T=12s), (A=2.0m, T=14s), and (A=3.0m, T=15s), not nine runs that include all possible combinations of the specified amplitude and periods.
Obviously it would be very easy to write a batch file specification that includes thousands of runs, more practical usage will most likely iterate over a small number of parameters at at a time.
"},{"location":"Tutorials/Simulation/SimulatorParameters/#running-an-example","title":"Running an example","text":"For a simpler example, a batch file that iterates across a range of sea-states is used. As a concise example, the following file illustrates this, comments have been removed for brevity.
duration: 3\nseed: 42\nphysics_rtf: 11\nenable_gui: False\nphysics_step: 0.01\ndoor_state: ['closed']\nscale_factor: [0.6, 1.0]\nbattery_soc: 0.5\nIncidentWaveSpectrumType:\n - Bretschneider:\n Hs: [2.0, 4.0]\n Tp: [14.0, 16.0]\n
To run this example, create the above file in a new directory and name it \"IrregularWaves.yaml\", source the simulator installation directory, and start the simulation using the batch tool. (Note that the run duration is very short for this example to allow it to complete quickly)
$ mkdir FOO\n$ cd FOO\n$ Create file using editor of your choice, name it IrregularWaves.yaml\n$ . ~/mbari_wec_ws/install/setup.bash\n$ ros2 launch buoy_gazebo mbari_wec_batch.launch.py sim_params_yaml:=IrregularWaves.yaml\n
Running these commands will run the simulation, all output is stored in a directory named similar to `batch_results_20230228210735', the trailing numbers indicate a timestamp. Inside this directory the yaml file is repeated, along with a log file 'batch_results.log' that lists all of the simulation runs that were performed. Alongside that are specific directories that hold output from each run. For convenience, a symbolic link is formed that points at the most recent batch output directory.
Within the output directory, there is a file named 'batch_runs.log' that shows each individual run that was performed, and the associated parameter. In this case it has the following contents:
# Generated 4 simulation runs\nRunIndex, SimReturnCode, StartTime, rosbag2FileName, PhysicsStep, PhysicsRTF, Seed, Duration, DoorState, ScaleFactor, BatteryState, IncWaveSpectrumType;In\ncWaveSpectrumParams\n0, 0, 20230228212457, rosbag2_batch_sim_0_20230228212457, 0.01, 11.0, 42, 3.0, closed, 0.6, 0.5, Bretschneider;Hs:2.0;Tp:14.0\n1, 0, 20230228212502, rosbag2_batch_sim_1_20230228212502, 0.01, 11.0, 42, 3.0, closed, 0.6, 0.5, Bretschneider;Hs:4.0;Tp:16.0\n2, 0, 20230228212506, rosbag2_batch_sim_2_20230228212506, 0.01, 11.0, 42, 3.0, closed, 1.0, 0.5, Bretschneider;Hs:2.0;Tp:14.0\n3, 0, 20230228212510, rosbag2_batch_sim_3_20230228212510, 0.01, 11.0, 42, 3.0, closed, 1.0, 0.5, Bretschneider;Hs:4.0;Tp:16.0\n
For simulations that take longer to run, it can be convenient to tail this log file from the terminal to keep track of progress. i.e. From the directory the batch was started from:
$ tail -f latest_batch_results/batch_runs.log\n
"},{"location":"Tutorials/Simulation/SimulatorParameters/#finding-the-output","title":"Finding the output","text":"Within the batch process output directory, (e.g. `batch_results_20230228210735'), the output of each simulation run is stored within a single sub-directory. The resulting directory tree from the above example is as follows:
$ tree batch_results_20230301200627/\nbatch_results_20230301200627/\n\u251c\u2500\u2500 batch_runs.log\n\u251c\u2500\u2500 IrregularWaves_20230301200627.yaml\n\u251c\u2500\u2500 results_run_0_20230301200627\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 rosbag2\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 metadata.yaml\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 rosbag2_0.db3\n\u251c\u2500\u2500 results_run_1_20230301200632\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 rosbag2\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 metadata.yaml\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 rosbag2_0.db3\n\u251c\u2500\u2500 results_run_2_20230301200636\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 rosbag2\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 metadata.yaml\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 rosbag2_0.db3\n\u2514\u2500\u2500 results_run_3_20230301200640\n \u2514\u2500\u2500 rosbag2\n \u251c\u2500\u2500 metadata.yaml\n \u2514\u2500\u2500 rosbag2_0.db3\n
This output includes rosbag files and .csv files that are in the same format as the files generated on the physical buoy. In general the information in these two files are the same, the .csv files are in clear text and are easy to inspect, and can be processed by the same tools used for the actual buoy data. The rosbag files are binary files that encode all of the ROS2 messages on the computer during the simulation. These files can be processed by a number of tools for post-processing and inspection of results. It is also possible to load the rosbag files into plotjuggler for plotting and inspection, as described in the View Messages with Plotjuggler Tutorial.
"}]} \ No newline at end of file +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Background","text":"The Monterey Bay Aquarium Research Institute (MBARI) Wave-Energy Converter is a point-absorber type wave-energy converter that has been operating in Monterey Bay, CA since 2014. This system was developed as part of MBARI's goals of advancing and demonstrating an autonomous and persistent presence of oceanographic instrumentation in the worlds oceans. This project is complemented by developments in autonomous underwater vehicles, underwater vehicle docking, oceanographic instrumentation, autonomy, and science use.
The MBARI-WEC is currently maintained by MBARI and operates for six-month periods near the MBARI facility in Moss Landing, California, and averages about 250 Watts of power capture, averaged through the weather cycles and seasons.
The MBARI-WEC is a complete system with a four-quadrant electro-hydraulic power-take-off device, board battery storage, control-computers, sensors and instrumentation, and an always-on cell-modem connection to the internet. The architecture of the system is such that critical functions are performed by micro-controllers throughout the system that implement default behaviors and stream sensor data continuously. A Linux computer on the buoy performs data-logging and provides a command interface to the underlying micro-controllers. The system is designed such that the Linux computer is not necessary for safe behavior, if this computer re-boots or goes offline, the system will default to safe behaviors. Additionally, the micro-controllers will ignore damaging commands from the Linux computer. This architecture allows control algorithms running on the Linux computer to be started, stopped, and changed while the device is at sea through the cell-modem connection.
This project provides a software interface to the system to allow such algorithms to be efficiently developed, tested, and executed. Using this interface, MBARI intends to make the system available to external researchers. By providing access to the hardware during the ongoing MBARI deployments of this system, the intention is to provide access to hardware that is often otherwise unavailable. To facilitate this, the project has developed a simulator that provides the same interface as the real hardware, allowing projects the ability to develop and test their work independently, before deployment on the real system which will occur under MBARI supervision.
The following sections of this documentation outlines the physical system, describes the software- interfaces available, describes the simulation environment, and provides all information needed to interact with this project. The software interface is built upon the ROS 2 framework, and the simulation environment uses Gazebo Simulator. In addition to descriptions of these systems, the documentation provides a number of tutorials intended to lead a new user through the installation of the necessary tools, basic operation of the system, and provide guidance on implement new algorithms to run on the simulator and ultimately the buoy. This is an open-source project with all necessary code and resources freely available.
"},{"location":"ControlAndTelemetry/","title":"Buoy Control and Telemetry","text":""},{"location":"architecture/","title":"Architecture","text":"The MBARI Wave-Energy-Converter is a small point absorber design that includes a surface expression, an electro-hydraulic PTO, and a submerged heave-cone device. The system is moored to the seafloor (typically in 80m of water) through a chain-catenary mooring connected to an anchor. As waves excite the system, a differential motion results between the buoy at the surface and the submerged heave cone. Resisting this motion results in energy being absorbed by the system, and this energy is converted to electrical form and stored in a battery bank on the buoy. The rest of this section provides details about the various components of the system
"},{"location":"architecture/#buoy-heave-cone-and-mooring","title":"Buoy, Heave Cone, and Mooring","text":"The buoy in the MBARI-WEC has a diameter of 2.6m, a water-plane area of 5 m^2, and a mass of 1400kg. This buoy houses the system battery and compute infrastructure, described below.
The heave-cone component sits at about 30m depth and provides inertia and drag for the surface-buoy to pull against. The heave-cone has operable doors that can be opened to reduce the drag and inertial of this component in high sea-states. When the doors are open, the heave-cone has added-mass of about 10,000kg, in addition to it's own 600kg mass. When opened, the added-mass reduces to about 3,000kg, which reduces the inertial forcing and increases the natural frequency of the buoy -- heave-cone pair.
A chain-catenary mooring and anchor connects to heave-cone to the ocean floor, keeping the buoy on-station. The system loading due to the mooring increases in higher winds and currents, but remains relatively low compared with the inertia forces the heave-cone creates.
"},{"location":"architecture/#pto-system","title":"PTO System","text":"The power take-off device is located below the buoy and converts the differential motion and forces between the buoy and heave-cone from mechanical to electrical energy. This device is an electro-hydraulic device in which a piston pumps oil through a hydraulic motor, causing an electrical motor/generator to spin and generate electrical energy. In parallel to the hydraulic ram, a pneumatic piston charged with an inert gas provides a sprint returning force for the system.
The combination of the hydraulic and pneumatic pistons creates a spring-damper system for which the spring constant is set by the amount of gas in the system, and the damping behavior is adjustable electronically by varying the torque on the hydraulic motor in response to conditions. The electrical-drive is a four-quadrant device in which the electric motor can operate as a generator in which energy flows into the battery, or as a motor in which energy is drawn from the battery. The winding-currents (and resulting torque) can be set arbitrarily, but by default the power take-off device acts as a generator, i.e. a damper resisting motion.
"},{"location":"architecture/#electrical-system","title":"Electrical System","text":"The electrical system of this buoy consists of a 325V battery system for energy storage, and a 24V system for powering ancillary instrumentation. The 325V battery is connected directly to the power take-off device motor drive electronics. In normal use, the motor-drive device is generating electrical energy at 325V which charges the battery system. In the case the battery is full (or dis-connected), the motor-drive system directs excess energy to an electrical load-dump device. This submerged heater plays a critical role in maintaining a load on the power take-off device at all times.
The electrical system also includes 300V-24V power supplies that provides 24 volts to the compute and instrumentation infrastructure in the system. In the case of low battery voltage due to an extended period of calm seas, the
"},{"location":"architecture/#compute-and-control-systems","title":"Compute and Control Systems","text":"The compute and control architecture of the system is such that critical functions are performed by micro-controllers throughout the system that implement default behaviors and stream sensor data continuously. A Linux computer on the buoy performs data-logging and provides a command interface to the underlying micro-controllers. See figure. The system is designed such that the Linux computer is not necessary for safe behavior, if this computer re-boots or goes offline, the system will default to safe behaviors. Additionally, the micro-controllers will ignore damaging commands from the Linux computer. This architecture allows control algorithms running on the Linux computer to be started, stopped, and changed while the device is at sea through the cell-modem connection.
The fundamental system behaviors are performed by a network of micro-controller based compute nodes, that communicate with the buoy Linux computer and with one-another through a Controller Area Network (CAN) bus. There are four of these controllers as follows:
Battery Controller (BC_): This micro-controller monitors battery voltage, currents, state-of-charge, cell-balance, and environmental conditions inside the battery enclosure. This controller is largely a data-telemetry gathering item, but also includes an important low-voltage disconnect features which shuts down the 24V battery bus during periods of low-battery state-of-charge. During these periods the system continues to convert wave-energy and charge the batteries, but all sources of significant battery drain are disconnected which allows the battery to re-charge to a serviceable level, even in calm conditions.
Spring Controller (SC_): This micro-controller primarily monitors the piston position and a load-cell located between the buoy and power take-off component. Additionally, this controller responds to commands to change the gas pressure in each chamber of the pneumatic spring, a pump to move gas from the lower pressure chamber to the higher pressure chamber, and a valve to do the opposite. Additionally, this controller can turn power on and off to the heave-cone.
Power Converter (PC_): This controller implements the field-oriented control of the winding current in the generator. It's core function is to control the winding current to a set target, but it performs a number of other specialized functions as well. In particular, the converter monitors and reports motor RPM, and by default sets the motor winding current target as a pre-defined function of RPM. This default behavior can be modified (scale and offset), or over-ridden entirely. In this later case the power converter only over-rides the default winding-current for a short period of time (2 seconds). If a new winding-current command does not arrive in that time, the system reverts to the built-in default behavior. The power converter also can switch power on and off to the batteries, divert power to an internal load-dump if the batteries are full or disconnected, and monitors the bus voltage and other health functions. The power converter also obeys current draw/charge limits specified depending on the size and capability of the attached battery pack.
Heave-cone Controller (TC_): This controller is responsible for opening and closing the heave-cone doors when commanded. For storm-safety, the controller will open the doors one hour after power from the surface is lost, or a watchdog timer expires. Battery back-up of the system enables this, and the buoy system must issue a watchdog reset at least every hour to keep the doors closed. In addition, this controller has an IMU and pressure sensor that provides attitude and depth information about the heave-cone. Note: This is denoted \"TC_\", the T stands for trefoil, which is French for clover and refers to the heave-cone doors themselves.
The on-board linux computer is accessible from shore over a radio link (cell-modem, satellite, or line-of-site radio). This link enables enabling data-telemetry, real-time control, and software-updates to be applied to the Linux computer.
"},{"location":"architecture/#sensors-and-measurements","title":"Sensors and Measurements","text":"** Load Cell: ** This is a load cell between the buoy and the power take-off device, and has a range of up to 20,000lbs.
** Piston Position: ** Inside the pneumatic spring there is a laser range-finder that continuously monitors the position of the power take-off piston.
** Pneumatic Pressures: ** The spring controller monitors the pressures of the two gas-chambers that make up the pneumatic spring.
** Hydraulic Compensator Pressure ** The hydraulic system has a small pressurized accumulator attached to the low-pressure side of the hydraulic circuit. This applies a small pressure (3psi) to ensure seawater does not enter the hydraulic system. An abrupt drop in this pressure indicates a hydraulic leak and is therefore monitored and reported.
** Buoy Inertial Measurement Unit: ** On-board the buoy there is a GPS disciplined six DOF attitude-heading and reference system. This unit monitors and reports the buoys attitude and GPS location.
** Heave Cone Inertial Measurement Unit: ** On-board the heave-cone there is an attitude-heading and reference system with a magnetometer and pressure sensor that reports the heave-cones orientation and depth.
** Electrical System Sensors: ** Voltages and currents are monitored throughout the electrical system and reported by various controllers. In particular the motor winding currents, the load-dump current, and the battery current are all independently monitored and reported.
"},{"location":"atseaoperation/","title":"At Sea Operation","text":"Under Construction
"},{"location":"citation/","title":"Citation","text":"If you use this software, please cite it as below:
Hamilton, A., Anderson, M., Poubel, L., Dutia, D., Carroll, M., Zhang, M., McEwen, R., & Mayans, J. (2023). MBARI Wave Energy Conversion Simulation (Version 1.0.0) [Computer software]. https://github.com/osrf/mbari_wec\n
@software{Hamilton_MBARI_Wave_Energy_2023,\n author = {Hamilton, Andrew and Anderson, Michael and Poubel, Louise and Dutia, Dharini and Carroll, Michael and Zhang, Mabel and McEwen, Rob and Mayans, Joan},\n month = may,\n title = {{MBARI Wave Energy Conversion Simulation}},\n url = {https://github.com/osrf/mbari_wec},\n version = {1.0.0},\n year = {2023}\n}\n
"},{"location":"license/","title":"License","text":"Copyright 2022 Open Source Robotics Foundation, Inc. and Monterey Bay Aquarium Research Institute
Licensed under the Apache License, Version 2.0
"},{"location":"resources/","title":"Resources","text":""},{"location":"resources/#forum","title":"Forum","text":"This project maintains a discussion forum at TBD and we try to respond as quickly as possible to all questions and discussion. This is the best way to reach the developers and maintainers of this project
"},{"location":"resources/#source-code","title":"Source Code","text":"The simulator used in this project utilizes the Gazebo simulator as the base, with a number of custom PlugIns developed by this project to implement specific features needed to simulate this WEC. Gazebo and this projects plugins are open source projects and the source code for these can be accessed at https://github.com/gazebosim/gz-sim and https://github.com/osrf/mbari_wec. Documentation in these respositories is intended for developers but does provide some detailed information about how the simulation works.
"},{"location":"resources/#publications","title":"Publications","text":""},{"location":"resources/#references","title":"References","text":""},{"location":"roadmap/","title":"Project Roadmap","text":"Under Construction
"},{"location":"ros2/","title":"ROS 2 Interface","text":"Under Construction
"},{"location":"simulation/","title":"Simulation","text":"A computer simulation environment has been created that simulates the dynamics of the wave-energy converter and emulates the ROS 2 messages used on the buoy system. This simulation includes a solver of the multi-body dynamics of the system, a numerical representation of the behavior and losses of the electro-hydraulic PTO, and a numerical simulation of the wave-buoy interaction processes based on linear hydrodynamic theory.
The aim of this simulation is to provide a computer based simulation that replicates the physical systems behavior and also interacts with computer code in the same way the physical buoy system does. The aim is that code running on a desktop Linux machine can run unchanged on the physical buoy, and that the response of the simulated buoy is indistinguishable from the real system. The following sections provide some details of this simulator, and additional specifics are in the theory section.
"},{"location":"simulation/#gazebo","title":"Gazebo","text":"The simulation environment here is built upon the popular Gazebo Simulator that is supported by the Open Source Robotics Foundation. The core of this simulator is provided by a physics engine that is responsible for solving the 6 degree-of-freedom multi-body equations of motion. This engine determines the position in space solution of all the bodies in a simulation, subject to the forces that are acting on those bodies in any particular environment. This multi-body simulator also can enforce constraints on, and joints between, the bodies, allowing one to develop simulations in which two bodies are only allowed to slide by one another in one degree of freedom, for instance. Gazebo includes a number of forcings that are typical in robotics, gravity, contact forces between bodies, hydrodynamic drag forces, etc. Also, when used with the DART physics engine, Gazebo can include a general added-mass matrix which adds inertial forcing on specified bodies due to the tendency of the fluid surrounding a body to be accelerated along with the body. This is a critically important feature in underwater robotics and simulation efforts. Of course, all the required forcings to
In particular, this project has created plugins that provide forcing on the components of wave-energy converter system that are specific to this device and application. These plugins are loaded at run-time so exist independently of the main Gazebo software, but behave in an integrated way during simulations. The plug-in code described in the following sections implement specific forcings needed by this project.
"},{"location":"simulation/#electro-hydraulic-pto","title":"Electro-Hydraulic PTO","text":"As described in the Architecture section, the MBARI wave-energy converter implements a custom developed electro-hydraulic power take-off device that converts mechanical power of a moving piston rod (force times velocity) to/from electrical energy that is supplied to or drawn from a battery system (voltage times current). This device consists of a linear hydraulic ram that is connected to a rotary hydraulic motor, linear motion of the ram translates to rotary motion of a shaft, and vice versa. The hydraulic motor shaft is directly coupled to a permanent magnet servo motor/driver. The power electronics maintains a commanded torque on the motor which results from current flowing to/from the battery system as appropriate. The Gazebo plugin developed to simulate this device takes in the piston velocity and commanded torque, and computes the resulting hydraulic pressure, motor RPM, and battery voltage and current. The plugin is programmed with the same default motor-current/RPM relationship as the physical system, and also respects the same limits on motor and battery current, motor RPMs, etc. A key simplifying assumption made in this model is that the rotary inertia of the motor shaft is negligible, and the hydraulic oil is incompressible. These assumptions cause the model of the PTO to be a quasi-static solution that neglects inertia forces of the rotary components of the motor. This has been shown to be a reasonable assumption at the motion frequencies the system operates at, and greatly increases the speed at which the solution progresses. The inertia of the piston itself is included in the simulation as it is an independent body in the equations of motion, constrained to move linearly with respect to the PTO housing.
"},{"location":"simulation/#pneumatic-spring","title":"Pneumatic Spring","text":""},{"location":"simulation/#free-surface-hydrodynamics","title":"Free-Surface Hydrodynamics","text":""},{"location":"simulation/#controller-interfaces","title":"Controller Interfaces","text":""},{"location":"theory/","title":"Theory","text":""},{"location":"theory/#modeling-technique-overview","title":"Modeling Technique Overview","text":"The numerical modeling used in this simulator relies upon the Gazebo simulators ability to solve for the motion of a collection of rigid bodies connected by various types of joints. Gazebo can use several different physics solvers to perform this solution, and the wave-energy buoy simulator in this project uses the DART physics engine. The physics engine solves the multi-body six degree-of-freedom problem for the motion of the connected buoy, power-take-off device, and heave cone, subject to initial conditions and forces that act on the various components as the simulation progresses. In the Gazebo simulator, these forces are provided by plugin code that applies forces to each body based on the state (position and velocity) of each component in the system at each timestep.
The Gazebo simulator already includes several plugins that provide relevant forces such as buoyancy and hydrodynamic drag. Additionally, several additional plugins have been created for this simulator that provide the forcings on the system due to the electro-hydraulic power-take-off system, the pneumatic spring system, the tether connecting the PTO to the heave-cone, the mooring system that anchors the system, and the forcing on the buoy from the ocean surface waves. The sections below outline some details about how each of these forcings are modelled and computed. First however, the specific physical characteristics of the MBARI WEC are tabulated for reference.
"},{"location":"theory/#physical-characteristics","title":"Physical Characteristics","text":"Each rigid body in the simulation has a \"Link Frame\" coordinate system in which all other characteristics of the body are defined in for computational purposes. This link-frame coordinate system is often selected to be at the location of a joint that connects the various bodies (which are also called links in the vernacular of Gazebo). Also, the description below includes alternative notation for added mass, e.g. , in the style of Newman.
"},{"location":"theory/#surface-buoy","title":"Surface Buoy","text":"Description Units Buoy Mass 1400 kg Displacement (undisturbed buoy) 2.39 m Center of Gravity in Link Frame (x,y,z) (0.0, 0.0, 2.03) m Center of Buoyancy in Link Frame (x,y,z) (0.0, 0.0, 2.05) m Center of Waterplane in Link Frame, including PTO and cone (0.0, 0.0, 2.27) m Second moment of area of water plane, about roll axis 1.37 m Second moment of area of water plane, about pitch axis 1.37 m Roll Moment of Inertia (MOI) about center of mass 1450 kg m Pitch Moment of Inertia about center of mass 1450 kg m Yaw Moment of Inertia about center of mass 670 kg m Waterplane Area (undisturbed buoy) 5.47 m Surge Added Mass () 260 kg Surge-Pitch Added Mass, origin at c.m. () 150 kg Sway Added Mass ( ) 260 kg Sway-Roll Added Mass, origin at c.m. () -150 kg Heave Added Mass () 3080 kg Roll Added Mass MOI, origin at c.m. () 330 kg m Pitch Added Mass MOI, origin at c.m. () 330 kg m Surge Quadratic Drag -430 kg/m Sway Quadratic Drag -430 kg/m Heave Quadratic Drag -3280 kg/m Roll Quadratic Drag: -880 kg m Pitch Quadratic Drag: -880 kg m Yaw Quadratic Drag: -50 kg mUnder compression and expansion, the pressure, volume and temperature of the Nitrogen in each chamber evolves according to Ideal Gas Law:
and a polytropic process:
with hysteresis, there are two values for the polytropic index, and , to capture behavior when the gas is compressing or expanding. Using this quasi-static solution and discrete time steps, and also incorporating hysteresis, the process becomes:
where is the current time step.
Whenever the piston velocity is slow enough, the process is dominated by heat loss and modeled with Newton's Law of Cooling (using forward difference) followed by an update of pressure using Ideal Gas Law:
The mass of the Nitrogen in each chamber is determined from inputs in the SDF:
and is used for mass flow between chambers in simulating the pump/valve.
"},{"location":"theory/#determining-parameter-values","title":"Determining Parameter Values","text":"Linear regression was used to determine the polytropic indices for each chamber using empirical data from the physical system. Using pressure vs volume curves, is determined from increasing volume, and is determined from decreasing volume. The data is then preconditioned by taking the logarithm to linearize and perform regression to find the parameters. For a polytropic process:
so,
in block matrix notation where and are the arrays of volume and pressure data, respectively. The other parameters in the system are taken from CAD or empirically determined by comparing logged data from prescribed motion between simulation and the physical test bench.
"},{"location":"theory/#tether-forces","title":"Tether Forces","text":""},{"location":"theory/#mooring-forces","title":"Mooring Forces","text":""},{"location":"theory/#ocean-wave-forces","title":"Ocean Wave Forces","text":""},{"location":"tutorials/","title":"Tutorials","text":""},{"location":"tutorials/#installation","title":"Installation","text":"Docker images that include the neccessary software and dependencies have been created for convenience.
"},{"location":"Tutorials/Install/Install_docker/#requirements","title":"Requirements","text":"Install Docker using installation instructions.
Complete the Linux Postinstall steps to allow you to manage Docker as a non-root user.
If you have an NVIDIA graphics card, it can help speed up rendering. Install nvidia-docker.
MBARI maintains Docker images for the two most recent releases on their DockerHub: - mbari/mbari_wec:latest
- mbari/mbari_wec:previous
run.bash
script.git clone -b main https://github.com/osrf/mbari_wec.git\ncd ~/mbari_wec/docker/\n
Or wget https://raw.githubusercontent.com/osrf/mbari_wec/main/docker/run.bash\nchmod +x run.bash\n
If you have an NVIDIA graphics card
./run.bash mbari/mbari_wec:latest\n
Otherwise ./run.bash mbari/mbari_wec:latest --no-nvidia\n
"},{"location":"Tutorials/Install/Install_docker/#build-from-dockerfile","title":"Build from Dockerfile","text":"An alternative to using the images from MBARI's DockerHub would be to build from a Dockerfile. This is convenient if you would like to make any changes.
git clone -b main https://github.com/osrf/mbari_wec.git\ncd ~/mbari_wec/docker/\n
If you have an NVIDIA graphics card
./build.bash nvidia_opengl_ubuntu22\n./build.bash mbari_wec\n
Otherwise ./build.bash mbari_wec --no-nvidia\n
If you have an NVIDIA graphics card
./run.bash mbari_wec\n
Otherwise ./run.bash mbari_wec --no-nvidia\n
./join.bash mbari_wec\n
"},{"location":"Tutorials/Install/Install_docker/#quick-start","title":"Quick start","text":"Quick start scripts are provided in the home directory:
This sources the compiled workspace:
. setup.bash\n
This sources the compiled workspace and launches the simulation:
./run_simulation.bash\n
"},{"location":"Tutorials/Install/Install_docker/#run-an-example-to-test","title":"Run an example to test","text":"In a new terminal (whether on host machine or in Docker container), source the workspace
. ~/mbari_wec_ws/install/setup.bash\n
Launch the simulation
ros2 launch buoy_gazebo mbari_wec.launch.py\n
The simulation software should now be available. To run and test, proceed to the Run the Simulator tutorial series.
"},{"location":"Tutorials/Install/Install_source/","title":"Install from source","text":""},{"location":"Tutorials/Install/Install_source/#requirements","title":"Requirements","text":"Use Ubuntu 22.04.
Install ROS 2 Humble MBARI WEC is tested against the cyclonedds rmw implementation, so set that up as follows:
sudo apt install -y ros-humble-rmw-cyclonedds-cpp\nexport RMW_IMPLEMENTATION=rmw_cyclonedds_cpp\n
Install Gazebo Garden
Install necessary tools
sudo apt install python3-vcstool python3-colcon-common-extensions python3-pip git wget\n
Install necessary libraries
curl -s --compressed \"https://hamilton8415.github.io/ppa/KEY.gpg\" | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/ppa.gpg >/dev/null\nsudo curl -s --compressed -o /etc/apt/sources.list.d/my_list_file.list \"https://hamilton8415.github.io/ppa/my_list_file.list\"\nsudo apt update\nsudo apt install libfshydrodynamics=1.3.1\n
Create a workspace, for example:
mkdir -p ~/mbari_wec_ws/src\ncd ~/mbari_wec_ws/src\n
Clone all source repos with the help of vcstool
:
wget https://raw.githubusercontent.com/osrf/mbari_wec/main/mbari_wec_all.yaml\nvcs import < mbari_wec_all.yaml\ncd ~/mbari_wec_ws\n
Set the Gazebo version to Garden. This is needed because we're not using an official ROS + Gazebo combination (place this in ~/.bashrc for convenience if rebuilding often):
export GZ_VERSION=garden\n
Install ROS dependencies
sudo pip3 install -U rosdep\nsudo rosdep init\nrosdep update\nrosdep install --from-paths src --ignore-src -r -y -i\n
Build and install
source /opt/ros/humble/setup.bash\ncd ~/mbari_wec_ws\ncolcon build\n
The simulation software should build without errors. To run and test, proceed to the Run the Simulator tutorial series. Or run a quick test as described below to confirm all has worked as expected.
"},{"location":"Tutorials/Install/Install_source/#run-an-example-to-test","title":"Run an example to test","text":"In a new terminal, source the workspace
. ~/mbari_wec_ws/install/setup.bash\n
Set SDF_PATH
to allow robot_state_publisher
parse the robot description from the sdformat model (place this in ~/.bashrc for convenience if rebuilding often):
export SDF_PATH=$GZ_SIM_RESOURCE_PATH\n
Launch the simulation
ros2 launch buoy_gazebo mbari_wec.launch.py\n
In this tutorial you will implement a simple linear damper controller for the piston in the WEC Power-Take-Off (PTO). Given motor RPM, it outputs desired motor winding current (interpolated from RPM->Torque lookup table) to generate a torque to resist piston velocity with a damping force. Configurable gains (scale/retract factor) are applied before output. In the end, you will have a working linear damper controller that is very close to the controller running on both the physical and simulated buoy.
"},{"location":"Tutorials/ROS2/CppLinearDamperExample/#prerequisite","title":"Prerequisite","text":"This tutorial assumes you are familiar the steps from the previous tutorial and have built your own custom C++ ROS 2 controller package from the mbari_wec_template_cpp template repository which we will use to implement a simple linear damper controller.
To begin, you should have a C++ ROS 2 controller package that looks similar to:
mbari_wec_linear_damper_cpp\n \u251c\u2500\u2500 CMakeLists.txt\n \u251c\u2500\u2500 config\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 controller.yaml\n \u251c\u2500\u2500 include\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 mbari_wec_linear_damper_cpp\n \u2502\u00a0\u00a0 \u251c\u2500\u2500 controller.hpp\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 control_policy.hpp\n \u251c\u2500\u2500 launch\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 controller.launch.py\n \u251c\u2500\u2500 LICENSE\n \u251c\u2500\u2500 package.xml\n \u251c\u2500\u2500 README.md\n \u2514\u2500\u2500 src\n \u2514\u2500\u2500 controller.cpp\n
with the files modified from the previous tutorial. If you haven't already, follow the steps in the above mentioned link to create a package for this tutorial named mbari_wec_linear_damper_cpp
.
A complete example starting from the template may be found here. Line numbers in this tutorial correspond to the lines in relevant files in the full example.
"},{"location":"Tutorials/ROS2/CppLinearDamperExample/#parameters","title":"Parameters","text":"Parameters for the controller are:
torque_constant
: Motor Torque Constant (N-m/Amp) Constant to convert desired torque to applied motor winding current n_spec
: Input Motor Speed (RPM) Breakpoints (RPM) is the input to the controller and n_spec
are the x-components of the breakpoints for the interpolant, torque_spec
: Desired Output Motor Torque (N-m) Breakpoints Torque (N-m) is the eventual desired output of the controller given an input N
(motor RPM) and torque_spec
/ torque_constant
(Amps) are the y-components of the breakpoints for the interpolant. The controller actually outputs motor winding current (Amps) to generate a torque in the opposite direction of piston velocity to generate a damping force.These can be configured using the config/controller.yaml
file.
/linear_damper:\n ros__parameters:\n torque_constant: 0.438\n n_spec: [0.0, 300.0, 600.0, 1000.0, 1700.0, 4400.0, 6790.0]\n torque_spec: [0.0, 0.0, 0.8, 2.9, 5.6, 9.8, 16.6]\n
As you can see, as motor speed increases, so does the damping torque. For low RPM (up to 300), there is no damping.
Initialize these variables and create the interpolator, winding_current
, in ControlPolicy
in include/mbari_wec_linear_damper_cpp/control_policy.hpp
. This example makes use of <simple_interp/interp1d.hpp>
from mbari_wec_utils
, so don't forget to include that as well as <algorithm>
and <vector>
.
#include <algorithm>\n#include <vector>\n\n#include <mbari_wec_linear_damper_cpp/controller.hpp>\n\n// interp1d for rpm->winding current\n#include <simple_interp/interp1d.hpp>\n\n\n/* Simple Linear Damper Control Policy.\n Implements a simple linear damper controller for the piston in the WEC\n Power-Take-Off (PTO). Given motor RPM, outputs desired motor winding current (interpolated\n from RPM->Torque lookup table) to resist piston velocity. Configurable gains\n (scale/retract factor) are applied before output.\n*/\nstruct ControlPolicy\n{\n // declare/init any parameter variables here\n double Torque_constant; // N-m/Amps\n std::vector<double> N_Spec; // RPM\n std::vector<double> Torque_Spec; // N-m\n std::vector<double> I_Spec; // Amps\n\n // interpolator for rpm -> winding current\n simple_interp::Interp1d winding_current;\n\n ControlPolicy()\n : Torque_constant(0.438F),\n N_Spec{0.0F, 300.0F, 600.0F, 1000.0F, 1700.0F, 4400.0F, 6790.0F},\n Torque_Spec{0.0F, 0.0F, 0.8F, 2.9F, 5.6F, 9.8F, 16.6F},\n I_Spec(Torque_Spec.size(), 0.0F),\n winding_current(N_Spec, I_Spec)\n {\n update_params();\n }\n
Update the dependent variable, I_Spec
, as well as the interpolator, winding_current
.
// Update dependent variables after reading in params\n void update_params()\n {\n std::transform(\n Torque_Spec.cbegin(), Torque_Spec.cend(),\n I_Spec.begin(),\n [tc = Torque_constant](const double & ts) {return ts / tc;});\n\n winding_current.update(N_Spec, I_Spec);\n }\n
Finally, define the set_params
function of the Controller
class and declare/get/set/update these parameters from ROS 2 (as set in config/controller.yaml
).
// Use ROS2 declare_parameter and get_parameter to set policy params\nvoid Controller::set_params()\n{\n this->declare_parameter(\"torque_constant\", policy_->Torque_constant);\n policy_->Torque_constant = this->get_parameter(\"torque_constant\").as_double();\n\n this->declare_parameter(\n \"n_spec\", std::vector<double>(\n policy_->N_Spec.begin(),\n policy_->N_Spec.end()));\n std::vector<double> temp_double_arr = this->get_parameter(\"n_spec\").as_double_array();\n policy_->N_Spec.assign(temp_double_arr.begin(), temp_double_arr.end());\n\n this->declare_parameter(\n \"torque_spec\", std::vector<double>(\n policy_->Torque_Spec.begin(),\n policy_->Torque_Spec.end()));\n temp_double_arr = this->get_parameter(\"torque_spec\").as_double_array();\n policy_->Torque_Spec.assign(temp_double_arr.begin(), temp_double_arr.end());\n\n // recompute any dependent variables\n policy_->update_params();\n RCLCPP_INFO_STREAM(rclcpp::get_logger(this->get_name()), *policy_);\n}\n
Add a helper function, for the ControlPolicy
class for this example to report the parameters used.
// Helper function to print policy parameters\nstd::ostream & operator<<(std::ostream & os, const ControlPolicy & policy)\n{\n os << \"ControlPolicy:\" << std::endl;\n\n os << \"\\tTorque_constant: \" << policy.Torque_constant << std::endl;\n\n os << \"\\tN_Spec: \" << std::flush;\n std::copy(policy.N_Spec.cbegin(), policy.N_Spec.cend(), std::ostream_iterator<double>(os, \",\"));\n os << \"\\b \\b\" << std::endl;\n\n os << \"\\tTorque_Spec: \" << std::flush;\n std::copy(\n policy.Torque_Spec.cbegin(),\n policy.Torque_Spec.cend(),\n std::ostream_iterator<double>(os, \",\"));\n os << \"\\b \\b\" << std::endl;\n\n os << \"\\tI_Spec: \" << std::flush;\n std::copy(policy.I_Spec.cbegin(), policy.I_Spec.cend(), std::ostream_iterator<double>(os, \",\"));\n os << \"\\b \\b\" << std::endl;\n\n return os;\n}\n
"},{"location":"Tutorials/ROS2/CppLinearDamperExample/#control-policy-target","title":"Control Policy Target","text":"To implement the torque control control policy, we use the target
function in ControlPolicy
. This is where we accept feedback data and return a command value. In this case, we need the motor rpm
, and the gains applied to the winding current damping, scale_factor
and retract_factor
. Typical values for these gains are
// Calculate target value from feedback inputs\n double target(\n const double & rpm,\n const double & scale_factor,\n const double & retract_factor)\n {\n double N = fabs(rpm);\n double I = winding_current.eval(N);\n\n // apply damping gain\n I *= scale_factor;\n\n // Hysteresis due to gravity / wave assist\n if (rpm > 0.0F) {\n I *= -retract_factor;\n }\n\n return I;\n }\n
So, as you can see we apply a positive damping torque when RPM is negative (piston extending), and a positive damping torque when RPM is positive (piston retracting). The damping torque required is reduced when retracting.
"},{"location":"Tutorials/ROS2/CppLinearDamperExample/#controller","title":"Controller","text":"All that is left is to connect the necessary feedback data to the ControlPolicy
. In this case, rpm
, scale
, and retract
are present in buoy_interfaces.msg.PCRecord
on the /power_data
topic published by the Power Controller running on the buoy.
To access the data, all that is required is to define the callback void Controller::power_callback(const buoy_interfaces::msg::PCRecord & data)
in the Controller
class, and pass the data to this->policy_->target
to get the desired winding current command. Various commands are available, and this time we will be using this->send_pc_wind_curr_command(wind_curr_amps);
// Callback for '/power_data' topic from Power Controller\nvoid Controller::power_callback(const buoy_interfaces::msg::PCRecord & data)\n{\n // Update class variables, get control policy target, send commands, etc.\n // get target value from control policy\n double wind_curr = this->policy_->target(data.rpm, data.scale, data.retract);\n\n RCLCPP_INFO_STREAM(\n rclcpp::get_logger(\n this->get_name()),\n \"WindingCurrent: f(\" << data.rpm << \", \" << data.scale << \", \" << data.retract << \") = \" <<\n wind_curr);\n\n auto future = this->send_pc_wind_curr_command(wind_curr);\n}\n
Finally, let's set the Power Controller's publish rate to the maximum of 50Hz. Uncomment the line to set the PC Pack Rate in Controller
constructor:
Controller::Controller(const std::string & node_name)\n: buoy_api::Interface<Controller>(node_name),\n policy_(std::make_unique<ControlPolicy>())\n{\n this->set_params();\n\n // set packet rates from controllers here\n // controller defaults to publishing @ 10Hz\n // call these to set rate to 50Hz or provide argument for specific rate\n // this->set_sc_pack_rate(); // set SC publish rate to 50Hz\n this->set_pc_pack_rate(); // set PC publish rate to 50Hz\n}\n
In this tutorial, we've named this controller linear_damper
. Don't forget to update controller names along with other changes according to the previous tutorial.
Make sure to build and source your workspace. This tutorial assumes you cloned your package to ~/controller_ws/src
and you have sourced mbari_wec_gz
and mbari_wec_utils
$ cd ~/controller_ws\n$ colcon build\n$ source install/local_setup.bash\n
We will be using ros2 launch
and launch/controller.launch.py
to run our new controller.
To run the controller along with the simulation, launch your controller: $ ros2 launch mbari_wec_linear_damper_cpp controller.launch.py
Then, in another terminal (with mbari_wec_gz
sourced), launch the sim: $ ros2 launch buoy_gazebo mbari_wec.launch.py
and click the play button.
You should see output similar to:
[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.623306255] [mbari_wec_linear_damper_cpp]: Found all required services.\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.623437968] [mbari_wec_linear_damper_cpp]: ControlPolicy:\n[mbari_wec_linear_damper_cpp-1] Torque_constant: 0.438\n[mbari_wec_linear_damper_cpp-1] N_Spec: 0,300,600,1000,1700,4400,6790\n[mbari_wec_linear_damper_cpp-1] Torque_Spec: 0,0,0.8,2.9,5.6,9.8,16.6\n[mbari_wec_linear_damper_cpp-1] I_Spec: 0,0,1.82648,6.621,12.7854,22.3744,37.8995\n[mbari_wec_linear_damper_cpp-1]\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.623585767] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2911.62, 1, 0.6) = -10.2531\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.623769091] [mbari_wec_linear_damper_cpp]: Successfully set publish_rate for power_controller\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.723139301] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2881.34, 1, 0.6) = -10.1886\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.723199020] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2881.34, 1, 0.6) = -10.1886\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.743295542] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2864.91, 1, 0.6) = -10.1535\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.763406662] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2845.41, 1, 0.6) = -10.112\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.783518884] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2822.43, 1, 0.6) = -10.063\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.803625212] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2796.29, 1, 0.6) = -10.0073\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.823736947] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2767.06, 1, 0.6) = -9.94502\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.843817290] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2734.81, 1, 0.6) = -9.87631\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.863931284] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2699.67, 1, 0.6) = -9.80143\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.884041064] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2661.74, 1, 0.6) = -9.7206\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.904159386] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2621.09, 1, 0.6) = -9.63398\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.924232170] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2577.85, 1, 0.6) = -9.54184\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.944361837] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2532.15, 1, 0.6) = -9.44445\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.964467851] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2484.08, 1, 0.6) = -9.34203\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215528.984588134] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2433.76, 1, 0.6) = -9.23479\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215529.004697490] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2381.3, 1, 0.6) = -9.12301\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215529.024807195] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2326.81, 1, 0.6) = -9.00691\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215529.044928940] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2270.4, 1, 0.6) = -8.88669\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215529.065039660] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2212.17, 1, 0.6) = -8.76262\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215529.085145551] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2152.24, 1, 0.6) = -8.63491\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215529.105256007] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2090.71, 1, 0.6) = -8.50379\n[mbari_wec_linear_damper_cpp-1] [INFO] [1680215529.125363653] [mbari_wec_linear_damper_cpp]: WindingCurrent: f(2027.67, 1, 0.6) = -8.36947\n
"},{"location":"Tutorials/ROS2/CppOpenLoopControl/","title":"Open-Loop Force Command Example (C++)","text":""},{"location":"Tutorials/ROS2/CppTemplate/","title":"Quick Start \u2014 Writing External Controller With GitHub Template Repository","text":"In this tutorial, you will make and customize a GitHub repository from a GitHub Template with a ROS 2 C++ package and code ready to implement your own external controller utilizing the buoy_api_cpp
interface. This interface may be used with the both the simulated and physical buoy.
There are two GitHub template repositories set up (C++/Python) for a quick start on writing a custom controller utilizing buoy_api_cpp and buoy_api_py. Please see C++ examples and Python examples for example controller implementations.
You may also refer to GitHub's template documentation
To start using the C++ GitHub template
Navigate to mbari_wec_template_cpp and click the green button with the text Use this template
and select Create a new repository
Next, set up the repository like you would any new GitHub repository choosing the owner, repository name, public/private, etc.
Make a ROS 2 workspace
$ mkdir -p ~/controller_ws/src\n$ cd ~/controller_ws/src\n
Now that your new repository is set up, clone it to your local machine, make a branch, etc.
$ git clone https://github.com/<owner>/<repo_name>.git\n$ cd ~/controller_ws\n
You should now have a C++ ROS 2 package with the following structure in your workspace src
:
<repo_name>\n \u251c\u2500\u2500 CMakeLists.txt\n \u251c\u2500\u2500 config\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 controller.yaml\n \u251c\u2500\u2500 include\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 mbari_wec_template_cpp\n \u2502\u00a0\u00a0 \u251c\u2500\u2500 controller.hpp\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 control_policy.hpp\n \u251c\u2500\u2500 launch\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 controller.launch.py\n \u251c\u2500\u2500 LICENSE\n \u251c\u2500\u2500 package.xml\n \u251c\u2500\u2500 README.md\n \u2514\u2500\u2500 src\n \u2514\u2500\u2500 controller.cpp\n
"},{"location":"Tutorials/ROS2/CppTemplate/#customizing-the-controller","title":"Customizing the controller","text":"You may also refer to the README.md
in your newly cloned repository.
Replace mbari_wec_template_cpp
with your package name and modify other fields as necessary in:
package.xml
(lines 4-8)<?xml version=\"1.0\"?>\n<?xml-model href=\"http://download.ros.org/schema/package_format3.xsd\" schematypens=\"http://www.w3.org/2001/XMLSchema\"?>\n<package format=\"3\">\n <name>repo_name</name> <!-- Update package name -->\n <version>3.14</version> <!-- Update version -->\n <description>Your Controller Description</description> <!-- Update description -->\n <maintainer email=\"your@email\">Your Name</maintainer> <!-- Update email and name -->\n <license>Your License</license> <!-- Update license -->\n
CMakeLists.txt
(line 2)cmake_minimum_required(VERSION 3.8)\nproject(mbari_wec_template_cpp) # Update ${PROJECT_NAME}\n\nif(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES \"Clang\")\n add_compile_options(-Wall -Wextra -Wpedantic)\nendif()\n\n# find dependencies\nfind_package(ament_cmake REQUIRED)\nfind_package(rclcpp REQUIRED)\nfind_package(buoy_interfaces REQUIRED)\nfind_package(buoy_api_cpp REQUIRED COMPONENTS buoy_api)\n\nadd_executable(${PROJECT_NAME} src/controller.cpp)\ntarget_link_libraries(${PROJECT_NAME} PUBLIC buoy_api_cpp::buoy_api)\nament_target_dependencies(${PROJECT_NAME} PUBLIC rclcpp buoy_interfaces)\ntarget_include_directories(${PROJECT_NAME} PUBLIC\n $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>\n $<INSTALL_INTERFACE:include>)\ntarget_compile_features(${PROJECT_NAME} PUBLIC c_std_99 cxx_std_17) # Require C99 and C++17\n
launch/controller.launch.py
(line 22) Update package_name
and node name with your controller name (same as the name in config/controller.yaml
)package_name = 'your_package_name' # Update package name (same as in CMakeLists.txt)\n\ndef generate_launch_description():\n ld = LaunchDescription()\n config = os.path.join(\n get_package_share_directory(package_name),\n 'config',\n 'controller.yaml'\n )\n\n node = Node(\n package=package_name,\n name='your_controller_name', # ensure same as name in config.yaml\n executable=package_name,\n
config/controller.yaml
(line 1) Update first line with your controller name (same as node name in launch file)/your_controller_name:\n ros__parameters:\n foo: 1.0\n
and rename the folder:
include/mbari_wec_template_cpp
(containing controller.hpp
and control_policy.hpp
) to your package nameresulting in the following folder structure:
<your_package_name>\n \u251c\u2500\u2500 CMakeLists.txt\n \u251c\u2500\u2500 config\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 controller.yaml\n \u251c\u2500\u2500 include\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 <your_package_name>\n \u2502\u00a0\u00a0 \u251c\u2500\u2500 controller.hpp\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 control_policy.hpp\n \u251c\u2500\u2500 launch\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 controller.launch.py\n \u251c\u2500\u2500 LICENSE\n \u251c\u2500\u2500 package.xml\n \u251c\u2500\u2500 README.md\n \u2514\u2500\u2500 src\n \u2514\u2500\u2500 controller.cpp\n
Update the include paths in:
controller.cpp
(lines 18-19) src/controller.cpp
#include <mbari_wec_template_cpp/control_policy.hpp> // update include path\n#include <mbari_wec_template_cpp/controller.hpp> // update include path\n
control_policy.hpp
(line 22) include/your_package_name/control_policy.hpp
#include <mbari_wec_template_cpp/controller.hpp> // update include path\n
Also, update include guards:
control_policy.hpp
include/your_package_name/control_policy.hpp
#ifndef YOUR_PACKAGE_NAME__CONTROL_POLICY_HPP_\n#define YOUR_PACKAGE_NAME__CONTROL_POLICY_HPP_\n
...
\u200b#endif // YOUR_PACKAGE_NAME__CONTROL_POLICY_HPP_\n
controller.hpp
include/your_package_name/controller.hpp
#ifndef YOUR_PACKAGE_NAME__CONTROLLER_HPP_\n#define YOUR_PACKAGE_NAME__CONTROLLER_HPP_\n
...
\u200b#endif // YOUR_PACKAGE_NAME__CONTROLLER_HPP_\n
Modify CMakeLists.txt
as desired and add any dependencies in package.xml
following standard ROS 2 documentation.
Assuming you have followed the above,
include/<your_package_name>/control_policy.hpp
src/controller.cpp
are stubbed out to implement your custom external controller. You may also use config/controller.yaml
for any policy parameters.
You may use the struct ControlPolicy
in control_policy.hpp
to implement your controller.
struct ControlPolicy\n{\n // declare/init any parameter variables here\n double foo{1.0};\n double bar{10.0*foo};\n\n ControlPolicy()\n : foo{1.0},\n bar{10.0*foo}\n {\n update_params();\n }\n\n // Update dependent variables after reading in params\n void update_params()\n {\n bar = 10.0*foo;\n }\n\n // Modify function inputs as desired\n // Calculate target value from feedback inputs\n double target(\n const double & /*some*/,\n const double & /*feedback*/,\n const double & /*values*/)\n {\n\n // secret sauce\n\n return 0.0; // obviously, modify to return proper target value\n }\n};\n
// declare/init any parameter variables here\n double foo{1.0};\n double bar{10.0*foo};\n\n ControlPolicy()\n : foo{1.0},\n bar{10.0*foo}\n
update_params
on line 39 // Update dependent variables after reading in params\n void update_params()\n {\n bar = 10.0*foo;\n }\n
set_params
function of the Controller
class on line 58// Use ROS2 declare_parameter and get_parameter to set policy params\nvoid Controller::set_params()\n{\n this->declare_parameter(\"foo\", policy_->foo);\n policy_->foo = this->get_parameter(\"foo\").as_double();\n\n // recompute any dependent variables\n policy_->update_params();\n}\n
target
function on line 46. Modify the input args as well as the return value as necessary // Modify function inputs as desired\n // Calculate target value from feedback inputs\n double target(\n const double & /*some*/,\n const double & /*feedback*/,\n const double & /*values*/)\n {\n\n // secret sauce\n\n return 0.0; // obviously, modify to return proper target value\n }\n
"},{"location":"Tutorials/ROS2/CppTemplate/#controller","title":"Controller","text":"The Controller
class contains an instance of ControlPolicy
as the member variable, this->policy
. The this->policy->target
function may be called anywhere within the Controller
class. You may call it inside any of the data callbacks to enable feedback control (for example):
// To subscribe to any topic, simply declare & define the specific callback, e.g. power_callback\n\n // Callback for '/power_data' topic from Power Controller\n void power_callback(const buoy_interfaces::msg::PCRecord & data)\n {\n // get target value from control policy\n double wind_curr = policy_->target(data.rpm, data.scale, data.retract);\n\n auto future = this->send_pc_wind_curr_command(wind_curr);\n }\n
Or, set up a loop in main
and run open-loop:
int main(int argc, char ** argv)\n{\n rclcpp::init(argc, argv);\n\n auto controller = std::make_shared<Controller>(\"controller\");\n rclcpp::Rate rate(50.0);\n while (rclcpp::ok()) {\n rclcpp::spin_once(controller);\n rate.sleep();\n }\n rclcpp::shutdown();\n\n return 0;\n}\n
You may get feedback data from any of the buoy topics by simply creating a specific callback listed below. For feedback data you'd like to use in another area of the class, feel free to assign them to class variables.
(Delete any callbacks you don't need in the Controller
class)
Available callback functions:
/ahrs_data
\u2192 void ahrs_callback(const buoy_interfaces::msg::XBRecord & data){}
/battery_data
\u2192 void battery_callback(const buoy_interfaces::msg::BCRecord & data){}
/spring_data
\u2192 void spring_callback(const buoy_interfaces::msg::SCRecord & data){}
/power_data
\u2192 void power_callback(const buoy_interfaces::msg::PCRecord & data){}
/trefoil_data
\u2192 void trefoil_callback(const buoy_interfaces::msg::TFRecord & data){}
/powerbuoy_data
\u2192 void powerbuoy_callback(const buoy_interfaces::msg::PBRecord & data){}
You may also send commands from within the Controller
class:
this->send_pump_command(duration_mins);
this->send_valve_command(duration_sec);
this->send_pc_wind_curr_command(wind_curr_amps);
this->send_pc_bias_curr_command(bias_curr_amps);
this->send_pc_scale_command(scale_factor);
this->send_pc_retract_command(retract_factor);
In the Controller
constructor, you may also uncomment lines 31 or 32 to set the publish rates for the Spring or Power Controllers on the buoy. These controllers default to publishing feedback at 10Hz to conserve data/bandwidth (on the physical buoy). For feedback control, you have the option to increase the publish rate. You can call commands to set the rates anywhere from 10Hz to 50Hz (default argument is 50Hz).
Controller::Controller(const std::string & node_name)\n: buoy_api::Interface<Controller>(node_name),\n policy_(std::make_unique<ControlPolicy>())\n{\n this->set_params();\n\n // set packet rates from controllers here\n // controller defaults to publishing @ 10Hz\n // call these to set rate to 50Hz or provide argument for specific rate\n // this->set_sc_pack_rate(); // set SC publish rate to 50Hz\n // this->set_pc_pack_rate(); // set PC publish rate to 50Hz\n
The Controller
is also capable of synchronizing its clock from the sim /clock
by uncommenting line 36. Since the Controller
inherits from rclcpp::Node
, you may use this->get_clock()
and other various time-related functions of rclcpp::Node
.
// Use this to set node clock to use sim time from /clock (from gazebo sim time)\n // Access node clock via this->get_clock() or other various time-related functions of rclcpp::Node\n // this->use_sim_time();\n
"},{"location":"Tutorials/ROS2/CppTemplate/#build-test-run","title":"Build, Test, Run","text":"At this point, your new package should build, pass tests, and run against the sim (will connect but do nothing).
It is assumed that you have already installed or built the buoy packages.
From your workspace (e.g. ~/controller_ws
) build your package:
$ colcon build\nStarting >>> mbari_wec_template_cpp\nFinished <<< mbari_wec_template_cpp [25.0s]\n\nSummary: 1 package finished [25.2s]\n
You may also build only your new controller package (if you have other packages in the workspace) using: $ colcon build --packages-up-to <your_package_name>
Then, source and test:
$ source install/local_setup.bash\n$ colcon test\nStarting >>> mbari_wec_template_cpp\nFinished <<< mbari_wec_template_cpp [1.38s]\n\nSummary: 1 package finished [1.54s]\n
Or, you may test only your new controller package using: $ colcon test --packages-select <your_package_name>
Next, in another terminal run the sim (after sourcing the sim packages of course): $ ros2 launch buoy_gazebo mbari_wec.launch.py
Now, in the previous terminal, launch the empty controller:
$ ros2 launch <your_package_name> controller.launch.py\n
And you should see something similar to:
[INFO] [launch]: Default logging verbosity is set to INFO\n[INFO] [mbari_wec_template_cpp-1]: process started with pid [1297902]\n[mbari_wec_template_cpp-1] [INFO] [1678127525.594948064] [mbari_wec_template_cpp]: Subscribing to XBRecord on '/ahrs_data' and '/xb_record'\n[mbari_wec_template_cpp-1] [INFO] [1678127525.595508167] [mbari_wec_template_cpp]: Subscribing to BCRecord on '/battery_data' and '/bc_record'\n[mbari_wec_template_cpp-1] [INFO] [1678127525.595795098] [mbari_wec_template_cpp]: Subscribing to SCRecord on '/spring_data' and '/sc_record'\n[mbari_wec_template_cpp-1] [INFO] [1678127525.596027219] [mbari_wec_template_cpp]: Subscribing to PCRecord on '/power_data' and '/pc_record'\n[mbari_wec_template_cpp-1] [INFO] [1678127525.596275007] [mbari_wec_template_cpp]: Subscribing to TFRecord on '/trefoil_data' and '/tf_record'\n[mbari_wec_template_cpp-1] [INFO] [1678127525.596593805] [mbari_wec_template_cpp]: Subscribing to PBRecord on '/powerbuoy_data'\n[mbari_wec_template_cpp-1] [INFO] [1678127525.697067297] [mbari_wec_template_cpp]: /pc_pack_rate_command not available\n[mbari_wec_template_cpp-1] [INFO] [1678127525.797309937] [mbari_wec_template_cpp]: /sc_pack_rate_command not available\n[mbari_wec_template_cpp-1] [INFO] [1678127525.797524439] [mbari_wec_template_cpp]: Found all required services.\n
"},{"location":"Tutorials/ROS2/CppTemplate/#example","title":"Example","text":"An example using this interface will follow in the next tutorial: Linear Damper Example (C++)
"},{"location":"Tutorials/ROS2/MessagesAndServices/","title":"ROS 2 Messages and Services","text":""},{"location":"Tutorials/ROS2/PythonLinearDamperExample/","title":"Quick Start -- Simple Linear Damper Controller (Python)","text":"In this tutorial you will implement a simple linear damper controller for the piston in the WEC Power-Take-Off (PTO). Given motor RPM, it outputs desired motor winding current (interpolated from RPM->Torque lookup table) to generate a torque to resist piston velocity with a damping force. Configurable gains (scale/retract factor) are applied before output. In the end, you will have a working linear damper controller that is very close to the controller running on both the physical and simulated buoy.
"},{"location":"Tutorials/ROS2/PythonLinearDamperExample/#prerequisite","title":"Prerequisite","text":"This tutorial assumes you are familiar the steps from the previous tutorial and have built your own custom Python ROS 2 controller package from the mbari_wec_template_py template repository which we will use to implement a simple linear damper controller.
To begin, you should have a Python ROS 2 controller package that looks similar to:
mbari_wec_linear_damper_py\n\u251c\u2500\u2500 config\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 controller.yaml\n\u251c\u2500\u2500 CONTRIBUTING.md\n\u251c\u2500\u2500 launch\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 controller.launch.py\n\u251c\u2500\u2500 LICENSE\n\u251c\u2500\u2500 mbari_wec_linear_damper_py\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 controller.py\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 __init__.py\n\u251c\u2500\u2500 package.xml\n\u251c\u2500\u2500 README.md\n\u251c\u2500\u2500 resource\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 mbari_wec_linear_damper_py\n\u251c\u2500\u2500 setup.cfg\n\u251c\u2500\u2500 setup.py\n\u2514\u2500\u2500 test\n \u251c\u2500\u2500 test_copyright.py\n \u251c\u2500\u2500 test_flake8.py\n \u2514\u2500\u2500 test_pep257.py\n
with the files modified from the previous tutorial. If you haven't already, follow the steps in the above mentioned link to create a package for this tutorial named mbari_wec_linear_damper_py
.
A complete example starting from the template may be found here. Line numbers in this tutorial correspond to the lines in relevant files in the full example.
"},{"location":"Tutorials/ROS2/PythonLinearDamperExample/#parameters","title":"Parameters","text":"Parameters for the controller are:
torque_constant
: Motor Torque Constant (N-m/Amp) Constant to convert desired torque to applied motor winding current n_spec
: Input Motor Speed (RPM) Breakpoints (RPM) is the input to the controller and n_spec
are the x-components of the breakpoints for the interpolant, torque_spec
: Desired Output Motor Torque (N-m) Breakpoints Torque (N-m) is the eventual desired output of the controller given an input N
(motor RPM) and torque_spec
/ torque_constant
(Amps) are the y-components of the breakpoints for the interpolant. The controller actually outputs motor winding current (Amps) to generate a torque in the opposite direction of piston velocity to generate a damping force.These can be configured using the config/controller.yaml
file.
/linear_damper:\n ros__parameters:\n torque_constant: 0.438\n n_spec: [0.0, 300.0, 600.0, 1000.0, 1700.0, 4400.0, 6790.0]\n torque_spec: [0.0, 0.0, 0.8, 2.9, 5.6, 9.8, 16.6]\n
As you can see, as motor speed increases, so does the damping torque. For low RPM (up to 300), there is no damping.
Initialize these variables in ControlPolicy
in mbari_wec_linear_damper_py/controller.py
. This example makes use of numpy.array
as well as scipy.interpolate.interp1d
, so don't forget to include those.
import numpy as np\nfrom scipy import interpolate\n\n\nclass ControlPolicy(object):\n \"\"\"\n Simple Linear Damper Control Policy.\n Implements a simple linear damper controller for the piston in the WEC\n Power-Take-Off (PTO). Given motor RPM, outputs desired motor winding current (interpolated\n from RPM->Torque lookup table) to resist piston velocity. Configurable gains\n (scale/retract factor) are applied before output.\n \"\"\"\n\n def __init__(self):\n # Define any parameter variables here\n self.Torque_constant = 0.438 # N-m/Amps\n # Desired damping Torque vs RPM relationship\n self.N_Spec = np.array([0.0, 300.0, 600.0, 1000.0, 1700.0, 4400.0, 6790.0]) # RPM\n self.Torque_Spec = np.array([0.0, 0.0, 0.8, 2.9, 5.6, 9.8, 16.6]) # N-m\n
Update the dependent variable, I_Spec
, and create the interpolator, windcurr_interp1d
, which uses interp1d
from scipy.interpolate
.
def update_params(self):\n \"\"\"Update dependent variables after reading in params.\"\"\"\n # Convert to Motor Winding Current vs RPM and generate interpolator for f(RPM) = I\n self.I_Spec = self.Torque_Spec / self.Torque_constant # Amps\n self.windcurr_interp1d = interpolate.interp1d(self.N_Spec, self.I_Spec,\n fill_value=self.I_Spec[-1],\n bounds_error=False)\n
Finally, in the Controller
class, declare/get/set/update these parameters from ROS 2 (as set in config/controller.yaml
).
def set_params(self):\n \"\"\"Use ROS2 declare_parameter and get_parameter to set policy params.\"\"\"\n self.declare_parameter('torque_constant', self.policy.Torque_constant)\n self.policy.Torque_constant = \\\n self.get_parameter('torque_constant').get_parameter_value().double_value\n\n self.declare_parameter('n_spec', self.policy.N_Spec.tolist())\n self.policy.N_Spec = \\\n np.array(self.get_parameter('n_spec').get_parameter_value().double_array_value)\n\n self.declare_parameter('torque_spec', self.policy.Torque_Spec.tolist())\n self.policy.Torque_Spec = \\\n np.array(self.get_parameter('torque_spec').get_parameter_value().double_array_value)\n\n # recompute any dependent variables\n self.policy.update_params()\n self.get_logger().info(str(self.policy))\n
Add a helper function, __str__
, in the ControlPolicy
class for this example to report the parameters used.
def __str__(self):\n return \"\"\"ControlPolicy:\n\\tTorque_constant: {tc}\n\\tN_Spec: {nspec}\n\\tTorque_Spec: {tspec}\n\\tI_Spec: {ispec}\"\"\".format(tc=self.Torque_constant,\n nspec=self.N_Spec,\n tspec=self.Torque_Spec,\n ispec=self.I_Spec)\n
"},{"location":"Tutorials/ROS2/PythonLinearDamperExample/#control-policy-target","title":"Control Policy Target","text":"To implement the torque control control policy, we use the target
function in ControlPolicy
. This is where we accept feedback data and return a command value. In this case, we need the motor rpm
, and the gains applied to the winding current damping, scale_factor
and retract_factor
. Typical values for these gains are
def target(self, rpm, scale_factor, retract_factor):\n \"\"\"Calculate target value from feedback inputs.\"\"\"\n N = abs(rpm)\n I = self.windcurr_interp1d(N)\n\n # Apply damping gain\n I *= scale_factor\n\n # Hysteresis due to gravity / wave assist\n if rpm > 0.0:\n I *= -retract_factor\n\n return float(I)\n
So, as you can see we apply a positive damping torque when RPM is negative (piston extending), and a positive damping torque when RPM is positive (piston retracting). The damping torque required is reduced when retracting.
"},{"location":"Tutorials/ROS2/PythonLinearDamperExample/#controller","title":"Controller","text":"All that is left is to connect the necessary feedback data to the ControlPolicy
. In this case, rpm
, scale
, and retract
are present in buoy_interfaces.msg.PCRecord
on the /power_data
topic published by the Power Controller running on the buoy.
To access the data, all that is required is to define the callback def power_callback(self, data)
in the Controller
class, and pass the data to self.policy.target
to get the desired winding current command. Various commands are available, and this time we will be using self.send_pc_wind_curr_command(wind_curr, blocking=False)
def power_callback(self, data):\n \"\"\"Provide feedback of '/power_data' topic from Power Controller.\"\"\"\n # Update class variables, get control policy target, send commands, etc.\n wind_curr = self.policy.target(data.rpm, data.scale, data.retract)\n\n self.get_logger().info('WindingCurrent:' +\n f' f({data.rpm:.02f}, {data.scale:.02f}, {data.retract:.02f})' +\n f' = {wind_curr:.02f}')\n\n self.send_pc_wind_curr_command(wind_curr, blocking=False)\n
Finally, let's set the Power Controller's publish rate to the maximum of 50Hz. Uncomment the line to set the PC Pack Rate in Controller.__init__
:
def __init__(self):\n super().__init__('linear_damper')\n\n self.policy = ControlPolicy()\n self.set_params()\n\n # set packet rates from controllers here\n # controller defaults to publishing feedback @ 10Hz\n # call these to set rate to 50Hz or provide argument for specific rate\n self.set_pc_pack_rate(blocking=False) # set PC feedback publish rate to 50Hz\n
In this tutorial, we've named this controller linear_damper
. Don't forget to update controller names along with other changes according to the previous tutorial.
Make sure to build and source your workspace. This tutorial assumes you cloned your package to ~/controller_ws/src
and you have sourced mbari_wec_gz
and mbari_wec_utils
$ cd ~/controller_ws\n$ colcon build\n$ source install/local_setup.bash\n
We will be using ros2 launch
and launch/controller.launch.py
to run our new controller.
To run the controller along with the simulation, launch your controller: $ ros2 launch mbari_wec_linear_damper_cpp controller.launch.py
Then, in another terminal (with mbari_wec_gz
sourced), launch the sim: $ ros2 launch buoy_gazebo mbari_wec.launch.py
and click the play button.
You should see output similar to:
[linear_damper-1] [INFO] [1677864397.617058507] [linear_damper]: Found all required services.\n[linear_damper-1] [INFO] [1677864397.618426488] [linear_damper]: ControlPolicy:\n[linear_damper-1] Torque_constant: 0.438\n[linear_damper-1] N_Spec: [ 0. 300. 600. 1000. 1700. 4400. 6790.]\n[linear_damper-1] Torque_Spec: [ 0. 0. 0.8 2.9 5.6 9.8 16.6]\n[linear_damper-1] I_Spec: [ 0. 0. 1.82648402 6.62100457 12.78538813 22.37442922\n[linear_damper-1] 37.89954338]\n[linear_damper-1] [INFO] [1677864197.432679525] [linear_damper]: WindingCurrent: f(4962.91, 1.00, 0.60) = -15.62\n[linear_damper-1] [INFO] [1677864197.532727531] [linear_damper]: WindingCurrent: f(7764.73, 1.00, 0.60) = -22.74\n[linear_damper-1] [INFO] [1677864197.632748699] [linear_damper]: WindingCurrent: f(10504.88, 1.00, 0.60) = -22.74\n[linear_damper-1] [INFO] [1677864197.732851121] [linear_damper]: WindingCurrent: f(11491.33, 1.00, 0.60) = -22.74\n[linear_damper-1] [INFO] [1677864197.833078440] [linear_damper]: WindingCurrent: f(11075.84, 1.00, 0.60) = -22.74\n[linear_damper-1] [INFO] [1677864197.933050356] [linear_damper]: WindingCurrent: f(9546.51, 1.00, 0.60) = -22.74\n[linear_damper-1] [INFO] [1677864198.033185882] [linear_damper]: WindingCurrent: f(7499.68, 1.00, 0.60) = -22.74\n[linear_damper-1] [INFO] [1677864198.133197926] [linear_damper]: WindingCurrent: f(5190.35, 1.00, 0.60) = -16.51\n[linear_damper-1] [INFO] [1677864198.233322713] [linear_damper]: WindingCurrent: f(2353.02, 1.00, 0.60) = -9.06\n[linear_damper-1] [INFO] [1677864198.333507127] [linear_damper]: WindingCurrent: f(-257.59, 1.00, 0.60) = 0.00\n[linear_damper-1] [INFO] [1677864198.433489830] [linear_damper]: WindingCurrent: f(-2185.58, 1.00, 0.60) = 14.51\n[linear_damper-1] [INFO] [1677864198.533538450] [linear_damper]: WindingCurrent: f(-2987.98, 1.00, 0.60) = 17.36\n[linear_damper-1] [INFO] [1677864198.633671249] [linear_damper]: WindingCurrent: f(-3513.15, 1.00, 0.60) = 19.22\n[linear_damper-1] [INFO] [1677864198.733703803] [linear_damper]: WindingCurrent: f(-3738.12, 1.00, 0.60) = 20.02\n[linear_damper-1] [INFO] [1677864198.833889518] [linear_damper]: WindingCurrent: f(-3751.64, 1.00, 0.60) = 20.07\n[linear_damper-1] [INFO] [1677864198.933993414] [linear_damper]: WindingCurrent: f(-3595.71, 1.00, 0.60) = 19.52\n[linear_damper-1] [INFO] [1677864199.034078009] [linear_damper]: WindingCurrent: f(-3306.87, 1.00, 0.60) = 18.49\n[linear_damper-1] [INFO] [1677864199.134273438] [linear_damper]: WindingCurrent: f(-3012.52, 1.00, 0.60) = 17.45\n[linear_damper-1] [INFO] [1677864199.234371669] [linear_damper]: WindingCurrent: f(-2617.97, 1.00, 0.60) = 16.05\n[linear_damper-1] [INFO] [1677864199.334275962] [linear_damper]: WindingCurrent: f(-2269.58, 1.00, 0.60) = 14.81\n[linear_damper-1] [INFO] [1677864199.434369620] [linear_damper]: WindingCurrent: f(-1893.56, 1.00, 0.60) = 13.47\n[linear_damper-1] [INFO] [1677864199.534461914] [linear_damper]: WindingCurrent: f(-1513.34, 1.00, 0.60) = 11.14\n[linear_damper-1] [INFO] [1677864199.634556815] [linear_damper]: WindingCurrent: f(-1128.46, 1.00, 0.60) = 7.75\n[linear_damper-1] [INFO] [1677864199.734798736] [linear_damper]: WindingCurrent: f(-825.91, 1.00, 0.60) = 4.53\n[linear_damper-1] [INFO] [1677864199.834753871] [linear_damper]: WindingCurrent: f(-586.78, 1.00, 0.60) = 1.75\n[linear_damper-1] [INFO] [1677864199.934809041] [linear_damper]: WindingCurrent: f(-393.25, 1.00, 0.60) = 0.57\n[linear_damper-1] [INFO] [1677864200.035109715] [linear_damper]: WindingCurrent: f(-132.04, 1.00, 0.60) = 0.00\n[linear_damper-1] [INFO] [1677864200.134981992] [linear_damper]: WindingCurrent: f(92.19, 1.00, 0.60) = -0.00\n[linear_damper-1] [INFO] [1677864200.235094219] [linear_damper]: WindingCurrent: f(338.10, 1.00, 0.60) = -0.14\n[linear_damper-1] [INFO] [1677864200.335164181] [linear_damper]: WindingCurrent: f(636.96, 1.00, 0.60) = -1.36\n[linear_damper-1] [INFO] [1677864200.435227880] [linear_damper]: WindingCurrent: f(863.33, 1.00, 0.60) = -2.99\n
"},{"location":"Tutorials/ROS2/PythonOpenLoopControl/","title":"Open-Loop Force Command Example (Python)","text":""},{"location":"Tutorials/ROS2/PythonTemplate/","title":"Quick Start \u2014 Writing External Controller With GitHub Template Repository","text":"In this tutorial, you will make and customize a GitHub repository from a GitHub Template with a ROS 2 Python package and code ready to implement your own external controller utilizing the buoy_api_py
interface. This interface may be used with the both the simulated and physical buoy.
There are two GitHub template repositories set up (C++/Python) for a quick start on writing a custom controller utilizing buoy_api_cpp and buoy_api_py. Please see C++ examples and Python examples for example controller implementations.
You may also refer to GitHub's template documentation
To start using the Python GitHub template
Navigate to mbari_wec_template_py and click the green button with the text Use this template
and select Create a new repository
Next, set up the repository like you would any new GitHub repository choosing the owner, repository name, public/private, etc.
$ mkdir -p ~/controller_ws/src\n$ cd ~/controller_ws/src\n
$ git clone https://github.com/<owner>/<repo_name>.git\n$ cd ~/controller_ws\n
You should now have a Python ROS 2 package with the following structure:
<repo_name>\n \u251c\u2500\u2500 config\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 controller.yaml\n \u251c\u2500\u2500 launch\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 controller.launch.py\n \u251c\u2500\u2500 LICENSE\n \u251c\u2500\u2500 mbari_wec_template_py\n \u2502\u00a0\u00a0 \u251c\u2500\u2500 controller.py\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 __init__.py\n \u251c\u2500\u2500 package.xml\n \u251c\u2500\u2500 README.md\n \u251c\u2500\u2500 resource\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 mbari_wec_template_py\n \u251c\u2500\u2500 setup.cfg\n \u251c\u2500\u2500 setup.py\n \u2514\u2500\u2500 test\n \u251c\u2500\u2500 test_copyright.py\n \u251c\u2500\u2500 test_flake8.py\n \u2514\u2500\u2500 test_pep257.py\n
"},{"location":"Tutorials/ROS2/PythonTemplate/#customizing-the-controller","title":"Customizing the controller","text":"You may also refer to the README.md
in your newly cloned repository.
Replace mbari_wec_template_py
with your package name and modify other fields as necessary in:
<?xml version=\"1.0\"?>\n<?xml-model href=\"http://download.ros.org/schema/package_format3.xsd\" schematypens=\"http://www.w3.org/2001/XMLSchema\"?>\n<package format=\"3\">\n <name>repo_name</name> <!-- Update package name -->\n <version>3.14</version> <!-- Update version -->\n <description>Your Controller Description</description> <!-- Update description -->\n <maintainer email=\"your@email\">Your Name</maintainer> <!-- Update email and name -->\n <license>Your License</license> <!-- Update license -->\n
package_name = 'your_package_name' # Update package name\n\nsetup(\n name=package_name,\n version='3.14', # Update version\n packages=[f'{package_name}'],\n data_files=[\n ('share/ament_index/resource_index/packages',\n ['resource/' + package_name]),\n ('share/' + package_name, ['package.xml']),\n (os.path.join('share', package_name, 'launch'), glob('launch/*.launch.py')),\n (os.path.join('share', package_name, 'config'), glob('config/*.yaml'))\n ],\n install_requires=['setuptools'],\n zip_safe=True,\n maintainer='Your Name', # Update name\n maintainer_email='your@email', # Update email\n description='Your package description', # Update package description\n license='Your License', # Update license\n tests_require=['pytest'],\n entry_points={\n 'console_scripts': [\n f'your_controller_name = {package_name}.controller:main', # Update controller executable name\n
[develop]\nscript_dir=$base/lib/your_package_name\n[install]\ninstall_scripts=$base/lib/your_package_name\n
package_name = 'your_package_name' # Update package name\n\n\ndef generate_launch_description():\n ld = LaunchDescription()\n config = os.path.join(\n get_package_share_directory(package_name),\n 'config',\n 'controller.yaml'\n )\n\n node = Node(\n package=package_name,\n name='your_controller_name', # Update controller name (same as name in config.yaml)\n executable='your_controller_name', # Update controller executable name from setup.py\n
/your_controller_name:\n ros__parameters:\n foo: 1.0\n
and rename two files/folders
resource/mbari_wec_template_py
mbari_wec_template_py
containing controller.py
resulting in the following folder structure:
repo_name\n \u251c\u2500\u2500 config\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 controller.yaml\n \u251c\u2500\u2500 launch\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 controller.launch.py\n \u251c\u2500\u2500 LICENSE\n \u251c\u2500\u2500 your_package_name\n \u2502\u00a0\u00a0 \u251c\u2500\u2500 controller.py\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 __init__.py\n \u251c\u2500\u2500 package.xml\n \u251c\u2500\u2500 README.md\n \u251c\u2500\u2500 resource\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 your_package_name\n \u251c\u2500\u2500 setup.cfg\n \u251c\u2500\u2500 setup.py\n \u2514\u2500\u2500 test\n \u251c\u2500\u2500 test_copyright.py\n \u251c\u2500\u2500 test_flake8.py\n \u2514\u2500\u2500 test_pep257.py\n
Modify setup.py
as desired and add any dependencies in package.xml
following standard ROS 2 documentation.
Assuming you have followed the above and renamed the Python package mbari_wec_template_py
to your package name, <your_package_name>/controller.py
is stubbed out to implement your custom external controller. You may also use config/controller.yaml
for any policy parameters.
You may use the class ControlPolicy
in <your_package_name>/controller.py
to implement your controller.
class ControlPolicy(object):\n\n def __init__(self):\n # Define any parameter variables here\n self.foo = 1.0\n\n self.update_params()\n\n def update_params(self):\n \"\"\"Update dependent variables after reading in params.\"\"\"\n self.bar = 10.0 * self.foo\n\n pass # remove if there's anything to set above\n\n # Modify function inputs as desired\n def target(self, *args, **kwargs):\n \"\"\"Calculate target value from feedback inputs.\"\"\"\n\n # secret sauce\n\n return 0.0 # obviously, modify to return proper target value\n
__init__
on line 23 def __init__(self):\n # Define any parameter variables here\n self.foo = 1.0\n\n self.update_params()\n
update_params
on line 29 def update_params(self):\n \"\"\"Update dependent variables after reading in params.\"\"\"\n self.bar = 10.0 * self.foo\n\n pass # remove if there's anything to set above\n
set_params
function of the Controller
class on line 118 def set_params(self):\n \"\"\"Use ROS2 declare_parameter and get_parameter to set policy params.\"\"\"\n self.declare_parameter('foo', self.policy.foo)\n self.policy.foo = \\\n self.get_parameter('foo').get_parameter_value().double_value\n\n # recompute any dependent variables\n self.policy.update_params()\n
target
function on line 36. Modify the input args as well as the return value as necessary # Modify function inputs as desired\n def target(self, *args, **kwargs): # noqa: D202\n \"\"\"Calculate target value from feedback inputs.\"\"\"\n\n # secret sauce\n\n return 0.0 # obviously, modify to return proper target value\n
"},{"location":"Tutorials/ROS2/PythonTemplate/#controller","title":"Controller","text":"The Controller
class contains an instance of ControlPolicy
as the member variable, self.policy
. The self.policy.target
function may be called anywhere within the Controller
class. You may call it inside any of the data callbacks to enable feedback control (for example):
# To subscribe to any topic, simply define the specific callback, e.g. power_callback\n def power_callback(self, data):\n '''Callback for '/power_data' topic from Power Controller'''\n # get target value from control policy\n target_value = self.policy.target(data.rpm, data.scale, data.retract)\n\n # send a command, e.g. winding current\n self.send_pc_wind_curr_command(target_value, blocking=False)\n
Or, set up a loop in main()
and run open-loop:
def main():\n rclpy.init()\n controller = Controller()\n rate = controller.create_rate(50.0) # Hz\n while rclpy.ok():\n\n # Open-loop control logic\n\n rclpy.spin_once(controller)\n rate.sleep()\n rclpy.shutdown()\n
You may get feedback data from any of the buoy topics by simply creating a specific callback listed below. For feedback data you'd like to use in another area of the class, feel free to assign them to class variables.
(Delete any callbacks you don't need in the Controller
class)
Available callback functions:
/ahrs_data
\u2192 def ahrs_callback(self, data):
/battery_data
\u2192 def battery_callback(self, data):
/spring_data
\u2192 def spring_callback(self, data):
/power_data
\u2192 def power_callback(self, data):
/trefoil_data
\u2192 def trefoil_callback(self, data):
/powerbuoy_data
\u2192 def powerbuoy_callback(self, data):
You may also send commands from within the Controller
class:
self.send_pump_command(duration_mins, blocking=False)
self.send_valve_command(duration_sec, blocking=False)
self.send_pc_wind_curr_command(wind_curr_amps, blocking=False)
self.send_pc_bias_curr_command(bias_curr_amps, blocking=False)
self.send_pc_scale_command(scale_factor, blocking=False)
self.send_pc_retract_command(retract_factor, blocking=False)
In the Controller
constructor, you may also uncomment lines 55 or 56 to set the publish rates for the Spring or Power Controllers on the buoy. These controllers default to publishing at 10Hz. You can call commands to set the rates anywhere from 10Hz to 50Hz (default argument is 50Hz).
def __init__(self):\n super().__init__('controller')\n\n self.policy = ControlPolicy()\n self.set_params()\n\n # set packet rates from controllers here\n # controller defaults to publishing @ 10Hz\n # call these to set rate to 50Hz or provide argument for specific rate\n # self.set_pc_pack_rate(blocking=False) # set PC publish rate to 50Hz\n # self.set_sc_pack_rate(blocking=False) # set SC publish rate to 50Hz\n
The Controller
is also capable of synchronizing its clock from the sim /clock
by uncommenting line 61. Since the Controller
inherits from rclpy.Node
, you may use self.get_clock()
and other various time-related functions of rclpy.Node
.
# Use this to set node clock to use sim time from /clock (from gazebo sim time)\n # Access node clock via self.get_clock() or other various\n # time-related functions of rclpy.Node\n # self.use_sim_time()\n
"},{"location":"Tutorials/ROS2/PythonTemplate/#build-test-run","title":"Build, Test, Run","text":"At this point, your new package should build, pass tests, and run against the sim (will connect but do nothing).
It is assumed that you have already installed or built the buoy packages.
From your workspace (e.g. ~/controller_ws
) build your package:
$ colcon build\nStarting >>> mbari_wec_template_py\n--- stderr: mbari_wec_template_py\n/usr/lib/python3/dist-packages/setuptools/command/install.py:34: SetuptoolsDeprecationWarning: setup.py install is deprecated. Use build and pip and other standards-based tools.\n warnings.warn(\n---\nFinished <<< mbari_wec_template_py [0.74s]\n\nSummary: 1 package finished [0.89s]\n 1 package had stderr output: mbari_wec_template_py\n
You may also build only your new controller package (if you have other packages in the workspace) using: $ colcon build --packages-up-to <your_package_name>
Then, source and test:
$ source install/local_setup.bash\n$ colcon test\nStarting >>> mbari_wec_template_py\n--- stderr: mbari_wec_template_py\n\n=============================== warnings summary ===============================\ntest/test_flake8.py::test_flake8\ntest/test_flake8.py::test_flake8\n Warning: SelectableGroups dict interface is deprecated. Use select.\n\n-- Docs: https://docs.pytest.org/en/stable/warnings.html\n---\nFinished <<< mbari_wec_template_py [0.74s]\n\nSummary: 1 package finished [0.87s]\n 1 package had stderr output: mbari_wec_template_py\n
Or, you may test only your new controller package using: $ colcon test --packages-select <your_package_name>
Next, in another terminal run the sim (after sourcing the sim packages of course): $ ros2 launch buoy_gazebo mbari_wec.launch.py
Now, in the previous terminal, launch the empty controller:
$ ros2 launch <your_package_name> controller.launch.py\n
And you should see something similar to:
[INFO] [launch]: Default logging verbosity is set to INFO\n[INFO] [controller-1]: process started with pid [1409887]\n[controller-1] [INFO] [1678130539.867493131] [controller]: Subscribing to <class 'buoy_interfaces.msg._xb_record.XBRecord'> on '/ahrs_data'\n[controller-1] [INFO] [1678130540.031500810] [controller]: Subscribing to <class 'buoy_interfaces.msg._bc_record.BCRecord'> on '/battery_data'\n[controller-1] [INFO] [1678130540.031972332] [controller]: Subscribing to <class 'buoy_interfaces.msg._sc_record.SCRecord'> on '/spring_data'\n[controller-1] [INFO] [1678130540.032390456] [controller]: Subscribing to <class 'buoy_interfaces.msg._pc_record.PCRecord'> on '/power_data'\n[controller-1] [INFO] [1678130540.032810815] [controller]: Subscribing to <class 'buoy_interfaces.msg._tf_record.TFRecord'> on '/trefoil_data'\n[controller-1] [INFO] [1678130540.033268687] [controller]: Subscribing to <class 'buoy_interfaces.msg._xb_record.XBRecord'> on '/xb_record'\n[controller-1] [INFO] [1678130540.033703510] [controller]: Subscribing to <class 'buoy_interfaces.msg._bc_record.BCRecord'> on '/bc_record'\n[controller-1] [INFO] [1678130540.034091374] [controller]: Subscribing to <class 'buoy_interfaces.msg._sc_record.SCRecord'> on '/sc_record'\n[controller-1] [INFO] [1678130540.034467140] [controller]: Subscribing to <class 'buoy_interfaces.msg._pc_record.PCRecord'> on '/pc_record'\n[controller-1] [INFO] [1678130540.034868686] [controller]: Subscribing to <class 'buoy_interfaces.msg._tf_record.TFRecord'> on '/tf_record'\n[controller-1] [INFO] [1678130540.035298496] [controller]: Subscribing to <class 'buoy_interfaces.msg._pb_record.PBRecord'> on '/powerbuoy_data'\n[controller-1] [INFO] [1678130540.286577653] [controller]: /pc_pack_rate_command not available\n[controller-1] [INFO] [1678130540.537643441] [controller]: /sc_pack_rate_command not available\n[controller-1] [INFO] [1678130540.538230613] [controller]: Found all required services.\n
"},{"location":"Tutorials/ROS2/PythonTemplate/#example","title":"Example","text":"An example using this interface will follow in the next tutorial: Linear Damper Example (Python)
"},{"location":"Tutorials/Simulation/RunSimulator/","title":"Running the Simulator","text":""},{"location":"Tutorials/Simulation/RunSimulator/#introduction","title":"Introduction","text":"This tutorial will illustrate how to start the buoy simulation in Gazebo, when the simulation is running, a rendering of the buoy system motions will be visible, and ROS2 messages will be published that represent the buoy systems state. The simulation also provides the same ROS2 services the real buoy does, so will respond to ROS2 messages appropriately.
Subsequent tutorials will illustrate how to view and plot data being published by the simulation, view data logs being generated, and control the simulated buoy with the command-line tool that is available on the buoy.
"},{"location":"Tutorials/Simulation/RunSimulator/#how-to-run","title":"How to Run","text":"To run the simulator, it is necessary to source the workspace in a separate terminal than was used to build the application. Therefore, open a new terminal window and do the following:
$ . ~/mbari_wec_ws/install/setup.bash\n
$ ros2 launch buoy_gazebo mbari_wec.launch.py \n
The Gazebo rendering of the buoy system should become visible and appear as follows:
To start the simulation, press the \"play\" arrow in the lower left, the buoy should start to move in response to incoming waves.
It is also possible to adjust various parameters such as the sea-state, visibility of the rendering, and speed the simulation will run relative to real-time. These topics are covered in a later tutorial.
To view the ROS2 messages and associated data while the simulation runs, proceed to the next tutorial: View ROS2 Messages
"},{"location":"Tutorials/Simulation/RunSimulator/#notes-on-workspaces","title":"Notes on Workspaces","text":"The step of sourcing the workspace is important and must be done in every window that the simulation and associated tools are accessed from. It is possible, and possibly convenient, to put the source command from step 1 above in the .bashrc or similar place if a different shell is being used. This will automatically perform this step anytime a new window is opened. Beware however, the software can not be built from source in a window where this workspace has previously been sourced. So when re-compiling the source often it can be problematic to have this line in .bashrc. When primarily running the simulations without frequent re-building of the software, it is then appropriate to use .bashrc in this way, just remember to take it out before opening a window where you want to do a \"colcon build\".
"},{"location":"Tutorials/Simulation/SimulatorInteractionPbcmd/","title":"Run-Time Control using pbcmd","text":""},{"location":"Tutorials/Simulation/SimulatorInteractionPbcmd/#introduction","title":"Introduction","text":"There is a command interpreter running on the physical buoy that provides some control over the behavior of the buoy while it is deployed. This is a Linux executable that implements a number of commands, accepts appropriate arguments, and issues commands over the CANbus or ROS 2 on the buoy to effect a change of buoy behavior. These commands can also be issued programmatically as described in subsequent tutorials, but pbcmd is the human interface.
This same command and interface is implemented in the simulation environment, so it is possible to change the behavior of the simulated buoy from the command line in the same way as can be done on the physical buoy. However many of the possible commands are not sensible in the simulated environment, so are not implemented, but instead return a message indicating they aren't relevant in simulation. A key example is the command to open and close the heave-cone doors, because the simulator can not change this behavior while running, the command to do so is inactive. Similarly, it makes no sense to turn on and off the electrical ground fault detector that exists on the buoy, but does not exist in simulation.
"},{"location":"Tutorials/Simulation/SimulatorInteractionPbcmd/#pbcmd-usage","title":"pbcmd Usage","text":"Issuing 'pbcmd' at the command prompt provides guidance on the possible commands. In simulation it also indicates which commands are supported in simulation, on the at-sea system, all commands have an effect.
First source the workspace as usual:
$ cd /path/to/workspace\n$ source install/setup.bash\n
Then, run pbcmd
or any of the commands listed in its output:
$ pbcmd\n\npbcmd: Multi-call command Power Buoy dispatcher\nCommands currently supported in Simulation:\n* pump - Spring Controller pump off or on for a time in minutes <= 10\n* valve - Spring Controller valve off or on for a time in seconds <= 12\n* sc_pack_rate - Set the CANBUS packet rate from the spring controller\n* pc_Scale - Set the scale factor\n* pc_Retract - Set the retract factor\n* pc_WindCurr - Set the winding current target\n* pc_BiasCurr - Set the winding current bias\n* pc_PackRate - Set the CANBUS packet rate\n\nCommands currently not supported in Simulation:\n* bender - Sets the state of the bender module\n* reset_battery - Reset battery controller (caution - no args)\n\n* tether - Spring Controller tether power on or off\n* reset_spring - Reset Spring Controller (caution - no args)\n\n* pc_VTargMax - Set the max target voltage\n* pc_ChargeCurrLim - Set the maximum battery charge current\n* pc_DrawCurrLim - Set the maximum battery current draw\n* pc_BattSwitch - Set the battery switch state\n* pc_Gain - Set the gain scheduler gain\n* pc_StdDevTarg - Set the target RPM standard deviation\n\n* tf_SetPos - Open/close the doors in the heave-cone\n* tf_SetActualPos - Open/close the doors in the heave-cone\n* tf_SetMode - Set controller mode\n* tf_SetChargeMode - Set Battery Charge mode\n* tf_SetStateMachine - Set Battery Charge mode\n* tf_SetCurrLim - Set controller current limit\n* tf_WatchDog - Toggle controller watchdog (caution - no args)\n* tf_Reset - Reset Controller (caution - no args)\n\n\nFor help on a command use the command name, i.e. \"bender\";\n\nExcept the reset commands which take no arguments.\n\nDO NOT enter reset_battery and expect to get help. The command will execute!\n
Note that at the end of this usage message, there is a hint that typing most commands without an argument will supply some further help.
"},{"location":"Tutorials/Simulation/SimulatorInteractionPbcmd/#command-descriptions","title":"Command descriptions","text":"pump - This command turns on and off the gas pump that pumps Nitrogen from the upper pneumatic spring chamber to the lower spring chamber. The result of this is the mean position of the position will slowly rise. The rate is about 1 inch per minute of pump action, a required argument for this command specifies how long the pump will run for, in minutes, and must be between 0 and 10. After this timeout the pump will stop even if no further commands are issued. A \"pump 0\" command stops the pump immediately.
valve - The valve command releases Nitrogen gas from the lower chamber to the upper chamber, resulting in the mean position of the piston lowering. This process is much faster so the required argument for this command is in seconds, and must be between 0 and 10. After this timeout the valve closes even if no further commands are issued. A \"valve 0\" command closes the pump immediately.
sc_pack_rate - This command sets the data packet rate for data coming from the spring controller, the required argument between 10 and 50 indicates the desired data rate in Hz. This controls the rate of the ROS 2 messages from the spring controller on the buoy, and in the simulator.
pc_scale - This command adjusts the multiplier that is applied to the default motor-current/RPM relationship that is programmed into the power converter. This value can be from 0.4 to 1.5, allowing a range of damping to be applied.
pc_Retract - This command adjusts an additional multiplier that is applied to the default motor-current/RPM relationship during piston retraction. This value can be from 0.4 to 1.0, and allows the damping behavior of the system to be asymmetrical, promoting retraction since the pneumatic spring can not pull as hard as the waves can.
pc_WindCurr - This command directly sets the winding current in the electric motor and accepts a value between -35 Amps and +35 Amps. This value is the quadrature current in the permanent magnet electric motor, and therefore corresponds directly with applied torque. A positive winding current produces a torque that applies a force that retracts the piston. The controller applies this specified torque for two seconds. After that time, if no new pc_WindCurr command is executed, the system returns to following the default motor-current/RPM relationship (adjusted by the Scale and Retract factor as described). This allows safety in the case of a communication failure, and makes it a bit impractical to manipulate the winding current manually from the keyboard. As described in subsequent tutorials, programmatically adjusting this value in response to the behavior of the wave-energy converter is the primary automated external control mechanism.
pc_BiasCurr - This command applies an offset to the default motor-current/RPM relationship. This value can be between -15 Amps and +15 Amps and is applied for 10 seconds before the system reverts to the default motor-current/RPM relationship. A positive current corresponds to a torque that tends to retract the piston. This command is useful for temporarily changing the equilibrium point of the piston at sea.
pc_pack_rate - This command sets the data packet rate for data coming from the power converter, the required argument between 10 and 50 indicates the desired data rate in Hz. This controls the rate of the ROS 2 messages from the power converter on the buoy, and in the simulator.
As an example, issue the following commands in a terminal where the workspace has been sourced:
Launch the simulation without incident waves by issuing the following commands, the first command over-rides the default sea-state and results in no incident wave-forcing on the buoy when the \"regenerate_models\" flag is set to false:
$ empy -D 'inc_wave_spectrum_type=\"None\"' -o ~/mbari_wec_ws/install/buoy_description/share/buoy_description/models/mbari_wec/model.sdf ~/mbari_wec_ws/install/buoy_description/share/buoy_description/models/mbari_wec/model.sdf.em\n\n$ ros2 launch buoy_gazebo mbari_wec.launch.py regenerate_models:=false\n
Start the simulation in the GUI by pressing the play button.
Start PlotJuggler
$ ros2 run plotjuggler plotjuggler &\n
Select messages wcurrent and rpm from the /power_data topic and the range_finder message from the /spring_data topic, and then create plots to display these messages in separate windows.
Issue the following command to introduce a 10A winding current offset in the motor-current/RPM relationship.
$ pc_BiasCurr 10\n
After 20 seconds of simulation time or so, the plotjuggler window should look approximately as below. One can see that the command resulted in an additional 10 Amps of motor winding current being present (time = 6 seconds in plot), this additional torque spins the motor and a force is applied to retract the piston, which is evident in the range_finder data.
After a 10 second timeout, because no new current over-ride command is issued, the system reverts to the default motor-current/RPM relationship that doesn't include the offset (time = 16 seconds in plot). After this extra torque is removed, the weight of the heave cone causes the piston to extend back to it's nominal mean position causing the motor to spin in the opposite direction (time = 16-20 seconds in the plot). The default motor-current/RPM relationship is programmed to resist this motion and a smaller positive motor-current is applied during this time.
After time = 21 seconds, the energy of the raised heave-cone has been dissipated into the PTO system and the system comes mostly to rest. The slow creep of the piston beyond 21 seconds is due to the heating effects of the pneumatic spring, heat has been created in the gas spring which slowly dissipates to the environment and the lower spring pressure drops slowly and lowers the piston gradually.
To extend the previous example, some interesting exercises to try are the following:
Repeat the above example but also plot the battery voltage and current. Observe how current flows from the battery to raise the piston and heave cone when the pc_BiasCurr 10 command is issued, and then when the timeout occurs, the potential energy of the raised heave cone is converted to electrical energy and current flows into the battery, with a commensurate change in battery voltage.
Plot the upper and lower spring pressures, observer how they change relative to piston position, and their decay in time after the piston comes to rest.
Repeatedly issue the \"pc_BiasCurr 10\" command before the timeout expires, note how the timeout is extended to 10 seconds beyond the last command issued.
Issue a \"pc_WindCurr 10\" command instead of the pc_BiasCurr command. This command directly sets the winding current, so the resulting winding current is not affected by the motor RPM until after the timeout has expired. For this command, the timeout is two seconds.
Plot other messages while manipulating the winding current. Note how the buoy and heave-cone positions as well as the load cell value respond to the dynamics of the floating bodies.
\"PlotJuggler\" is a plotting program that includes support for ROS 2 messages, and allows real-time plotting of data from ROS 2 messages while the simulator runs, as well as plotting of logged data.
"},{"location":"Tutorials/Simulation/SimulatorOutputPlotjuggler/#installation-and-running","title":"Installation and Running","text":"To install, see instructions here. If using the supplied docker images, this step is not necessary as the software is already installed.
To start, issue the following command in a window where the environment has already been sourced using $ . ~/mbari_wec_ws/install/setup.bash
:
$ ros2 run plotjuggler plotjuggler \n
In the window on the left that shows the available topics, select the /arhs_data, /power_data, /spring_data, battery_data, heavecone_data and /xb_data topics and click \"OK\". Note that for these topics to be available, the simulation needs to be running.
The selected topics will appear in the \"Timeseries List\" window, and selecting the carrot to the left of each topic will expand them and show the data that can be plotted. Note that these topics and data are the same as are visible using the $ ros2 topic list
and $ ros2 topic echo
commands from the command-line.
Dragging any data item into the plot field on the right will plot that data on a scrolling graph. The time-extent of the graph can be changed using the \"Buffer\" text-box under the \"Streaming\" box in the upper left. Graphs can be split horizontally and vertically to make room for more data items, see this guide for information on manipulating the PlotJuggler windows.
After a bit of data selection, the window can look like the example below and show many data items in real-time. Under the \"File\" box in the upper left, there are options to save and retrieve this layout to avoid setting up the windows at each invocation of PlotJuggler. PlotJuggler will continue to run through re-starts of the simulator, so it is often not necessary to re-start PlotJuggler often.
While running, the simulator generates exactly the same ROS 2 messages that the buoy hardware does during operation. These are grouped into ROS 2 topics that corresponds to data being produced by each micro-controller or instrument on the buoy. To see all ROS 2 topics being published to on the system, issue the following command (after sourcing the workspace if needed in a new terminal $ . ~/mbari_wec_ws/install/setup.bash
$ ros2 topic list \n/ahrs_data\n/clock\n/joint_states\n/parameter_events\n/power_data\n/rosout\n/spring_data\n/tf\n/tf_static\n/xb_imu\n
The topics /ahrs_data, /battery_data, /spring_data, /power_data, and /heavecone_data corresponds to the buoy-based instrumentation (AHRS), battery controller, spring controller, power-converter controller, and heave-cone controller. Several of these topics are only available in simulation, and only /ahrs_data, /battery_data, /spring_data, /power_data, and /heavecone_data will be present on the real buoy.
To see the data being published in these topics, issue the following command and the data will scroll by, for example:
$ ros2 topic echo power_data\n---\nheader:\n stamp:\n sec: 712\n nanosec: 710000000\n frame_id: ''\nseq_num: 6703\nrpm: 369.927978515625\nsd_rpm: 0.0\nvoltage: 313.98431396484375\ndraw_curr_limit: 0.0\nbcurrent: -0.14509780704975128\nwcurrent: -0.2554447054862976\ntorque: 0.0\ndiff_press: 2.9100000858306885\nbias_current: 0.0\nloaddc: 0.0\nscale: 1.0\nretract: 0.6000000238418579\ntarget_v: 0.0\ntarget_a: -0.2554447054862976\ncharge_curr_limit: 0.0\nstatus: 0\n---\n
The data in each topic corresponds to the message descriptions which can be seen here along with a description of each field.
The next tutorial \"View Messages with Plotjuggler\" shows how to conveniently plot these data items while the simulator is running.
"},{"location":"Tutorials/Simulation/SimulatorParameters/","title":"Parameters and Batch Runs","text":""},{"location":"Tutorials/Simulation/SimulatorParameters/#introduction","title":"Introduction","text":"When running a simple instance of the simulator as described in the Run the Simulator Tutorial. i.e. using:
$ ros2 launch buoy_gazebo mbari_wec.launch.py\n
the simulation uses a number of defaults for a range of parameters. In most cases, one may want to run the simulator with different values for these parameters, and/or run a number of simulations that iterate across a range of values for particular parameters. For instance, one may want to run the simulator in a batch mode that runs the same buoy and controller set-up in a range of sea-states.
To facilitate this, a batch tool is provided that allows one to specify ranges for a number of parameters, and then run a number of simulations for all combinations of parameters, saving the results separately.
This tutorial describes these capabilities, demonstrates with some examples, and discusses how this tool can be used.
"},{"location":"Tutorials/Simulation/SimulatorParameters/#parameters","title":"Parameters","text":"There are a number of parameters that impact the behavior of the simulator, and must be specified at start-up:
The above parameters are specified in a .yaml file that the batch-run tool reads in before execution begins. A commented example is below and illustrates the use of the above parameters. Lines that begin with # are comments and have no impact.
#\n# Batch-Specific Scalar Parameters\n#\nduration: 300\nseed: 42\nphysics_rtf: 11\n#\n# Run-Specific Parameters (Test Matrix)\n#\nphysics_step: [0.001, 0.01, 0.1]\ndoor_state: ['closed', 'open']\nscale_factor: [0.5, 0.75, 1.0, 1.3, 1.4]\nenable_gui: False\n# May specify vector/scalar battery_soc (0.0 to 1.0) or battery_emf (270V to 320V)\nbattery_soc: [0.25, 0.5, 0.75, 1.0]\n# battery_emf: [282.5, 295.0, 307.5, 320.0]\nIncidentWaveSpectrumType:\n - MonoChromatic:\n # A & T may be vector/scalar in pairs (A & T same length)\n A: [1.0, 2.0, 3.0]\n T: [12.0, 14.0, 15.0]\n - Bretschneider:\n # Hs & Tp may be vector/scalar in pairs (Hs & Tp same length)\n Hs: 3.0\n Tp: 14.0\n # Multiple Custom Spectra must be listed individually (f & Szz are already vectors of same size)\n - Custom:\n f: [0.0, 0.2, 0.4, 0.6, 2.0]\n Szz: [0.0, 0.4, 1.0, 1.0, 0.0]\n
As seen in this example, some parameters are enforced to be scalar values and apply to the entire batch of specified runs. These are the specification of simulation duration (300), random seed (42), and the physics real-time factor (11).
The remaining run-specific parameters can be specified as arrays, and the batch-run tool then executes simulations for all possible combinations of these values. Note that some values are specified in pairs. For instance, three mono-chromatic waves are specified by this file with a specification of (A=1.0m, T=12s), (A=2.0m, T=14s), and (A=3.0m, T=15s), not nine runs that include all possible combinations of the specified amplitude and periods.
Obviously it would be very easy to write a batch file specification that includes thousands of runs, more practical usage will most likely iterate over a small number of parameters at at a time.
"},{"location":"Tutorials/Simulation/SimulatorParameters/#running-an-example","title":"Running an example","text":"For a simpler example, a batch file that iterates across a range of sea-states is used. As a concise example, the following file illustrates this, comments have been removed for brevity.
duration: 3\nseed: 42\nphysics_rtf: 11\nenable_gui: False\nphysics_step: 0.01\ndoor_state: ['closed']\nscale_factor: [0.6, 1.0]\nbattery_soc: 0.5\nIncidentWaveSpectrumType:\n - Bretschneider:\n Hs: [2.0, 4.0]\n Tp: [14.0, 16.0]\n
To run this example, create the above file in a new directory and name it \"IrregularWaves.yaml\", source the simulator installation directory, and start the simulation using the batch tool. (Note that the run duration is very short for this example to allow it to complete quickly)
$ mkdir FOO\n$ cd FOO\n$ Create file using editor of your choice, name it IrregularWaves.yaml\n$ . ~/mbari_wec_ws/install/setup.bash\n$ ros2 launch buoy_gazebo mbari_wec_batch.launch.py sim_params_yaml:=IrregularWaves.yaml\n
Running these commands will run the simulation, all output is stored in a directory named similar to `batch_results_20230228210735', the trailing numbers indicate a timestamp. Inside this directory the yaml file is repeated, along with a log file 'batch_results.log' that lists all of the simulation runs that were performed. Alongside that are specific directories that hold output from each run. For convenience, a symbolic link is formed that points at the most recent batch output directory.
Within the output directory, there is a file named 'batch_runs.log' that shows each individual run that was performed, and the associated parameter. In this case it has the following contents:
# Generated 4 simulation runs\nRunIndex, SimReturnCode, StartTime, rosbag2FileName, PhysicsStep, PhysicsRTF, Seed, Duration, DoorState, ScaleFactor, BatteryState, IncWaveSpectrumType;In\ncWaveSpectrumParams\n0, 0, 20230228212457, rosbag2_batch_sim_0_20230228212457, 0.01, 11.0, 42, 3.0, closed, 0.6, 0.5, Bretschneider;Hs:2.0;Tp:14.0\n1, 0, 20230228212502, rosbag2_batch_sim_1_20230228212502, 0.01, 11.0, 42, 3.0, closed, 0.6, 0.5, Bretschneider;Hs:4.0;Tp:16.0\n2, 0, 20230228212506, rosbag2_batch_sim_2_20230228212506, 0.01, 11.0, 42, 3.0, closed, 1.0, 0.5, Bretschneider;Hs:2.0;Tp:14.0\n3, 0, 20230228212510, rosbag2_batch_sim_3_20230228212510, 0.01, 11.0, 42, 3.0, closed, 1.0, 0.5, Bretschneider;Hs:4.0;Tp:16.0\n
For simulations that take longer to run, it can be convenient to tail this log file from the terminal to keep track of progress. i.e. From the directory the batch was started from:
$ tail -f latest_batch_results/batch_runs.log\n
"},{"location":"Tutorials/Simulation/SimulatorParameters/#finding-the-output","title":"Finding the output","text":"Within the batch process output directory, (e.g. `batch_results_20230228210735'), the output of each simulation run is stored within a single sub-directory. The resulting directory tree from the above example is as follows:
$ tree batch_results_20230301200627/\nbatch_results_20230301200627/\n\u251c\u2500\u2500 batch_runs.log\n\u251c\u2500\u2500 IrregularWaves_20230301200627.yaml\n\u251c\u2500\u2500 results_run_0_20230301200627\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 rosbag2\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 metadata.yaml\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 rosbag2_0.db3\n\u251c\u2500\u2500 results_run_1_20230301200632\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 rosbag2\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 metadata.yaml\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 rosbag2_0.db3\n\u251c\u2500\u2500 results_run_2_20230301200636\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 rosbag2\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 metadata.yaml\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 rosbag2_0.db3\n\u2514\u2500\u2500 results_run_3_20230301200640\n \u2514\u2500\u2500 rosbag2\n \u251c\u2500\u2500 metadata.yaml\n \u2514\u2500\u2500 rosbag2_0.db3\n
This output includes rosbag files and .csv files that are in the same format as the files generated on the physical buoy. In general the information in these two files are the same, the .csv files are in clear text and are easy to inspect, and can be processed by the same tools used for the actual buoy data. The rosbag files are binary files that encode all of the ROS2 messages on the computer during the simulation. These files can be processed by a number of tools for post-processing and inspection of results. It is also possible to load the rosbag files into plotjuggler for plotting and inspection, as described in the View Messages with Plotjuggler Tutorial.
"}]} \ No newline at end of file diff --git a/main/simulation/index.html b/main/simulation/index.html index f2f284b2..e0c369b8 100644 --- a/main/simulation/index.html +++ b/main/simulation/index.html @@ -345,6 +345,48 @@ + + + + +A computer simulation environment has created that that emulates the ROS 2 messages used on the buoy system, and connects them to a numerical simulation of the system dynamics and behavior of the electro-hydraulic Power-Take-Off device. The aim of this simulation is to provide a computer based simulation that for most behaviors, interacts with computer code in the same way the physical buoy system does. The aim is that code running on a desktop Linux machine can run unchanged on the physical buoy, and that the response of the simulated buoy is indistinguishable from the real system. The following sections provide some details of this simulator, and additional details are in the theory section.
+A computer simulation environment has been created that simulates the dynamics of the wave-energy converter and emulates the ROS 2 messages used on the buoy system. This simulation includes a solver of the multi-body dynamics of the system, a numerical representation of the behavior and losses of the electro-hydraulic PTO, and a numerical simulation of the wave-buoy interaction processes based on linear hydrodynamic theory.
+The aim of this simulation is to provide a computer based simulation that replicates the physical systems behavior and also interacts with computer code in the same way the physical buoy system does. The aim is that code running on a desktop Linux machine can run unchanged on the physical buoy, and that the response of the simulated buoy is indistinguishable from the real system. The following sections provide some details of this simulator, and additional specifics are in the theory section.
The simulation environment here is built upon the popular Gazebo Simulator that is supported by the Open Source Robotics Foundation. The core of this simulator is provided by a physics engine that is responsible for solving the 6 degree-of-freedom multi-body equations of motion. This engine determines the position in space solution of all the bodies in a simulation, subject to the forces that are acting on those bodies in any particular environment. This multi-body simulator also can enforce constraints on, and joints between, the bodies, allowing one to develop simulations in which two bodies are only allowed to slide by one another in one degree of freedom, for instance. Gazebo includes a number of forcings that are typical in robotics, gravity, contact forces between bodies, hydrodynamic drag forces, etc. Also, when used with the DART physics engine, Gazebo can include a general added-mass matrix which adds inertial forcing on specified bodies due to the tendency of the fluid surrounding a body to be accelerated along with the body. This is a critically important feature in underwater robotics and simulation efforts. Of course, all the required forcings to
+In particular, this project has created plugins that provide forcing on the components of wave-energy converter system that are specific to this device and application. These plugins are loaded at run-time so exist independently of the main Gazebo software, but behave in an integrated way during simulations. The plug-in code described in the following sections implement specific forcings needed by this project.
+As described in the Architecture section, the MBARI wave-energy converter implements a custom developed electro-hydraulic power take-off device that converts mechanical power of a moving piston rod (force times velocity) to/from electrical energy that is supplied to or drawn from a battery system (voltage times current). This device consists of a linear hydraulic ram that is connected to a rotary hydraulic motor, linear motion of the ram translates to rotary motion of a shaft, and vice versa. The hydraulic motor shaft is directly coupled to a permanent magnet servo motor/driver. The power electronics maintains a commanded torque on the motor which results from current flowing to/from the battery system as appropriate. The Gazebo plugin developed to simulate this device takes in the piston velocity and commanded torque, and computes the resulting hydraulic pressure, motor RPM, and battery voltage and current. The plugin is programmed with the same default motor-current/RPM relationship as the physical system, and also respects the same limits on motor and battery current, motor RPMs, etc. A key simplifying assumption made in this model is that the rotary inertia of the motor shaft is negligible, and the hydraulic oil is incompressible. These assumptions cause the model of the PTO to be a quasi-static solution that neglects inertia forces of the rotary components of the motor. This has been shown to be a reasonable assumption at the motion frequencies the system operates at, and greatly increases the speed at which the solution progresses. The inertia of the piston itself is included in the simulation as it is an independent body in the equations of motion, constrained to move linearly with respect to the PTO housing.
+