Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
d924996
Add C++ GoogleTest target for the encoder
patkenneally Sep 28, 2026
eb18099
Add gtest for speed quantization over consecutive steps
patkenneally Sep 28, 2026
5e0620f
Add gtest for the off signal zeroing the output
patkenneally Sep 28, 2026
56fad43
Add gtest for the nominal signal after an off signal
patkenneally Sep 28, 2026
9e60fa5
Add gtest for the stuck signal holding the previous output
patkenneally Sep 28, 2026
d962c27
Rename the Python encoder test to snake_case and remove boilerplate
patkenneally Sep 28, 2026
e3722a5
Reduce the Python encoder test to a smoke test of the bindings
patkenneally Sep 28, 2026
443429b
Remove the empty destructor and redundant return statements
patkenneally Sep 28, 2026
9012e03
Mark the inherited virtual methods override
patkenneally Sep 28, 2026
598a358
Use std::trunc and std::numbers::pi from the C++ headers
patkenneally Sep 28, 2026
fbb023c
Declare the encode local variables const at first use
patkenneally Sep 28, 2026
3e556d7
Add class member initializers
patkenneally Sep 28, 2026
b3dfdcc
Use the buffered input message for the zero time step output
patkenneally Sep 28, 2026
a95b5fa
Correct the comment on the input message read
patkenneally Sep 28, 2026
984c63a
Add EncoderSignal enum class for the wheel signal states
patkenneally Sep 28, 2026
a2a9244
Remove the unused signal state macros from simDefinitions.h
patkenneally Sep 28, 2026
8a08a4a
Forward C++ exceptions from the encoder to Python
patkenneally Sep 28, 2026
7c25aac
Throw from reset when the speed input message is not linked
patkenneally Sep 28, 2026
9fe9e31
Replace the public encoder config members with validated setters
patkenneally Sep 28, 2026
06a09b7
Reject a wheel count above RW_EFF_CNT
patkenneally Sep 28, 2026
e1c38c8
Require the wheel count and clicks per rotation in the constructor
patkenneally Sep 28, 2026
1df44b2
Replace the public signal state array with per-wheel accessors
patkenneally Sep 28, 2026
66f2859
Add a signalStates attribute that sets all wheel signal states at once
patkenneally Sep 28, 2026
10d90fb
Remove the per-step error for signal states the setters now reject
patkenneally Sep 28, 2026
6c0f50a
Remove the unused encoder logger
patkenneally Sep 28, 2026
f19226f
Keep the configured signal states across reset
patkenneally Sep 28, 2026
589523a
Pass the wheel thetas through the encoder on every step
patkenneally Sep 28, 2026
278d9b4
Apply the signal state on steps with a zero time step
patkenneally Sep 28, 2026
1aae2a8
Set the output of unused wheels to zero when the wheel count changes
patkenneally Sep 28, 2026
61e08b8
Add encoder fuzz target with an angle conservation property
patkenneally Sep 28, 2026
f7f5711
Add fuzz property for the signal state effects
patkenneally Sep 28, 2026
529e127
Add fuzz property that wheels beyond numRW stay zero
patkenneally Sep 28, 2026
a808568
Add Doxygen comments to the encoder public API
patkenneally Sep 28, 2026
daf6cc8
Simplify the language of the encoder comments and docs
patkenneally Sep 28, 2026
656357b
Correct the truncation wording in the encoder documentation
patkenneally Sep 28, 2026
69995e5
Document the encoder constructor, setters, signal states and errors
patkenneally Sep 28, 2026
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
6 changes: 1 addition & 5 deletions src/architecture/utilities/simDefinitions.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,11 @@
#ifndef SIM_MACROS_H
#define SIM_MACROS_H

#define EPOCH_YEAR 2019
#define EPOCH_YEAR 2'019
#define EPOCH_MONTH 01
#define EPOCH_DAY 01
#define EPOCH_HOUR 00
#define EPOCH_MIN 00
#define EPOCH_SEC 0.00

#define SIGNAL_NOMINAL 0
#define SIGNAL_OFF 1
#define SIGNAL_STUCK 2

#endif // SIM_MACROS_H
2 changes: 2 additions & 0 deletions src/simulation/deviceInterface/encoder/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ xmera_add_swig_module("${module}")
target_sources("${module}" PRIVATE
encoder.cpp
)

