diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 46ff8cbd..a904ea8a 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -39,7 +39,7 @@ jobs: if: always() with: files: | - ./test/build/xml_out/* + ./test/build/xml_out/**/*.xml - name: Analyze with SonarCloud if: github.repository == 'AllYarnsAreBeautiful/ayab-firmware' && ( ( github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == 'AllYarnsAreBeautiful/ayab-firmware' ) || github.event_name == 'push' ) env: diff --git a/src/ayab/global_knitter.cpp b/src/ayab/global_knitter.cpp index 6b330cc9..44ae8a1d 100644 --- a/src/ayab/global_knitter.cpp +++ b/src/ayab/global_knitter.cpp @@ -35,11 +35,9 @@ void GlobalKnitter::setUpInterrupt() { m_instance->setUpInterrupt(); } -#ifndef AYAB_TESTS void GlobalKnitter::isr() { m_instance->isr(); } -#endif Err_t GlobalKnitter::initMachine(Machine_t machine) { return m_instance->initMachine(machine); diff --git a/src/ayab/global_tester.cpp b/src/ayab/global_tester.cpp index cef5676e..173c49ee 100644 --- a/src/ayab/global_tester.cpp +++ b/src/ayab/global_tester.cpp @@ -78,8 +78,6 @@ void GlobalTester::quitCmd() { m_instance->quitCmd(); } -#ifndef AYAB_TESTS void GlobalTester::encoderChange() { m_instance->encoderChange(); } -#endif // AYAB_TESTS diff --git a/src/ayab/knitter.cpp b/src/ayab/knitter.cpp index 50f02765..5035b50e 100644 --- a/src/ayab/knitter.cpp +++ b/src/ayab/knitter.cpp @@ -89,12 +89,11 @@ void Knitter::init() { void Knitter::setUpInterrupt() { // (re-)attach ENC_PIN_A(=2), interrupt #0 detachInterrupt(digitalPinToInterrupt(ENC_PIN_A)); -#ifndef AYAB_TESTS + // Attaching ENC_PIN_A, Interrupt #0 // This interrupt cannot be enabled until // the machine type has been validated. attachInterrupt(digitalPinToInterrupt(ENC_PIN_A), GlobalKnitter::isr, CHANGE); -#endif // AYAB_TESTS } /*! diff --git a/src/ayab/knitter.h b/src/ayab/knitter.h index 70c04792..e9e7a410 100644 --- a/src/ayab/knitter.h +++ b/src/ayab/knitter.h @@ -70,9 +70,7 @@ class GlobalKnitter final { static void init(); static void setUpInterrupt(); -#ifndef AYAB_TESTS static void isr(); -#endif static Err_t startKnitting(uint8_t startNeedle, uint8_t stopNeedle, uint8_t *pattern_start, bool continuousReportingEnabled); diff --git a/src/ayab/solenoids.cpp b/src/ayab/solenoids.cpp index b74c8124..be52d46e 100644 --- a/src/ayab/solenoids.cpp +++ b/src/ayab/solenoids.cpp @@ -58,9 +58,7 @@ void Solenoids::setSolenoid(uint8_t solenoid, bool state) { bitClear(solenoidState, solenoid); } if (oldState != solenoidState) { -#ifndef AYAB_TESTS write(solenoidState); -#endif } } @@ -73,9 +71,7 @@ void Solenoids::setSolenoid(uint8_t solenoid, bool state) { void Solenoids::setSolenoids(uint16_t state) { if (state != solenoidState) { solenoidState = state; -#ifndef AYAB_TESTS write(state); -#endif } } diff --git a/src/ayab/tester.cpp b/src/ayab/tester.cpp index 67c98d62..77a0d914 100644 --- a/src/ayab/tester.cpp +++ b/src/ayab/tester.cpp @@ -188,7 +188,6 @@ void Tester::loop() { } } -#ifndef AYAB_TESTS /*! * \brief Interrupt service routine for encoder A. */ @@ -196,7 +195,6 @@ void Tester::encoderChange() { digitalWrite(LED_PIN_A, digitalRead(ENC_PIN_A)); digitalWrite(LED_PIN_B, digitalRead(ENC_PIN_B)); } -#endif // AYAB_TESTS // Private member functions @@ -216,11 +214,9 @@ void Tester::setUp() { GlobalCom::sendMsg(AYAB_API::testRes, buf); helpCmd(); -#ifndef AYAB_TESTS // Attach interrupts for both encoder pins attachInterrupt(digitalPinToInterrupt(ENC_PIN_A), GlobalTester::encoderChange, CHANGE); attachInterrupt(digitalPinToInterrupt(ENC_PIN_B), GlobalTester::encoderChange, CHANGE); -#endif // AYAB_TESTS m_autoReadOn = false; m_autoTestOn = false; diff --git a/src/ayab/tester.h b/src/ayab/tester.h index 71a8d71d..7cb7eb04 100644 --- a/src/ayab/tester.h +++ b/src/ayab/tester.h @@ -50,9 +50,7 @@ class TesterInterface { virtual void autoTestCmd() = 0; virtual void stopCmd() = 0; virtual void quitCmd() = 0; -#ifndef AYAB_TESTS - virtual void encoderChange(); -#endif + virtual void encoderChange() = 0; }; // Container class for the static methods that implement the hardware test @@ -83,9 +81,7 @@ class GlobalTester final { static void autoTestCmd(); static void stopCmd(); static void quitCmd(); -#ifndef AYAB_TESTS static void encoderChange(); -#endif }; class Tester : public TesterInterface { @@ -103,9 +99,7 @@ class Tester : public TesterInterface { void autoTestCmd() final; void stopCmd() final; void quitCmd() final; -#ifndef AYAB_TESTS void encoderChange() final; -#endif private: void setUp(); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 2fde0172..db227210 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -83,7 +83,7 @@ set(COMMON_LINKER_FLAGS ${ARDUINO_MOCK_LIBS_DIR}/dist/lib/libarduino_mock.a ${ARDUINO_MOCK_LIBS_DIR}/lib/gtest/gtest/src/gtest-build/lib/libgmock.a ${CMAKE_THREAD_LIBS_INIT} - -lgcov + --coverage ) set(HARD_I2C_LIB ${LIBRARY_DIRECTORY}/Adafruit_MCP23008/Adafruit_MCP23008.cpp @@ -166,7 +166,55 @@ target_link_libraries(${PROJECT_NAME}_knitter ) add_dependencies(${PROJECT_NAME}_knitter arduino_mock) +add_executable(${PROJECT_NAME}_e2e + ${PROJECT_SOURCE_DIR}/test_e2e.cpp + ${PROJECT_SOURCE_DIR}/test_knitting_machine.cpp + + ${PROJECT_SOURCE_DIR}/mocks/knitting_machine.cpp + + ${SOURCE_DIRECTORY}/encoders.cpp + ${SOURCE_DIRECTORY}/global_encoders.cpp + + ${SOURCE_DIRECTORY}/solenoids.cpp + ${SOURCE_DIRECTORY}/global_solenoids.cpp + ${HARD_I2C_LIB} + + ${SOURCE_DIRECTORY}/beeper.cpp + ${SOURCE_DIRECTORY}/global_beeper.cpp + + ${SOURCE_DIRECTORY}/com.cpp + ${SOURCE_DIRECTORY}/global_com.cpp + + ${SOURCE_DIRECTORY}/tester.cpp + ${SOURCE_DIRECTORY}/global_tester.cpp + + ${SOURCE_DIRECTORY}/knitter.cpp + ${SOURCE_DIRECTORY}/global_knitter.cpp + + ${SOURCE_DIRECTORY}/fsm.cpp + ${SOURCE_DIRECTORY}/global_fsm.cpp +) +target_include_directories(${PROJECT_NAME}_e2e + PRIVATE + ${COMMON_INCLUDES} + ${EXTERNAL_LIB_INCLUDES} +) +target_compile_definitions(${PROJECT_NAME}_e2e + PRIVATE + ${COMMON_DEFINES} + __AVR_ATmega328P__ +) +target_compile_options(${PROJECT_NAME}_e2e PRIVATE + ${COMMON_FLAGS} + -fno-inline +) +target_link_libraries(${PROJECT_NAME}_e2e + ${COMMON_LINKER_FLAGS} +) +add_dependencies(${PROJECT_NAME}_e2e arduino_mock) + enable_testing() include(GoogleTest) gtest_discover_tests(${PROJECT_NAME}_uno TEST_PREFIX uno_ XML_OUTPUT_DIR ./xml_out) gtest_discover_tests(${PROJECT_NAME}_knitter TEST_PREFIX knitter_ XML_OUTPUT_DIR ./xml_out) +gtest_discover_tests(${PROJECT_NAME}_e2e TEST_PREFIX e2e_ XML_OUTPUT_DIR ./xml_out) diff --git a/test/mocks/io_expanders_mock.h b/test/mocks/io_expanders_mock.h new file mode 100644 index 00000000..549ec9b0 --- /dev/null +++ b/test/mocks/io_expanders_mock.h @@ -0,0 +1,116 @@ +/*!` + * \file io_expanders_mock.h + * + * This file is part of AYAB. + * + * AYAB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AYAB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AYAB. If not, see . + * + * Original Work Copyright 2013 Christian Obersteiner, Andreas Müller + * Modified Work Copyright 2020 Sturla Lange, Tom Price + * http://ayab-knitting.com + */ + +#ifndef IO_EXPANDERS_MOCK_H_ +#define IO_EXPANDERS_MOCK_H_ + +#include +#include + +#include + +using namespace ::testing; + +/*! \brief Simulate I2C I/O expanders + * + * This class connects to arduino-mock's I2C (Wire) support and + * intercepts writes to the simulated I2C bus, emulating a pair of + * MCP23008 I/O expanders. + * + * Of the MCP23008 protocol, only the bare minimum necessary to support + * GPIO writes is implemented. + * + * The current GPIO state can be retrieved at any time by calling + * gpioState(). The result is the state of the 16 digital outputs, + * in the order that they are normally connected to the knitting + * machine's solenoids, i.e. starting from the leftmost solenoid. + * + * Note that a KH270 machine only has 12 solenoids, but since this + * class simulates the I/O expanders, that always have 16 outputs, + * that fact is not relevant here. + * + * Creating an instance of this class takes over WireMock, no expectations + * should be setup on it from outside until that instance is destroyed + * and releaseWireMock is called. + */ +class IOExpandersMock { +public: + IOExpandersMock(WireMock *wireMock) { + EXPECT_CALL(*wireMock, beginTransmission(_)) + .Times(AnyNumber()) + .WillRepeatedly(Invoke(this, &IOExpandersMock::beginTransmission)); + EXPECT_CALL(*wireMock, write(An())) + .Times(AnyNumber()) + .WillRepeatedly(Invoke(this, &IOExpandersMock::write)); + + // We're not interested in calls to the following methods, but they + // happen and if we don't set up expectations for them GoogleMock will + // emit warnings. + EXPECT_CALL(*wireMock, begin()).Times(AnyNumber()); + EXPECT_CALL(*wireMock, endTransmission()).Times(AnyNumber()); + EXPECT_CALL(*wireMock, requestFrom(_, _)).Times(AnyNumber()); + EXPECT_CALL(*wireMock, read).Times(AnyNumber()); + } + + std::array gpioState() { + std::array result; + for (int i = 0; i < 8; i++) { + result[i] = lowByte & (1 << i); + result[i + 8] = highByte & (1 << i); + } + return result; + } + +private: + uint8_t i2c_address = 0, i2c_byteIndex = 0, i2c_register = 0; + uint8_t highByte = 0, lowByte = 0; + + void beginTransmission(uint8_t address) { + i2c_address = address; + i2c_byteIndex = i2c_register = 0; + } + + uint8_t write(uint8_t data) { + switch (i2c_byteIndex++) { + case 0: + i2c_register = data; + break; + case 1: + switch (i2c_register) { + case MCP23008_GPIO: + switch (i2c_address & ~MCP23008_ADDRESS) { + case 0: + lowByte = data; + break; + case 1: + highByte = data; + break; + } + break; + } + } + return 0; + } +}; + +#endif // IO_EXPANDERS_MOCK_H_ diff --git a/test/mocks/knitting_machine.cpp b/test/mocks/knitting_machine.cpp new file mode 100644 index 00000000..2cc56d42 --- /dev/null +++ b/test/mocks/knitting_machine.cpp @@ -0,0 +1,119 @@ +#include +#include +#include + +#include "knitting_machine.h" + +bool KnittingMachine::getEncoderOutput1() const { + return (m_beltPosition + 1) % STEPS_PER_NEEDLE >= 2; +} + +bool KnittingMachine::getEncoderOutput2() const { + return m_beltPosition % STEPS_PER_NEEDLE <= 1; +} + +bool KnittingMachine::getBeltPhase() const { + if (!m_hasBeltShift) { + return false; + } + return ((m_beltPosition + beltPeriod() + m_beltPhaseOffset) % beltPeriod()) >= (beltPeriod() / 2); +} + +void KnittingMachine::moveBeltRight() { + m_beltPosition = (m_beltPosition + 1) % beltPeriod(); +} + +void KnittingMachine::moveBeltLeft() { + m_beltPosition = (m_beltPosition + (beltPeriod() - 1)) % beltPeriod(); +} + + +float KnittingMachine::getPositionSensorVoltage(qneedle_t sensorNeedlePos) const { + float sensorPosition = sensorNeedlePos.asNeedle(); + for (std::pair magnet : m_carriageMagnets) { + float magnetPosition = m_carriagePosition.asNeedle() + magnet.first; + if (std::abs(sensorPosition - magnetPosition) <= m_positionSensorRange) { + return magnet.second ? POSITION_SENSOR_HIGH_VOLTAGE + : POSITION_SENSOR_LOW_VOLTAGE; + } + } + return POSITION_SENSOR_MID_VOLTAGE; +} + +float KnittingMachine::getLeftPositionSensorVoltage() { + return getPositionSensorVoltage(m_leftPositionSensorPosition); +} + +float KnittingMachine::getRightPositionSensorVoltage() { + return getPositionSensorVoltage(m_rightPositionSensorPosition); +} + +float KnittingMachine::getRightPositionSensorKSignal() { + return getRightPositionSensorVoltage() >= POSITION_SENSOR_HIGH_VOLTAGE + ? POSITION_SENSOR_LOW_VOLTAGE + : POSITION_SENSOR_MID_VOLTAGE; +} + +void KnittingMachine::addCarriageMagnet(float offsetFromCenter, bool polarity) { + m_carriageMagnets.push_back(std::make_pair(offsetFromCenter, polarity)); +} + +void KnittingMachine::addGCarriageMagnets() { + addCarriageMagnet(12.25, false); + addCarriageMagnet(10.5, true); + addCarriageMagnet(-10.5, true); + addCarriageMagnet(-12.25, false); +} + +void KnittingMachine::putCarriageCenterInFrontOfNeedle(int position) { + m_carriagePosition = + qneedle_t::fromNeedle(position); // convert to 1/4 of needles +} + +int KnittingMachine::getCarriageCenterNeedle() { + return m_carriagePosition.closestNeedle(); +} + +bool KnittingMachine::carriageEngagesBelt() const { + // TODO disengage carriage when it moves outside the bed + // TODO simulate carriage belt hooks (currently simulates + // a single hook at carriage center) + // TODO simulate belt slack + int period = STEPS_PER_NEEDLE * m_solenoidCount; + if (m_hasBeltShift) { + period /= 2; + } + return (m_beltPosition % period) == + (m_carriagePosition.value % period + period) % period; +} + +void KnittingMachine::moveCarriageRight() { + if (carriageEngagesBelt()) { + moveBeltRight(); + } + ++m_carriagePosition; +} + +void KnittingMachine::moveCarriageLeft() { + if (carriageEngagesBelt()) { + moveBeltLeft(); + } + --m_carriagePosition; +} + +int KnittingMachine::beltPeriod() const { + return m_solenoidCount * STEPS_PER_NEEDLE; +} + +bool KnittingMachine::moveCarriageCenterTowardsNeedle(int position) { + qneedle_t target = qneedle_t::fromNeedle(position); + if (m_carriagePosition == target) { + return false; + } + if (target > m_carriagePosition) { + moveCarriageRight(); + } else { + moveCarriageLeft(); + } + return true; +} \ No newline at end of file diff --git a/test/mocks/knitting_machine.h b/test/mocks/knitting_machine.h new file mode 100644 index 00000000..48c7d406 --- /dev/null +++ b/test/mocks/knitting_machine.h @@ -0,0 +1,235 @@ +#ifndef KNITTINGMACHINE_H_ +#define KNITTINGMACHINE_H_ + +#include +#include +#include +#include + +/** + * The KnittingMachine class is a model of an electromechanical knitting + * machine, without the firmware part. It does not know anything about the AYAB + * code or the Arduino API. + * + * Its inputs are machine user actions (e.g. moving a carriage across the bed) + * and electronic inputs (solenoid power status). + * + * Its outputs are the physical results of the inputs (e.g. positions of needles + * after selection), and electronic outputs (encoder and position sensor + * signals). + */ +class KnittingMachine { +public: + /** + * Return the state of the V1 encoder output + */ + bool getEncoderOutput1() const; + + /** + * Return the state of the V2 encoder output + */ + bool getEncoderOutput2() const; + + /** + * Return the state of the belt phase output + */ + bool getBeltPhase() const; + + /** + * Move belt 1/4 needle to the right + */ + void moveBeltRight(); + + /** + * Move belt 1/4 needle to the left + */ + void moveBeltLeft(); + + /** + * Get the voltage at the left position sensor + */ + float getLeftPositionSensorVoltage(); + + /** + * Get the voltage at the right position sensor + */ + float getRightPositionSensorVoltage(); + + /** + * Get the voltage at the right position sensor's K digital output + * (low when a K carriage's magnet is detected, floating otherwise) + */ + float getRightPositionSensorKSignal(); + + /** + * Add a magnet to the carriage. + * + * \param offsetFromCenter the position of the magnet (in needle widths) + * given as distance from the carriage center + * (positive = right) + * \param polarity the magnet's polarity — (true = North, like K carriage) + */ + void addCarriageMagnet(float offsetFromCenter, bool polarity); + + /** + * Helper to add all G-carriage magnets (as measured on a KG-89) + */ + void addGCarriageMagnets(); + + /** + * Set the carriage's position on the bed. This does not move the belt, + * it is akin to directly inserting the carriage on the bed at the requested + * position. + * + * \param position the new carriage position, in needles. 0 = carriage center + * in front of the left position sensor (which + * is also the position of needle 0 AKA L100). + * Values outside of the machine's range of needles are + * permitted. + */ + void putCarriageCenterInFrontOfNeedle(int position); + + /** + * Return the needle number the carriage center is the closest to. + * + * This may be outside of the machine's bed, e.g. -1 is one needle + * width left of needle 0. + */ + int getCarriageCenterNeedle(); + + /** + * Move the carriage to the right by 1/4 needle + */ + void moveCarriageRight(); + + /** + * Move the carriage to the left by 1/4 needle + */ + void moveCarriageLeft(); + + /** + * Move the carriage center to get closer to the target needle. + * + * \returns false if the carriage is already at the requested position + */ + bool moveCarriageCenterTowardsNeedle(int position); + +private: + static constexpr int STEPS_PER_NEEDLE = 4; + static constexpr float POSITION_SENSOR_LOW_VOLTAGE = 0.2f; + static constexpr float POSITION_SENSOR_MID_VOLTAGE = 2.5f; + static constexpr float POSITION_SENSOR_HIGH_VOLTAGE = 4.7f; + + /** + * An internal type representing a position in 1/4 needle widths + */ + struct qneedle_t { + int value; + bool operator==(const qneedle_t &other) const { + return value == other.value; + } + bool operator!=(const qneedle_t &other) const { + return !(*this == other); + } + bool operator>(const qneedle_t &other) const { + return value > other.value; + } + qneedle_t operator-(const qneedle_t &other) const { + return {value - other.value}; + } + qneedle_t &operator++() { + ++value; + return *this; + } + qneedle_t &operator--() { + --value; + return *this; + } + float asNeedle() const { + return static_cast(value) / STEPS_PER_NEEDLE; + } + int closestNeedle() const { + return std::round(asNeedle()); + } + int leftNeedle() const { + return std::floor(asNeedle()); + } + static qneedle_t fromNeedle(float needle) { + return {(int)std::round(needle * STEPS_PER_NEEDLE)}; + } + }; + + /** + * How many steps the belt (and the rotary cams) goes through before ending up + * in its original state. + */ + int beltPeriod() const; + + /** + * Tells whether the carriage's belt hooks are aligned with + * the elongated belt holes, which are spaced /2 + * needles apart (on machines which have a belt shift), and + * needles apart otherwise. + */ + bool carriageEngagesBelt() const; + + /** + * Get the voltage at a position sensor given its position + */ + float getPositionSensorVoltage(qneedle_t sensorNeedlePosition) const; + + /** + * Location of the position sensors (referenced to needle 0). + */ + qneedle_t m_leftPositionSensorPosition = qneedle_t::fromNeedle(-0.5); + qneedle_t m_rightPositionSensorPosition = qneedle_t::fromNeedle(200); + + /** + * How far from the position sensor a magnet can be to be detected, + * in needle widths. + */ + float m_positionSensorRange = 0.75; + + /** + * How many solenoids the machine has. + */ + int m_solenoidCount = 16; + + /** + * Does the machine have a concept of belt shift, i.e. intermediate + * elongated holes on the belt? + */ + bool m_hasBeltShift = true; + + /** + * It the machine has two belt phases, at which point in the belt + * cycle does the phase change? + * Given in encoder steps, i.e. 1/4 needle widths. + */ + int m_beltPhaseOffset = 9; + + /** + * We only store the belt position modulo * 4. + * + * Its unit is an encoder step, i.e. 1/4 of a needle width. + * + * A belt position of 0 represents a belt position where an + * elongated hole is in front of the left position sensor. + */ + std::uint8_t m_beltPosition = 0; + + /** + * The carriage position in 1/4 needles. + * + * Position 0 is when the carriage center is over needle 0. + */ + qneedle_t m_carriagePosition = qneedle_t::fromNeedle(-32); + + /** + * A list of carriage magnets, defined by their offset from the carriage + * center and their polarity. \see addCarriageMagnet() + */ + std::vector> m_carriageMagnets; +}; + +#endif // KNITTINGMACHINE_H_ \ No newline at end of file diff --git a/test/mocks/knitting_machine_adapter.h b/test/mocks/knitting_machine_adapter.h new file mode 100644 index 00000000..2c0691e6 --- /dev/null +++ b/test/mocks/knitting_machine_adapter.h @@ -0,0 +1,71 @@ +#ifndef KNITTING_MACHINE_ADAPTER_H_ +#define KNITTING_MACHINE_ADAPTER_H_ + +#include + +#include + +#include "knitting_machine.h" + +#include "board.h" + +using namespace ::testing; + +/** + * This class connects a KnittingMachine (which does not know anything about + * testing/mocking, or the AYAB code) to ArduinoMock, so that calls to + * the relevant Arduino functions (e.g. `digitalRead`) made from the AYAB + * code will retrieve or set information on the KnittingMachine instance. + * + * Note that this class makes no attempt at clean-up (because the gMock + * API makes this basically impossible), so once it is destroyed, + * no call should be made to ArduinoMock until releaseArduinoMock() + * has been called. + */ +class KnittingMachineAdapter { +public: + enum Flags { + Default = 0, + DigitalRightSensor = 1, + }; + + KnittingMachineAdapter(KnittingMachine &km, ArduinoMock &arduinoMock, + Flags flags = Default) + : m_km(km), m_flags(flags) { + EXPECT_CALL(arduinoMock, digitalRead(ENC_PIN_A)) + .Times(AnyNumber()) + .WillRepeatedly( + Invoke([&] { return m_km.getEncoderOutput1() ? HIGH : LOW; })); + EXPECT_CALL(arduinoMock, digitalRead(ENC_PIN_B)) + .Times(AnyNumber()) + .WillRepeatedly( + Invoke([&] { return m_km.getEncoderOutput2() ? HIGH : LOW; })); + + EXPECT_CALL(arduinoMock, analogRead(EOL_PIN_L)) + .Times(AnyNumber()) + .WillRepeatedly(Invoke( + [&] { return m_km.getLeftPositionSensorVoltage() * 1023 / 5; })); + + EXPECT_CALL(arduinoMock, digitalRead(ENC_PIN_C)) + .Times(AnyNumber()) + .WillRepeatedly( + Invoke([&] { return m_km.getBeltPhase() ? HIGH : LOW; })); + + EXPECT_CALL(arduinoMock, analogRead(EOL_PIN_R)) + .Times(AnyNumber()) + .WillRepeatedly(Invoke([&] { + if (m_flags & DigitalRightSensor) { + // On the KH-910 EOL_PIN_R is plugged into the K digital signal + return m_km.getRightPositionSensorKSignal() * 1023 / 5; + } else { + return m_km.getRightPositionSensorVoltage() * 1023 / 5; + } + })); + } + +private: + KnittingMachine &m_km; + Flags m_flags; +}; + +#endif // KNITTING_MACHINE_ADAPTER_H_ \ No newline at end of file diff --git a/test/mocks/tester_mock.cpp b/test/mocks/tester_mock.cpp index ced0b559..2939c26b 100644 --- a/test/mocks/tester_mock.cpp +++ b/test/mocks/tester_mock.cpp @@ -103,3 +103,8 @@ void Tester::quitCmd() { assert(gTesterMock != nullptr); gTesterMock->quitCmd(); } + +void Tester::encoderChange() { + assert(gTesterMock != nullptr); + gTesterMock->encoderChange(); +} diff --git a/test/mocks/tester_mock.h b/test/mocks/tester_mock.h index 17ce5b20..d025786c 100644 --- a/test/mocks/tester_mock.h +++ b/test/mocks/tester_mock.h @@ -42,6 +42,7 @@ class TesterMock : public TesterInterface { MOCK_METHOD0(autoTestCmd, void()); MOCK_METHOD0(stopCmd, void()); MOCK_METHOD0(quitCmd, void()); + MOCK_METHOD0(encoderChange, void()); }; TesterMock *testerMockInstance(); diff --git a/test/test_e2e.cpp b/test/test_e2e.cpp new file mode 100644 index 00000000..5995d6f3 --- /dev/null +++ b/test/test_e2e.cpp @@ -0,0 +1,566 @@ +/*!` + * \file test_e2e.cpp + * + * This file is part of AYAB. + * + * AYAB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AYAB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AYAB. If not, see . + * + * Original Work Copyright 2013 Christian Obersteiner, Andreas Müller + * Modified Work Copyright 2020 Sturla Lange, Tom Price + * http://ayab-knitting.com + */ + +#include + +#include "beeper.h" +#include "com.h" +#include "encoders.h" +#include "fsm.h" +#include "knitter.h" +#include "solenoids.h" +#include "tester.h" + +#include "io_expanders_mock.h" + +#include "knitting_machine.h" +#include "knitting_machine_adapter.h" + +using namespace ::testing; + +// global definitions +BeeperInterface *GlobalBeeper::m_instance; +ComInterface *GlobalCom::m_instance; +EncodersInterface *GlobalEncoders::m_instance; +FsmInterface *GlobalFsm::m_instance; +KnitterInterface *GlobalKnitter::m_instance; +SolenoidsInterface *GlobalSolenoids::m_instance; +TesterInterface *GlobalTester::m_instance; + +struct E2ETest : public Test { + + E2ETest() { + GlobalBeeper::m_instance = &m_Beeper; + GlobalCom::m_instance = &m_Com; + GlobalEncoders::m_instance = &m_Encoders; + GlobalFsm::m_instance = &m_Fsm; + GlobalKnitter::m_instance = &m_Knitter; + GlobalSolenoids::m_instance = &m_Solenoids; + GlobalTester::m_instance = &m_Tester; + + m_arduinoMock = arduinoMockInstance(); + m_WireMock = WireMockInstance(); + } + + ~E2ETest() { + releaseArduinoMock(); + releaseWireMock(); + + GlobalBeeper::m_instance = nullptr; + GlobalCom::m_instance = nullptr; + GlobalEncoders::m_instance = nullptr; + GlobalFsm::m_instance = nullptr; + GlobalKnitter::m_instance = nullptr; + GlobalSolenoids::m_instance = nullptr; + GlobalTester::m_instance = nullptr; + } + + Beeper m_Beeper; + Com m_Com; + Encoders m_Encoders; + Fsm m_Fsm; + Knitter m_Knitter; + Solenoids m_Solenoids; + Tester m_Tester; + + ArduinoMock *m_arduinoMock; + WireMock *m_WireMock; +}; + +struct WithMachineAndTargetNeedle + : public E2ETest, + public WithParamInterface> {}; + +struct NeedleToStringParamName { + std::string operator()(const TestParamInfo &info) const { + return (info.param < 0 ? std::string("neg") : std::string()) + + PrintToString(std::abs(info.param)); + } +}; + +struct MachineAndNeedleToStringParamName { + std::string + operator()(const TestParamInfo> &info) const { + std::string machineName; + switch (std::get<0>(info.param)) { + case MachineType::Kh910: + machineName = "KH910"; + break; + case MachineType::Kh930: + machineName = "KH930"; + break; + case MachineType::Kh270: + machineName = "KH270"; + break; + default: + machineName = "Unknown"; + break; + } + std::string needleNum = + (std::get<1>(info.param) < 0 ? std::string("neg") : std::string()) + + PrintToString(std::abs(std::get<1>(info.param))); + return machineName + "_" + needleNum; + } +}; + +INSTANTIATE_TEST_SUITE_P(E2EParameterizedRight, WithMachineAndTargetNeedle, + Combine(Values(MachineType::Kh910, MachineType::Kh930), + Range(184, 216)), + MachineAndNeedleToStringParamName()); +INSTANTIATE_TEST_SUITE_P(E2EParameterizedLeft, WithMachineAndTargetNeedle, + Combine(Values(MachineType::Kh910, MachineType::Kh930), + Range(-32, 32)), + MachineAndNeedleToStringParamName()); + +TEST_F(E2ETest, BeeperBeeps) { + int millisElapsed = 0; + int buzzerValue = -1; + + EXPECT_CALL(*m_arduinoMock, millis) + .Times(AnyNumber()) + .WillRepeatedly(ReturnPointee(&millisElapsed)); + EXPECT_CALL(*m_arduinoMock, analogWrite(PIEZO_PIN, _)) + .Times(AnyNumber()) + .WillRepeatedly(SaveArg<1>(&buzzerValue)); + + m_Beeper.init(true); + + // trigger a beep + m_Beeper.ready(); + + // Beeper needs two calls to `schedule()` to actually do something + m_Beeper.schedule(); + m_Beeper.schedule(); + + EXPECT_EQ(buzzerValue, 0); + + millisElapsed += 50; + + m_Beeper.schedule(); + m_Beeper.schedule(); + + // about more than 10/255 duty cycle turns buzzer off + ASSERT_GT(buzzerValue, 10); +} + +TEST_F(E2ETest, SettingSolenoidsThroughI2C) { + IOExpandersMock ioExpanders(m_WireMock); + + m_Solenoids.init(); + + m_Solenoids.setSolenoids(0x1234); + + // Solenoid #0 is driven by the least-significant bit, + // so writing the GPIOs out in index order reverses the order + // from writing them as a hexadecimal number. + EXPECT_THAT(ioExpanders.gpioState(), ElementsAre(false, false, true, false, // + true, true, false, false, // + false, true, false, false, // + true, false, false, false // + )); + + m_Solenoids.setSolenoid(0, true); + EXPECT_EQ(ioExpanders.gpioState()[0], true); +} + +TEST_F(E2ETest, EncodersUpdatePosition) { + KnittingMachine km; + KnittingMachineAdapter kma(km, *m_arduinoMock); + + // TODO trigger this from simulated serial communication + GlobalKnitter::initMachine(MachineType::Kh910); + + ASSERT_EQ(m_Encoders.getPosition(), 0); + + for (int i = 0; i < 8; i++) { + km.moveBeltRight(); + + GlobalKnitter::isr(); + } + + ASSERT_EQ(m_Encoders.getPosition(), 2); + + for (int i = 0; i < 8; i++) { + km.moveBeltLeft(); + + GlobalKnitter::isr(); + } + + ASSERT_EQ(m_Encoders.getPosition(), 0); +} + +TEST_F(E2ETest, EncodersDetectKCarriageOnTheLeft) { + KnittingMachine km; + KnittingMachineAdapter kma(km, *m_arduinoMock); + + // Simulate a KH-910 K carriage, starting outside of the bed + km.addCarriageMagnet(0, true); + km.putCarriageCenterInFrontOfNeedle(-50); + + // TODO trigger this from simulated serial communication + GlobalKnitter::initMachine(MachineType::Kh910); + ASSERT_EQ(m_Encoders.getCarriage(), Carriage::NoCarriage); + + // Move the carriage to the right until its magnet gets in front of the sensor + while (km.moveCarriageCenterTowardsNeedle(1)) { + GlobalKnitter::isr(); + } + + // position should have been reset + ASSERT_NEAR(m_Encoders.getPosition(), + END_LEFT_PLUS_OFFSET[(uint8_t)MachineType::Kh910], 1); + ASSERT_EQ(m_Encoders.getCarriage(), Carriage::Knit); +} + +TEST_F(E2ETest, EncodersDetectLCarriageOnTheLeft) { + KnittingMachine km; + KnittingMachineAdapter kma(km, *m_arduinoMock); + + // Simulate a KH-910 L carriage, starting outside of the bed + km.addCarriageMagnet(0, false); + km.putCarriageCenterInFrontOfNeedle(-50); + + // TODO trigger this from simulated serial communication + GlobalKnitter::initMachine(MachineType::Kh910); + ASSERT_EQ(m_Encoders.getCarriage(), Carriage::NoCarriage); + + // Move the carriage to the right until its magnet gets in front of the sensor + while (km.moveCarriageCenterTowardsNeedle(1)) { + GlobalKnitter::isr(); + } + + // position should have been reset + ASSERT_NEAR(m_Encoders.getPosition(), + END_LEFT_PLUS_OFFSET[(uint8_t)MachineType::Kh910], 1); + ASSERT_EQ(m_Encoders.getCarriage(), Carriage::Lace); +} + +TEST_F(E2ETest, EncodersDetectGCarriageOnTheLeft) { + KnittingMachine km; + KnittingMachineAdapter kma(km, *m_arduinoMock); + + // Simulate a KH-910 G carriage, starting outside of the bed + km.addGCarriageMagnets(); + + km.putCarriageCenterInFrontOfNeedle(-150); + + // TODO trigger this from simulated serial communication + GlobalKnitter::initMachine(MachineType::Kh910); + ASSERT_EQ(m_Encoders.getCarriage(), Carriage::NoCarriage); + + // Move the carriage to the right until only the right magnet pair + // has passed the sensor + while (km.moveCarriageCenterTowardsNeedle(0)) { + GlobalKnitter::isr(); + } + + ASSERT_EQ(m_Encoders.getCarriage(), Carriage::Garter); + ASSERT_EQ(m_Encoders.getBeltShift(), BeltShift::Shifted); + + // Position should have been reset to END_LEFT_PLUS_OFFSET at the time the + // rightmost magnet passed the sensor, so now that the center is at needle 0, + // the position should be [position of the rightmost magnet] farther. + const int expectedPositionAtNeedle0 = + END_LEFT_PLUS_OFFSET[(uint8_t)MachineType::Kh910] + 12; + + ASSERT_NEAR(m_Encoders.getPosition(), expectedPositionAtNeedle0, 1); + + // Move the carriage to the right until the left magnet pair + // has passed the sensor as well + while (km.moveCarriageCenterTowardsNeedle(50)) { + GlobalKnitter::isr(); + } + + // Position should NOT have been reset when the left magnet pair passed the + // sensor, so now it should just be 50 needles to the right of previously + ASSERT_NEAR(m_Encoders.getPosition(), + expectedPositionAtNeedle0 + km.getCarriageCenterNeedle(), 1); +} + +TEST_P(WithMachineAndTargetNeedle, EncodersKeepTrackOfGCarriage) { + const MachineType machineType = std::get<0>(GetParam()); + const int targetNeedle = std::get<1>(GetParam()); + + KnittingMachine km; + const KnittingMachineAdapter::Flags adapterFlags = + machineType == MachineType::Kh910 + ? KnittingMachineAdapter::DigitalRightSensor + : KnittingMachineAdapter::Default; + KnittingMachineAdapter kma(km, *m_arduinoMock, adapterFlags); + + // Simulate a G carriage, starting outside of the bed + km.addGCarriageMagnets(); + + km.putCarriageCenterInFrontOfNeedle(-16); + + // TODO trigger this from simulated serial communication + GlobalKnitter::initMachine(machineType); + + // We will be returning to this needle. + const int referenceNeedle = 99; + + // Position should be reset to END_LEFT_PLUS_OFFSET at the time the + // rightmost magnet passes the sensor, so we can add that plus the + // position of the rightmost magnet to get the expected position. + const int internalPositionOffset = + END_LEFT_PLUS_OFFSET[(uint8_t)machineType] + 12; + + // Move the carriage to the right until its magnets have passed the left + // sensor for initial detection + while (km.moveCarriageCenterTowardsNeedle(referenceNeedle)) { + GlobalKnitter::isr(); + } + + // Confirm initial position detection + ASSERT_NEAR(m_Encoders.getPosition(), + internalPositionOffset + referenceNeedle, 1); + + // It's difficult to assign a specific meaning to either value of the + // "belt shift" at this point. For now we'll just lock down what the + // current code computes. + const auto expectedBeltShift = BeltShift::Shifted; + ASSERT_EQ(m_Encoders.getBeltShift(), expectedBeltShift); + + // Move the carriage to the target + while (km.moveCarriageCenterTowardsNeedle(targetNeedle)) { + GlobalKnitter::isr(); + } + + // Move back to the reference, checking the internal position/belt + // shift as we go + while (km.moveCarriageCenterTowardsNeedle(referenceNeedle)) { + GlobalKnitter::isr(); + + // Check that position and belt shift didn't get messed up + // We only need it to be correct while the point of selection is within the bed + if (km.getCarriageCenterNeedle() >= -16 && + km.getCarriageCenterNeedle() <= 216) { + ASSERT_NEAR(m_Encoders.getPosition(), + internalPositionOffset + km.getCarriageCenterNeedle(), 1); + + if (targetNeedle == 190 || targetNeedle == -11) { + GTEST_SKIP() << "Known failure, investigate later"; + } + ASSERT_EQ(m_Encoders.getBeltShift(), expectedBeltShift); + } + } +} + +TEST_F(E2ETest, EncodersDetectKCarriageOnTheRight_KH910) { + KnittingMachine km; + KnittingMachineAdapter kma(km, *m_arduinoMock, + KnittingMachineAdapter::DigitalRightSensor); + + // Simulate a KH-910 K carriage, starting outside of the bed + km.addCarriageMagnet(0, true); + km.putCarriageCenterInFrontOfNeedle(250); + + // TODO trigger this from simulated serial communication + GlobalKnitter::initMachine(MachineType::Kh910); + ASSERT_EQ(m_Encoders.getCarriage(), Carriage::NoCarriage); + + // Move the carriage to the left until its magnet gets just past the right + // sensor + while (km.moveCarriageCenterTowardsNeedle(199)) { + GlobalKnitter::isr(); + } + + // Position should have been reset to END_RIGHT_MINUS_OFFSET when the magnet + // passed the right sensor + ASSERT_NEAR(m_Encoders.getPosition(), + END_RIGHT_MINUS_OFFSET[(uint8_t)MachineType::Kh910], 1); + ASSERT_EQ(m_Encoders.getCarriage(), Carriage::Knit); +} + +TEST_F(E2ETest, EncodersDetectKCarriageOnTheRight_KH930) { + GTEST_SKIP() + << "Known failing " + "(https://github.com/AllYarnsAreBeautiful/ayab-firmware/issues/175)"; + + KnittingMachine km; + KnittingMachineAdapter kma(km, *m_arduinoMock); + + // Simulate a KH-930 K carriage, starting outside of the bed to the right + km.addCarriageMagnet(0, true); + km.putCarriageCenterInFrontOfNeedle(250); + + // TODO trigger this from simulated serial communication + GlobalKnitter::initMachine(MachineType::Kh930); + ASSERT_EQ(m_Encoders.getCarriage(), Carriage::NoCarriage); + + // Move the carriage to the left until its magnet gets just past the right + // sensor + while (km.moveCarriageCenterTowardsNeedle(198)) { + GlobalKnitter::isr(); + } + + // Position should have been reset to END_RIGHT_MINUS_OFFSET when the magnet + // passed the right sensor + ASSERT_NEAR(m_Encoders.getPosition(), + END_RIGHT_MINUS_OFFSET[(uint8_t)MachineType::Kh930], 2); + ASSERT_EQ(m_Encoders.getCarriage(), Carriage::Knit); +} + +TEST_F(E2ETest, EncodersDetectLCarriageOnTheRight_KH930) { + GTEST_SKIP() + << "Known failing " + "(https://github.com/AllYarnsAreBeautiful/ayab-firmware/issues/176)"; + + KnittingMachine km; + KnittingMachineAdapter kma(km, *m_arduinoMock); + + // Simulate a KH-930 L carriage, starting outside of the bed to the right + km.addCarriageMagnet(0, false); + km.putCarriageCenterInFrontOfNeedle(250); + + // TODO trigger this from simulated serial communication + GlobalKnitter::initMachine(MachineType::Kh930); + ASSERT_EQ(m_Encoders.getCarriage(), Carriage::NoCarriage); + + // Move the carriage to the left until its magnet gets just past the right + // sensor + while (km.moveCarriageCenterTowardsNeedle(198)) { + GlobalKnitter::isr(); + } + + // Position should have been reset to END_RIGHT_MINUS_OFFSET when the magnet + // passed the right sensor + ASSERT_NEAR(m_Encoders.getPosition(), + END_RIGHT_MINUS_OFFSET[(uint8_t)MachineType::Kh930], 2); + ASSERT_EQ(m_Encoders.getCarriage(), Carriage::Lace); +} + +TEST_F(E2ETest, EncodersDetectGCarriageOnTheRight_KH930) { + KnittingMachine km; + KnittingMachineAdapter kma(km, *m_arduinoMock); + + // Simulate a KH-930 G carriage, starting outside of the bed to the right + km.addGCarriageMagnets(); + km.putCarriageCenterInFrontOfNeedle(250); + + // TODO trigger this from simulated serial communication + GlobalKnitter::initMachine(MachineType::Kh930); + ASSERT_EQ(m_Encoders.getCarriage(), Carriage::NoCarriage); + + // Move the carriage to the left until its magnets get past the right + // sensor + const int targetNeedle = 100; + while (km.moveCarriageCenterTowardsNeedle(targetNeedle)) { + GlobalKnitter::isr(); + } + + // Position should be reset to END_LEFT_PLUS_OFFSET at the time the + // rightmost magnet passes the sensor, so we can add that plus the + // position of the rightmost magnet to get the expected position. + const int internalPositionOffset = + END_LEFT_PLUS_OFFSET[(uint8_t)MachineType::Kh930] + 12; + + GTEST_SKIP() + << "Known failing " + "(https://github.com/AllYarnsAreBeautiful/ayab-firmware/issues/175)"; + + // Position should have been reset to END_RIGHT_MINUS_OFFSET, + // minus the magnet distance, when the magnet passed the right sensor + ASSERT_NEAR(m_Encoders.getPosition(), + targetNeedle + internalPositionOffset, 2); + ASSERT_EQ(m_Encoders.getCarriage(), Carriage::Garter); +} + +TEST_P(WithMachineAndTargetNeedle, EncodersKeepTrackOfKCarriage) { + const MachineType machineType = std::get<0>(GetParam()); + const int targetNeedle = std::get<1>(GetParam()); + + KnittingMachine km; + const KnittingMachineAdapter::Flags adapterFlags = + machineType == MachineType::Kh910 + ? KnittingMachineAdapter::DigitalRightSensor + : KnittingMachineAdapter::Default; + KnittingMachineAdapter kma(km, *m_arduinoMock, adapterFlags); + + // Simulate a K carriage, starting outside of the bed + km.addCarriageMagnet(0, true); + + km.putCarriageCenterInFrontOfNeedle(-16); + + // TODO trigger this from simulated serial communication + GlobalKnitter::initMachine(machineType); + + // We will be returning to this needle. + const int referenceNeedle = 99; + + // Offset between actual position of carriage center, and internal position + // as maintained by the firmware. + // Position should be reset to END_LEFT_PLUS_OFFSET at the time the + // magnet passes the sensor, so we can add that to get the internal position. + const int internalPositionOffset = END_LEFT_PLUS_OFFSET[(uint8_t)machineType]; + + // Move the carriage to the right until its magnet has passed the left + // sensor for initial detection + while (km.moveCarriageCenterTowardsNeedle(referenceNeedle)) { + GlobalKnitter::isr(); + } + + // Confirm initial position detection + ASSERT_NEAR(m_Encoders.getPosition(), + internalPositionOffset + referenceNeedle, 1); + + // It's difficult to assign a specific meaning to either value of the + // "belt shift" at this point. For now we'll just lock down what the + // current code computes. + const auto expectedBeltShift = BeltShift::Regular; + + // Confirm initial belt shift + ASSERT_EQ(m_Encoders.getBeltShift(), expectedBeltShift); + + // Move the carriage to the target + while (km.moveCarriageCenterTowardsNeedle(targetNeedle)) { + GlobalKnitter::isr(); + } + + // Move back to the reference, checking the internal position/belt + // shift as we go + while (km.moveCarriageCenterTowardsNeedle(referenceNeedle)) { + GlobalKnitter::isr(); + + // Check that position and belt shift didn't get messed up + // We only need it to be correct while the point of selection is within the + // bed + if (km.getCarriageCenterNeedle() >= -16 && + km.getCarriageCenterNeedle() <= 216) { + // Note position tolerance increased from 1 to 2 because we simulate a + // machine where (as measured on an actual KH910) the distance between the + // left and right position sensors is slightly more than 200 needle + // widths, but the current firmware assumes it is exactly 199 needle + // widths. + ASSERT_NEAR(m_Encoders.getPosition(), + internalPositionOffset + km.getCarriageCenterNeedle(), 2); + + ASSERT_EQ(m_Encoders.getBeltShift(), expectedBeltShift); + } + } +} + +int main(int argc, char *argv[]) { + InitGoogleMock(&argc, argv); + return RUN_ALL_TESTS(); +} \ No newline at end of file diff --git a/test/test_knitting_machine.cpp b/test/test_knitting_machine.cpp new file mode 100644 index 00000000..b7be259b --- /dev/null +++ b/test/test_knitting_machine.cpp @@ -0,0 +1,243 @@ +/** + * This test suite is meant to validate that the KnittingMachine class + * faithfully reproduces the behavior of a real knitting machine. + */ + +#include +#include +#include + +#include "knitting_machine.h" + +using namespace ::testing; + +TEST(KnittingMachine, EncoderOutputsMovingRight) { + KnittingMachine km; + + ASSERT_EQ(false, km.getEncoderOutput1()); + ASSERT_EQ(true, km.getEncoderOutput2()); + + km.moveBeltRight(); + + ASSERT_EQ(true, km.getEncoderOutput1()); + ASSERT_EQ(true, km.getEncoderOutput2()); + + km.moveBeltRight(); + + ASSERT_EQ(true, km.getEncoderOutput1()); + ASSERT_EQ(false, km.getEncoderOutput2()); + + km.moveBeltRight(); + + ASSERT_EQ(false, km.getEncoderOutput1()); + ASSERT_EQ(false, km.getEncoderOutput2()); + + km.moveBeltRight(); + + ASSERT_EQ(false, km.getEncoderOutput1()); + ASSERT_EQ(true, km.getEncoderOutput2()); +} + +TEST(KnittingMachine, EncoderOutputsMovingLeft) { + KnittingMachine km; + + ASSERT_EQ(false, km.getEncoderOutput1()); + ASSERT_EQ(true, km.getEncoderOutput2()); + + km.moveBeltLeft(); + + ASSERT_EQ(false, km.getEncoderOutput1()); + ASSERT_EQ(false, km.getEncoderOutput2()); + + km.moveBeltLeft(); + + ASSERT_EQ(true, km.getEncoderOutput1()); + ASSERT_EQ(false, km.getEncoderOutput2()); + + km.moveBeltLeft(); + + ASSERT_EQ(true, km.getEncoderOutput1()); + ASSERT_EQ(true, km.getEncoderOutput2()); + + km.moveBeltLeft(); + + ASSERT_EQ(false, km.getEncoderOutput1()); + ASSERT_EQ(true, km.getEncoderOutput2()); +} + +TEST(KnittingMachine, KCarriageDetectionOnTheLeft) { + KnittingMachine km; + + km.putCarriageCenterInFrontOfNeedle(-100); + km.addCarriageMagnet(0, true); + + ASSERT_THAT(km.getLeftPositionSensorVoltage(), AllOf(Gt(0.4), Lt(3.4))); + + km.putCarriageCenterInFrontOfNeedle(0); + + ASSERT_THAT(km.getLeftPositionSensorVoltage(), Gt(3.4)); + + km.putCarriageCenterInFrontOfNeedle(5); + + ASSERT_THAT(km.getLeftPositionSensorVoltage(), AllOf(Gt(0.4), Lt(3.4))); +} + +TEST(KnittingMachine, LCarriageDetectionOnTheLeft) { + KnittingMachine km; + + km.addCarriageMagnet(0, false); + + km.putCarriageCenterInFrontOfNeedle(0); + + ASSERT_THAT(km.getLeftPositionSensorVoltage(), Lt(0.4)); + + km.putCarriageCenterInFrontOfNeedle(5); + + ASSERT_THAT(km.getLeftPositionSensorVoltage(), AllOf(Gt(0.4), Lt(3.4))); +} + +TEST(KnittingMachine, GCarriageDetectionOnTheLeft) { + KnittingMachine km; + + km.addGCarriageMagnets(); + + km.putCarriageCenterInFrontOfNeedle(-12); + ASSERT_THAT(km.getLeftPositionSensorVoltage(), Lt(0.4)); + + km.putCarriageCenterInFrontOfNeedle(-11); + ASSERT_THAT(km.getLeftPositionSensorVoltage(), Gt(3.4)); + + km.putCarriageCenterInFrontOfNeedle(0); + ASSERT_THAT(km.getLeftPositionSensorVoltage(), AllOf(Gt(0.4), Lt(3.4))); + + km.putCarriageCenterInFrontOfNeedle(10); + ASSERT_THAT(km.getLeftPositionSensorVoltage(), Gt(3.4)); + + km.putCarriageCenterInFrontOfNeedle(11); + ASSERT_THAT(km.getLeftPositionSensorVoltage(), Lt(0.4)); +} + +TEST(KnittingMachine, KCarriageDetectionOnTheRight) { + KnittingMachine km; + + km.putCarriageCenterInFrontOfNeedle(-100); + km.addCarriageMagnet(0, true); + + ASSERT_THAT(km.getRightPositionSensorVoltage(), AllOf(Gt(0.4), Lt(3.4))); + ASSERT_THAT(km.getRightPositionSensorKSignal(), AllOf(Gt(1), Lt(4))); + + km.putCarriageCenterInFrontOfNeedle(200); + + ASSERT_THAT(km.getRightPositionSensorVoltage(), Gt(3.4)); + ASSERT_THAT(km.getRightPositionSensorKSignal(), Lt(0.4)); + + km.putCarriageCenterInFrontOfNeedle(190); + + ASSERT_THAT(km.getRightPositionSensorVoltage(), AllOf(Gt(0.4), Lt(3.4))); + ASSERT_THAT(km.getRightPositionSensorKSignal(), AllOf(Gt(1), Lt(4))); +} + +TEST(KnittingMachine, MoveCarriageIncrementally) { + KnittingMachine km; + + km.putCarriageCenterInFrontOfNeedle(-1); + ASSERT_EQ(km.getCarriageCenterNeedle(), -1); + + // One step to the right isn't enough to change needle + km.moveCarriageRight(); + ASSERT_EQ(km.getCarriageCenterNeedle(), -1); + + // Three more will do the trick + km.moveCarriageRight(); + km.moveCarriageRight(); + km.moveCarriageRight(); + ASSERT_EQ(km.getCarriageCenterNeedle(), 0); + + // There's a helper to move until a position is reached + int stepCount = 0; + while (km.moveCarriageCenterTowardsNeedle(5)) { + stepCount++; + } + + ASSERT_EQ(km.getCarriageCenterNeedle(), 5); + ASSERT_EQ(stepCount, 20); + + // You can go to the left as well + while (km.moveCarriageCenterTowardsNeedle(0)) + ; + ASSERT_EQ(km.getCarriageCenterNeedle(), 0); +} + +TEST(KnittingMachine, BeltPhaseSignal) { + KnittingMachine km; + + ASSERT_FALSE(km.getBeltPhase()); + + for (int i = 0; i < 8 * 4; i++) { + km.moveBeltLeft(); + } + ASSERT_TRUE(km.getBeltPhase()); + + for (int i = 0; i < 8 * 4; i++) { + km.moveBeltRight(); + } + ASSERT_FALSE(km.getBeltPhase()); +} + +TEST(KnittingMachine, CarriageMovesBeltOnlyWhenInSync) { + KnittingMachine km; + + ASSERT_FALSE(km.getBeltPhase()); + + // Setting the carriage in a position where its belt hooks don't engage belt + // holes (because they are offset). + km.putCarriageCenterInFrontOfNeedle(1); + + // So when it moves to the left, the belt doesn't move, its phase doesn't + // change. + while (km.moveCarriageCenterTowardsNeedle(0)) { + ASSERT_FALSE(km.getBeltPhase()); + } + + // Now that the carriage is at a belt position divisible by 8, it's engaged + // and the belt moves. + while (km.moveCarriageCenterTowardsNeedle(-4)) { + } + ASSERT_TRUE(km.getBeltPhase()); +} + +/** + * Helper to print the value of all signals while moving a carriage + * across the bed. + * The output can be extracted and compared with a similar scan + * recorded on an actual machine. + * Sample `ctest` invocation to extract data: + * + * ctest --test-dir test/build -V -R KCarriageScanBed|grep SCAN=|cut -d= -f2 + */ +void doBedScan(KnittingMachine &km) { + float centerNeedle = -32; + km.putCarriageCenterInFrontOfNeedle(centerNeedle); + + while (km.moveCarriageCenterTowardsNeedle(231)) { + centerNeedle += 1.0 / 4; + printf("SCAN=%g\t%g\t%g\t%g\t%d\n", centerNeedle, + km.getLeftPositionSensorVoltage(), + km.getRightPositionSensorVoltage(), + km.getRightPositionSensorKSignal(), km.getBeltPhase() ? 5 : 0); + } +} + +TEST(KnittingMachine, GCarriageScanBed) { + KnittingMachine km; + + km.addGCarriageMagnets(); + doBedScan(km); +} + +TEST(KnittingMachine, KCarriageScanBed) { + KnittingMachine km; + + km.addCarriageMagnet(0, true); + doBedScan(km); +} \ No newline at end of file diff --git a/test/test_tester.cpp b/test/test_tester.cpp index 7d8a225e..7b950fae 100644 --- a/test/test_tester.cpp +++ b/test/test_tester.cpp @@ -46,6 +46,7 @@ class TesterTest : public ::testing::Test { void SetUp() override { arduinoMock = arduinoMockInstance(); serialMock = serialMockInstance(); + wireMock = WireMockInstance(); // serialCommandMock = serialCommandMockInstance(); // pointers to global instances @@ -64,12 +65,14 @@ class TesterTest : public ::testing::Test { void TearDown() override { releaseArduinoMock(); releaseSerialMock(); + releaseWireMock(); } ArduinoMock *arduinoMock; FsmMock *fsmMock; KnitterMock *knitterMock; SerialMock *serialMock; + WireMock *wireMock; void expect_startTest(unsigned long t) { EXPECT_CALL(*fsmMock, getState).WillOnce(Return(OpState::ready));