Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,13 @@ if (ROBOTICS_TOOLBOX_STANDALONE)

FetchContent_MakeAvailable(emil)

# Declared before numerical_toolbox is made available: numerical-toolbox-cpp declares ui in
# its own standalone block, and FetchContent honours the first declaration it sees, so this
# keeps the pin here authoritative.
if (ROBOTICS_TOOLBOX_BUILD_SIMULATOR)
FetchContent_Declare(
ui
GIT_REPOSITORY https://github.com/embedded-pro/ui-cpp.git
GIT_TAG main
)

# Explicit rather than inferred from this repository's simulator flag: e-foc configures
# its Qt tools on every host build, so no consumer flag means the same thing everywhere.
set(UI_BUILD_QT_BACKEND On CACHE BOOL "" FORCE)
endif()

Expand Down
1 change: 1 addition & 0 deletions simulator/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
add_subdirectory(shell)
add_subdirectory(dynamics)
1 change: 1 addition & 0 deletions simulator/dynamics/RobotArm/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ emil_build_for(robotics.simulator.dynamics.robot_arm HOST All PREREQUISITE_BOOL
target_link_libraries(robotics.simulator.dynamics.robot_arm PRIVATE
robotics.simulator.dynamics.robotarm.application
robotics.simulator.dynamics.robotarm.view
robotics.simulator.shell
)

target_sources(robotics.simulator.dynamics.robot_arm PRIVATE
Expand Down
12 changes: 2 additions & 10 deletions simulator/dynamics/RobotArm/Main.cpp
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
#include "simulator/dynamics/RobotArm/view/RobotArmMainWindow.hpp"
#include "ui/theme/Theme.hpp"
#include <QApplication>
#include "simulator/shell/AppRunner.hpp"

int main(int argc, char* argv[])
{
QApplication app(argc, argv);

ui::theme::SetCurrent(ui::theme::Instrument());

simulator::dynamics::view::RobotArmMainWindow window;
window.show();

return app.exec();
return simulator::shell::Run<simulator::dynamics::view::RobotArmMainWindow>(argc, argv, ui::theme::Instrument());
}
7 changes: 7 additions & 0 deletions simulator/dynamics/RobotArm/application/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ target_include_directories(robotics.simulator.dynamics.robotarm.application PUBL
target_link_libraries(robotics.simulator.dynamics.robotarm.application PUBLIC
robotics.dynamics
robotics.kinematics
ui.model
)

target_sources(robotics.simulator.dynamics.robotarm.application PRIVATE
RobotArmSimulator.cpp
RobotArmSimulator.hpp
RobotArmForm.cpp
RobotArmForm.hpp
)

if(MSVC)
Expand All @@ -22,3 +25,7 @@ else()
-Wno-error
)
endif()

if (ROBOTICS_TOOLBOX_BUILD_TESTS)
add_subdirectory(test)
endif()
122 changes: 122 additions & 0 deletions simulator/dynamics/RobotArm/application/RobotArmForm.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#include "simulator/dynamics/RobotArm/application/RobotArmForm.hpp"
#include <numbers>

namespace simulator::dynamics
{
namespace
{
using ui::model::ActionSpec;
using ui::model::Condition;
using ui::model::FieldKind;
using ui::model::FieldSpec;
using ui::model::GroupSpec;
using ui::model::OptionSpec;

constexpr float torqueScale{ 10.0f };
constexpr float degreesToRadians{ std::numbers::pi_v<float> / 180.0f };

constexpr std::array<OptionSpec, 2> degreeOptions{
OptionSpec{ "2 DOF (Planar)", 2 },
OptionSpec{ "3 DOF (Spatial)", 3 }
};

constexpr Condition spatialOnly{ field::degreesOfFreedom, 0b10 };

constexpr std::array<GroupSpec, 7> groups{
GroupSpec{ field::robot, "Robot Configuration", {} },
GroupSpec{ field::firstLink, "Link 1", {} },
GroupSpec{ field::secondLink, "Link 2", {} },
GroupSpec{ field::thirdLink, "Link 3", spatialOnly },
GroupSpec{ field::torques, "Joint Torques (N·m)", {} },
GroupSpec{ field::positions, "Initial Position (deg)", {} },
GroupSpec{ field::simulation, "Simulation", {} }
};

constexpr std::array<FieldSpec, 14> fields{
FieldSpec{ field::degreesOfFreedom, field::robot, FieldKind::Choice, "DOF:", "", {}, degreeOptions, {}, {} },
FieldSpec{ field::firstLength, field::firstLink, FieldKind::Number, "Length (m):", "", { 0.05, 2.0, 0.05, 0.5, 2 }, {}, {}, {} },
FieldSpec{ field::firstMass, field::firstLink, FieldKind::Number, "Mass (kg):", "", { 0.1, 20.0, 0.1, 1.0, 2 }, {}, {}, {} },
FieldSpec{ field::secondLength, field::secondLink, FieldKind::Number, "Length (m):", "", { 0.05, 2.0, 0.05, 0.4, 2 }, {}, {}, {} },
FieldSpec{ field::secondMass, field::secondLink, FieldKind::Number, "Mass (kg):", "", { 0.1, 20.0, 0.1, 0.8, 2 }, {}, {}, {} },
FieldSpec{ field::thirdLength, field::thirdLink, FieldKind::Number, "Length (m):", "", { 0.05, 2.0, 0.05, 0.3, 2 }, {}, spatialOnly, {} },
FieldSpec{ field::thirdMass, field::thirdLink, FieldKind::Number, "Mass (kg):", "", { 0.1, 20.0, 0.1, 0.6, 2 }, {}, spatialOnly, {} },
FieldSpec{ field::firstTorque, field::torques, FieldKind::Slider, "τ1:", "", { -200.0, 200.0, 1.0, 0.0, 0, 50.0 }, {}, {}, {} },
FieldSpec{ field::secondTorque, field::torques, FieldKind::Slider, "τ2:", "", { -200.0, 200.0, 1.0, 0.0, 0, 50.0 }, {}, {}, {} },
FieldSpec{ field::thirdTorque, field::torques, FieldKind::Slider, "τ3:", "", { -200.0, 200.0, 1.0, 0.0, 0, 50.0 }, {}, spatialOnly, {} },
FieldSpec{ field::firstPosition, field::positions, FieldKind::Slider, "q1:", "", { -180.0, 180.0, 1.0, 0.0, 0, 45.0 }, {}, {}, {} },
FieldSpec{ field::secondPosition, field::positions, FieldKind::Slider, "q2:", "", { -180.0, 180.0, 1.0, 0.0, 0, 45.0 }, {}, {}, {} },
FieldSpec{ field::thirdPosition, field::positions, FieldKind::Slider, "q3:", "", { -180.0, 180.0, 1.0, 0.0, 0, 45.0 }, {}, spatialOnly, {} },
FieldSpec{ field::damping, field::simulation, FieldKind::Number, "Damping:", "", { 0.0, 5.0, 0.01, 0.05, 3 }, {}, {}, {} }
};

constexpr std::array<ActionSpec, 3> actions{
ActionSpec{ field::start, "Start", ui::theme::ButtonRole::Start, 0 },
ActionSpec{ field::stop, "Stop", ui::theme::ButtonRole::Stop, 0 },
ActionSpec{ field::reset, "Reset", ui::theme::ButtonRole::Reset, 0 }
};

constexpr std::array<ui::model::FieldId, 3> lengthFields{ field::firstLength, field::secondLength, field::thirdLength };
constexpr std::array<ui::model::FieldId, 3> massFields{ field::firstMass, field::secondMass, field::thirdMass };
constexpr std::array<ui::model::FieldId, 3> torqueFields{ field::firstTorque, field::secondTorque, field::thirdTorque };
constexpr std::array<ui::model::FieldId, 3> positionFields{ field::firstPosition, field::secondPosition, field::thirdPosition };
}

RobotArmForm::RobotArmForm()
: spec{ groups, fields, actions, {} }
, model{ spec, values, {} }
{}

ui::model::FormModel& RobotArmForm::Model()
{
return model;
}

const ui::model::FormModel& RobotArmForm::Model() const
{
return model;
}

std::size_t RobotArmForm::DegreesOfFreedom() const
{
return static_cast<std::size_t>(model.SelectedData(field::degreesOfFreedom));
}

RobotArmConfig RobotArmForm::BuildConfiguration() const
{
RobotArmConfig config;

config.dof = static_cast<int>(DegreesOfFreedom());
config.damping = model.Float(field::damping);

config.linkLengths.resize(DegreesOfFreedom());
config.linkMasses.resize(DegreesOfFreedom());

for (std::size_t i = 0; i < DegreesOfFreedom(); ++i)
{
config.linkLengths[i] = model.Float(lengthFields[i]);
config.linkMasses[i] = model.Float(massFields[i]);
}

return config;
}

std::vector<float> RobotArmForm::Torques() const
{
std::vector<float> torques(DegreesOfFreedom());

for (std::size_t i = 0; i < torques.size(); ++i)
torques[i] = model.Float(torqueFields[i]) / torqueScale;

return torques;
}

std::vector<float> RobotArmForm::InitialPositions() const
{
std::vector<float> positions(DegreesOfFreedom());

for (std::size_t i = 0; i < positions.size(); ++i)
positions[i] = model.Float(positionFields[i]) * degreesToRadians;

return positions;
}
}
62 changes: 62 additions & 0 deletions simulator/dynamics/RobotArm/application/RobotArmForm.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#pragma once

#include "simulator/dynamics/RobotArm/application/RobotArmTypes.hpp"
#include "ui/model/FormModel.hpp"
#include <array>
#include <vector>

namespace simulator::dynamics
{
namespace field
{
inline constexpr ui::model::FieldId degreesOfFreedom{ 1 };
inline constexpr ui::model::FieldId firstLength{ 2 };
inline constexpr ui::model::FieldId firstMass{ 3 };
inline constexpr ui::model::FieldId secondLength{ 4 };
inline constexpr ui::model::FieldId secondMass{ 5 };
inline constexpr ui::model::FieldId thirdLength{ 6 };
inline constexpr ui::model::FieldId thirdMass{ 7 };
inline constexpr ui::model::FieldId firstTorque{ 8 };
inline constexpr ui::model::FieldId secondTorque{ 9 };
inline constexpr ui::model::FieldId thirdTorque{ 10 };
inline constexpr ui::model::FieldId firstPosition{ 11 };
inline constexpr ui::model::FieldId secondPosition{ 12 };
inline constexpr ui::model::FieldId thirdPosition{ 13 };
inline constexpr ui::model::FieldId damping{ 14 };

inline constexpr ui::model::GroupId robot{ 1 };
inline constexpr ui::model::GroupId firstLink{ 2 };
inline constexpr ui::model::GroupId secondLink{ 3 };
inline constexpr ui::model::GroupId thirdLink{ 4 };
inline constexpr ui::model::GroupId torques{ 5 };
inline constexpr ui::model::GroupId positions{ 6 };
inline constexpr ui::model::GroupId simulation{ 7 };

inline constexpr ui::model::ActionId start{ 1 };
inline constexpr ui::model::ActionId stop{ 2 };
inline constexpr ui::model::ActionId reset{ 3 };
}

class RobotArmForm
{
public:
RobotArmForm();

[[nodiscard]] ui::model::FormModel& Model();
[[nodiscard]] const ui::model::FormModel& Model() const;

[[nodiscard]] RobotArmConfig BuildConfiguration() const;
[[nodiscard]] std::vector<float> Torques() const;
[[nodiscard]] std::vector<float> InitialPositions() const;

private:
[[nodiscard]] std::size_t DegreesOfFreedom() const;

static constexpr std::size_t fieldCount{ 14 };

std::array<ui::model::FieldValue, fieldCount> values{};

ui::model::FormSpec spec;
ui::model::FormModel model;
};
}
12 changes: 12 additions & 0 deletions simulator/dynamics/RobotArm/application/test/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
add_executable(robotics.simulator.dynamics.robotarm.application_test)
emil_build_for(robotics.simulator.dynamics.robotarm.application_test HOST All PREREQUISITE_BOOL ROBOTICS_TOOLBOX_BUILD_SIMULATOR)
emil_add_test(robotics.simulator.dynamics.robotarm.application_test)

target_link_libraries(robotics.simulator.dynamics.robotarm.application_test PUBLIC
gmock_main
robotics.simulator.dynamics.robotarm.application
)

target_sources(robotics.simulator.dynamics.robotarm.application_test PRIVATE
TestRobotArmForm.cpp
)
127 changes: 127 additions & 0 deletions simulator/dynamics/RobotArm/application/test/TestRobotArmForm.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#include "simulator/dynamics/RobotArm/application/RobotArmForm.hpp"
#include <gmock/gmock.h>
#include <numbers>

namespace
{
using simulator::dynamics::RobotArmForm;
namespace field = simulator::dynamics::field;

class RobotArmFormTest
: public ::testing::Test
{
protected:
RobotArmForm form;
};
}

TEST_F(RobotArmFormTest, TheDefaultsMatchThePanelThisReplaced)
{
const auto config = form.BuildConfiguration();

EXPECT_EQ(config.dof, 2);
EXPECT_NEAR(config.damping, 0.05f, 1e-5f);

ASSERT_EQ(config.linkLengths.size(), 2u);
EXPECT_NEAR(config.linkLengths[0], 0.5f, 1e-4f);
EXPECT_NEAR(config.linkLengths[1], 0.4f, 1e-4f);
EXPECT_NEAR(config.linkMasses[0], 1.0f, 1e-4f);
EXPECT_NEAR(config.linkMasses[1], 0.8f, 1e-4f);
}

TEST_F(RobotArmFormTest, TheThirdLinkAppearsOnlyForTheSpatialRobot)
{
EXPECT_FALSE(form.Model().IsGroupVisible(field::thirdLink));
EXPECT_FALSE(form.Model().IsVisible(field::thirdLength));
EXPECT_FALSE(form.Model().IsVisible(field::thirdTorque));
EXPECT_FALSE(form.Model().IsVisible(field::thirdPosition));

form.Model().SetSelection(field::degreesOfFreedom, 1);

EXPECT_TRUE(form.Model().IsGroupVisible(field::thirdLink));
EXPECT_TRUE(form.Model().IsVisible(field::thirdLength));
EXPECT_TRUE(form.Model().IsVisible(field::thirdTorque));
EXPECT_TRUE(form.Model().IsVisible(field::thirdPosition));
}

TEST_F(RobotArmFormTest, TheThirdLinkReachesTheConfigurationOnlyWhenSelected)
{
EXPECT_EQ(form.BuildConfiguration().linkLengths.size(), 2u);

form.Model().SetSelection(field::degreesOfFreedom, 1);

const auto config = form.BuildConfiguration();
ASSERT_EQ(config.linkLengths.size(), 3u);
EXPECT_EQ(config.dof, 3);
EXPECT_NEAR(config.linkLengths[2], 0.3f, 1e-4f);
EXPECT_NEAR(config.linkMasses[2], 0.6f, 1e-4f);
}

TEST_F(RobotArmFormTest, TheDegreeCountComesFromTheOptionDataNotItsPosition)
{
EXPECT_EQ(form.BuildConfiguration().dof, 2);

form.Model().SetSelection(field::degreesOfFreedom, 1);

EXPECT_EQ(form.BuildConfiguration().dof, 3);
}

TEST_F(RobotArmFormTest, TheTorqueSlidersCarryTenthsOfANewtonMetre)
{
form.Model().SetNumber(field::firstTorque, 125.0);
form.Model().SetNumber(field::secondTorque, -40.0);

const auto torques = form.Torques();

ASSERT_EQ(torques.size(), 2u);
EXPECT_NEAR(torques[0], 12.5f, 1e-4f);
EXPECT_NEAR(torques[1], -4.0f, 1e-4f);
}

TEST_F(RobotArmFormTest, ThePositionSlidersAreDegreesAndReachTheSimulatorAsRadians)
{
form.Model().SetNumber(field::firstPosition, 90.0);
form.Model().SetNumber(field::secondPosition, -180.0);

const auto positions = form.InitialPositions();

ASSERT_EQ(positions.size(), 2u);
EXPECT_NEAR(positions[0], std::numbers::pi_v<float> / 2.0f, 1e-5f);
EXPECT_NEAR(positions[1], -std::numbers::pi_v<float>, 1e-5f);
}

TEST_F(RobotArmFormTest, TheTorqueAndPositionVectorsFollowTheSelectedDegreeCount)
{
EXPECT_EQ(form.Torques().size(), 2u);
EXPECT_EQ(form.InitialPositions().size(), 2u);

form.Model().SetSelection(field::degreesOfFreedom, 1);

EXPECT_EQ(form.Torques().size(), 3u);
EXPECT_EQ(form.InitialPositions().size(), 3u);
}

TEST_F(RobotArmFormTest, TheDraggedControlsAreSlidersCarryingTheirTickIntervals)
{
for (const auto id : { field::firstTorque, field::secondTorque, field::thirdTorque })
{
EXPECT_EQ(form.Model().Field(id).kind, ui::model::FieldKind::Slider);
EXPECT_NEAR(form.Model().Field(id).number.tickInterval, 50.0, 1e-9);
}

for (const auto id : { field::firstPosition, field::secondPosition, field::thirdPosition })
{
EXPECT_EQ(form.Model().Field(id).kind, ui::model::FieldKind::Slider);
EXPECT_NEAR(form.Model().Field(id).number.tickInterval, 45.0, 1e-9);
}
}

TEST_F(RobotArmFormTest, TheHiddenThirdLinkKeepsItsValuesAcrossASwitch)
{
form.Model().SetSelection(field::degreesOfFreedom, 1);
form.Model().SetNumber(field::thirdLength, 1.25);
form.Model().SetSelection(field::degreesOfFreedom, 0);
form.Model().SetSelection(field::degreesOfFreedom, 1);

EXPECT_NEAR(form.BuildConfiguration().linkLengths[2], 1.25f, 1e-4f);
}
Loading
Loading