add_subdirectory(_UnitTest)
13 changes: 13 additions & 0 deletions src/simulation/deviceInterface/encoder/_UnitTest/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# The encoder .cpp is compiled only into the SWIG Python module (not a linkable
# C++ library), so pull the source in directly here.
add_executable(test_encoder
../encoder.cpp
test_encoder.cpp
)
target_include_directories(test_encoder PRIVATE "${CMAKE_SOURCE_DIR}")
target_link_libraries(test_encoder PRIVATE
GTest::gtest_main
Xmera::Core
ArchitectureUtilities
)
gtest_discover_tests(test_encoder)
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: ISC
// Copyright (c) 2026, Laboratory for Atmospheric and Space Physics, University of Colorado at Boulder

#ifndef ENCODER_TEST_HELPERS_HPP
#define ENCODER_TEST_HELPERS_HPP

#include <architecture/messaging/messaging.h>
#include <architecture/msgPayloadDef/RWSpeedMsgPayload.h>

#include <simulation/deviceInterface/encoder/encoder.h>

#include <cstddef>
#include <cstdint>
#include <vector>

namespace encodertest {
//! One second in nanoseconds. The tests use this value as the time step.
constexpr uint64_t oneSecond = 1'000'000'000ULL;

//! This harness connects an encoder to an input speed message and an output reader.
class EncoderHarness {
public:
//! Make an encoder with the given wheel count and clicks per rotation, then connect its messages.
EncoderHarness(std::size_t numRW, std::uint32_t clicksPerRotation) {
this->encoder.setNumRW(numRW);
this->encoder.setClicksPerRotation(clicksPerRotation);
this->encoder.rwSpeedInMsg.subscribeTo(&this->speedInMsg);
this->speedOut = this->encoder.rwSpeedOutMsg.addSubscriber();
}

EncoderHarness(EncoderHarness const &) = delete;
EncoderHarness &operator=(EncoderHarness const &) = delete;

//! Write the wheel speeds to the input message, update the encoder at time t, and read the output.
RWSpeedMsgPayload step(std::vector<double> const &wheelSpeeds, uint64_t t) {
RWSpeedMsgPayload payload{};
for (std::size_t i = 0; i < wheelSpeeds.size(); ++i) { payload.wheelSpeeds[i] = wheelSpeeds[i]; }
this->speedInMsg.write(payload, 0, t);
this->encoder.updateState(t);
return this->speedOut();
}

Encoder encoder; //!< encoder under test
Message<RWSpeedMsgPayload> speedInMsg; //!< input wheel speed message
ReadFunctor<RWSpeedMsgPayload> speedOut; //!< reader of the encoder output message
};
} // namespace encodertest

#endif // ENCODER_TEST_HELPERS_HPP
115 changes: 115 additions & 0 deletions src/simulation/deviceInterface/encoder/_UnitTest/test_encoder.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// SPDX-License-Identifier: ISC
// Copyright (c) 2026, Laboratory for Atmospheric and Space Physics, University of Colorado at Boulder

#include "encoderTestHelpers.hpp"

#include <gtest/gtest.h>

#include <numbers>
#include <stdexcept>

using encodertest::EncoderHarness;
using encodertest::oneSecond;

namespace {
constexpr double pi = std::numbers::pi;
constexpr double tolerance = 1e-9;
} // namespace

//! On the first step the time step is zero, so the encoder sends the true wheel speeds.
TEST(Encoder, firstStepSendsTrueSpeeds) {
EncoderHarness harness(3, 2);
harness.encoder.reset(0);

RWSpeedMsgPayload const out = harness.step({100.0, 200.0, 300.0}, 0);

EXPECT_DOUBLE_EQ(out.wheelSpeeds[0], 100.0);
EXPECT_DOUBLE_EQ(out.wheelSpeeds[1], 200.0);
EXPECT_DOUBLE_EQ(out.wheelSpeeds[2], 300.0);
}

