From 18560e13bd2cecff664619116513835801b5f3e3 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 08:26:15 +0000 Subject: [PATCH 1/9] Add closed-loop Goby runs of MOOS-IvP missions ivpico_mission_check pokes a mission and checks the helm's postings and state while goby_helm_ivp flies the mission's recorded .bhv against Goby's basic frontseat simulator (time warp 10). run.sh brings up gobyd, the simulator, the frontseat interface and the helm for one mission. ctest closed_loop_ covers s1_alpha, s2_bravo, s3_charlie and s4_delta. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VLcYK4e29B5FiPbDDJy5fp --- src/test/CMakeLists.txt | 11 ++ src/test/goby/CMakeLists.txt | 1 + src/test/goby/mission_check/CMakeLists.txt | 7 + src/test/goby/mission_check/mission_check.cpp | 157 ++++++++++++++++++ .../mission_check/mission_check_config.proto | 35 ++++ src/test/missions/README.md | 18 ++ src/test/missions/goby/run.sh | 78 +++++++++ src/test/missions/goby/s1_alpha/check.pb.cfg | 5 + src/test/missions/goby/s1_alpha/helm.pb.cfg | 5 + src/test/missions/goby/s2_bravo/check.pb.cfg | 6 + src/test/missions/goby/s2_bravo/helm.pb.cfg | 5 + .../missions/goby/s3_charlie/check.pb.cfg | 5 + src/test/missions/goby/s3_charlie/helm.pb.cfg | 5 + src/test/missions/goby/s4_delta/check.pb.cfg | 6 + src/test/missions/goby/s4_delta/helm.pb.cfg | 6 + 15 files changed, 350 insertions(+) create mode 100644 src/test/goby/CMakeLists.txt create mode 100644 src/test/goby/mission_check/CMakeLists.txt create mode 100644 src/test/goby/mission_check/mission_check.cpp create mode 100644 src/test/goby/mission_check/mission_check_config.proto create mode 100755 src/test/missions/goby/run.sh create mode 100644 src/test/missions/goby/s1_alpha/check.pb.cfg create mode 100644 src/test/missions/goby/s1_alpha/helm.pb.cfg create mode 100644 src/test/missions/goby/s2_bravo/check.pb.cfg create mode 100644 src/test/missions/goby/s2_bravo/helm.pb.cfg create mode 100644 src/test/missions/goby/s3_charlie/check.pb.cfg create mode 100644 src/test/missions/goby/s3_charlie/helm.pb.cfg create mode 100644 src/test/missions/goby/s4_delta/check.pb.cfg create mode 100644 src/test/missions/goby/s4_delta/helm.pb.cfg diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index 105aa71..d310c54 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -17,6 +17,17 @@ foreach(fixture ${fixtures}) endforeach() if(GOBY_FOUND) + add_subdirectory(goby) + + set(simulator_port 54400) + foreach(mission s1_alpha s2_bravo s3_charlie s4_delta) + add_test(NAME closed_loop_${mission} + COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/missions/goby/run.sh ${mission} + $ $ ${simulator_port}) + set_tests_properties(closed_loop_${mission} PROPERTIES TIMEOUT 180) + math(EXPR simulator_port "${simulator_port} + 1") + endforeach() + add_test(NAME goby_helm_ivp COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/goby/test_goby_helm_ivp.sh $ $) diff --git a/src/test/goby/CMakeLists.txt b/src/test/goby/CMakeLists.txt new file mode 100644 index 0000000..2ed8d7b --- /dev/null +++ b/src/test/goby/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(mission_check) diff --git a/src/test/goby/mission_check/CMakeLists.txt b/src/test/goby/mission_check/CMakeLists.txt new file mode 100644 index 0000000..4b23823 --- /dev/null +++ b/src/test/goby/mission_check/CMakeLists.txt @@ -0,0 +1,7 @@ +goby_add_app(TARGET ivpico_mission_check + SOURCES mission_check.cpp + PROTOS mission_check_config.proto + PROTOC_OUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/proto + PROTO_IMPORT_DIRS ${PROJECT_SOURCE_DIR}/src/lib ${openocean_messages_PROTO_DIR} + LINK_LIBRARIES goby_zeromq ivpico_interface) +target_include_directories(ivpico_mission_check PRIVATE ${PROJECT_SOURCE_DIR}/src/bin/goby) diff --git a/src/test/goby/mission_check/mission_check.cpp b/src/test/goby/mission_check/mission_check.cpp new file mode 100644 index 0000000..caf0311 --- /dev/null +++ b/src/test/goby/mission_check/mission_check.cpp @@ -0,0 +1,157 @@ +// Drives a closed-loop mission run of goby_helm_ivp and checks how it went. +// +// Once the helm reports PARK or DRIVE and the frontseat has reported nav, +// mission time starts. The checker +// publishes each configured poke (e.g. DEPLOY=true) as a Variable at its +// time, watches the helm's Postings for each expected key = value before its +// deadline, and fails at once if the helm is MALCONFIG or halts with a +// behavior error. After `duration` it exits 0 if every expectation was met +// (and, with drive_at_end, the helm is still in DRIVE), and 1 otherwise. + +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include "groups.h" +#include "interface/ivp.pb.h" +#include "mission_check_config.pb.h" + +using goby::glog; + +namespace ivpico +{ +namespace goby +{ +class MissionCheck : public ::goby::zeromq::SingleThreadApplication +{ + public: + MissionCheck() : SingleThreadApplication(10 * boost::units::si::hertz) + { + interprocess().subscribe([this](const ivp::interface::Posting& posting) + { handle(posting); }); + interprocess().subscribe([this](const ivp::interface::Status& status) + { handle(status); }); + interprocess().subscribe<::goby::middleware::frontseat::groups::node_status>( + [this](const ::goby::middleware::frontseat::protobuf::NodeStatus&) + { + have_nav_ = true; + maybe_start(); + }); + } + + private: + double now() const { return ::goby::time::SystemClock::now<::goby::time::SITime>().value(); } + + void handle(const ivp::interface::Posting& posting) + { + if (start_ < 0) + return; + const std::string value = posting.value_case() == ivp::interface::Posting::kSval + ? posting.sval() + : number(posting.dval()); + for (int i = 0; i < cfg().expect_posting_size(); ++i) + { + const auto& expect = cfg().expect_posting(i); + if (!met_.count(i) && expect.key() == posting.key() && same(expect.value(), value)) + { + met_.insert(i); + glog.is_verbose() && glog << "met " << expect.ShortDebugString() << " at " + << now() - start_ << " s" << std::endl; + } + } + } + + void handle(const ivp::interface::Status& status) + { + state_ = status.state(); + maybe_start(); + if (state_ == ivp::interface::Status::MALCONFIG) + finish(false, "helm is MALCONFIG"); + else if (status.allstop() == ivp::interface::BEHAVIOR_ERROR) + finish(false, "behavior error: " + status.halt_message()); + } + + void maybe_start() + { + if (start_ < 0 && have_nav_ && + (state_ == ivp::interface::Status::PARK || state_ == ivp::interface::Status::DRIVE)) + start_ = now(); + } + + void loop() override + { + if (start_ < 0) + return; + const double t = now() - start_; + for (int i = 0; i < cfg().poke_size(); ++i) + { + const auto& poke = cfg().poke(i); + if (!poked_.count(i) && t >= poke.at()) + { + ivp::interface::Variable variable; + variable.set_key(poke.key()); + variable.set_sval(poke.value()); + interprocess().publish(variable); + poked_.insert(i); + } + } + for (int i = 0; i < cfg().expect_posting_size(); ++i) + { + const auto& expect = cfg().expect_posting(i); + if (!met_.count(i) && t > expect.before()) + finish(false, "no " + expect.key() + " = " + expect.value() + " by " + + number(expect.before()) + " s"); + } + if (t >= cfg().duration()) + { + if (cfg().drive_at_end() && state_ != ivp::interface::Status::DRIVE) + finish(false, "helm not in DRIVE at the end"); + else + finish(true, ""); + } + } + + void finish(bool ok, const std::string& why) + { + if (done_) + return; + done_ = true; + std::cout << (ok ? "PASSED" : "FAILED: " + why) << std::endl; + quit(ok ? 0 : 1); + } + + static std::string number(double d) + { + std::ostringstream os; + os << d; + return os.str(); + } + + static bool same(const std::string& expected, const std::string& value) + { + char* end_a = nullptr; + char* end_b = nullptr; + const double a = std::strtod(expected.c_str(), &end_a); + const double b = std::strtod(value.c_str(), &end_b); + if (*end_a == '\0' && *end_b == '\0' && !expected.empty() && !value.empty()) + return std::fabs(a - b) < 1e-6; + return expected == value; + } + + double start_{-1}; + ivp::interface::Status::State state_{ivp::interface::Status::STATE_UNKNOWN}; + std::set poked_, met_; + bool have_nav_{false}; + bool done_{false}; +}; +} // namespace goby +} // namespace ivpico + +int main(int argc, char* argv[]) { return goby::run(argc, argv); } diff --git a/src/test/goby/mission_check/mission_check_config.proto b/src/test/goby/mission_check/mission_check_config.proto new file mode 100644 index 0000000..1822248 --- /dev/null +++ b/src/test/goby/mission_check/mission_check_config.proto @@ -0,0 +1,35 @@ +syntax = "proto2"; + +import "goby/middleware/protobuf/app_config.proto"; +import "goby/zeromq/protobuf/interprocess_config.proto"; + +package ivpico.goby.protobuf; + +message MissionCheckConfig +{ + optional .goby.middleware.protobuf.AppConfig app = 1; + optional .goby.zeromq.protobuf.InterProcessPortalConfig interprocess = 2; + + // mission seconds to run, from when the helm is first in PARK or DRIVE + required double duration = 10; + + // published as a Variable at this many mission seconds + message Poke + { + required double at = 1; + required string key = 2; + required string value = 3; + } + repeated Poke poke = 11; + + // a Posting (by a behavior, a mode, initialize, ...) of key = value + message ExpectPosting + { + required string key = 1; + required string value = 2; + required double before = 3; + } + repeated ExpectPosting expect_posting = 12; + + optional bool drive_at_end = 13 [default = false]; +} diff --git a/src/test/missions/README.md b/src/test/missions/README.md index 5e0b6e8..b0e87e6 100644 --- a/src/test/missions/README.md +++ b/src/test/missions/README.md @@ -17,3 +17,21 @@ flowchart LR - `replay ` feeds the recorded inputs to the IvPico helm at pHelmIvP's iteration times and compares what each helm posted. `replay.cpp` describes what's compared and the tolerances. Re-record the fixtures whenever `external/moos-ivp` moves. + +## Closed-loop runs under Goby + +*This section was written by Claude.* + +`goby/run.sh ...` (ctest: `closed_loop_`) flies a mission's recorded `.bhv` with `goby_helm_ivp` against Goby's basic frontseat simulator, with time warp 10: + +```mermaid +flowchart LR + check["ivpico_mission_check"] -- "pokes (Variable)" --> helm["goby_helm_ivp"] + helm -- "Posting, Status" --> check + helm -- "DesiredCourse" --> fs["goby_frontseat_interface
+ basic simulator"] + fs -- "NodeStatus" --> helm +``` + +- `goby//helm.pb.cfg` is the helm configuration (the `.moos` file's pHelmIvP block). +- `goby//check.pb.cfg` lists the pokes, the postings expected by a deadline, and the mission duration. Mission time starts once the helm is running and has nav. +- `IVPICO_KEEP_RUN=1` keeps the run's configs and logs. diff --git a/src/test/missions/goby/run.sh b/src/test/missions/goby/run.sh new file mode 100755 index 0000000..a33934d --- /dev/null +++ b/src/test/missions/goby/run.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# Runs one MOOS-IvP mission closed loop under Goby: gobyd, goby's basic +# frontseat simulator and goby_frontseat_interface, goby_helm_ivp on the +# mission's recorded .bhv (../fixtures/), and ivpico_mission_check, +# which pokes the mission and checks /check.pb.cfg. Time warp 10. +# +# Usage: run.sh +set -euo pipefail +# own process group per background job, so cleanup also reaches their children +set -m + +here=$(cd "$(dirname "$0")" && pwd) +mission=$1 helm=$2 check=$3 port=$4 +work=$(mktemp -d) +platform="ivpico_${mission}_$$" +pids=() +cleanup() { + for pid in "${pids[@]}"; do kill -- "-${pid}" 2> /dev/null || true; done + wait 2> /dev/null || true + [[ -n "${IVPICO_KEEP_RUN:-}" ]] && echo "kept ${work}" >&2 || rm -rf "${work}" +} +trap cleanup EXIT + +common="app { + geodesy { lat_origin: 43.8253 lon_origin: -70.3304 } + simulation { time { use_sim_time: true warp_factor: 10 } } +} +interprocess { platform: \"${platform}\" }" + +echo "${common}" > "${work}/gobyd.pb.cfg" +cat > "${work}/frontseat.pb.cfg" << EOF2 +${common} +frontseat_cfg { + require_helm: true + helm_running_timeout: 10 + frontseat_connected_timeout: 10 + status_period: 5 + exit_on_error: false + [goby.middleware.frontseat.protobuf.basic_simulator_config] { + tcp_address: "127.0.0.1" + tcp_port: ${port} + start { + lat: 43.8253 + lon: -70.3304 + duration: 0 + control_freq: 10 + vehicle { accel: 1.0 hdg_rate: 50 z_rate: 2 } + } + } +} +EOF2 +{ + echo "${common}" + sed "s|@FIXTURE@|${here}/../fixtures/${mission}|" "${here}/${mission}/helm.pb.cfg" + echo "frontseat { enable: true }" +} > "${work}/helm.pb.cfg" +{ + echo "${common}" + cat "${here}/${mission}/check.pb.cfg" +} > "${work}/check.pb.cfg" + +goby_basic_frontseat_simulator "${port}" > "${work}/simulator.log" 2>&1 & +pids+=($!) +gobyd "${work}/gobyd.pb.cfg" > "${work}/gobyd.log" 2>&1 & +pids+=($!) +sleep 1 +goby_frontseat_interface_basic_simulator "${work}/frontseat.pb.cfg" > "${work}/frontseat.log" 2>&1 & +pids+=($!) +"${helm}" "${work}/helm.pb.cfg" > "${work}/helm.log" 2>&1 & +pids+=($!) + +if ! "${check}" "${work}/check.pb.cfg" -v > "${work}/check.log" 2>&1; then + grep -v "^\s*$" "${work}/check.log" | tail -15 >&2 + echo "goby_helm_ivp log:" >&2 + tail -15 "${work}/helm.log" >&2 + exit 1 +fi +grep -E "met |PASSED" "${work}/check.log" diff --git a/src/test/missions/goby/s1_alpha/check.pb.cfg b/src/test/missions/goby/s1_alpha/check.pb.cfg new file mode 100644 index 0000000..5f13876 --- /dev/null +++ b/src/test/missions/goby/s1_alpha/check.pb.cfg @@ -0,0 +1,5 @@ +duration: 480 +poke { at: 5 key: "DEPLOY" value: "true" } +poke { at: 5 key: "MOOS_MANUAL_OVERRIDE" value: "false" } +# recorded at 370 s +expect_posting { key: "RETURN" value: "true" before: 460 } diff --git a/src/test/missions/goby/s1_alpha/helm.pb.cfg b/src/test/missions/goby/s1_alpha/helm.pb.cfg new file mode 100644 index 0000000..b599a43 --- /dev/null +++ b/src/test/missions/goby/s1_alpha/helm.pb.cfg @@ -0,0 +1,5 @@ +helm { + behavior_file: "@FIXTURE@/alpha.bhv" + domain { name: "speed" low: 0 high: 4 points: 41 } + domain { name: "course" low: 0 high: 359 points: 360 } +} diff --git a/src/test/missions/goby/s2_bravo/check.pb.cfg b/src/test/missions/goby/s2_bravo/check.pb.cfg new file mode 100644 index 0000000..d4af3c9 --- /dev/null +++ b/src/test/missions/goby/s2_bravo/check.pb.cfg @@ -0,0 +1,6 @@ +duration: 300 +poke { at: 5 key: "DEPLOY" value: "true" } +poke { at: 5 key: "MOOS_MANUAL_OVERRIDE" value: "false" } +expect_posting { key: "MODE" value: "ACTIVE:SURVEYING" before: 10 } +# recorded at 214 s +expect_posting { key: "RETURN" value: "true" before: 270 } diff --git a/src/test/missions/goby/s2_bravo/helm.pb.cfg b/src/test/missions/goby/s2_bravo/helm.pb.cfg new file mode 100644 index 0000000..1a686b0 --- /dev/null +++ b/src/test/missions/goby/s2_bravo/helm.pb.cfg @@ -0,0 +1,5 @@ +helm { + behavior_file: "@FIXTURE@/bravo.bhv" + domain { name: "speed" low: 0 high: 4 points: 21 } + domain { name: "course" low: 0 high: 359 points: 360 } +} diff --git a/src/test/missions/goby/s3_charlie/check.pb.cfg b/src/test/missions/goby/s3_charlie/check.pb.cfg new file mode 100644 index 0000000..84cece2 --- /dev/null +++ b/src/test/missions/goby/s3_charlie/check.pb.cfg @@ -0,0 +1,5 @@ +duration: 200 +poke { at: 5 key: "DEPLOY" value: "true" } +poke { at: 5 key: "MOOS_MANUAL_OVERRIDE" value: "false" } +expect_posting { key: "MODE" value: "ACTIVE:LOITERING" before: 10 } +drive_at_end: true diff --git a/src/test/missions/goby/s3_charlie/helm.pb.cfg b/src/test/missions/goby/s3_charlie/helm.pb.cfg new file mode 100644 index 0000000..5b5ebbd --- /dev/null +++ b/src/test/missions/goby/s3_charlie/helm.pb.cfg @@ -0,0 +1,5 @@ +helm { + behavior_file: "@FIXTURE@/charlie.bhv" + domain { name: "speed" low: 0 high: 4 points: 21 } + domain { name: "course" low: 0 high: 359 points: 360 } +} diff --git a/src/test/missions/goby/s4_delta/check.pb.cfg b/src/test/missions/goby/s4_delta/check.pb.cfg new file mode 100644 index 0000000..7c91f44 --- /dev/null +++ b/src/test/missions/goby/s4_delta/check.pb.cfg @@ -0,0 +1,6 @@ +duration: 200 +poke { at: 5 key: "DEPLOY" value: "true" } +poke { at: 5 key: "MOOS_MANUAL_OVERRIDE" value: "false" } +expect_posting { key: "MODE" value: "ACTIVE:LOITERING" before: 10 } +# periodic surfacing, recorded at 142 s +expect_posting { key: "TIME_AT_SURFACE" value: "1" before: 180 } diff --git a/src/test/missions/goby/s4_delta/helm.pb.cfg b/src/test/missions/goby/s4_delta/helm.pb.cfg new file mode 100644 index 0000000..e427d45 --- /dev/null +++ b/src/test/missions/goby/s4_delta/helm.pb.cfg @@ -0,0 +1,6 @@ +helm { + behavior_file: "@FIXTURE@/delta.bhv" + domain { name: "depth" low: 0 high: 199 points: 200 } + domain { name: "speed" low: 0 high: 4 points: 41 } + domain { name: "course" low: 0 high: 359 points: 360 } +} From 17f3aa624ae25b481871a9213552079334857f95 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 23:39:45 +0000 Subject: [PATCH 2/9] Run the Goby tests with 'goby test' The goby_helm_ivp smoke test and the closed-loop mission runs are now declarative 'goby test' files: fixtures start gobyd, the simulator, the frontseat interface and the helm, and the runs publish the pokes and expect the helm's postings and state. This replaces ivpico_mission_check, run.sh and test_goby_helm_ivp.sh. 'goby test' is newer than Goby 3.6.1, so init.sh installs Goby from the continuous repository, and the Goby tests are only added when find_package(GOBY) sets GOBY_EXECUTABLE. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VLcYK4e29B5FiPbDDJy5fp --- init.sh | 7 +- src/test/CMakeLists.txt | 20 ++- src/test/goby/CMakeLists.txt | 1 - src/test/goby/helm_ivp/east.bhv | 6 + src/test/goby/helm_ivp/helm_ivp.pb.cfg | 8 + src/test/goby/helm_ivp/test.yml | 44 +++++ src/test/goby/mission_check/CMakeLists.txt | 7 - src/test/goby/mission_check/mission_check.cpp | 157 ------------------ .../mission_check/mission_check_config.proto | 35 ---- src/test/goby/test_goby_helm_ivp.sh | 77 --------- src/test/missions/README.md | 11 +- src/test/missions/goby/closed_loop.yml | 37 +++++ src/test/missions/goby/common.pb.cfg | 4 + src/test/missions/goby/frontseat.pb.cfg | 18 ++ src/test/missions/goby/run.sh | 78 --------- src/test/missions/goby/s1_alpha/check.pb.cfg | 5 - src/test/missions/goby/s1_alpha/helm.pb.cfg | 3 +- src/test/missions/goby/s1_alpha/test.yml | 38 +++++ src/test/missions/goby/s2_bravo/check.pb.cfg | 6 - src/test/missions/goby/s2_bravo/helm.pb.cfg | 3 +- src/test/missions/goby/s2_bravo/test.yml | 42 +++++ .../missions/goby/s3_charlie/check.pb.cfg | 5 - src/test/missions/goby/s3_charlie/helm.pb.cfg | 3 +- src/test/missions/goby/s3_charlie/test.yml | 42 +++++ src/test/missions/goby/s4_delta/check.pb.cfg | 6 - src/test/missions/goby/s4_delta/helm.pb.cfg | 3 +- src/test/missions/goby/s4_delta/test.yml | 42 +++++ 27 files changed, 311 insertions(+), 397 deletions(-) delete mode 100644 src/test/goby/CMakeLists.txt create mode 100644 src/test/goby/helm_ivp/east.bhv create mode 100644 src/test/goby/helm_ivp/helm_ivp.pb.cfg create mode 100644 src/test/goby/helm_ivp/test.yml delete mode 100644 src/test/goby/mission_check/CMakeLists.txt delete mode 100644 src/test/goby/mission_check/mission_check.cpp delete mode 100644 src/test/goby/mission_check/mission_check_config.proto delete mode 100755 src/test/goby/test_goby_helm_ivp.sh create mode 100644 src/test/missions/goby/closed_loop.yml create mode 100644 src/test/missions/goby/common.pb.cfg create mode 100644 src/test/missions/goby/frontseat.pb.cfg delete mode 100755 src/test/missions/goby/run.sh delete mode 100644 src/test/missions/goby/s1_alpha/check.pb.cfg create mode 100644 src/test/missions/goby/s1_alpha/test.yml delete mode 100644 src/test/missions/goby/s2_bravo/check.pb.cfg create mode 100644 src/test/missions/goby/s2_bravo/test.yml delete mode 100644 src/test/missions/goby/s3_charlie/check.pb.cfg create mode 100644 src/test/missions/goby/s3_charlie/test.yml delete mode 100644 src/test/missions/goby/s4_delta/check.pb.cfg create mode 100644 src/test/missions/goby/s4_delta/test.yml diff --git a/init.sh b/init.sh index d2db034..5759f1a 100755 --- a/init.sh +++ b/init.sh @@ -35,15 +35,16 @@ packages=(cmake ninja-build g++ git ca-certificates protobuf-compiler libprotobu $SUDO apt-get update apt_install "${packages[@]}" -# Goby3, for src/bin/goby, from packages.gobysoft.org +# Goby3, for src/bin/goby, from packages.gobysoft.org: continuous builds for +# 'goby test' (newer than 3.6.1) keyring=/usr/share/keyrings/gobysoft-archive-keyring.gpg if [ ! -f "${keyring}" ]; then curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x19478082E2F8D3FE" | gpg --dearmor | $SUDO tee "${keyring}" > /dev/null fi . /etc/os-release -echo "deb [signed-by=${keyring}] http://packages.gobysoft.org/${ID}/release/ ${VERSION_CODENAME}/" | - $SUDO tee /etc/apt/sources.list.d/gobysoft_release.list > /dev/null +echo "deb [signed-by=${keyring}] http://packages.gobysoft.org/${ID}/continuous/ ${VERSION_CODENAME}/" | + $SUDO tee /etc/apt/sources.list.d/gobysoft_continuous.list > /dev/null $SUDO apt-get update # libgoby3-dev only recommends these, but goby's CMake package links libais.so, # and goby_add_app() runs protoc-gen-dccl diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index d310c54..d4582d1 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -16,19 +16,23 @@ foreach(fixture ${fixtures}) endif() endforeach() -if(GOBY_FOUND) - add_subdirectory(goby) +# 'goby test' is in Goby builds after 3.6.1, which set GOBY_EXECUTABLE +if(GOBY_FOUND AND GOBY_EXECUTABLE) + set(goby_test_defines + -D HELM=$ -D INTERFACE_LIB=$) + + add_test(NAME goby_helm_ivp + COMMAND ${GOBY_EXECUTABLE} test run ${CMAKE_CURRENT_SOURCE_DIR}/goby/helm_ivp/test.yml + ${goby_test_defines}) set(simulator_port 54400) foreach(mission s1_alpha s2_bravo s3_charlie s4_delta) add_test(NAME closed_loop_${mission} - COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/missions/goby/run.sh ${mission} - $ $ ${simulator_port}) + COMMAND ${GOBY_EXECUTABLE} test run ${CMAKE_CURRENT_SOURCE_DIR}/missions/goby/${mission}/test.yml + ${goby_test_defines} -D MISSION=${mission} -D PORT=${simulator_port}) set_tests_properties(closed_loop_${mission} PROPERTIES TIMEOUT 180) math(EXPR simulator_port "${simulator_port} + 1") endforeach() - - add_test(NAME goby_helm_ivp - COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/goby/test_goby_helm_ivp.sh - $ $) +elseif(GOBY_FOUND) + message(STATUS "Skipping the Goby tests: this Goby has no 'goby test' (needs a build after 3.6.1)") endif() diff --git a/src/test/goby/CMakeLists.txt b/src/test/goby/CMakeLists.txt deleted file mode 100644 index 2ed8d7b..0000000 --- a/src/test/goby/CMakeLists.txt +++ /dev/null @@ -1 +0,0 @@ -add_subdirectory(mission_check) diff --git a/src/test/goby/helm_ivp/east.bhv b/src/test/goby/helm_ivp/east.bhv new file mode 100644 index 0000000..fef847f --- /dev/null +++ b/src/test/goby/helm_ivp/east.bhv @@ -0,0 +1,6 @@ +Behavior = BHV_ConstantHeading +{ + name = east + heading = 90 + duration = no-time-limit +} diff --git a/src/test/goby/helm_ivp/helm_ivp.pb.cfg b/src/test/goby/helm_ivp/helm_ivp.pb.cfg new file mode 100644 index 0000000..fe83b02 --- /dev/null +++ b/src/test/goby/helm_ivp/helm_ivp.pb.cfg @@ -0,0 +1,8 @@ +app { geodesy { lat_origin: 43.8 lon_origin: -70.3 } } +helm { + behavior_file: "east.bhv" + domain { name: "course" low: 0 high: 359 points: 360 } + domain { name: "speed" low: 0 high: 4 points: 21 optional: true } + start_in_drive: true +} +frontseat { enable: true } diff --git a/src/test/goby/helm_ivp/test.yml b/src/test/goby/helm_ivp/test.yml new file mode 100644 index 0000000..8104b3f --- /dev/null +++ b/src/test/goby/helm_ivp/test.yml @@ -0,0 +1,44 @@ +# goby_helm_ivp's frontseat bridge, end to end: +# +# 1. a constant-heading behavior (course 90) with the helm starting in DRIVE; +# 2. one frontseat NodeStatus gives the helm its navigation, converted to x/y +# with the app's datum (time 0: the helm uses its own clock); +# 3. the helm should then publish a DesiredCourse with heading 90 and a +# HelmStateReport of HELM_DRIVE, and its own Status in DRIVE. +# +# Set with -D: HELM (goby_helm_ivp), INTERFACE_LIB (libivpico_interface.so) +load_shared_library: ["${INTERFACE_LIB}"] + +endpoint: + - { name: vehicle, zeromq: { platform: ivpico_helm_ivp } } + +fixture: + - name: gobyd + command: gobyd -v + env: ['GOBY_INTERPROCESS=platform: "ivpico_helm_ivp"'] + ready: { output_regex: "gobyd is ready" } + - name: helm + command: ${HELM} helm_ivp.pb.cfg + env: ['GOBY_INTERPROCESS=platform: "ivpico_helm_ivp"'] + +test: + name: goby_helm_ivp frontseat bridge + require_app: [{ name: [goby_helm_ivp] }] + run: + - name: navigation in, desired course out + publish: + - group: goby::middleware::frontseat::node_status + type: PROTOBUF/goby.middleware.frontseat.protobuf.NodeStatus + value: 'time: 0 name: "test" global_fix { lat: 43.8 lon: -70.3 } pose { heading: 0 } speed { over_ground: 0 }' + expect: + - group: goby::middleware::frontseat::desired_course + type: goby.middleware.frontseat.protobuf.DesiredCourse + condition: + - { field: heading, op: GE, value: 89.99 } + - { field: heading, op: LE, value: 90.01 } + - group: goby::middleware::frontseat::helm_state + type: goby.middleware.frontseat.protobuf.HelmStateReport + match: "state: HELM_DRIVE" + - group: ivpico::helm::status + type: ivp.interface.Status + match: "state: DRIVE" diff --git a/src/test/goby/mission_check/CMakeLists.txt b/src/test/goby/mission_check/CMakeLists.txt deleted file mode 100644 index 4b23823..0000000 --- a/src/test/goby/mission_check/CMakeLists.txt +++ /dev/null @@ -1,7 +0,0 @@ -goby_add_app(TARGET ivpico_mission_check - SOURCES mission_check.cpp - PROTOS mission_check_config.proto - PROTOC_OUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/proto - PROTO_IMPORT_DIRS ${PROJECT_SOURCE_DIR}/src/lib ${openocean_messages_PROTO_DIR} - LINK_LIBRARIES goby_zeromq ivpico_interface) -target_include_directories(ivpico_mission_check PRIVATE ${PROJECT_SOURCE_DIR}/src/bin/goby) diff --git a/src/test/goby/mission_check/mission_check.cpp b/src/test/goby/mission_check/mission_check.cpp deleted file mode 100644 index caf0311..0000000 --- a/src/test/goby/mission_check/mission_check.cpp +++ /dev/null @@ -1,157 +0,0 @@ -// Drives a closed-loop mission run of goby_helm_ivp and checks how it went. -// -// Once the helm reports PARK or DRIVE and the frontseat has reported nav, -// mission time starts. The checker -// publishes each configured poke (e.g. DEPLOY=true) as a Variable at its -// time, watches the helm's Postings for each expected key = value before its -// deadline, and fails at once if the helm is MALCONFIG or halts with a -// behavior error. After `duration` it exits 0 if every expectation was met -// (and, with drive_at_end, the helm is still in DRIVE), and 1 otherwise. - -#include - -#include -#include -#include - -#include -#include -#include -#include - -#include "groups.h" -#include "interface/ivp.pb.h" -#include "mission_check_config.pb.h" - -using goby::glog; - -namespace ivpico -{ -namespace goby -{ -class MissionCheck : public ::goby::zeromq::SingleThreadApplication -{ - public: - MissionCheck() : SingleThreadApplication(10 * boost::units::si::hertz) - { - interprocess().subscribe([this](const ivp::interface::Posting& posting) - { handle(posting); }); - interprocess().subscribe([this](const ivp::interface::Status& status) - { handle(status); }); - interprocess().subscribe<::goby::middleware::frontseat::groups::node_status>( - [this](const ::goby::middleware::frontseat::protobuf::NodeStatus&) - { - have_nav_ = true; - maybe_start(); - }); - } - - private: - double now() const { return ::goby::time::SystemClock::now<::goby::time::SITime>().value(); } - - void handle(const ivp::interface::Posting& posting) - { - if (start_ < 0) - return; - const std::string value = posting.value_case() == ivp::interface::Posting::kSval - ? posting.sval() - : number(posting.dval()); - for (int i = 0; i < cfg().expect_posting_size(); ++i) - { - const auto& expect = cfg().expect_posting(i); - if (!met_.count(i) && expect.key() == posting.key() && same(expect.value(), value)) - { - met_.insert(i); - glog.is_verbose() && glog << "met " << expect.ShortDebugString() << " at " - << now() - start_ << " s" << std::endl; - } - } - } - - void handle(const ivp::interface::Status& status) - { - state_ = status.state(); - maybe_start(); - if (state_ == ivp::interface::Status::MALCONFIG) - finish(false, "helm is MALCONFIG"); - else if (status.allstop() == ivp::interface::BEHAVIOR_ERROR) - finish(false, "behavior error: " + status.halt_message()); - } - - void maybe_start() - { - if (start_ < 0 && have_nav_ && - (state_ == ivp::interface::Status::PARK || state_ == ivp::interface::Status::DRIVE)) - start_ = now(); - } - - void loop() override - { - if (start_ < 0) - return; - const double t = now() - start_; - for (int i = 0; i < cfg().poke_size(); ++i) - { - const auto& poke = cfg().poke(i); - if (!poked_.count(i) && t >= poke.at()) - { - ivp::interface::Variable variable; - variable.set_key(poke.key()); - variable.set_sval(poke.value()); - interprocess().publish(variable); - poked_.insert(i); - } - } - for (int i = 0; i < cfg().expect_posting_size(); ++i) - { - const auto& expect = cfg().expect_posting(i); - if (!met_.count(i) && t > expect.before()) - finish(false, "no " + expect.key() + " = " + expect.value() + " by " + - number(expect.before()) + " s"); - } - if (t >= cfg().duration()) - { - if (cfg().drive_at_end() && state_ != ivp::interface::Status::DRIVE) - finish(false, "helm not in DRIVE at the end"); - else - finish(true, ""); - } - } - - void finish(bool ok, const std::string& why) - { - if (done_) - return; - done_ = true; - std::cout << (ok ? "PASSED" : "FAILED: " + why) << std::endl; - quit(ok ? 0 : 1); - } - - static std::string number(double d) - { - std::ostringstream os; - os << d; - return os.str(); - } - - static bool same(const std::string& expected, const std::string& value) - { - char* end_a = nullptr; - char* end_b = nullptr; - const double a = std::strtod(expected.c_str(), &end_a); - const double b = std::strtod(value.c_str(), &end_b); - if (*end_a == '\0' && *end_b == '\0' && !expected.empty() && !value.empty()) - return std::fabs(a - b) < 1e-6; - return expected == value; - } - - double start_{-1}; - ivp::interface::Status::State state_{ivp::interface::Status::STATE_UNKNOWN}; - std::set poked_, met_; - bool have_nav_{false}; - bool done_{false}; -}; -} // namespace goby -} // namespace ivpico - -int main(int argc, char* argv[]) { return goby::run(argc, argv); } diff --git a/src/test/goby/mission_check/mission_check_config.proto b/src/test/goby/mission_check/mission_check_config.proto deleted file mode 100644 index 1822248..0000000 --- a/src/test/goby/mission_check/mission_check_config.proto +++ /dev/null @@ -1,35 +0,0 @@ -syntax = "proto2"; - -import "goby/middleware/protobuf/app_config.proto"; -import "goby/zeromq/protobuf/interprocess_config.proto"; - -package ivpico.goby.protobuf; - -message MissionCheckConfig -{ - optional .goby.middleware.protobuf.AppConfig app = 1; - optional .goby.zeromq.protobuf.InterProcessPortalConfig interprocess = 2; - - // mission seconds to run, from when the helm is first in PARK or DRIVE - required double duration = 10; - - // published as a Variable at this many mission seconds - message Poke - { - required double at = 1; - required string key = 2; - required string value = 3; - } - repeated Poke poke = 11; - - // a Posting (by a behavior, a mode, initialize, ...) of key = value - message ExpectPosting - { - required string key = 1; - required string value = 2; - required double before = 3; - } - repeated ExpectPosting expect_posting = 12; - - optional bool drive_at_end = 13 [default = false]; -} diff --git a/src/test/goby/test_goby_helm_ivp.sh b/src/test/goby/test_goby_helm_ivp.sh deleted file mode 100755 index 0a0a7c9..0000000 --- a/src/test/goby/test_goby_helm_ivp.sh +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env bash -# Runs goby_helm_ivp with gobyd and checks its frontseat bridge end to end: -# -# 1. a constant-heading behavior (course 90) with the helm starting in DRIVE; -# 2. one frontseat NodeStatus, published with `goby zeromq publish`, gives the -# helm its navigation, converted to x/y with the app's datum; -# 3. the helm should then publish a DesiredCourse with heading 90 and a -# HelmStateReport of HELM_DRIVE, and its own Status in DRIVE. -# -# Usage: test_goby_helm_ivp.sh -set -euo pipefail - -helm=$1 -export GOBY_TOOL_LOAD_SHARED_LIBRARY=$2 -work=$(mktemp -d) -export GOBY_INTERPROCESS="platform: \"ivpico_test_$$\"" -pids=() -cleanup() { - kill "${pids[@]}" 2> /dev/null || true - wait 2> /dev/null || true - rm -rf "${work}" -} -trap cleanup EXIT - -cat > "${work}/test.bhv" << 'BHV' -Behavior = BHV_ConstantHeading -{ - name = east - heading = 90 - duration = no-time-limit -} -BHV -cat > "${work}/helm_ivp.pb.cfg" << CFG -app { geodesy { lat_origin: 43.8 lon_origin: -70.3 } } -helm { - behavior_file: "${work}/test.bhv" - domain { name: "course" low: 0 high: 359 points: 360 } - domain { name: "speed" low: 0 high: 4 points: 21 optional: true } - start_in_drive: true -} -frontseat { enable: true } -CFG - -gobyd > "${work}/gobyd.log" 2>&1 & -pids+=($!) -sleep 1 -"${helm}" "${work}/helm_ivp.pb.cfg" > "${work}/helm.log" 2>&1 & -pids+=($!) -sleep 2 - -goby zeromq publish goby::middleware::frontseat::node_status \ - PROTOBUF/goby.middleware.frontseat.protobuf.NodeStatus \ - "time: $(date +%s) name: \"test\" global_fix { lat: 43.8 lon: -70.3 } pose { heading: 0 } speed { over_ground: 0 }" - -subscribe() { - timeout "$1" goby zeromq subscribe "$2" > "${work}/$3" 2>&1 || true -} -subscribe 4 goby::middleware::frontseat::desired_course desired.txt -subscribe 2 goby::middleware::frontseat::helm_state helm_state.txt -subscribe 2 ivpico::helm::status status.txt - -failed=0 -check() { - if ! grep -q "$2" "${work}/$1"; then - echo "failed: no \"$2\" in $1:" >&2 - tail -5 "${work}/$1" >&2 - failed=1 - fi -} -check desired.txt "heading: 90" -check helm_state.txt "HELM_DRIVE" -check status.txt "state: DRIVE" -if [ ${failed} -ne 0 ]; then - echo "goby_helm_ivp log:" >&2 - tail -20 "${work}/helm.log" >&2 -fi -exit ${failed} diff --git a/src/test/missions/README.md b/src/test/missions/README.md index b0e87e6..0be77bf 100644 --- a/src/test/missions/README.md +++ b/src/test/missions/README.md @@ -22,16 +22,17 @@ Re-record the fixtures whenever `external/moos-ivp` moves. *This section was written by Claude.* -`goby/run.sh ...` (ctest: `closed_loop_`) flies a mission's recorded `.bhv` with `goby_helm_ivp` against Goby's basic frontseat simulator, with time warp 10: +`goby test` runs `goby//test.yml` (ctest: `closed_loop_`), which flies a mission's recorded `.bhv` with `goby_helm_ivp` against Goby's basic frontseat simulator, with time warp 10: ```mermaid flowchart LR - check["ivpico_mission_check"] -- "pokes (Variable)" --> helm["goby_helm_ivp"] - helm -- "Posting, Status" --> check + test["goby test"] -- "pokes (Variable)" --> helm["goby_helm_ivp"] + helm -- "Posting, Status" --> test helm -- "DesiredCourse" --> fs["goby_frontseat_interface
+ basic simulator"] fs -- "NodeStatus" --> helm ``` +- `goby/closed_loop.yml` starts the fixtures (`gobyd`, the simulator, the frontseat interface and the helm) and waits for them; each `test.yml` includes it. +- `goby//test.yml` sends the pokes and expects postings by a deadline. Its times are real seconds, a tenth of mission time. - `goby//helm.pb.cfg` is the helm configuration (the `.moos` file's pHelmIvP block). -- `goby//check.pb.cfg` lists the pokes, the postings expected by a deadline, and the mission duration. Mission time starts once the helm is running and has nav. -- `IVPICO_KEEP_RUN=1` keeps the run's configs and logs. +- `goby test visualize -D MISSION= ...` draws the plan; a failed run's report names each fixture's log. diff --git a/src/test/missions/goby/closed_loop.yml b/src/test/missions/goby/closed_loop.yml new file mode 100644 index 0000000..b43ba6b --- /dev/null +++ b/src/test/missions/goby/closed_loop.yml @@ -0,0 +1,37 @@ +# Fixtures and startup for the closed-loop mission tests (/test.yml): +# goby_helm_ivp flies the mission's recorded .bhv against Goby's basic +# frontseat simulator with time warp 10, so test times are a tenth of mission +# time. Each test continues once the frontseat and helm are running. +# +# Set with -D: MISSION, PORT (simulator), HELM (goby_helm_ivp), +# INTERFACE_LIB (libivpico_interface.so) +load_shared_library: ["${INTERFACE_LIB}"] + +endpoint: + - { name: vehicle, zeromq: { platform: "ivpico_${MISSION}" } } + +fixture: + - name: gobyd + command: gobyd common.pb.cfg -v + env: ['GOBY_INTERPROCESS=platform: "ivpico_${MISSION}"'] + ready: { output_regex: "gobyd is ready" } + - name: simulator + command: goby_basic_frontseat_simulator ${PORT} + - name: frontseat + command: cat common.pb.cfg frontseat.pb.cfg | sed "s/@PORT@/${PORT}/" | goby_frontseat_interface_basic_simulator - + env: ['GOBY_INTERPROCESS=platform: "ivpico_${MISSION}"'] + - name: helm + command: cat common.pb.cfg ${MISSION}/helm.pb.cfg | ${HELM} - + env: ['GOBY_INTERPROCESS=platform: "ivpico_${MISSION}"'] + +test: + require_app: [{ name: [goby_frontseat_interface, goby_helm_ivp] }] + run: + - name: frontseat and helm running + timeout: 10 + expect: + - group: goby::middleware::frontseat::node_status + type: goby.middleware.frontseat.protobuf.NodeStatus + - group: ivpico::helm::status + type: ivp.interface.Status + condition: [{ field: state, op: REGEX, value: "^(PARK|DRIVE)$" }] diff --git a/src/test/missions/goby/common.pb.cfg b/src/test/missions/goby/common.pb.cfg new file mode 100644 index 0000000..88a15e9 --- /dev/null +++ b/src/test/missions/goby/common.pb.cfg @@ -0,0 +1,4 @@ +app { + geodesy { lat_origin: 43.8253 lon_origin: -70.3304 } + simulation { time { use_sim_time: true warp_factor: 10 } } +} diff --git a/src/test/missions/goby/frontseat.pb.cfg b/src/test/missions/goby/frontseat.pb.cfg new file mode 100644 index 0000000..176c6e8 --- /dev/null +++ b/src/test/missions/goby/frontseat.pb.cfg @@ -0,0 +1,18 @@ +frontseat_cfg { + require_helm: true + helm_running_timeout: 10 + frontseat_connected_timeout: 10 + status_period: 5 + exit_on_error: false + [goby.middleware.frontseat.protobuf.basic_simulator_config] { + tcp_address: "127.0.0.1" + tcp_port: @PORT@ + start { + lat: 43.8253 + lon: -70.3304 + duration: 0 + control_freq: 10 + vehicle { accel: 1.0 hdg_rate: 50 z_rate: 2 } + } + } +} diff --git a/src/test/missions/goby/run.sh b/src/test/missions/goby/run.sh deleted file mode 100755 index a33934d..0000000 --- a/src/test/missions/goby/run.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env bash -# Runs one MOOS-IvP mission closed loop under Goby: gobyd, goby's basic -# frontseat simulator and goby_frontseat_interface, goby_helm_ivp on the -# mission's recorded .bhv (../fixtures/), and ivpico_mission_check, -# which pokes the mission and checks /check.pb.cfg. Time warp 10. -# -# Usage: run.sh -set -euo pipefail -# own process group per background job, so cleanup also reaches their children -set -m - -here=$(cd "$(dirname "$0")" && pwd) -mission=$1 helm=$2 check=$3 port=$4 -work=$(mktemp -d) -platform="ivpico_${mission}_$$" -pids=() -cleanup() { - for pid in "${pids[@]}"; do kill -- "-${pid}" 2> /dev/null || true; done - wait 2> /dev/null || true - [[ -n "${IVPICO_KEEP_RUN:-}" ]] && echo "kept ${work}" >&2 || rm -rf "${work}" -} -trap cleanup EXIT - -common="app { - geodesy { lat_origin: 43.8253 lon_origin: -70.3304 } - simulation { time { use_sim_time: true warp_factor: 10 } } -} -interprocess { platform: \"${platform}\" }" - -echo "${common}" > "${work}/gobyd.pb.cfg" -cat > "${work}/frontseat.pb.cfg" << EOF2 -${common} -frontseat_cfg { - require_helm: true - helm_running_timeout: 10 - frontseat_connected_timeout: 10 - status_period: 5 - exit_on_error: false - [goby.middleware.frontseat.protobuf.basic_simulator_config] { - tcp_address: "127.0.0.1" - tcp_port: ${port} - start { - lat: 43.8253 - lon: -70.3304 - duration: 0 - control_freq: 10 - vehicle { accel: 1.0 hdg_rate: 50 z_rate: 2 } - } - } -} -EOF2 -{ - echo "${common}" - sed "s|@FIXTURE@|${here}/../fixtures/${mission}|" "${here}/${mission}/helm.pb.cfg" - echo "frontseat { enable: true }" -} > "${work}/helm.pb.cfg" -{ - echo "${common}" - cat "${here}/${mission}/check.pb.cfg" -} > "${work}/check.pb.cfg" - -goby_basic_frontseat_simulator "${port}" > "${work}/simulator.log" 2>&1 & -pids+=($!) -gobyd "${work}/gobyd.pb.cfg" > "${work}/gobyd.log" 2>&1 & -pids+=($!) -sleep 1 -goby_frontseat_interface_basic_simulator "${work}/frontseat.pb.cfg" > "${work}/frontseat.log" 2>&1 & -pids+=($!) -"${helm}" "${work}/helm.pb.cfg" > "${work}/helm.log" 2>&1 & -pids+=($!) - -if ! "${check}" "${work}/check.pb.cfg" -v > "${work}/check.log" 2>&1; then - grep -v "^\s*$" "${work}/check.log" | tail -15 >&2 - echo "goby_helm_ivp log:" >&2 - tail -15 "${work}/helm.log" >&2 - exit 1 -fi -grep -E "met |PASSED" "${work}/check.log" diff --git a/src/test/missions/goby/s1_alpha/check.pb.cfg b/src/test/missions/goby/s1_alpha/check.pb.cfg deleted file mode 100644 index 5f13876..0000000 --- a/src/test/missions/goby/s1_alpha/check.pb.cfg +++ /dev/null @@ -1,5 +0,0 @@ -duration: 480 -poke { at: 5 key: "DEPLOY" value: "true" } -poke { at: 5 key: "MOOS_MANUAL_OVERRIDE" value: "false" } -# recorded at 370 s -expect_posting { key: "RETURN" value: "true" before: 460 } diff --git a/src/test/missions/goby/s1_alpha/helm.pb.cfg b/src/test/missions/goby/s1_alpha/helm.pb.cfg index b599a43..0d35793 100644 --- a/src/test/missions/goby/s1_alpha/helm.pb.cfg +++ b/src/test/missions/goby/s1_alpha/helm.pb.cfg @@ -1,5 +1,6 @@ helm { - behavior_file: "@FIXTURE@/alpha.bhv" + behavior_file: "../fixtures/s1_alpha/alpha.bhv" domain { name: "speed" low: 0 high: 4 points: 41 } domain { name: "course" low: 0 high: 359 points: 360 } } +frontseat { enable: true } diff --git a/src/test/missions/goby/s1_alpha/test.yml b/src/test/missions/goby/s1_alpha/test.yml new file mode 100644 index 0000000..ba30b82 --- /dev/null +++ b/src/test/missions/goby/s1_alpha/test.yml @@ -0,0 +1,38 @@ +# s1_alpha: survey, then return. +# Times are real seconds: a tenth of mission time. +include: [../closed_loop.yml] + +test: + name: s1_alpha closed loop + run: + - name: mission + mode: PARALLEL + step: + - name: deploy + publish: + - group: ivpico::helm::variable + type: PROTOBUF/ivp.interface.Variable + value: 'key: "DEPLOY" sval: "true"' + delay: 0.5 + - group: ivpico::helm::variable + type: PROTOBUF/ivp.interface.Variable + value: 'key: "MOOS_MANUAL_OVERRIDE" sval: "false"' + delay: 0.5 + expect: + # recorded at 370 s of mission time + - group: ivpico::helm::posting + type: ivp.interface.Posting + match: 'key: "RETURN" sval: "true"' + timeout: 46 + - name: no errors + expect: + - group: ivpico::helm::status + type: ivp.interface.Status + condition: [{ field: state, op: EQ, value: MALCONFIG }] + count: 0 + timeout: 48 + - group: ivpico::helm::status + type: ivp.interface.Status + condition: [{ field: allstop, op: EQ, value: BEHAVIOR_ERROR }] + count: 0 + timeout: 48 diff --git a/src/test/missions/goby/s2_bravo/check.pb.cfg b/src/test/missions/goby/s2_bravo/check.pb.cfg deleted file mode 100644 index d4af3c9..0000000 --- a/src/test/missions/goby/s2_bravo/check.pb.cfg +++ /dev/null @@ -1,6 +0,0 @@ -duration: 300 -poke { at: 5 key: "DEPLOY" value: "true" } -poke { at: 5 key: "MOOS_MANUAL_OVERRIDE" value: "false" } -expect_posting { key: "MODE" value: "ACTIVE:SURVEYING" before: 10 } -# recorded at 214 s -expect_posting { key: "RETURN" value: "true" before: 270 } diff --git a/src/test/missions/goby/s2_bravo/helm.pb.cfg b/src/test/missions/goby/s2_bravo/helm.pb.cfg index 1a686b0..db4a114 100644 --- a/src/test/missions/goby/s2_bravo/helm.pb.cfg +++ b/src/test/missions/goby/s2_bravo/helm.pb.cfg @@ -1,5 +1,6 @@ helm { - behavior_file: "@FIXTURE@/bravo.bhv" + behavior_file: "../fixtures/s2_bravo/bravo.bhv" domain { name: "speed" low: 0 high: 4 points: 21 } domain { name: "course" low: 0 high: 359 points: 360 } } +frontseat { enable: true } diff --git a/src/test/missions/goby/s2_bravo/test.yml b/src/test/missions/goby/s2_bravo/test.yml new file mode 100644 index 0000000..d4df408 --- /dev/null +++ b/src/test/missions/goby/s2_bravo/test.yml @@ -0,0 +1,42 @@ +# s2_bravo: survey, then return. +# Times are real seconds: a tenth of mission time. +include: [../closed_loop.yml] + +test: + name: s2_bravo closed loop + run: + - name: mission + mode: PARALLEL + step: + - name: deploy + publish: + - group: ivpico::helm::variable + type: PROTOBUF/ivp.interface.Variable + value: 'key: "DEPLOY" sval: "true"' + delay: 0.5 + - group: ivpico::helm::variable + type: PROTOBUF/ivp.interface.Variable + value: 'key: "MOOS_MANUAL_OVERRIDE" sval: "false"' + delay: 0.5 + expect: + - group: ivpico::helm::posting + type: ivp.interface.Posting + match: 'key: "MODE" sval: "ACTIVE:SURVEYING"' + timeout: 1 + # recorded at 214 s of mission time + - group: ivpico::helm::posting + type: ivp.interface.Posting + match: 'key: "RETURN" sval: "true"' + timeout: 27 + - name: no errors + expect: + - group: ivpico::helm::status + type: ivp.interface.Status + condition: [{ field: state, op: EQ, value: MALCONFIG }] + count: 0 + timeout: 30 + - group: ivpico::helm::status + type: ivp.interface.Status + condition: [{ field: allstop, op: EQ, value: BEHAVIOR_ERROR }] + count: 0 + timeout: 30 diff --git a/src/test/missions/goby/s3_charlie/check.pb.cfg b/src/test/missions/goby/s3_charlie/check.pb.cfg deleted file mode 100644 index 84cece2..0000000 --- a/src/test/missions/goby/s3_charlie/check.pb.cfg +++ /dev/null @@ -1,5 +0,0 @@ -duration: 200 -poke { at: 5 key: "DEPLOY" value: "true" } -poke { at: 5 key: "MOOS_MANUAL_OVERRIDE" value: "false" } -expect_posting { key: "MODE" value: "ACTIVE:LOITERING" before: 10 } -drive_at_end: true diff --git a/src/test/missions/goby/s3_charlie/helm.pb.cfg b/src/test/missions/goby/s3_charlie/helm.pb.cfg index 5b5ebbd..5198ead 100644 --- a/src/test/missions/goby/s3_charlie/helm.pb.cfg +++ b/src/test/missions/goby/s3_charlie/helm.pb.cfg @@ -1,5 +1,6 @@ helm { - behavior_file: "@FIXTURE@/charlie.bhv" + behavior_file: "../fixtures/s3_charlie/charlie.bhv" domain { name: "speed" low: 0 high: 4 points: 21 } domain { name: "course" low: 0 high: 359 points: 360 } } +frontseat { enable: true } diff --git a/src/test/missions/goby/s3_charlie/test.yml b/src/test/missions/goby/s3_charlie/test.yml new file mode 100644 index 0000000..1a10a4a --- /dev/null +++ b/src/test/missions/goby/s3_charlie/test.yml @@ -0,0 +1,42 @@ +# s3_charlie: loiter, and still driving at the end. +# Times are real seconds: a tenth of mission time. +include: [../closed_loop.yml] + +test: + name: s3_charlie closed loop + run: + - name: mission + mode: PARALLEL + step: + - name: deploy + publish: + - group: ivpico::helm::variable + type: PROTOBUF/ivp.interface.Variable + value: 'key: "DEPLOY" sval: "true"' + delay: 0.5 + - group: ivpico::helm::variable + type: PROTOBUF/ivp.interface.Variable + value: 'key: "MOOS_MANUAL_OVERRIDE" sval: "false"' + delay: 0.5 + expect: + - group: ivpico::helm::posting + type: ivp.interface.Posting + match: 'key: "MODE" sval: "ACTIVE:LOITERING"' + timeout: 1 + - name: no errors + expect: + - group: ivpico::helm::status + type: ivp.interface.Status + condition: [{ field: state, op: EQ, value: MALCONFIG }] + count: 0 + timeout: 20 + - group: ivpico::helm::status + type: ivp.interface.Status + condition: [{ field: allstop, op: EQ, value: BEHAVIOR_ERROR }] + count: 0 + timeout: 20 + - name: still driving + expect: + - group: ivpico::helm::status + type: ivp.interface.Status + match: "state: DRIVE" diff --git a/src/test/missions/goby/s4_delta/check.pb.cfg b/src/test/missions/goby/s4_delta/check.pb.cfg deleted file mode 100644 index 7c91f44..0000000 --- a/src/test/missions/goby/s4_delta/check.pb.cfg +++ /dev/null @@ -1,6 +0,0 @@ -duration: 200 -poke { at: 5 key: "DEPLOY" value: "true" } -poke { at: 5 key: "MOOS_MANUAL_OVERRIDE" value: "false" } -expect_posting { key: "MODE" value: "ACTIVE:LOITERING" before: 10 } -# periodic surfacing, recorded at 142 s -expect_posting { key: "TIME_AT_SURFACE" value: "1" before: 180 } diff --git a/src/test/missions/goby/s4_delta/helm.pb.cfg b/src/test/missions/goby/s4_delta/helm.pb.cfg index e427d45..c374db7 100644 --- a/src/test/missions/goby/s4_delta/helm.pb.cfg +++ b/src/test/missions/goby/s4_delta/helm.pb.cfg @@ -1,6 +1,7 @@ helm { - behavior_file: "@FIXTURE@/delta.bhv" + behavior_file: "../fixtures/s4_delta/delta.bhv" domain { name: "depth" low: 0 high: 199 points: 200 } domain { name: "speed" low: 0 high: 4 points: 41 } domain { name: "course" low: 0 high: 359 points: 360 } } +frontseat { enable: true } diff --git a/src/test/missions/goby/s4_delta/test.yml b/src/test/missions/goby/s4_delta/test.yml new file mode 100644 index 0000000..8b3478e --- /dev/null +++ b/src/test/missions/goby/s4_delta/test.yml @@ -0,0 +1,42 @@ +# s4_delta: loiter with periodic surfacing. +# Times are real seconds: a tenth of mission time. +include: [../closed_loop.yml] + +test: + name: s4_delta closed loop + run: + - name: mission + mode: PARALLEL + step: + - name: deploy + publish: + - group: ivpico::helm::variable + type: PROTOBUF/ivp.interface.Variable + value: 'key: "DEPLOY" sval: "true"' + delay: 0.5 + - group: ivpico::helm::variable + type: PROTOBUF/ivp.interface.Variable + value: 'key: "MOOS_MANUAL_OVERRIDE" sval: "false"' + delay: 0.5 + expect: + - group: ivpico::helm::posting + type: ivp.interface.Posting + match: 'key: "MODE" sval: "ACTIVE:LOITERING"' + timeout: 1 + # recorded at 142 s of mission time + - group: ivpico::helm::posting + type: ivp.interface.Posting + match: 'key: "TIME_AT_SURFACE" dval: 1' + timeout: 18 + - name: no errors + expect: + - group: ivpico::helm::status + type: ivp.interface.Status + condition: [{ field: state, op: EQ, value: MALCONFIG }] + count: 0 + timeout: 20 + - group: ivpico::helm::status + type: ivp.interface.Status + condition: [{ field: allstop, op: EQ, value: BEHAVIOR_ERROR }] + count: 0 + timeout: 20 From 38e2a6dd2096f68f2b13859a8d789f320dbc4318 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 23:44:28 +0000 Subject: [PATCH 3/9] init.sh: make Goby optional, and add its apt source only when needed --no-goby leaves Goby3 out (build.sh then skips the Goby app); the "Record missions" workflow uses it, since it only builds MOOS-IvP. The packages.gobysoft.org continuous source and its key are only added when apt has no candidate for libgoby3-dev. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VLcYK4e29B5FiPbDDJy5fp --- .github/workflows/record-missions.yml | 2 +- README.md | 2 +- init.sh | 64 ++++++++++++++++----------- 3 files changed, 40 insertions(+), 28 deletions(-) diff --git a/.github/workflows/record-missions.yml b/.github/workflows/record-missions.yml index fd87b35..228af75 100644 --- a/.github/workflows/record-missions.yml +++ b/.github/workflows/record-missions.yml @@ -30,7 +30,7 @@ jobs: - name: Init run: | git config --global --add safe.directory '*' - ./init.sh + ./init.sh --no-goby # all of MOOS-IvP, not just the helm libraries apt-get install -y --no-install-recommends make patch diff --git a/README.md b/README.md index a14cc87..19f9ac9 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ One way to think of this project is as a generator for the equivalent of the MOO *This section was written by Claude.* -`init.sh` checks out the submodules and installs the Ubuntu packages needed to build them and IvPico. These include Goby3 from [packages.gobysoft.org](http://packages.gobysoft.org), for the Goby app; without Goby3, `build.sh` skips the Goby app. `build.sh` then builds, all in `build/`: +`init.sh` checks out the submodules and installs the Ubuntu packages needed to build them and IvPico. These include Goby3, for the Goby app, adding [packages.gobysoft.org](http://packages.gobysoft.org)'s continuous builds if apt doesn't already have it; `init.sh --no-goby` leaves Goby3 out, and `build.sh` then skips the Goby app. `build.sh` then builds, all in `build/`: - openocean-messages, into `build/external/openocean-messages`; - MOOS-IvP's helm libraries (`IVP_BUILD_HELM_LIBS_ONLY`, as shared libraries, without MOOS or its apps), into `build/external/moos-ivp`, installed into `build/external/install` for `find_package(IVP)`; diff --git a/init.sh b/init.sh index 5759f1a..7f6ccca 100755 --- a/init.sh +++ b/init.sh @@ -1,22 +1,28 @@ #!/usr/bin/env bash # Checks out the submodules and installs the Ubuntu packages needed to build them and IvPico. # -# Usage: init.sh +# Usage: init.sh [--no-goby] +# --no-goby don't install Goby3 (build.sh then skips the Goby app) set -euo pipefail cd "$(dirname "$0")" -case "${1:-}" in - "") ;; - -h | --help) - sed -n '4p' "$0" | sed 's/^# //' - exit 0 - ;; - *) - sed -n '4p' "$0" | sed 's/^# //' >&2 - exit 1 - ;; -esac +usage() { sed -n '4,5p' "$0" | sed 's/^# //'; } + +goby=true +for arg in "$@"; do + case "${arg}" in + --no-goby) goby=false ;; + -h | --help) + usage + exit 0 + ;; + *) + usage >&2 + exit 1 + ;; + esac +done git submodule update --init --recommend-shallow @@ -31,23 +37,29 @@ apt_install() { # moos-ivp's helm libraries (IVP_BUILD_HELM_LIBS_ONLY) need only CMake and a compiler # xz-utils: the replay tests' recorded missions are compressed -packages=(cmake ninja-build g++ git ca-certificates protobuf-compiler libprotobuf-dev curl gpg xz-utils) +packages=(cmake ninja-build g++ git ca-certificates protobuf-compiler libprotobuf-dev xz-utils) $SUDO apt-get update apt_install "${packages[@]}" -# Goby3, for src/bin/goby, from packages.gobysoft.org: continuous builds for -# 'goby test' (newer than 3.6.1) -keyring=/usr/share/keyrings/gobysoft-archive-keyring.gpg -if [ ! -f "${keyring}" ]; then - curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x19478082E2F8D3FE" | - gpg --dearmor | $SUDO tee "${keyring}" > /dev/null +if ${goby}; then + # Goby3, for src/bin/goby. If apt doesn't have it yet, add packages.gobysoft.org's + # continuous builds, which have 'goby test' (newer than 3.6.1) + goby_policy=$(apt-cache policy libgoby3-dev) + if ! grep -q "Candidate: [^(]" <<< "${goby_policy}"; then + apt_install curl gpg + keyring=/usr/share/keyrings/gobysoft-archive-keyring.gpg + if [ ! -f "${keyring}" ]; then + curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x19478082E2F8D3FE" | + gpg --dearmor | $SUDO tee "${keyring}" > /dev/null + fi + . /etc/os-release + echo "deb [signed-by=${keyring}] http://packages.gobysoft.org/${ID}/continuous/ ${VERSION_CODENAME}/" | + $SUDO tee /etc/apt/sources.list.d/gobysoft_continuous.list > /dev/null + $SUDO apt-get update + fi + # libgoby3-dev only recommends these, but goby's CMake package links libais.so, + # and goby_add_app() runs protoc-gen-dccl + apt_install libgoby3-dev goby3-apps libais-dev dccl5-compiler fi -. /etc/os-release -echo "deb [signed-by=${keyring}] http://packages.gobysoft.org/${ID}/continuous/ ${VERSION_CODENAME}/" | - $SUDO tee /etc/apt/sources.list.d/gobysoft_continuous.list > /dev/null -$SUDO apt-get update -# libgoby3-dev only recommends these, but goby's CMake package links libais.so, -# and goby_add_app() runs protoc-gen-dccl -apt_install libgoby3-dev goby3-apps libais-dev dccl5-compiler external/openocean-messages/init.sh --cxx From 3a101be3f72e404993ab18e21ed0da603a96897f Mon Sep 17 00:00:00 2001 From: Toby Schneider Date: Wed, 4 Nov 2026 10:47:26 +1100 Subject: [PATCH 4/9] Claude branch names --- CLAUDE.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 61184c1..0690848 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,3 +17,7 @@ Guidance for Claude when working in this repository. - When in doubt leave the documentation unwritten, but provide an outline for the human developers to use to fill out the rest of the document. - Prefer figures to text when an idea can be better explained graphically. +## Branches + +- Name a feature branch with a short description of the change: `add-goby-test-tool`, `fix-udpm-reassembly`. Name a bug fix branch with the issue it closes after the description: `fix-bad-bug/13` + From 3d4a3488a18de202196e5b6385cddd549ab8d57f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 23:55:46 +0000 Subject: [PATCH 5/9] build.sh: install everything into build/usr openocean-messages, MOOS-IvP's helm libraries and IvPico are installed into BUILD_DIR/usr, so build/usr/bin can go on PATH. IvPico installs its libraries, and installed programs find them through an $ORIGIN/../lib RPATH. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VLcYK4e29B5FiPbDDJy5fp --- CMakeLists.txt | 4 ++++ README.md | 10 +++++----- build.sh | 15 +++++++++------ src/lib/CMakeLists.txt | 4 ++++ 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 61f9a2d..a71fa79 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,10 @@ set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/") option(BUILD_SHARED_LIBS "Build shared libraries" ON) +include(GNUInstallDirs) +# installed programs find the libraries installed beside them, e.g. in build.sh's build/usr +set(CMAKE_INSTALL_RPATH "$ORIGIN/../${CMAKE_INSTALL_LIBDIR}") + find_package(Protobuf REQUIRED) find_package(openocean_messages 0.1 REQUIRED COMPONENTS cpp) diff --git a/README.md b/README.md index 19f9ac9..82cad1e 100644 --- a/README.md +++ b/README.md @@ -26,10 +26,10 @@ One way to think of this project is as a generator for the equivalent of the MOO *This section was written by Claude.* -`init.sh` checks out the submodules and installs the Ubuntu packages needed to build them and IvPico. These include Goby3, for the Goby app, adding [packages.gobysoft.org](http://packages.gobysoft.org)'s continuous builds if apt doesn't already have it; `init.sh --no-goby` leaves Goby3 out, and `build.sh` then skips the Goby app. `build.sh` then builds, all in `build/`: +`init.sh` checks out the submodules and installs the Ubuntu packages needed to build them and IvPico. These include Goby3, for the Goby app, adding [packages.gobysoft.org](http://packages.gobysoft.org)'s continuous builds if apt doesn't already have it; `init.sh --no-goby` leaves Goby3 out, and `build.sh` then skips the Goby app. `build.sh` then builds, all in `build/`, and installs each into `build/usr`: - openocean-messages, into `build/external/openocean-messages`; -- MOOS-IvP's helm libraries (`IVP_BUILD_HELM_LIBS_ONLY`, as shared libraries, without MOOS or its apps), into `build/external/moos-ivp`, installed into `build/external/install` for `find_package(IVP)`; +- MOOS-IvP's helm libraries (`IVP_BUILD_HELM_LIBS_ONLY`, as shared libraries, without MOOS or its apps), into `build/external/moos-ivp`, installed first so that IvPico can `find_package(IVP)`; - IvPico, against both. ``` @@ -38,7 +38,7 @@ One way to think of this project is as a generator for the equivalent of the MOO ctest --test-dir build ``` -`build.sh` passes any arguments on to IvPico's CMake, and `BUILD_DIR` overrides the build directory. +`build.sh` passes any arguments on to IvPico's CMake, and `BUILD_DIR` overrides the build directory. Add `build/usr/bin` to your `PATH` to run the installed apps. To update a submodule, check out the commit you want in it and commit the new pointer, e.g. `git -C external/openocean-messages pull origin main && git add external/openocean-messages`. @@ -51,8 +51,8 @@ To update a submodule, check out the commit you want in it and commit the new po To run the alpha mission against Goby's basic frontseat simulator: ``` -export PATH=$PWD/build/src/bin/goby/helm_ivp:$PATH -export GOBY_TOOL_LOAD_SHARED_LIBRARY=$PWD/build/src/lib/libivpico_interface.so +export PATH=$PWD/build/usr/bin:$PATH +export GOBY_TOOL_LOAD_SHARED_LIBRARY=$PWD/build/usr/lib/libivpico_interface.so cd launch/goby/alpha goby_launch alpha.launch ./deploy.sh diff --git a/build.sh b/build.sh index bfbe048..3076bf5 100755 --- a/build.sh +++ b/build.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Builds the dependencies from their submodules (openocean-messages, and MOOS-IvP's helm libraries), -# then IvPico against them, all in BUILD_DIR (default build). Arguments are passed on to IvPico's -# cmake. +# then IvPico against them, all in BUILD_DIR (default build), and installs them all into +# BUILD_DIR/usr (e.g. add build/usr/bin to PATH). Arguments are passed on to IvPico's cmake. set -euo pipefail cd "$(dirname "$0")" @@ -14,10 +14,10 @@ done BUILD_DIR=$(realpath -m "${BUILD_DIR:-build}") OPENOCEAN_BUILD_DIR=${BUILD_DIR}/external/openocean-messages IVP_BUILD_DIR=${BUILD_DIR}/external/moos-ivp -# find_package(IVP) only works from an installation -IVP_INSTALL_DIR=${BUILD_DIR}/external/install +PREFIX=${BUILD_DIR}/usr BUILD_DIR=${OPENOCEAN_BUILD_DIR} external/openocean-messages/build.sh -DBUILD_TESTING=OFF +cmake --install "${OPENOCEAN_BUILD_DIR}" --prefix "${PREFIX}" > /dev/null cmake -S external/moos-ivp/ivp/src -B "${IVP_BUILD_DIR}" -G Ninja \ -DIVP_BUILD_HELM_LIBS_ONLY=ON -DBUILD_SHARED_LIBS=ON \ @@ -25,10 +25,13 @@ cmake -S external/moos-ivp/ivp/src -B "${IVP_BUILD_DIR}" -G Ninja \ -DIVP_INC_DIRECTORY="${IVP_BUILD_DIR}/include" \ -DIVP_BIN_DIRECTORY="${IVP_BUILD_DIR}/bin" ninja -C "${IVP_BUILD_DIR}" -cmake --install "${IVP_BUILD_DIR}" --prefix "${IVP_INSTALL_DIR}" > /dev/null +# find_package(IVP) only works from an installation +cmake --install "${IVP_BUILD_DIR}" --prefix "${PREFIX}" > /dev/null cmake -S . -B "${BUILD_DIR}" -G Ninja \ -Dopenocean_messages_DIR="${OPENOCEAN_BUILD_DIR}" \ - -DIVP_DIR="${IVP_INSTALL_DIR}/lib/cmake/IVP" \ + -DIVP_DIR="${PREFIX}/lib/cmake/IVP" \ + -DCMAKE_INSTALL_PREFIX="${PREFIX}" \ "$@" ninja -C "${BUILD_DIR}" +cmake --install "${BUILD_DIR}" > /dev/null diff --git a/src/lib/CMakeLists.txt b/src/lib/CMakeLists.txt index f901ee3..e3925da 100644 --- a/src/lib/CMakeLists.txt +++ b/src/lib/CMakeLists.txt @@ -14,3 +14,7 @@ target_link_libraries(ivpico_helm PUBLIC ivpico_interface PRIVATE IVP::helmivp IVP::geodaid IVP::behaviors IVP::contacts IVP::logic IVP::ivpbuild IVP::ivpsolve IVP::ivpcore IVP::mbutil) + +install(TARGETS ivpico_interface ivpico_helm + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) From 7b60e0b5b2908138347cb81888385756dd49a15a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 00:05:10 +0000 Subject: [PATCH 6/9] alpha launch: add goby_opencpn_interface, and a README The vehicle is served as an AIS target on TCP port 54000 for OpenCPN. launch/goby/alpha/README.md covers running the mission and connecting OpenCPN; the top-level README points to it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VLcYK4e29B5FiPbDDJy5fp --- README.md | 12 +------- launch/goby/alpha/README.md | 38 +++++++++++++++++++++++++ launch/goby/alpha/alpha.launch | 2 ++ launch/goby/alpha/config/opencpn.pb.cfg | 17 +++++++++++ 4 files changed, 58 insertions(+), 11 deletions(-) create mode 100644 launch/goby/alpha/README.md create mode 100644 launch/goby/alpha/config/opencpn.pb.cfg diff --git a/README.md b/README.md index 82cad1e..e6c3ea3 100644 --- a/README.md +++ b/README.md @@ -48,15 +48,5 @@ To update a submodule, check out the commit you want in it and commit the new po `goby_helm_ivp` runs the helm in Goby3. It publishes and subscribes the `ivp.interface` messages on the `ivpico::helm::*` groups (`src/bin/goby/groups.h`), and can stand in for the MOOS helm behind `goby_frontseat_interface` (`frontseat { enable: true }`). `goby_helm_ivp -e` prints an example configuration. -To run the alpha mission against Goby's basic frontseat simulator: - -``` -export PATH=$PWD/build/usr/bin:$PATH -export GOBY_TOOL_LOAD_SHARED_LIBRARY=$PWD/build/usr/lib/libivpico_interface.so -cd launch/goby/alpha -goby_launch alpha.launch -./deploy.sh -``` - -`deploy.sh` publishes DEPLOY=true and lifts the manual override with `goby zeromq publish`, which can publish any other `Variable` or `Command` the same way. +`launch/goby/alpha` runs the alpha mission against Goby's basic frontseat simulator, with OpenCPN as a chart display: see its [README](launch/goby/alpha/README.md). diff --git a/launch/goby/alpha/README.md b/launch/goby/alpha/README.md new file mode 100644 index 0000000..6d7a7e1 --- /dev/null +++ b/launch/goby/alpha/README.md @@ -0,0 +1,38 @@ +# alpha + +*This section was written by Claude.* + +MOOS-IvP's alpha mission (a survey, then a return) with `goby_helm_ivp` driving Goby's basic frontseat simulator, at time warp 10. + +```mermaid +flowchart LR + helm["goby_helm_ivp"] -- "DesiredCourse" --> fs["goby_frontseat_interface"] + fs <--> sim["goby_basic_frontseat_simulator"] + fs -- "NodeStatus" --> helm + fs -- "NodeStatus" --> ocpn["goby_opencpn_interface"] + ocpn -- "AIS, TCP 54000" --> opencpn["OpenCPN"] +``` + +## Running + +From the top of IvPico, after `./build.sh`: + +``` +export PATH=$PWD/build/usr/bin:$PATH +export GOBY_TOOL_LOAD_SHARED_LIBRARY=$PWD/build/usr/lib/libivpico_interface.so +cd launch/goby/alpha +goby_launch alpha.launch +./deploy.sh +``` + +`deploy.sh` publishes DEPLOY=true and lifts the manual override with `goby zeromq publish`, which can publish any other `Variable` or `Command` the same way. + +## Viewing in OpenCPN + +`goby_opencpn_interface` serves the vehicle's position as AIS on TCP port 54000 (`config/opencpn.pb.cfg`). + +1. Install OpenCPN: `sudo add-apt-repository ppa:opencpn/opencpn && sudo apt install opencpn`. +2. In Options: Connections, add a Network connection: TCP, address 127.0.0.1, port 54000. +3. The vehicle appears as an AIS target near 43.825 N, 70.330 W. Charts are optional (Options: Charts: Chart Downloader). + +See Goby's [visualization interfaces](https://github.com/GobySoft/goby3/blob/3.0/src/doc/markdown/doc507_goby_viz.md) for more. diff --git a/launch/goby/alpha/alpha.launch b/launch/goby/alpha/alpha.launch index 8a9939a..625900e 100755 --- a/launch/goby/alpha/alpha.launch +++ b/launch/goby/alpha/alpha.launch @@ -7,5 +7,7 @@ gobyd config/gobyd.pb.cfg goby_frontseat_interface_basic_simulator config/frontseat.pb.cfg goby_helm_ivp config/helm_ivp.pb.cfg +# the vehicle as an AIS target for OpenCPN (see README.md) +goby_opencpn_interface config/opencpn.pb.cfg # goby_liaison (from the goby3-gui package) shows and publishes messages at http://localhost:50001 # goby_liaison config/liaison.pb.cfg diff --git a/launch/goby/alpha/config/opencpn.pb.cfg b/launch/goby/alpha/config/opencpn.pb.cfg new file mode 100644 index 0000000..4d5ed4d --- /dev/null +++ b/launch/goby/alpha/config/opencpn.pb.cfg @@ -0,0 +1,17 @@ +app { + simulation { + time { + use_sim_time: true + warp_factor: 10 + } + } +} + +interprocess { + platform: "frontseat_sim_example" +} + +# OpenCPN connects here (Options: Connections: Network, TCP, 127.0.0.1:54000) +ais_server { + bind_port: 54000 +} From ed4f0833229ad14b0cb31c1970a2403bf7ccf212 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 00:50:44 +0000 Subject: [PATCH 7/9] alpha launch: view the vehicle in pMarineViewer viewer.launch runs a MOOS community (alpha.moos, with pNodeReporter and pMarineViewer set up as in MOOS-IvP's s1_alpha) and goby_moos_gateway with a new IvPico plugin. The plugin posts what pHelmIvP and its navigation would (NAV_*, the helm's postings and IVPHELM_*), and gives configured MOOS variables, such as pMarineViewer's buttons, back to the helm. allstop_string() moves from the replay test into the helm library, beside modes_string(), for IVPHELM_ALLSTOP and IVPHELM_SUMMARY. The alpha launch's Goby platform is now "alpha", so the behaviors' labels match the MOOS mission's. init.sh installs Goby's MOOS support and the MOOS-IvP apps. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VLcYK4e29B5FiPbDDJy5fp --- CMakeLists.txt | 5 +- init.sh | 3 + launch/goby/alpha/alpha.moos | 87 +++++++++++++++++ launch/goby/alpha/config/frontseat.pb.cfg | 6 +- launch/goby/alpha/config/gobyd.pb.cfg | 2 +- launch/goby/alpha/config/helm_ivp.pb.cfg | 2 +- launch/goby/alpha/config/liaison.pb.cfg | 2 +- launch/goby/alpha/config/moos_gateway.pb.cfg | 22 +++++ launch/goby/alpha/config/opencpn.pb.cfg | 2 +- launch/goby/alpha/deploy.sh | 2 +- launch/goby/alpha/viewer.launch | 7 ++ src/bin/goby/CMakeLists.txt | 6 ++ src/bin/goby/moos_gateway/CMakeLists.txt | 8 ++ src/bin/goby/moos_gateway/moos_gateway.cpp | 97 +++++++++++++++++++ .../moos_gateway/moos_gateway_config.proto | 20 ++++ src/lib/CMakeLists.txt | 2 +- src/lib/helm/status_strings.cpp | 37 +++++++ src/lib/helm/status_strings.h | 21 ++++ src/test/missions/replay.cpp | 24 +---- 19 files changed, 323 insertions(+), 32 deletions(-) create mode 100644 launch/goby/alpha/alpha.moos create mode 100644 launch/goby/alpha/config/moos_gateway.pb.cfg create mode 100755 launch/goby/alpha/viewer.launch create mode 100644 src/bin/goby/moos_gateway/CMakeLists.txt create mode 100644 src/bin/goby/moos_gateway/moos_gateway.cpp create mode 100644 src/bin/goby/moos_gateway/moos_gateway_config.proto create mode 100644 src/lib/helm/status_strings.cpp create mode 100644 src/lib/helm/status_strings.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a71fa79..ed1c740 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,9 +21,12 @@ find_package(openocean_messages 0.1 REQUIRED COMPONENTS cpp) # MOOS-IvP's helm libraries (IVP_BUILD_HELM_LIBS_ONLY) find_package(IVP CONFIG REQUIRED) -find_package(GOBY 3.0 COMPONENTS zeromq) +find_package(GOBY 3.0 COMPONENTS zeromq OPTIONAL_COMPONENTS moos) if(NOT GOBY_FOUND) message(STATUS "Goby3 not found: not building the Goby app") +elseif(GOBY_moos_FOUND) + # goby_moos links MOOS::MOOS + find_package(MOOS 10 QUIET) endif() include(CTest) diff --git a/init.sh b/init.sh index 7f6ccca..7eec28b 100755 --- a/init.sh +++ b/init.sh @@ -60,6 +60,9 @@ if ${goby}; then # libgoby3-dev only recommends these, but goby's CMake package links libais.so, # and goby_add_app() runs protoc-gen-dccl apt_install libgoby3-dev goby3-apps libais-dev dccl5-compiler + # pMarineViewer for the Goby launches (viewer.launch), through goby_moos_gateway; + # MOOS-IvP's apps need libmoos-ivp but don't depend on it + apt_install libgoby3-moos-dev goby3-moos moosdb10 moos-ivp-apps moos-ivp-gui libmoos-ivp fi external/openocean-messages/init.sh --cxx diff --git a/launch/goby/alpha/alpha.moos b/launch/goby/alpha/alpha.moos new file mode 100644 index 0000000..ceeb734 --- /dev/null +++ b/launch/goby/alpha/alpha.moos @@ -0,0 +1,87 @@ +// pMarineViewer for the Goby alpha mission (viewer.launch), as in MOOS-IvP's +// s1_alpha mission. goby_moos_gateway posts NAV_*, the helm's postings and +// IVPHELM_* here, and gives pMarineViewer's buttons back to goby_helm_ivp. + +ServerHost = localhost +ServerPort = 9000 +Community = alpha + +MOOSTimeWarp = 10 + +LatOrigin = 43.825300 +LongOrigin = -70.330400 + +ProcessConfig = pNodeReporter +{ + AppTick = 2 + CommsTick = 2 + + platform_type = kayak + platform_color = yellow + platform_length = 4 +} + +ProcessConfig = pMarineViewer +{ + AppTick = 4 + CommsTick = 4 + + tiff_file = forrest19.tif + //tiff_file = MIT_SP.tif + + set_pan_x = -90 + set_pan_y = -280 + zoom = 0.65 + vehicle_shape_scale = 1.5 + hash_delta = 50 + hash_shade = 0.22 + hash_viewable = true + + trails_point_size = 1 + + //op_vertex = x=-83, y=-47, lwidth=1,lcolor=yellow,looped=true,group=moa + //op_vertex = x=-46.4, y=-129.2, lwidth=1,lcolor=yellow,looped=true,group=moa + //op_vertex = x=94.6, y=-62.2, lwidth=1,lcolor=yellow,looped=true,group=moa + //op_vertex = x=58, y=20, lwidth=1,lcolor=yellow,looped=true,group=moa + + // Appcast configuration + appcast_height = 75 + appcast_width = 30 + appcast_viewable = true + appcast_color_scheme = indigo + nodes_font_size = xlarge + procs_font_size = xlarge + appcast_font_size = large + + // datum_viewable = true + // datum_size = 18 + // gui_size = small + + // left_context[survey-point] = DEPLOY=true + // left_context[survey-point] = MOOS_MANUAL_OVERRIDE=false + // left_context[survey-point] = RETURN=false + + right_context[return] = DEPLOY=true + right_context[return] = MOOS_MANUAL_OVERRIDE=false + right_context[return] = RETURN=false + + scope = SUMMARY + scope = RETURN + scope = WPT_STAT + scope = VIEW_SEGLIST + scope = VIEW_POINT + scope = VIEW_POLYGON + scope = MVIEWER_LCLICK + scope = MVIEWER_RCLICK + + button_one = DEPLOY # DEPLOY=true + button_one = MOOS_MANUAL_OVERRIDE=false # RETURN=false + button_two = RETURN # RETURN=true + button_three = SLOWER # WPT_UPDATE=speed=1.5 + button_four = FASTER # WPT_UPDATE=speed=3.5 + + action = MENU_KEY=deploy # DEPLOY = true # RETURN = false + action+ = MENU_KEY=deploy # MOOS_MANUAL_OVERRIDE=false + action = RETURN=true + action = UPDATES_RETURN=speed=1.4 +} diff --git a/launch/goby/alpha/config/frontseat.pb.cfg b/launch/goby/alpha/config/frontseat.pb.cfg index c7c86c2..a524661 100644 --- a/launch/goby/alpha/config/frontseat.pb.cfg +++ b/launch/goby/alpha/config/frontseat.pb.cfg @@ -12,7 +12,7 @@ app { } interprocess { - platform: "frontseat_sim_example" + platform: "alpha" } frontseat_cfg { @@ -25,8 +25,8 @@ frontseat_cfg { tcp_address: "127.0.0.1" tcp_port: 54321 start { - lat: 43.825 # deg - lon: -70.330 # deg + lat: 43.825300 # deg + lon: -70.330400 # deg duration: 0 # 0 == no timeout, otherwise timeout in seconds control_freq: 10 # Hz vehicle { diff --git a/launch/goby/alpha/config/gobyd.pb.cfg b/launch/goby/alpha/config/gobyd.pb.cfg index c32d052..73b925c 100644 --- a/launch/goby/alpha/config/gobyd.pb.cfg +++ b/launch/goby/alpha/config/gobyd.pb.cfg @@ -8,5 +8,5 @@ app { } interprocess { - platform: "frontseat_sim_example" + platform: "alpha" } diff --git a/launch/goby/alpha/config/helm_ivp.pb.cfg b/launch/goby/alpha/config/helm_ivp.pb.cfg index 566a6aa..8d890a1 100644 --- a/launch/goby/alpha/config/helm_ivp.pb.cfg +++ b/launch/goby/alpha/config/helm_ivp.pb.cfg @@ -12,7 +12,7 @@ app { } interprocess { - platform: "frontseat_sim_example" + platform: "alpha" } helm { diff --git a/launch/goby/alpha/config/liaison.pb.cfg b/launch/goby/alpha/config/liaison.pb.cfg index a8771f1..f5022dd 100644 --- a/launch/goby/alpha/config/liaison.pb.cfg +++ b/launch/goby/alpha/config/liaison.pb.cfg @@ -8,7 +8,7 @@ app { } interprocess { - platform: "frontseat_sim_example" + platform: "alpha" } http_address: "localhost" diff --git a/launch/goby/alpha/config/moos_gateway.pb.cfg b/launch/goby/alpha/config/moos_gateway.pb.cfg new file mode 100644 index 0000000..435c408 --- /dev/null +++ b/launch/goby/alpha/config/moos_gateway.pb.cfg @@ -0,0 +1,22 @@ +app { + simulation { + time { + use_sim_time: true + warp_factor: 10 + } + } +} + +interprocess { + platform: "alpha" +} + +moos { + server: "localhost" + port: 9000 +} + +[ivpico.goby.protobuf.ivpico] { + # what pMarineViewer's buttons and actions post (alpha.moos) + to_helm: ["DEPLOY", "MOOS_MANUAL_OVERRIDE", "RETURN", "WPT_UPDATE", "UPDATES_RETURN"] +} diff --git a/launch/goby/alpha/config/opencpn.pb.cfg b/launch/goby/alpha/config/opencpn.pb.cfg index 4d5ed4d..c74f67e 100644 --- a/launch/goby/alpha/config/opencpn.pb.cfg +++ b/launch/goby/alpha/config/opencpn.pb.cfg @@ -8,7 +8,7 @@ app { } interprocess { - platform: "frontseat_sim_example" + platform: "alpha" } # OpenCPN connects here (Options: Connections: Network, TCP, 127.0.0.1:54000) diff --git a/launch/goby/alpha/deploy.sh b/launch/goby/alpha/deploy.sh index 568fff4..75931b1 100755 --- a/launch/goby/alpha/deploy.sh +++ b/launch/goby/alpha/deploy.sh @@ -2,7 +2,7 @@ # Deploys the vehicle: DEPLOY=true for the behaviors, and lifts the manual override. set -euo pipefail -export GOBY_INTERPROCESS='platform: "frontseat_sim_example"' +export GOBY_INTERPROCESS='platform: "alpha"' export GOBY_TOOL_LOAD_SHARED_LIBRARY=${GOBY_TOOL_LOAD_SHARED_LIBRARY:-libivpico_interface.so} goby zeromq publish ivpico::helm::variable PROTOBUF/ivp.interface.Variable 'key: "DEPLOY" sval: "true"' diff --git a/launch/goby/alpha/viewer.launch b/launch/goby/alpha/viewer.launch new file mode 100755 index 0000000..8a67761 --- /dev/null +++ b/launch/goby/alpha/viewer.launch @@ -0,0 +1,7 @@ +#!/usr/bin/env goby_launch + +# pMarineViewer, as in MOOS-IvP's alpha mission (see README.md): start after alpha.launch +MOOSDB alpha.moos +pNodeReporter alpha.moos +[env=IVP_IMAGE_DIRS=${IVP_IMAGE_DIRS:-$PWD/../../../external/moos-ivp/ivp/data}] pMarineViewer alpha.moos +[env=GOBY_MOOS_GATEWAY_PLUGINS=${GOBY_MOOS_GATEWAY_PLUGINS:-$PWD/../../../build/usr/lib/libivpico_moos_gateway_plugin.so}] goby_moos_gateway config/moos_gateway.pb.cfg diff --git a/src/bin/goby/CMakeLists.txt b/src/bin/goby/CMakeLists.txt index 3701d9d..be966c4 100644 --- a/src/bin/goby/CMakeLists.txt +++ b/src/bin/goby/CMakeLists.txt @@ -1 +1,7 @@ add_subdirectory(helm_ivp) + +if(GOBY_moos_FOUND AND MOOS_FOUND) + add_subdirectory(moos_gateway) +else() + message(STATUS "Goby's MOOS support not found: not building the goby_moos_gateway plugin") +endif() diff --git a/src/bin/goby/moos_gateway/CMakeLists.txt b/src/bin/goby/moos_gateway/CMakeLists.txt new file mode 100644 index 0000000..e6da492 --- /dev/null +++ b/src/bin/goby/moos_gateway/CMakeLists.txt @@ -0,0 +1,8 @@ +goby_add_library(TARGET ivpico_moos_gateway_plugin + SOURCES moos_gateway.cpp + PROTOS moos_gateway_config.proto + PROTOC_OUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/proto + PROTO_IMPORT_DIRS ${PROJECT_SOURCE_DIR}/src/lib ${openocean_messages_PROTO_DIR} + LINK_LIBRARIES goby_moos ivpico_helm) +target_include_directories(ivpico_moos_gateway_plugin PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/..) +install(TARGETS ivpico_moos_gateway_plugin LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}) diff --git a/src/bin/goby/moos_gateway/moos_gateway.cpp b/src/bin/goby/moos_gateway/moos_gateway.cpp new file mode 100644 index 0000000..c28d507 --- /dev/null +++ b/src/bin/goby/moos_gateway/moos_gateway.cpp @@ -0,0 +1,97 @@ +// goby_moos_gateway plugin that gives a MOOS community what pHelmIvP and its +// navigation would post (NAV_*, the helm's postings, IVPHELM_*), so MOOS-IvP +// tools such as pNodeReporter and pMarineViewer work with goby_helm_ivp. + +#include + +#include +#include +#include +#include +#include + +#include "groups.h" +#include "helm/status_strings.h" +#include "interface/ivp.pb.h" +#include "moos_gateway_config.pb.h" + +namespace ivpico +{ +namespace goby +{ +class MOOSGatewayTranslation : public ::goby::moos::Translator +{ + public: + MOOSGatewayTranslation(const ::goby::apps::moos::protobuf::GobyMOOSGatewayConfig& cfg) + : ::goby::moos::Translator(cfg), cfg_(cfg.GetExtension(protobuf::ivpico)) + { + using ::goby::middleware::frontseat::protobuf::NodeStatus; + goby() + .interprocess() + .subscribe<::goby::middleware::frontseat::groups::node_status, NodeStatus, + ::goby::middleware::MarshallingScheme::PROTOBUF>( + [this](const NodeStatus& status) + { ::goby::moos::convert_and_publish_node_status(status, moos().comms()); }); + goby().interprocess().subscribe( + [this](const ivp::interface::Posting& posting) { post(posting); }); + goby().interprocess().subscribe([this](const ivp::interface::Status& status) + { post(status); }); + + for (const auto& var : cfg_.to_helm()) + moos().add_trigger(var, [this](const CMOOSMsg& msg) { to_helm(msg); }); + } + + private: + void post(const ivp::interface::Posting& posting) + { + if (posting.value_case() == ivp::interface::Posting::kSval) + moos().comms().Notify(posting.key(), posting.sval()); + else + moos().comms().Notify(posting.key(), posting.dval()); + } + + void post(const ivp::interface::Status& status) + { + auto& comms = moos().comms(); + comms.Notify("IVPHELM_STATE", ivp::interface::Status::State_Name(status.state())); + comms.Notify("IVPHELM_ALLSTOP", helm::allstop_string(status)); + const std::string modes = helm::modes_string(status); + comms.Notify("IVPHELM_SUMMARY", "modes=" + (modes.empty() ? "none" : modes)); + } + + void to_helm(const CMOOSMsg& msg) + { + // the helm's own postings of these variables come back from the MOOSDB + if (msg.GetSource() == moos().comms().GetMOOSName()) + return; + + ivp::interface::Variable variable; + variable.set_key(msg.GetKey()); + if (msg.IsString()) + variable.set_sval(msg.GetString()); + else + variable.set_dval(msg.GetDouble()); + goby().interprocess().publish(variable); + } + + protobuf::MOOSGatewayConfig cfg_; +}; +} // namespace goby +} // namespace ivpico + +extern "C" +{ + void goby3_moos_gateway_load( + goby::zeromq::MultiThreadApplication* + handler) + { + handler->launch_thread(); + } + + void goby3_moos_gateway_unload( + goby::zeromq::MultiThreadApplication* + handler) + { + handler->join_thread(); + } +} diff --git a/src/bin/goby/moos_gateway/moos_gateway_config.proto b/src/bin/goby/moos_gateway/moos_gateway_config.proto new file mode 100644 index 0000000..81ee3f2 --- /dev/null +++ b/src/bin/goby/moos_gateway/moos_gateway_config.proto @@ -0,0 +1,20 @@ +syntax = "proto2"; + +import "goby/moos/protobuf/moos_gateway_config.proto"; +import "goby/protobuf/option_extensions.proto"; + +package ivpico.goby.protobuf; + +message MOOSGatewayConfig +{ + repeated string to_helm = 1 [ + (.goby.field).description = + "MOOS variables given to goby_helm_ivp as Variables, such as those posted by " + "pMarineViewer's buttons" + ]; +} + +extend .goby.apps.moos.protobuf.GobyMOOSGatewayConfig +{ + optional MOOSGatewayConfig ivpico = 1100; +} diff --git a/src/lib/CMakeLists.txt b/src/lib/CMakeLists.txt index e3925da..c16ed7a 100644 --- a/src/lib/CMakeLists.txt +++ b/src/lib/CMakeLists.txt @@ -8,7 +8,7 @@ protobuf_generate( target_include_directories(ivpico_interface PUBLIC $) target_link_libraries(ivpico_interface PUBLIC openocean_messages::openocean_messages) -add_library(ivpico_helm helm/helm.cpp helm/conversions.cpp) +add_library(ivpico_helm helm/helm.cpp helm/conversions.cpp helm/status_strings.cpp) target_include_directories(ivpico_helm PUBLIC $) target_link_libraries(ivpico_helm PUBLIC ivpico_interface diff --git a/src/lib/helm/status_strings.cpp b/src/lib/helm/status_strings.cpp new file mode 100644 index 0000000..680396d --- /dev/null +++ b/src/lib/helm/status_strings.cpp @@ -0,0 +1,37 @@ +#include "status_strings.h" + +namespace ivpico +{ +namespace helm +{ +std::string allstop_string(const ivp::interface::Status& status) +{ + switch (status.allstop()) + { + case ivp::interface::CLEAR: return "clear"; + case ivp::interface::MANUAL_OVERRIDE: return "ManualOverride"; + case ivp::interface::BEHAVIOR_ERROR: return "BehaviorError"; + case ivp::interface::NOTHING_TO_DO: return "NothingToDo"; + case ivp::interface::NO_GOAL_BEHAVIOR: return "NoGoalBehavior"; + case ivp::interface::DISABLED_BY_STANDBY_HELM: return "DisabledByStandbyHelm"; + case ivp::interface::MISSING_DECISION_VARIABLES: + { + std::string s = "MissingDecVars:"; + for (int i = 0; i < status.missing_decision_variables_size(); ++i) + s += (i ? "," : "") + status.missing_decision_variables(i); + return s; + } + default: return ""; + } +} + +std::string modes_string(const ivp::interface::Status& status) +{ + std::string s; + for (const auto& mode : status.mode()) + s += (s.empty() ? "" : "#") + mode.variable() + "@" + mode.value(); + return s; +} + +} // namespace helm +} // namespace ivpico diff --git a/src/lib/helm/status_strings.h b/src/lib/helm/status_strings.h new file mode 100644 index 0000000..5827f3f --- /dev/null +++ b/src/lib/helm/status_strings.h @@ -0,0 +1,21 @@ +#ifndef IVPICO_HELM_STATUS_STRINGS_H +#define IVPICO_HELM_STATUS_STRINGS_H + +#include + +#include "interface/ivp.pb.h" + +namespace ivpico +{ +namespace helm +{ +// pHelmIvP's IVPHELM_ALLSTOP value +std::string allstop_string(const ivp::interface::Status& status); + +// The "modes" field of pHelmIvP's IVPHELM_SUMMARY, e.g. MODE@ACTIVE:SURVEYING +std::string modes_string(const ivp::interface::Status& status); + +} // namespace helm +} // namespace ivpico + +#endif diff --git a/src/test/missions/replay.cpp b/src/test/missions/replay.cpp index c93f29b..a84dab0 100644 --- a/src/test/missions/replay.cpp +++ b/src/test/missions/replay.cpp @@ -50,6 +50,7 @@ #include #include "helm/helm.h" +#include "helm/status_strings.h" namespace fs = std::filesystem; using ivp::interface::HelmConfig; @@ -318,27 +319,6 @@ std::string number(double d) return os.str(); } -std::string allstop_string(const Status& status) -{ - switch (status.allstop()) - { - case ivp::interface::CLEAR: return "clear"; - case ivp::interface::MANUAL_OVERRIDE: return "ManualOverride"; - case ivp::interface::BEHAVIOR_ERROR: return "BehaviorError"; - case ivp::interface::NOTHING_TO_DO: return "NothingToDo"; - case ivp::interface::NO_GOAL_BEHAVIOR: return "NoGoalBehavior"; - case ivp::interface::DISABLED_BY_STANDBY_HELM: return "DisabledByStandbyHelm"; - case ivp::interface::MISSING_DECISION_VARIABLES: - { - std::string s = "MissingDecVars:"; - for (int i = 0; i < status.missing_decision_variables_size(); ++i) - s += (i ? "," : "") + status.missing_decision_variables(i); - return s; - } - default: return ""; - } -} - std::string behavior_event_var(ivp::interface::BehaviorEvent::Kind kind) { using ivp::interface::BehaviorEvent; @@ -558,7 +538,7 @@ int main(int argc, char* argv[]) const int tick = static_cast(k); add_change(ivpico, "IVPHELM_STATE", tick, Status::State_Name(out.status.state())); if (out.status.allstop() != ivp::interface::ALLSTOP_UNKNOWN) - add_change(ivpico, "IVPHELM_ALLSTOP", tick, allstop_string(out.status)); + add_change(ivpico, "IVPHELM_ALLSTOP", tick, ivpico::helm::allstop_string(out.status)); std::string active; for (const auto& b : out.status.behavior()) From 8b36b6978f2e86d7255b5a9af69b66ad9f127bec Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 00:50:44 +0000 Subject: [PATCH 8/9] alpha launch: add set_datum_warp.sh, and document the viewer set_datum_warp.sh sets the datum and time warp in every configuration and alpha.moos, starts the simulated vehicle at the datum, and picks the MOOS-IvP image covering the datum for pMarineViewer. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VLcYK4e29B5FiPbDDJy5fp --- launch/goby/alpha/README.md | 27 +++++++++++ launch/goby/alpha/set_datum_warp.sh | 74 +++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100755 launch/goby/alpha/set_datum_warp.sh diff --git a/launch/goby/alpha/README.md b/launch/goby/alpha/README.md index 6d7a7e1..f19539a 100644 --- a/launch/goby/alpha/README.md +++ b/launch/goby/alpha/README.md @@ -11,6 +11,13 @@ flowchart LR fs -- "NodeStatus" --> helm fs -- "NodeStatus" --> ocpn["goby_opencpn_interface"] ocpn -- "AIS, TCP 54000" --> opencpn["OpenCPN"] + fs -- "NodeStatus" --> gw["goby_moos_gateway"] + helm -- "Posting, Status" --> gw + gw -- "buttons (Variable)" --> helm + subgraph moos["MOOS (viewer.launch)"] + gw <--> db["MOOSDB"] <--> pnr["pNodeReporter"] + db <--> pmv["pMarineViewer"] + end ``` ## Running @@ -27,6 +34,26 @@ goby_launch alpha.launch `deploy.sh` publishes DEPLOY=true and lifts the manual override with `goby zeromq publish`, which can publish any other `Variable` or `Command` the same way. +As in MOOS-IvP's alpha, the helm parks when the survey is done (pHelmIvP's `park_on_allstop`); deploy again to return. + +## Viewing in pMarineViewer + +`viewer.launch` shows the vehicle in pMarineViewer as in MOOS-IvP's alpha mission, with its buttons (DEPLOY, RETURN, ...) working. Start it after `alpha.launch`: + +``` +goby_launch viewer.launch +``` + +It runs a MOOS community (`alpha.moos`), which `goby_moos_gateway` with IvPico's plugin (`src/bin/goby/moos_gateway`) gives what pHelmIvP and its navigation would post: `NAV_*`, the helm's postings (`VIEW_*`, `WPT_STAT`, ...) and `IVPHELM_*`. The MOOS variables listed in `config/moos_gateway.pb.cfg` go back to the helm as `Variable`s. + +## Changing the datum and time warp + +``` +./set_datum_warp.sh --datum 42.358456 -71.087589 --warp 5 +``` + +sets them in every configuration (and `alpha.moos`), starts the vehicle at the datum, and chooses the MOOS-IvP image covering it for pMarineViewer. With no options it prints the current settings. + ## Viewing in OpenCPN `goby_opencpn_interface` serves the vehicle's position as AIS on TCP port 54000 (`config/opencpn.pb.cfg`). diff --git a/launch/goby/alpha/set_datum_warp.sh b/launch/goby/alpha/set_datum_warp.sh new file mode 100755 index 0000000..36ca7c3 --- /dev/null +++ b/launch/goby/alpha/set_datum_warp.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# Sets the alpha mission's datum (also the simulator's start position) and time warp in its +# Goby configuration and alpha.moos, choosing the MOOS-IvP image that covers the datum. +# +# Usage: set_datum_warp.sh [--datum ] [--warp ] +# With no options, prints the current settings. +set -euo pipefail + +cd "$(dirname "$0")" + +usage() { sed -n '5,6p' "$0" | sed 's/^# //'; } +fail() { + echo "$1" >&2 + usage >&2 + exit 1 +} +is_number() { [[ $1 =~ ^-?[0-9]+(\.[0-9]+)?$ ]]; } + +lat="" lon="" warp="" +while [ $# -gt 0 ]; do + case "$1" in + --datum) + [ $# -ge 3 ] && is_number "$2" && is_number "$3" || fail "--datum needs a latitude and longitude" + lat=$2 lon=$3 + shift 3 + ;; + --warp) + [ $# -ge 2 ] && is_number "$2" || fail "--warp needs a number" + warp=$2 + shift 2 + ;; + -h | --help) + usage + exit 0 + ;; + *) fail "unknown option: $1" ;; + esac +done + +goby_cfgs=(config/*.pb.cfg) +image_dir=${IVP_IMAGE_DIRS:-../../../external/moos-ivp/ivp/data} + +# the smallest image whose .info bounds contain the datum +image_for() { + for info in "${image_dir}"/*.info; do + awk -v lat="$1" -v lon="$2" -v tif="$(basename "${info%.info}").tif" -F= ' + { gsub(/[ \t]/, ""); b[$1] = $2 } + END { + if (!("lat_north" in b) || !("lon_east" in b)) exit + if (lat <= b["lat_north"] && lat >= b["lat_south"] && lon <= b["lon_east"] && lon >= b["lon_west"]) + print (b["lat_north"] - b["lat_south"]) * (b["lon_east"] - b["lon_west"]), tif + }' "${info}" + done | sort -g | head -1 | cut -d' ' -f2 +} + +if [ -n "${lat}" ]; then + sed -i -E "s/(lat_origin: ).*/\1${lat}/; s/(lon_origin: ).*/\1${lon}/" "${goby_cfgs[@]}" + sed -i -E "s/^(\s*lat: )[-0-9.]+/\1${lat}/; s/^(\s*lon: )[-0-9.]+/\1${lon}/" config/frontseat.pb.cfg + sed -i -E "s/^(LatOrigin\s*=\s*).*/\1${lat}/; s/^(LongOrigin\s*=\s*).*/\1${lon}/" alpha.moos + image=$(image_for "${lat}" "${lon}") + if [ -n "${image}" ]; then + sed -i -E "s/^(\s*tiff_file\s*=\s*).*/\1${image}/" alpha.moos + else + echo "No image in ${image_dir} covers ${lat}, ${lon}: set tiff_file in alpha.moos" >&2 + fi +fi + +if [ -n "${warp}" ]; then + sed -i -E "s/(warp_factor: ).*/\1${warp}/" "${goby_cfgs[@]}" + sed -i -E "s/^(MOOSTimeWarp\s*=\s*).*/\1${warp}/" alpha.moos +fi + +grep -HE "lat_origin|lon_origin|warp_factor|^\s*(lat|lon): " "${goby_cfgs[@]}" | sed -E 's/\s+/ /g' +grep -HE "^(LatOrigin|LongOrigin|MOOSTimeWarp)|^\s*tiff_file" alpha.moos | sed -E 's/\s+/ /g' From c71dbd59ce668c3c1d2faf1982254434e708e254 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 04:16:28 +0000 Subject: [PATCH 9/9] alpha launch: use MOOS-IvP's s1_alpha behaviors and uSimMarine-like dynamics config/alpha.bhv is now MOOS-IvP's s1_alpha mission's, so the Goby and MOOS missions fly the same survey (1.2 m/s) and return (2 m/s). The simulator's turn rate (4 deg/s) and acceleration (0.25 m/s^2) are those measured from uSimMarineV22 in the recorded s1_alpha mission. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VLcYK4e29B5FiPbDDJy5fp --- launch/goby/alpha/config/alpha.bhv | 63 ++++++++++++++--------- launch/goby/alpha/config/frontseat.pb.cfg | 4 +- 2 files changed, 42 insertions(+), 25 deletions(-) diff --git a/launch/goby/alpha/config/alpha.bhv b/launch/goby/alpha/config/alpha.bhv index 26b21d5..7847455 100644 --- a/launch/goby/alpha/config/alpha.bhv +++ b/launch/goby/alpha/config/alpha.bhv @@ -12,22 +12,39 @@ Behavior = BHV_Waypoint condition = DEPLOY = true endflag = RETURN = true + endflag = SUMMARY=speed=$[SPEED],time=$[DUR_RUN_TIME] + + configflag = CRUISE_SPD = $[SPEED] + + activeflag = INFO=$[OWNSHIP] + activeflag = INFO=$[BHVNAME] + activeflag = INFO=$[BHVTYPE] + + cycleflag = CINFO=$[OSX],$[OSY] + + wptflag = PREV=$(PX),$(PY) + wptflag = NEXT=$(NX),$(NY) + wptflag = TEST=$(X),$(Y) + wptflag = OSPOS=$(OSX),$(OSY) + wptflag_on_start = true + updates = WPT_UPDATE perpetual = true + speed_alt = 1.2 + use_alt_speed = true lead = 8 lead_damper = 1 lead_to_start = true - speed = 12 // meters per second + speed = 3 // meters per second capture_line = true capture_radius = 5.0 slip_radius = 15.0 efficiency_measure = all - - polygon = 60,-40 : 60,-40 : 60,-160 : 150,-160 : 180,-100 : 150,-40 + polygon = 60,-40:60,-160:150,-160:180,-100:150,-40 order = normal - repeat = 1 + repeat = 0 visual_hints = nextpt_color=yellow visual_hints = nextpt_vertex_size=8 @@ -39,15 +56,15 @@ Behavior = BHV_Waypoint //---------------------------------------------- Behavior=BHV_Waypoint { - name = waypt_return - pwt = 100 - condition = RETURN = true - condition = DEPLOY = true - perpetual = true - updates = RETURN_UPDATE - endflag = RETURN = false - endflag = DEPLOY = false - endflag = MISSION = complete + name = waypt_return + pwt = 100 + condition = RETURN = true + condition = DEPLOY = true + perpetual = true + updates = RETURN_UPDATE + endflag = RETURN = false + endflag = DEPLOY = false + endflag = MISSION = complete speed = 2.0 capture_radius = 2.0 @@ -58,17 +75,17 @@ Behavior=BHV_Waypoint //---------------------------------------------- Behavior=BHV_ConstantSpeed { - name = const_speed - pwt = 200 - condition = SPD=true - condition = DEPLOY = true - perpetual = true - updates = SPEED_UPDATE - endflag = SPD = false + name = const_speed + pwt = 200 + condition = SPD=true + condition = DEPLOY = true + perpetual = true + updates = SPEED_UPDATE + endflag = SPD = false - speed = 0.5 - duration = 10 - duration_reset = CONST_SPD_RESET=true + speed = 0.5 + duration = 10 + duration_reset = CONST_SPD_RESET=true } diff --git a/launch/goby/alpha/config/frontseat.pb.cfg b/launch/goby/alpha/config/frontseat.pb.cfg index a524661..6b8c91f 100644 --- a/launch/goby/alpha/config/frontseat.pb.cfg +++ b/launch/goby/alpha/config/frontseat.pb.cfg @@ -30,8 +30,8 @@ frontseat_cfg { duration: 0 # 0 == no timeout, otherwise timeout in seconds control_freq: 10 # Hz vehicle { - accel: 1.0 # m/s^2 - hdg_rate: 50 # deg/s + accel: 0.25 # m/s^2 + hdg_rate: 4 # deg/s z_rate: 2 # m/s } }