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));