//! With two clicks per rotation, each output speed is a whole number of clicks times pi rad/s.
//! The encoder keeps the fraction of a click that remains and adds it to the next step.
TEST(Encoder, quantizesSpeedOverConsecutiveSteps) {
EncoderHarness harness(3, 2);
harness.encoder.reset(0);
std::vector<double> const speeds{100.0, 200.0, 300.0};

harness.step(speeds, 0);
RWSpeedMsgPayload const first = harness.step(speeds, oneSecond);
RWSpeedMsgPayload const second = harness.step(speeds, 2 * oneSecond);

EXPECT_NEAR(first.wheelSpeeds[0], 31.0 * pi, tolerance);
EXPECT_NEAR(first.wheelSpeeds[1], 63.0 * pi, tolerance);
EXPECT_NEAR(first.wheelSpeeds[2], 95.0 * pi, tolerance);
EXPECT_NEAR(second.wheelSpeeds[0], 32.0 * pi, tolerance);
EXPECT_NEAR(second.wheelSpeeds[1], 64.0 * pi, tolerance);
EXPECT_NEAR(second.wheelSpeeds[2], 95.0 * pi, tolerance);
}

//! When the signal is off, the encoder sends zero speed.
TEST(Encoder, offSignalSendsZeroSpeed) {
EncoderHarness harness(3, 2);
harness.encoder.reset(0);
std::vector<double> const speeds{100.0, 200.0, 300.0};
harness.step(speeds, 0);
harness.step(speeds, oneSecond);

for (int i = 0; i < 3; ++i) { harness.encoder.rwSignalState[i] = EncoderSignal::Off; }
RWSpeedMsgPayload const out = harness.step(speeds, 2 * oneSecond);

EXPECT_DOUBLE_EQ(out.wheelSpeeds[0], 0.0);
EXPECT_DOUBLE_EQ(out.wheelSpeeds[1], 0.0);
EXPECT_DOUBLE_EQ(out.wheelSpeeds[2], 0.0);
}

//! An off signal erases the remaining clicks. After the signal is nominal again, the count starts from zero.
TEST(Encoder, nominalSignalAfterOffStartsFromZeroClicks) {
EncoderHarness harness(3, 2);
harness.encoder.reset(0);
std::vector<double> const speeds{100.0, 200.0, 300.0};
harness.step(speeds, 0);
harness.step(speeds, oneSecond);
harness.step(speeds, 2 * oneSecond);

for (int i = 0; i < 3; ++i) { harness.encoder.rwSignalState[i] = EncoderSignal::Off; }
harness.step(speeds, 3 * oneSecond);
for (int i = 0; i < 3; ++i) { harness.encoder.rwSignalState[i] = EncoderSignal::Nominal; }
RWSpeedMsgPayload const out = harness.step({500.0, 400.0, 300.0}, 4 * oneSecond);

EXPECT_NEAR(out.wheelSpeeds[0], 159.0 * pi, tolerance);
EXPECT_NEAR(out.wheelSpeeds[1], 127.0 * pi, tolerance);
EXPECT_NEAR(out.wheelSpeeds[2], 95.0 * pi, tolerance);
}

//! When the signal is stuck, the encoder sends the speeds of the previous step and ignores the new input.
TEST(Encoder, stuckSignalHoldsPreviousSpeed) {
EncoderHarness harness(3, 2);
harness.encoder.reset(0);
harness.step({500.0, 400.0, 300.0}, 0);
RWSpeedMsgPayload const before = harness.step({500.0, 400.0, 300.0}, oneSecond);

for (int i = 0; i < 3; ++i) { harness.encoder.rwSignalState[i] = EncoderSignal::Stuck; }
RWSpeedMsgPayload const out = harness.step({100.0, 200.0, 300.0}, 2 * oneSecond);

EXPECT_DOUBLE_EQ(out.wheelSpeeds[0], before.wheelSpeeds[0]);
EXPECT_DOUBLE_EQ(out.wheelSpeeds[1], before.wheelSpeeds[1]);
EXPECT_DOUBLE_EQ(out.wheelSpeeds[2], before.wheelSpeeds[2]);
}

//! The encoder cannot operate without an input message. Reset rejects an encoder with no connected input message.
TEST(Encoder, resetRejectsUnlinkedInputMessage) {
Encoder encoder;
encoder.setNumRW(3);
encoder.setClicksPerRotation(2);

EXPECT_THROW(encoder.reset(0), std::invalid_argument);
}

//! The setters reject zero for the wheel count and for the clicks per rotation.
TEST(Encoder, settersRejectZero) {
Encoder encoder;

EXPECT_THROW(encoder.setNumRW(0), std::invalid_argument);
EXPECT_THROW(encoder.setClicksPerRotation(0), std::invalid_argument);
}
Loading